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

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:animation1.3.01.3.1
androidx.compose.animation:animation-core1.3.01.3.1
androidx.compose.animation:animation-graphics1.3.01.3.1
androidx.compose.foundation:foundation1.3.01.3.1
androidx.compose.foundation:foundation-layout1.3.01.3.1
androidx.compose.material:material1.3.01.3.1
androidx.compose.material:material-icons-core1.3.01.3.1
androidx.compose.material:material-icons-extended1.3.01.3.1
androidx.compose.material:material-ripple1.3.01.3.1
androidx.compose.material3:material31.0.01.0.1
androidx.compose.material3:material3-window-size-class1.0.01.0.1
androidx.compose.runtime:runtime1.3.01.3.1
androidx.compose.runtime:runtime-livedata1.3.01.3.1
androidx.compose.runtime:runtime-rxjava21.3.01.3.1
androidx.compose.runtime:runtime-rxjava31.3.01.3.1
androidx.compose.runtime:runtime-saveable1.3.01.3.1
androidx.compose.ui:ui1.3.01.3.1
androidx.compose.ui:ui-geometry1.3.01.3.1
androidx.compose.ui:ui-graphics1.3.01.3.1
androidx.compose.ui:ui-test1.3.01.3.1
androidx.compose.ui:ui-test-junit41.3.01.3.1
androidx.compose.ui:ui-test-manifest1.3.01.3.1
androidx.compose.ui:ui-text1.3.01.3.1
androidx.compose.ui:ui-text-google-fonts1.3.01.3.1
androidx.compose.ui:ui-tooling1.3.01.3.1
androidx.compose.ui:ui-tooling-data1.3.01.3.1
androidx.compose.ui:ui-tooling-preview1.3.01.3.1
androidx.compose.ui:ui-unit1.3.01.3.1
androidx.compose.ui:ui-util1.3.01.3.1
androidx.compose.ui:ui-viewbinding1.3.01.3.1

工程中导入:

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-alpha011.7.20
1.3.21.7.20
1.3.11.7.10
1.3.01.7.10
1.3.0-rc021.7.10
1.3.0-rc011.7.10
1.3.0-beta011.7.10
1.2.01.7.0
1.2.0-rc021.6.21
1.2.0-rc011.6.21
1.2.0-beta031.6.21
1.2.0-beta021.6.21
1.2.0-beta011.6.21
1.2.0-alpha081.6.20
1.2.0-alpha071.6.10
1.2.0-alpha061.6.10
1.2.0-alpha051.6.10
1.2.0-alpha041.6.10
1.2.0-alpha031.6.10
1.2.0-alpha021.6.10
1.2.0-alpha011.6.10
1.1.11.6.10
1.1.01.6.10
1.1.0-rc031.6.10
1.1.0-rc021.6.10
1.1.0-rc011.6.0
1.1.0-beta041.6.0
1.1.0-beta031.5.31
1.1.0-beta021.5.31
1.1.0-beta011.5.31
1.1.0-alpha061.5.31
1.1.0-alpha051.5.31
1.0.51.5.31
1.0.41.5.31
1.1.0-alpha041.5.30
1.1.0-alpha031.5.30
1.0.31.5.30
1.1.0-alpha021.5.21
1.1.0-alpha011.5.21
1.0.21.5.21
1.0.11.5.21
1.0.01.5.10
1.0.0-rc021.5.10
1.0.0-rc011.5.10

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

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.1"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

主工程中build.gradle:

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注解,并且每个可组合函数都是可以重用的组件:

@Composable
@Preview
fun MyText() {
    Text(text = "hello world!")
}

预览效果:

1.2 使用资源获取文本

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

@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:设置文本的行高,对齐方式,文字方向和文字缩进样式

例子:

@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("hi\n")
                }
            }

            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 其他参数

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

@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去除首行的顶部行间距:

<string name="hello">hello!\nworld</string>
@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找到该组件:

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

预览效果:

2.2 其他参数

相较于TextImage的参数少很多:

@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例子,使用SrcIn模式合成一个红色:

@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例子,颜色增强:

@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例子,添加红色向量:

@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对象,不支持模式,只支持染色:

@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进行改变,才能够进行组件的刷新

@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指定光标位置:

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

例子:

@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 其他参数
@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()// 颜色集,通过设置相应的颜色,可以改变如错误发生时的颜色
) {
...
}

例子:

@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相同

@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(点击跳转详情),其他参数说明如下:

@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的作用域,也就是以行来摆放组件

例子:

@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相同:

@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相同:

@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"
            )
        }
    )
}

效果:

4.4 Switch

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

