compose--初入compose、资源获取、标准控件与布局

2022-12-06 15:33:57 浏览数 (1)

compose正式发布已经一年多了,越来越多的开发人员选择使用它,声明式UI也是未来的一个主流趋势,本人也是一年前学习后,并没有真正的使用,所以本着边学习,边分享的心态,准备写个compose系列的文章

首先compose目前只支持kotlin,基于google对移动端的鸿图,未来应该也不会支持其他语言,和传统安卓的xml布局不同,compose是通过kotlin定义一个一个组件,由于是通过代码定义的组件,每个组件都可以很方便的重用,这点在UI开发时确实便利了不少。至于声明式UI和命令式UI的区别,相信你会在后续实际使用时有很大的感触

一、认识compose

通过官方文档我们可以了解到compose的编程思想。官方地址:https://developer.android.google.cn/jetpack/compose/mental-model

我这边也是根据官方文档,对重要的部分和自己的想法进行融合,来介绍什么是compose。这部分内容都是概念性的,但是贯穿整个compose的学习,应该进行着重深入理解

1. 重组
1.1 安卓传统UI

先来说在安卓传统UI,大致的流程就是xml中我们定义了一系列的布局(组件)和控件后,由ActivityonCreate()触发xml解析,生成View树DecorView,并ActivityhandleResumeActivity()ViewRootImpl绑定,通过Binder通信,交由由WindowManagerService创建surface进行渲染,最终呈现在手机屏幕

当然了,我们只需要关注在onCreate()中设置xml即可,由于布局是一次性加载的,即生成View树的过程是同步进行的

1.2 compose UI

对与compose而言,每个可组合函数(组件)的调用可能发生在与调用方不同的线程上,即每个组件添加至View树的过程,都是通过协程进行的,上树的过程未必按代码调用的顺序执行

1.3 什么是重组?

在compose中,每个可组合函数调用直至渲染完成,称之为重组

通过异步上树虽然带来了性能的提升,但是管理方面变得困难,所以compose规定,每个可组合函数都是独立运行的存在,可组合函数内部应该仅处理的UI操作,重组的发生的时机并不由我们控制,而是由compose内部自动管理,后续我们可以使用状态来通知compose进行重组

二、创建compose项目

推荐使用最新的android studio,低版本并不支持compose,也可以查看官方文档-快速入门:https://developer.android.google.cn/jetpack/compose/setup

1.创建项目

我这边尝鲜使用MD3风格的项目,实际开发中google也推荐:UI设计从MD2转变为MD3

2.BOM

对于compose的版本管理,官方推荐使用BOM,导入BOM后的好处是:导入compose其他库组,都将使用BOM中定义的版本,后续更新,我们只需要更新BOM的版本即可。下面是官方给出的BOM:compose版本对应关系:

库组

版本 (2022.10.00)

版本 (2022.11.00)

androidx.compose.animation:animation

1.3.0

1.3.1

androidx.compose.animation:animation-core

1.3.0

1.3.1

androidx.compose.animation:animation-graphics

1.3.0

1.3.1

androidx.compose.foundation:foundation

1.3.0

1.3.1

androidx.compose.foundation:foundation-layout

1.3.0

1.3.1

androidx.compose.material:material

1.3.0

1.3.1

androidx.compose.material:material-icons-core

1.3.0

1.3.1

androidx.compose.material:material-icons-extended

1.3.0

1.3.1

androidx.compose.material:material-ripple

1.3.0

1.3.1

androidx.compose.material3:material3

1.0.0

1.0.1

androidx.compose.material3:material3-window-size-class

1.0.0

1.0.1

androidx.compose.runtime:runtime

1.3.0

1.3.1

androidx.compose.runtime:runtime-livedata

1.3.0

1.3.1

androidx.compose.runtime:runtime-rxjava2

1.3.0

1.3.1

androidx.compose.runtime:runtime-rxjava3

1.3.0

1.3.1

androidx.compose.runtime:runtime-saveable

1.3.0

1.3.1

androidx.compose.ui:ui

1.3.0

1.3.1

androidx.compose.ui:ui-geometry

1.3.0

1.3.1

androidx.compose.ui:ui-graphics

1.3.0

1.3.1

androidx.compose.ui:ui-test

1.3.0

1.3.1

androidx.compose.ui:ui-test-junit4

1.3.0

1.3.1

androidx.compose.ui:ui-test-manifest

1.3.0

1.3.1

androidx.compose.ui:ui-text

1.3.0

1.3.1

androidx.compose.ui:ui-text-google-fonts

1.3.0

1.3.1

androidx.compose.ui:ui-tooling

1.3.0

1.3.1

androidx.compose.ui:ui-tooling-data

1.3.0

1.3.1

androidx.compose.ui:ui-tooling-preview

1.3.0

1.3.1

androidx.compose.ui:ui-unit

1.3.0

1.3.1

androidx.compose.ui:ui-util

1.3.0

