美文网首页
compose--初入compose、资源获取、标准控件与布局

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

作者: aruba | 来源:发表于2022-12-05 21:37 被阅读0次

    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

    工程中导入:

    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

    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的高度进行填充,就需要用到IntrinsicSize
    IntrinsicSize表示允许父组件优先查询下子组件的高度,所以设置给父组件,这边给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 抽屉栏菜单布局

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/nxzyfdtx.html