@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为单选框

@Preview
@Composable
fun MyRadioButton() {
    var selected by remember { mutableStateOf(false) }

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

效果:

4.6 Checkbox

Checkbox为复选框

@Preview
@Composable
fun MyCheckbox() {
    var selected by remember { mutableStateOf(false) }

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

效果:

4.7 ExtendedFloatingActionButton

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

@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() },
) {
...
}

例子:

@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表示间距,用来代表一片隔离区域,隔离组件与组件

@Preview
@Composable
fun MySpacer() {
    Row {
        Text("hi")
        Spacer(modifier = Modifier.width(20.dp))
        Text("hi")
    }
}

预览效果:

6.Divider

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

@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的高度进行填充,就需要用到IntrinsicSizeIntrinsicSize表示允许父组件优先查询下子组件的高度,所以设置给父组件,这边给Row设置Modifier

@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

@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 基本使用
@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

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

预览效果:

3.Box

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

@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变大了:

@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

@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

@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可以改变槽位的位置,目前只支持底部居中和底部靠右:

@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来控制该子组件的显示:

@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还支持显示的时长,相应的点击动作,基于协程返回消失或点击动作的结果:

@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 MD2-drawerContent

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

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyScaffoldDrawer() {
    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. MD2-ModalDrawer

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

@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.MD2-ModalBottomSheetLayout

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

@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.MD2-BackdropScaffold

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

@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)
            ) {

            }
        }
    )
}

效果:

8.MD3-ModalNavigationDrawer

ModalNavigationDrawer是MD3中的抽屉栏,配合ModalDrawerSheet组件,可以达到抽屉栏列表MD3风格的样式:

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun MyPermanentNavigationDrawer() {
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()

    ModalNavigationDrawer(
        drawerState = drawerState,
        modifier = Modifier.fillMaxHeight(),
        drawerContent = {
            ModalDrawerSheet() {
                NavigationRailItem(
                    selected = true,
                    onClick = {},
                    icon = { Icon(Icons.Default.Home, contentDescription = null) },
                    label = { Text("home") }
                )
                NavigationRailItem(
                    selected = false,
                    onClick = {},
                    icon = { Icon(Icons.Default.Info, contentDescription = null) },
                    label = { Text("info") }
                )
                NavigationRailItem(
                    selected = false,
                    onClick = {},
                    icon = { Icon(Icons.Default.Call, contentDescription = null) },
                    label = { Text("call") }
                )
            }
        },
    ) {
        Scaffold(
            topBar = {
                CenterAlignedTopAppBar(
                    title = { Text("CenterAlignedTopAppBar") },
                    navigationIcon = {
                        Icon(
                            modifier = Modifier.clickable {
                                scope.launch {
                                    drawerState.apply {
                                        if (isClosed) open() else close()
                                    }
                                }
                            },
                            imageVector = Icons.Default.List,
                            contentDescription = null
                        )
                    },
                    actions = { /* App bar actions */ })
            },
        ) { paddings ->
            Box(modifier = Modifier.padding(paddings))
        }
    }
}

效果:

六、总结

最后总结下这篇文章的各个组件的作用,当然了compose中还有其他的组件,以及后续会出更多的新组件,目前也介绍了大部分组件的使用:

组件分类备注
Text文本
TextField文本输入
OutlinedTextField文本输入带边框
Image图片
Icon图标渲染方式比Image少
Button按钮
IconButton图标按钮
IconToggleButton选中状态图标按钮通过State切换是否选中
Switch开关样式图标按钮通过State切换是否选中
RadioButton单选按钮通过State切换是否选中
Checkbox复选按钮通过State切换是否选中
FloatingActionButton悬浮按钮
ExtendedFloatingActionButton可展开悬浮按钮通过State切换是否展开
SnackbarHost提示消息通过SnackbarHostState是否显示
Spacer间距
Divider分割线
Row横向布局
Column纵向布局
Box堆叠布局
Scaffold槽位布局通过ScaffoldState切换是否展开抽屉栏
TopAppBar标题导航栏
CenterAlignedTopAppBar标题居中导航栏
BottomAppBar底部导航栏
BottomNavigation底部导航栏
ModalDrawer抽屉栏通过DrawerState切换是否展开抽屉栏
ModalBottomSheetLayout底部抽屉菜单栏通过ModalBottomSheetState切换是否显示菜单栏
BackdropScaffold背景幕通过BackdropScaffoldState切换是否前幕布下移
ModalNavigationDrawer抽屉栏通过DrawerState切换是否展开抽屉栏
ModalDrawerSheet抽屉栏菜单布局
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值