1.3.1

androidx.compose.ui:ui-viewbinding

1.3.0

1.3.1

工程中导入:

代码语言:javascript复制
dependencies {
    def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
    implementation composeBom

...
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
    implementation 'androidx.activity:activity-compose'
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.ui:ui-tooling-preview"
    implementation 'androidx.compose.material3:material3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.1"
    debugImplementation "androidx.compose.ui:ui-tooling"
    debugImplementation "androidx.compose.ui:ui-test-manifest"
}
3.kotlin-compose compiler版本对应

BOM中不包含Compose编译器库,所以我们需要手动对应下kotlin版本与compose compiler版本,下面是两者的兼容关系,官网也可以查询到最新的对应关系:

https://developer.android.google.cn/jetpack/androidx/releases/compose-kotlin

Compose Compiler 版本

兼容的 Kotlin 版本

1.4.0-alpha01

1.7.20

1.3.2

1.7.20

1.3.1

1.7.10

1.3.0

1.7.10

1.3.0-rc02

1.7.10

1.3.0-rc01

1.7.10

1.3.0-beta01

1.7.10

1.2.0

1.7.0

1.2.0-rc02

1.6.21

1.2.0-rc01

1.6.21

1.2.0-beta03

1.6.21

1.2.0-beta02

1.6.21

1.2.0-beta01

1.6.21

1.2.0-alpha08

1.6.20

1.2.0-alpha07

1.6.10

1.2.0-alpha06

1.6.10

1.2.0-alpha05

1.6.10

1.2.0-alpha04

1.6.10

1.2.0-alpha03

1.6.10

1.2.0-alpha02

1.6.10

1.2.0-alpha01

1.6.10

1.1.1

1.6.10

1.1.0

1.6.10

1.1.0-rc03

1.6.10

1.1.0-rc02

1.6.10

1.1.0-rc01

1.6.0

1.1.0-beta04

1.6.0

1.1.0-beta03

1.5.31

1.1.0-beta02

1.5.31

1.1.0-beta01

1.5.31

1.1.0-alpha06

1.5.31

1.1.0-alpha05

1.5.31

1.0.5

1.5.31

1.0.4

1.5.31

1.1.0-alpha04

1.5.30

1.1.0-alpha03

1.5.30

1.0.3

1.5.30

1.1.0-alpha02

1.5.21

1.1.0-alpha01

1.5.21

1.0.2

1.5.21

1.0.1

1.5.21

1.0.0

1.5.10

1.0.0-rc02

1.5.10

1.0.0-rc01

1.5.10

我这边使用的是1.3.1,对应kotlin版本是1.7.10,工程中build.gradle

代码语言:javascript复制
android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.1"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

主工程中build.gradle:

代码语言:javascript复制
plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}
4.预览compose函数与启动
4.1 预览compose函数

引入了ui-tooling-preview库组后,我们可以使用@Preview注解可组合函数,并实现预览组件

4.2 启动

启动到模拟器的效果:

三、资源获取

xml中,我们常常会使用资源id获取到资源文件,比如:color、drawable、string等,在compose中,通过以下函数获取,这些函数都位于androidx.compose.ui.res包下:

当然我们并不需要使用里面全部的类,掌握下面列出的即可:

资源获取方式

描述

stringResource

获取对应id的string资源,并支持传入多个参数,来实现字符串格式化

colorResource

获取对应id的color资源

painterResource

获取对应id的图片资源,可以是一个vector,也可以是drawable

dimensionResource

获取对应id的dimen资源,由于compose推荐使用md主题设置dimen,用的也不多

四、标准控件

compose本身内置了一些组件,官方说法所有组件都是可组合函数,这边仅仅是便于传统开发理解,分成控件和布局来介绍,这些内置可组合函数分散在各个不同的库组内,如:androidx.compose.foundationandroidx.compose.foundation.layoutandroidx.compose.material3

其中控件大多位于md包下,他们都具有MD风格,也是官方推荐使用的组件:

1.Text

Text用于呈现一段文字,是使用最多的组件,官方也详细的介绍了该组件:https://developer.android.google.cn/jetpack/compose/text

1.1 基本使用

所有compose函数都要由@Composable注解,并且每个可组合函数都是可以重用的组件:

代码语言:javascript复制
@Composable
@Preview
fun MyText() {
    Text(text = "hello world!")
}

预览效果:

1.2 使用资源获取文本

通过stringResource(id)获取String,可以达到同样的效果

代码语言:javascript复制
@Composable
@Preview
fun MyText() {
    Text(text = stringResource(id = R.string.hello))
}
1.3 AnnotatedString

传统UI的TextView,可以通过Span来改变文本的内嵌样式,比如个别字颜色设置、设置背景颜色等效果

compose中可以使用AnnotatedString来达到这种效果,通过buildAnnotatedString()构建一个AnnotatedStringAnnotatedString可以包含多个 SpanStyle(点击跳转API)ParagraphStyle(点击跳转API)

  • SpanStyle:设置文本的内嵌样式
  • ParagraphStyle:设置文本的行高,对齐方式,文字方向和文字缩进样式

例子:

代码语言:javascript复制
@Composable
@Preview
fun MyText() {
    Text(
        text = buildAnnotatedString {
            withStyle(
                style = ParagraphStyle(
                    lineHeight = 30.sp,//行高
                    textAlign = TextAlign.Left,//左对齐
                    textIndent = TextIndent(firstLine = 10.sp)//缩进
                )
            ) {
                withStyle(
                    style = SpanStyle(
                        fontSize = 20.sp,
                        color = Color.Red,//设置颜色为红色
                        fontWeight = FontWeight.Medium//加粗
                    )
                ) {
                    append("hin")
                }
            }

            withStyle(
                style = ParagraphStyle(
                    lineHeight = 60.sp,
                )
            ) {
                withStyle(
                    style = SpanStyle(
                        color = Color.Red,
                        shadow = Shadow(//设置阴影
                            color = Color.Blue,//阴影颜色
                            blurRadius = 3f,//虚化
                            offset = Offset(5f, 20f)//x,y轴的偏移
                        )
                    )
                ) {
                    append("你好n")
                }
            }
        }
    )
}

预览效果:

1.4 其他参数

其他参数可以通过源码查看:

代码语言:javascript复制
@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,//修饰符
    color: Color = Color.Unspecified,//颜色
    fontSize: TextUnit = TextUnit.Unspecified,//字体
    fontStyle: FontStyle? = null,//字体样式,正常或斜体
    fontWeight: FontWeight? = null,//字体粗细
    fontFamily: FontFamily? = null,//字体
    letterSpacing: TextUnit = TextUnit.Unspecified,//字间距
    textDecoration: TextDecoration? = null,//字体装饰,删除线、下划线等
    textAlign: TextAlign? = null,//内容对齐方式,居中、左对齐、右对齐等
    lineHeight: TextUnit = TextUnit.Unspecified,//行高
    overflow: TextOverflow = TextOverflow.Clip,//内容超出处理方式,截断、使用...等
    softWrap: Boolean = true,//是否自动换行
    maxLines: Int = Int.MAX_VALUE,//最大行数
    onTextLayout: (TextLayoutResult) -> Unit = {},//文本变化导致重组的回调
    style: TextStyle = LocalTextStyle.current//更丰富的字体样式,包含上面大多数设置,以及SpanStyle和ParagraphStyle
) {
...
}

其中Modifier后续会详细介绍,举例使用里面的几个参数设置,如使用TextStyle去除首行的顶部行间距:

代码语言:javascript复制
<string name="hello">hello!nworld</string>
代码语言:javascript复制
@Composable
@Preview
fun MyText() {
    Text(
        text = stringResource(id = R.string.hello),
        fontWeight = FontWeight.Medium,
        overflow = TextOverflow.Clip,
        //将当前的style和另一个合并,以另一个设置的属性为优先
        style = LocalTextStyle.current.merge(
            TextStyle(
                lineHeight = 2.5.em,
                platformStyle = PlatformTextStyle(
                    includeFontPadding = false//配合trim
                ),
                lineHeightStyle = LineHeightStyle(
                    alignment = LineHeightStyle.Alignment.Center,
                    // trim生效需要includeFontPadding = false
                    // trim是指将行间距尽可能的去除
                    // FirstLineTop:将第一行顶部的行间距去除
                    trim = LineHeightStyle.Trim.FirstLineTop
                )
            )
        )
    )
}

预览效果:

2.Image

Image用于展现图片

2.1 基本使用

必传入参为图片资源对象painter和内容描述contentDescriptioncontentDescription主要是为了残疾人使用的,国外对于残疾人使用也非常的重视,此外使用python自动化测试也可以通过contentDescription找到该组件:

代码语言:javascript复制
@Composable
@Preview
fun MyImage() {
    Image(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),//指定图片资源
        contentDescription = "my image" //描述,残疾人以及自动化测试使用
    )
}

预览效果:

2.2 其他参数

相较于TextImage的参数少很多:

代码语言:javascript复制
@Composable
fun Image(
    painter: Painter,
    contentDescription: String?,
    modifier: Modifier = Modifier,//修饰符
    alignment: Alignment = Alignment.Center,//图片对齐方式
    contentScale: ContentScale = ContentScale.Fit,//图片的拉伸方式
    alpha: Float = DefaultAlpha,//图片透明度
    colorFilter: ColorFilter? = null//通过ColorFilter对颜色矩阵进行变换
) {
    
}

参数还是比较简单的,ContentScale的几种方式可以通过官网认识:ContentScale介绍(点击跳转),其中ColorFilter和传统UI自定义控件时,使用的高级渲染效果相同,ColorFilter分别拥有三个伴生方法,对应不同的渲染方式:

  • tint(color: Color, blendMode: BlendMode = BlendMode.SrcIn):高级渲染,参考Xfermod(点击跳转)
  • colorMatrix(colorMatrix: ColorMatrix):颜色矩阵变换,参考ColorMatrixColorFilter(点击跳转)
  • lighting(multiply: Color, add: Color):颜色向量增加,参考LightingColorFilter(点击跳转)

使用tint例子,使用SrcIn模式合成一个红色:

代码语言:javascript复制
@Composable
@Preview
fun MyImage() {
    Image(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),
        contentDescription = "my image",
        colorFilter = ColorFilter.tint(
            color = Color.Red,
            blendMode = BlendMode.SrcIn
        )
    )
}

预览效果:

使用colorMatrix例子,颜色增强:

代码语言:javascript复制
@Composable
@Preview
fun MyImage() {
    Row {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image1",
            colorFilter = ColorFilter.colorMatrix(
                ColorMatrix().apply {
                    setToScale(1.2f, 1.2f, 1.2f, 1f)//颜色增强
                }
            )
        )

        Spacer(modifier = Modifier.width(10.dp))

        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image2",
        )
    }
}

预览效果,左边为颜色增强后:

使用lighting例子,添加红色向量:

代码语言:javascript复制
@Composable
@Preview
fun MyImage() {
    Row {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image1",
            // 红色向量添加255,红色加绿色 = 黄色
            colorFilter = ColorFilter.lighting(
                Color(red = 0xff, green = 0xff, blue = 0xff),
                Color(red = 0xff, green = 0, blue = 0)
            )
        )

        Spacer(modifier = Modifier.width(10.dp))

        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = "my image2",
        )
    }
}

预览效果,左边为添加红色向量后:

2.3 Icon

同样用于显示图标,Icon功能比Image少,只支持tint,并且该tint为一个Color对象,不支持模式,只支持染色:

代码语言:javascript复制
@Composable
@Preview
fun MyImage() {
    Icon(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),
        contentDescription = "icon",
        tint = Color.Blue // 将图标染成蓝色
    )
}

预览效果:

3.TextField

TextField就是输入框,并且需要用到state,关于state后续会详细介绍

3.1 基本使用

TextField必须传入的两个参数,一个是value,一个是onValueChange ,结合之前的重组概念来理解,每次重组都会重新调用可组合函数,所以输入框内容value必须是一个全局对象,在compose中,可以使用remember函数来使得一个变量成为全局变量,从而不受重组时代码调用导致重新初始化操作的影响

此外,只有state的改变才能通知compose进行重组,所以value又必须是一个state对象,并在onValueChange中对state进行改变,才能够进行组件的刷新

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }// 定义state对象:text ,并设为全局
    TextField(
        value = text,//text 与TextField进行绑定
        onValueChange = { text = it },//当输入框值发生变换时,改变text值,从而引起状态的刷新,进而重组
        label = { Text("hint") }//提示
    )
}

效果:

3.2 TextFieldValue

value的参数类型除了支持String外,还支持TextFieldValueTextFieldValue具有更好的自定义性,如使用AnnotatedString使文本具有样式、TextRange指定光标位置:

代码语言:javascript复制
@Immutable
class TextFieldValue constructor(
    val annotatedString: AnnotatedString,//带样式的字符串
    selection: TextRange = TextRange.Zero,//
    composition: TextRange? = null
) {
...
}

例子:

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Preview
@Composable
fun TextFieldValuePreview(
) {
    val textFieldValueState = remember {
        mutableStateOf(
            TextFieldValue(
                annotatedString = buildAnnotatedString {
                    append("hi")

                    withStyle(
                        style = SpanStyle(
                            color = Color.Red,
                            //设置阴影
                            shadow = Shadow(
                                color = Color.Blue,//阴影颜色
                                blurRadius = 3f,//虚化
                            )
                        )
                    ) {
                        append("你好n")
                    }
                },
                selection = TextRange(2)// 光标默认显示在第二个字符位置
            )
        )
    }

    val showKeyboard = remember { mutableStateOf(true) }
    val focusRequester = remember { FocusRequester() }
    val keyboard = LocalSoftwareKeyboardController.current

    // 显示键盘
    LaunchedEffect(focusRequester) {
        if (showKeyboard.value) {
            focusRequester.requestFocus()
            delay(100)
            keyboard?.show()
        }
    }

    TextField(
        modifier = Modifier.focusRequester(focusRequester),
        value = textFieldValueState.value,
        onValueChange = {
        }
    )
}

效果:

3.3 其他参数
代码语言:javascript复制
@ExperimentalMaterial3Api
@Composable
fun TextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,// 是否可用
    readOnly: Boolean = false,// 是否只读
    textStyle: TextStyle = LocalTextStyle.current,// 和Text一样支持的TextStyle
    label: @Composable (() -> Unit)? = null,//提示,有内容时自动缩小并上移
    placeholder: @Composable (() -> Unit)? = null,//提示,有内容时自动消失
    leadingIcon: @Composable (() -> Unit)? = null,//文本前的图标
    trailingIcon: @Composable (() -> Unit)? = null,//文本尾的图标
    supportingText: @Composable (() -> Unit)? = null,//文本下方的文本
    isError: Boolean = false,//是否错误,错误会将label、下划线、下方文本、文本尾的图标的图标染红
    visualTransformation: VisualTransformation = VisualTransformation.None,//输入内容的视觉类型,如密码显示*
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,//键盘类型和imeAction
    keyboardActions: KeyboardActions = KeyboardActions.Default,//imeAction触发时的回调
    singleLine: Boolean = false,//是否单行
    maxLines: Int = Int.MAX_VALUE,//最大行数
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },//传入状态,从而监听用户触摸操作,如点击、拖拽
    shape: Shape = TextFieldDefaults.filledShape,//设置背景形状
    colors: TextFieldColors = TextFieldDefaults.textFieldColors()// 颜色集,通过设置相应的颜色,可以改变如错误发生时的颜色
) {
...
}

例子:

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }
    TextField(
        value = text,
        onValueChange = { text = it },
        placeholder = { Text("haha") },
        leadingIcon = {//设置文本前图片
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "leadingIcon"
            )
        },
        trailingIcon = {//设置文本后图片
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "leadingIcon"
            )
        },
        supportingText = {//设置文本下的文本
            Text("supportingText")
        },
        isError = true,// 设置发生错误
        visualTransformation = PasswordVisualTransformation(),//视觉为密码
        shape = RoundedCornerShape(10.dp),//背景为圆角
        colors = TextFieldDefaults.textFieldColors(//错误时,下划线显示黄色
            errorIndicatorColor = Color.Yellow
        )
    )
}

效果:

3.4 OutlinedTextField

OutlinedTextField是含有一个边框的输入框,其他用法和TextField相同

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyTextField() {
    var text by remember { mutableStateOf("") }

    OutlinedTextField(
        modifier = Modifier.padding(start = 10.dp, top = 10.dp),
        value = text,
        onValueChange = { text = it },
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Search
        ),
        keyboardActions = KeyboardActions { 
            
        }
    )
}

效果:

4. Button

Button需要传入一个点击事件onClicklambda表达式,和一个content内容组件的lambda表达式,border边框支持Shader(点击跳转详情),其他参数说明如下:

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,// 是否可用
    shape: Shape = ButtonDefaults.shape,// 背景形状
    colors: ButtonColors = ButtonDefaults.buttonColors(),//颜色集,背景、内容的可用和非可用颜色
    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),//阴影,默认、按下、不可用等状态下的阴影
    border: BorderStroke? = null,//边框,支持Shader
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,// 内容组件的padding
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },//触摸事件的状态改变
    content: @Composable RowScope.() -> Unit//按钮的内容组件
) {

}
4.1 基本使用

Buttoncontent是一个RowScope的作用域,也就是以行来摆放组件

例子:

代码语言:javascript复制
@Preview
@Composable
fun MyButton() {
    Button(
        onClick = { /*TODO*/ },
        colors = ButtonDefaults.buttonColors(
            containerColor = Color.Cyan,
            contentColor = Color.Red
        ),
        elevation = ButtonDefaults.buttonElevation(defaultElevation = 3.dp),
        border = BorderStroke(
            1.dp,
            Brush.linearGradient(
                0f to Color.Transparent,
                1f to Color.DarkGray
            )
        ),
        contentPadding = PaddingValues(
            start = 10.dp,
            top = 5.dp
        ),
        content = {
            Text("点我")
            Text("点我")
        }
    )
}

预览效果:

4.2 IconButton

IconButtoncontent需要传入一个Icon组件,其他用法和Button相同:

代码语言:javascript复制
@Composable
fun MyIconButton() {
    IconButton(
        onClick = { /*TODO*/ },
        colors = IconButtonDefaults.iconButtonColors(contentColor = Color.Green),
        content = {
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "icon"
            )
        }
    )
}

预览效果:

4.3 IconToggleButton

IconToggleButton具有选中和未选中状态,checked入参需要配合state对象使用,onCheckedChange用于选中状态切换的处理,其他用法和Button相同:

代码语言:javascript复制
@Preview
@Composable
fun MyIconToggleButton() {
    var checked by remember { mutableStateOf(false) }

    IconToggleButton(
        checked = checked,
        onCheckedChange = {
            checked = it
        },
        modifier = Modifier
            .width(100.dp)
            .height(100.dp),
        colors = IconButtonDefaults.iconToggleButtonColors(
            contentColor = Color.Green,//选中为绿色
            checkedContentColor = Color.Red//非选中为红色
        ),
        content = {
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "icon"
            )
        }
    )
}

效果:

https://upload-images.jianshu.io/upload_images/6288115-25c6151f7697ea9e.gif?imageMogr2/auto-orient/strip|imageView2/2/w/442/format/webp

4.4 Switch

Switch为开关样式的IconToggleButton组件,thumbContent参数支持指定开关按钮的Icon,其他用法与IconToggleButton相同:

代码语言:javascript复制
@Preview
@Composable
fun MySwitch() {
    var checked by remember { mutableStateOf(false) }

    Switch(
        checked = checked,
        onCheckedChange = { checked = it },
        thumbContent = {
            Icon(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "icon"
            )
        }
    )
}

效果:

4.5 RadioButton

RadioButton为单选框

代码语言:javascript复制
@Preview
@Composable
fun MyRadioButton() {
    var selected by remember { mutableStateOf(false) }

    RadioButton(
        selected = selected,
        onClick = { selected = !selected }
    )
}

效果:

https://upload-images.jianshu.io/upload_images/6288115-251284cd8afa5b54.gif?imageMogr2/auto-orient/strip|imageView2/2/w/442/format/webp

4.6 RadioButton

Checkbox为复选框

代码语言:javascript复制
@Preview
@Composable
fun MyCheckbox() {
    var selected by remember { mutableStateOf(false) }

    Checkbox(
        checked = selected,
        onCheckedChange = { selected = it }
    )
}

效果:

4.7 ExtendedFloatingActionButton

ExtendedFloatingActionButton为悬浮按钮,控制expanded参数可以展开和缩小,此外还支持shape设置背景形状、elevation设置阴影:

代码语言:javascript复制
@Composable
fun ExtendedFloatingActionButton(
    text: @Composable () -> Unit,// 文字
    icon: @Composable () -> Unit,// 图标
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    expanded: Boolean = true,// 是否展开
    shape: Shape = FloatingActionButtonDefaults.extendedFabShape,//背景形状
    containerColor: Color = FloatingActionButtonDefaults.containerColor,//容器颜色
    contentColor: Color = contentColorFor(containerColor),//内容组件颜色
    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),//阴影
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
...
}

例子:

代码语言:javascript复制
@Preview
@Composable
fun MyExtendedFloatingActionButton() {
    var expanded by remember { mutableStateOf(false) }
    
    ExtendedFloatingActionButton(
        text = { Text(text = "点我") },
        icon = { /*TODO*/ },
        onClick = { expanded = !expanded },
        expanded = expanded,
        shape = RoundedCornerShape(30.dp)
    )
}

效果:

5.Spacer

Spacer表示间距,用来代表一片隔离区域,隔离组件与组件

代码语言:javascript复制
@Preview
@Composable
fun MySpacer() {
    Row {
        Text("hi")
        Spacer(modifier = Modifier.width(20.dp))
        Text("hi")
    }
}

预览效果:

6.Divider

Divider可以用来表示一条分割线,默认是一条横向的,所以通过Modifier来改变

代码语言:javascript复制
@Preview
@Composable
fun MyDivider() {
    Row() {
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
        Divider(
            color = Color.Blue,
            modifier = Modifier
                .fillMaxHeight()//充满整个组件
                .width(1.dp)//宽度为1dp
        )
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
    }
}

预览效果:

6.1 IntrinsicSize

从上面的预览效果可以知道,将Divider设置为最大高度后,MyDivider组件充满了整个屏幕,如果想到达到Divider的高度不计入MyDivider的高度,并随着MyDivider的高度进行填充,就需要用到IntrinsicSize

IntrinsicSize表示允许父组件优先查询下子组件的高度,所以设置给父组件,这边给Row设置Modifier

代码语言:javascript复制
@Preview
@Composable
fun MyDivider2() {
    Row(modifier = Modifier.height(IntrinsicSize.Min)) {//高度设置为IntrinsicSize
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
        Divider(
            color = Color.Red,
            modifier = Modifier
                .fillMaxHeight()//充满整个组件
                .width(1.dp)//宽度为1dp
        )
        Text(
            "hi",
            modifier = Modifier.weight(1f)
        )
    }
}

预览效果:

五、标准布局

compose中的布局也不多,最基础的为ColumnRowBox,官方给出的定义如下图:

1.Row

上面我们使用过一个Row,它的作用域是RowScope,同横向LinearLayout

代码语言:javascript复制
@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,// 内容组件水平排列方式
    verticalAlignment: Alignment.Vertical = Alignment.Top,//内容组件垂直对齐方式
    content: @Composable RowScope.() -> Unit// 内容组件
) {
    
}
1.1 Arrangement

关于Arrangement的几种方式,官方给出的图示:

1.2 基本使用
代码语言:javascript复制
@Preview
@Composable
fun MyRow() {
    Row(
        modifier = Modifier.width(100.dp),
        horizontalArrangement = Arrangement.End,//内容组件往右对齐
        verticalAlignment = Alignment.CenterVertically//内容组件垂直居中
    ) {
        Text("hi")
        Text("你好n 张三")
    }
}

预览效果:

2.Column

Column就是竖直方向摆放组件的布局,用法上和Row相同,同竖向LinearLayout

代码语言:javascript复制
@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,//内容组件垂直对齐方式
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,//内容组件水平对齐方式
    content: @Composable ColumnScope.() -> Unit//内容组件
) {
    
}
2.1 基本使用
代码语言:javascript复制
@Preview
@Composable
fun MyColumn() {
    Column(
        modifier = Modifier.height(100.dp),
        horizontalAlignment = Alignment.CenterHorizontally,//内容组件水平居中
        verticalArrangement = Arrangement.SpaceBetween//内容组件垂直分布到两侧
    ) {
        Text("hi")
        Text("你好n 张三")
    }
}

预览效果:

3.Box

Box类似FrameLayout,可以堆叠摆放子组件

代码语言:javascript复制
@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,//内容组件的对齐方式
    propagateMinConstraints: Boolean = false,//是否指定内容组件使用该组件的最小约束(最小宽高)
    content: @Composable BoxScope.() -> Unit
) {

}
3.1 基本使用

下面两个Image的宽高设定为40dp,由于Box设置了最小约束为50dp和70dp,所以Image变大了:

代码语言:javascript复制
@Preview
@Composable
fun MyBox() {
    Box(
        modifier = Modifier
            .sizeIn(50.dp, 70.dp),//设置内容组件的最小宽度和高度为50dp、70dp,配合propagateMinConstraint=true使用
        propagateMinConstraints = true,//使内容组件最小宽度和高度生效
        contentAlignment = Alignment.BottomEnd
    ) {
        // propagateMinConstraints,内部需要一个组件撑大整体的大小
        Box(Modifier.size(50.dp,150.dp).background(Color.Cyan))
        Image(
            modifier = Modifier.size(40.dp, 40.dp),
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            contentScale = ContentScale.FillHeight // 图片高度拉伸
        )
        Image(
            modifier = Modifier.size(40.dp, 40.dp),
            painter = painterResource(id = R.drawable.ic_launcher_foreground),
            contentDescription = null,
            contentScale = ContentScale.FillHeight // 图片高度拉伸
        )
    }
}

预览效果:

4.Scaffold

Scaffold预设了很多槽位(存放子组件)和功能,Scaffold的学习可以通过官网:Scaffold官方示例(有些参数只有MD2版本才有)

4.1 topBar

槽位topBar就是给顶部子组件准备的,如:TopAppBar

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {//标题
                    Text(
                        modifier = Modifier.padding(start = 10.dp),
                        text = "topBar"
                    )
                },
                navigationIcon = {//导航图标
                    Icon(Icons.Default.ArrowBack, contentDescription = null)
                },
                actions = {//右侧按钮
                    IconButton(onClick = { /*TODO*/ }) {
                        Icon(Icons.Filled.Favorite, contentDescription = null)
                    }
                    IconButton(onClick = { /*TODO*/ }) {
                        Icon(Icons.Filled.Search, contentDescription = null)
                    }
                },
                colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
            )
        }
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

预览效果:

4.2 bottomBar

topBar对应,bottomBar是用来存放底部子组件的槽位,如:BottomAppBarBottomNavigation

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
    Scaffold(
        topBar = {
           ...
        },
        bottomBar = {
            BottomAppBar(
                containerColor = MaterialTheme.colorScheme.primaryContainer,
                tonalElevation = 2.dp,
            ) {
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(Icons.Filled.Home, contentDescription = null)
                }
                Spacer(modifier = Modifier.weight(1f))
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(Icons.Filled.ShoppingCart, contentDescription = null)
                }
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(Icons.Filled.Info, contentDescription = null)
                }
            }
        }
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

4.3 floatingActionButton

floatingActionButton是专门为FloatingActionButton准备的槽位,配合floatingActionButtonPosition可以改变槽位的位置,目前只支持底部居中和底部靠右:

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold() {
    Scaffold(
        topBar = {
            ...
        },
        bottomBar = {
            ...
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { /*TODO*/ }) {
                Text(text = "hi")
            }
        },
        floatingActionButtonPosition = FabPosition.Center
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

4.4 snackbarHost

snackbarHost槽位用于展示一个提示SnackbarHost,需要通过SnackbarHostState来控制该子组件的显示:

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold2() {
    val scaffoldState by remember { mutableStateOf(SnackbarHostState()) }
    val scope = rememberCoroutineScope()

    Scaffold(
        topBar = {
            ...
        },
        bottomBar = {
            ...
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                scope.launch {
                    scaffoldState.showSnackbar("hi,this is snack bar")
                }
            }) {
                Text(text = "hi")
            }
        },
        snackbarHost = { SnackbarHost(hostState = scaffoldState) },
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

SnackbarHostState还支持显示的时长,相应的点击动作,基于协程返回消失或点击动作的结果:

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffold2() {
    val scaffoldState by remember { mutableStateOf(SnackbarHostState()) }
    val scope = rememberCoroutineScope()

    Scaffold(
        topBar = {
            ...
        },
        bottomBar = {
            ...
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                scope.launch {
                    val result = scaffoldState.showSnackbar(
                        message = "hi,this is snack bar",
                        duration = SnackbarDuration.Short,
                        actionLabel = "click"
                    )

                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                            scaffoldState.currentSnackbarData?.dismiss()
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }) {
                Text(text = "hi")
            }
        },
        snackbarHost = { SnackbarHost(hostState = scaffoldState) },
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

4.5 drawerContent

drawerContent是抽屉菜单的槽位,它是一个ColumnScope,注意目前MD3版本并不支持,所以要使用,需要使用MD2Scaffold

代码语言:javascript复制
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyModalNavigationDrawer() {
    val drawerState = rememberScaffoldState()
    val scope = rememberCoroutineScope()

    androidx.compose.material.Scaffold(
        topBar = {
            TopAppBar(
                title = {//标题
                    Text(
                        modifier = Modifier.padding(start = 10.dp),
                        text = "topBar"
                    )
                },
                navigationIcon = {//导航图标
                    Icon(
                        modifier = Modifier.clickable {
                            scope.launch {
                                drawerState.drawerState.apply {
                                    if (isClosed) open() else close()
                                }
                            }
                        },
                        imageVector = Icons.Default.List,
                        contentDescription = null
                    )
                },
                colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
            )
        },
        drawerContent = {
            Text("title")

            Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
                Image(Icons.Default.Phone, contentDescription = null)
                Text(text = "my phone")
            }
            Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
                Image(Icons.Default.Call, contentDescription = null)
                Text(text = "call")
            }
            Row(modifier = Modifier.padding(start = 10.dp,top = 10.dp)) {
                Image(Icons.Default.Delete, contentDescription = null)
                Text(text = "delete cache")
            }
        },
        scaffoldState = drawerState
    ) { paddings ->
        Box(modifier = Modifier.padding(paddings))
    }
}

效果:

5. ModalDrawer

ModalDrawer仅仅是抽屉栏,同样只在MD2中才有,需要DrawerState控制展开和收起:

代码语言:javascript复制
@Preview
@Composable
fun MyModalDrawer() {
    val drawerState =
        androidx.compose.material.rememberDrawerState(androidx.compose.material.DrawerValue.Closed)
    val scope = rememberCoroutineScope()

    ModalDrawer(
        drawerState = drawerState,
        drawerContent = {
            // Drawer content
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Text("hi")
            }
        }
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            // Screen content
            Button(onClick = {
                scope.launch {
                    drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }) {
                Text("点我展开抽屉")
            }
        }
    }
}

效果:

此外BottomDrawer代表底部的抽屉栏,用法上和ModalDrawer差不多

6.ModalBottomSheetLayout

ModalBottomSheetLayout是底部菜单,需要使用ModalBottomSheetState控制显示和消失:

代码语言:javascript复制
@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun MyModalBottomSheetLayout() {
    val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
    val scope = rememberCoroutineScope()

    ModalBottomSheetLayout(
        sheetState = sheetState,
        sheetContent = {
            // Sheet content
            Box(
                modifier = Modifier.height(400.dp),
                contentAlignment = Alignment.Center
            ) {
                Text("Sheet")
            }
        }
    ) {
        // Screen content
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Button(onClick = {
                scope.launch {
                    sheetState.apply {
                        if (isVisible) hide() else show()
                    }
                }
            }) {
                Text("点我展开")
            }
        }
    }
}

此外,BottomSheetScaffold代表带有底部sheetContent槽位的Scaffold,用法和Scaffold差不多

7.BackdropScaffold

BackdropScaffold官方的说法为背景幕,就是两个布局可以堆叠,并前面的布局可以下移隐藏,通过BackdropScaffoldState控制是否隐藏:

代码语言:javascript复制
@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun MyBackdropScaffold() {
    val scaffoldState = rememberBackdropScaffoldState(
        BackdropValue.Concealed
    )
    val scope = rememberCoroutineScope()

    BackdropScaffold(
        scaffoldState = scaffoldState,
        appBar = {
            // Top app bar
            androidx.compose.material.TopAppBar(
                title = {//标题
                    Text(
                        modifier = Modifier.padding(start = 10.dp),
                        text = "topBar"
                    )
                },
                navigationIcon = {//导航图标
                    Icon(
                        modifier = Modifier.clickable {
                            scope.launch {
                                scaffoldState.apply {
                                    if (isConcealed) reveal() else conceal()
                                }
                            }
                        },
                        imageVector = Icons.Default.List,
                        contentDescription = null
                    )
                }
            )
        },
        backLayerContent = {
            // Back layer content
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.White)
            ) {

            }
        },
        frontLayerContent = {
            // Front layer content
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Magenta)
            ) {

            }
        }
    )
}

效果:

0 人点赞