Android全新UI框架之常用ComposeUI组件

在Compose中,每个组件都是一个带有@Composable注解的函数,被称为Composable。Compose已经预置了很多基于MD设计规范的Composable组件。

在布局方面,Compose提供了Column、Row、Box三种布局组件(感觉跟flutter差不多),类似于传统视图开发中的LinearLayout(Vertical)、LinearLayout(Horizontal)、RelateiveLayout,可以满足各类产品的常见布局需求。

Modifier修饰符

在传统开发中,使用xml文件来描述组件的样式,而compose则是使用Modifier修饰符。Modifier允许我们通过链式调用的方式为组件应用一系列的样式设置,如边距、字体、位移等。在Compose中,每个基础的Composable组件都有一个modifier参数,通过传入自定义的Modifier来修改组件的样式。

  • 常见修饰符

    Modifier.size 设置被修饰组件的大小 Image( painter = painterResource(id = R.drawable.ic_launcher),contentDescription = null, modifier = Modifier.size(width = 10.dp, height = 10.dp) )
    Modifier.background 设置被修饰组件的背景颜色。背景色支持纯色背景,也可以使用brush设置渐变背景 Image(painter = painterResource(id = R.drawable.ic_launcher),contentDescription = null,modifier = Modifier.size(width = 100.dp, height = 100.dp).background(brush = Brush.verticalGradient(colors = listOf(Color.Blue,Color.Red)))))
    Modifier.fillMaxXXX() 让组件在高度或者宽度上填满父空间 fillMaxSize():填满整个父空间, fillMaxHeight():高度填满父空间,fillMaxWidth():宽度填满父空间
    Modifier.border&Modifier.padding border用来为被修饰组件添加边框。边框可以指定颜色、粗细、以及通过shape指定形状。padding用来为被修饰组件增加间隙。可以在border前后各插入一个padding,区分对外和对内的间距 Box(modifier = Modifier.padding(5.dp).border(2.dp, Color.Green, shape = RoundedCornerShape(2.dp)).padding(5.dp)) { Spacer(modifier = Modifier.size(width = 100.dp, height = 10.dp).background(Color.Blue)) }
    Modifier.offset 用来移动被修饰组件的位置,分别传入水平方向和垂直方向的偏移量即可 Box(modifier = Modifier.size(100.dp).background(Color.Blue).offset(x = 10.dp, y = 10.dp).background(Color.Cyan)) {}
  • 作用域限定Modifier修饰符

    Compose充分发挥kotlin的语法特性,让某些Modifier修饰符只能在特定作用域中使用,有利于类型安全地调用它们。所谓"作用域",在kotlin中就是一个带有Receiver的代码块。注意Receiver代码块默认可以跨层级访问。在compose的DSL中,一般只需要调用当前作用域的方法,为此可以通过@LayoutScopeMarker注解来规避该问题。常见组件的Receiver作用域类型均已使用@LayoutScopeMarker注解进行了声明,使用了该注解之后,像跨级调用外层作用域的方法必须通过显式指明Receiver具体类型。

    @LayoutScopeMarker
    @Immutable
    interface ColumnScope
    

    常见的作用域限定Modifier修饰符:

    1. matchParentSize

      matchParentSize是只能在BoxScope中使用的作用域限定修饰符。当使用matchParentSize设置尺寸时,可以保证当前组件的尺寸与父组件相同。而父组件默认的是wrapContent。如果使用fillMaxSize取代matchParentSize,那么该组件的尺寸会被设置为父组件所允许的最大尺寸,这样会导致背景铺满整个屏幕,二者区别如下图所示。

          Column(modifier = Modifier.background(Color.DarkGray)) {
              Box() {
                  Box(modifier = Modifier
      //                .fillMaxSize()
                      .matchParentSize()
                      .background(Color.Cyan))
                  Spacer(modifier = Modifier.size(50.dp).padding(10.dp).background(Color.Red))
              }
          }
      

      matchParentSize效果如下所示:

      fillMaxSize效果如下所示:

    2. weight

      在RowScope和ColumnScope中,可以使用专属的weight修饰符来设置尺寸。与size不同的是weight允许组件通过百分比设置尺寸,也就是允许组件可以自适应适配各种屏幕尺寸的移动端设备。

          Column(modifier = Modifier
              .background(Color.DarkGray)
              .width(300.dp)
              .height(200.dp)) {
            Spacer(modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .background(Color.White))
              Spacer(modifier = Modifier
                  .fillMaxWidth()
                  .weight(1f)
                  .background(Color.Red))
              Spacer(modifier = Modifier.fillMaxWidth().weight(1f).background(Color.Blue))
          }
      
  • Modifier实现原理

    各位读者可以参考下面这篇博客:
    图解Compose Modifier实现原理 ,竟然如此简单!

常用的基础组件

文字组件
  1. Text文本
    在Compose中,Text是遵循MD设计规范的上层文本组件,如果想脱离MD,也可以直接使用更底层的文本组件BasicText。

    kotlin 复制代码
    @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
    )

    在很多应用场景中,我们需要在一段文字中对局部内容应用特别格式以示突出,比如一个超链接,此时需要用到AnnotatedString。AnnotatedString是一个数据类,除了文本值,还包含一个SpanStyle和ParagraphStyle的Range列表。SpanStyle用于描述在文本中子串的文字样式,ParagraphStyle则用于描述文本中子串的段落样式,Range确定子串的范围。
    用法如下所示:

    kotlin 复制代码
    //append用来添加子串的文本
    //withStyle为append的子串指定文字或段落样式
    Text(text = buildAnnotatedString {
        withStyle(style = SpanStyle(fontSize = 24.sp)){
             append("你现在学习的章节是")
         }
         withStyle(style = SpanStyle(fontSize = 24.sp, fontWeight = FontWeight.W900)){
             append("Text")
         }
         append("\n")
         withStyle(style = ParagraphStyle(lineHeight = 25.sp)){
             append("在刚刚讲过的内容中,我们学会了如何应用文字样式,以及如何限制文本的行数和处理溢出的视觉效果。")
             append("\n")
             append("现在,我们正在学习")
             withStyle(style = SpanStyle(fontWeight = FontWeight.W900, textDecoration = TextDecoration.Underline, color = Color(0xff59a869))){
                 append("AnnotatedString")
             }
         }
     })

    SpanStyle继承了TextStyle中关于文字样式相关的字段,而ParagraphStyle继承了TextStyle中控制段落的样式,例如textAlign、lineHeight等。SpanStyle和ParagraphStyle中的设置优先于TextStyle中同名属性设置。

  • SelectionContainer选中文字

    Text自身默认是不能被长按选择的,Compose提供了专门的SelectionContainer组件,对包裹的Text进行选中。

    kotlin 复制代码
    SelectionContainer() {
        Text(text = "我是可以被复制的文字")
    }
  • ClickableText

    Compose提供了一种可点击文本组件,可以响应对文字的点击,并返回点击位置。可以让AnnotatedString子串在相应的ClickedText中点击后,做出不同的动作。在AnnotatedString中可以为子串添加一个tag标签,在处理onClick事件时,根据tag实现不同的逻辑。

    kotlin 复制代码
     ClickableText(text = annotated, onClick ={
                        annotated.getStringAnnotations(
                            tag = "URL",
                            start = it,
                            end = it
                        ).firstOrNull()?.let {
                            Log.e("MainActivity",it.toString())
                        }
                    } )
     
     val annotated = buildAnnotatedString {
            withStyle(style = ParagraphStyle(lineHeight = 25.sp)){
                // 开始一个注解区域
                pushStringAnnotation(tag = "URL", annotation = "https://www.baidu.com")
                // 追加带有注解的文本
                withStyle(
                    style = SpanStyle(
                        fontWeight = FontWeight.W900,
                        textDecoration = TextDecoration.Underline,
                        color = Color(0xff59a869)
                    )
                ){
                    append("AnnotatedString")
                }
                // 结束注解区域
                pop()
                //下面这段文本不会有注解
                withStyle(
                    style = SpanStyle(
                        fontWeight = FontWeight.W900,
                        textDecoration = TextDecoration.Underline,
                        color = Color(0xff59a869)
                    )
                ){
                    append("NotAnnotatedString")
                }
            }
        }
  • TextField输入框

    TextField组件是我们最常用的文本输入框,它也遵循MD设计准则。它也有一个低级别的底层组件,BasicTextField,与TextField和OutlinedTextField不同的是,BasicTextField拥有更多的自定义效果。由于TextField和OutlinedTextField是根据MD准则设计的,无法直接修改输入框的高度,如果尝试修改高度,会看到输入区域被截断,影响正常输入。

    kotlin 复制代码
    fun TextField(
        value: TextFieldValue,
        onValueChange: (TextFieldValue) -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        readOnly: Boolean = false,
        textStyle: TextStyle = LocalTextStyle.current,
        label: @Composable (() -> Unit)? = null,
        placeholder: @Composable (() -> Unit)? = null,
        leadingIcon: @Composable (() -> Unit)? = null,
        trailingIcon: @Composable (() -> Unit)? = null,
        supportingText: @Composable (() -> Unit)? = null,
        isError: Boolean = false,
        visualTransformation: VisualTransformation = VisualTransformation.None,
        keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
        keyboardActions: KeyboardActions = KeyboardActions.Default,
        singleLine: Boolean = false,
        maxLines: Int = Int.MAX_VALUE,
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
        shape: Shape = TextFieldDefaults.filledShape,
        colors: TextFieldColors = TextFieldDefaults.textFieldColors()
    )
    fun BasicTextField(
        value: String,
        onValueChange: (String) -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        readOnly: Boolean = false,
        textStyle: TextStyle = TextStyle.Default,
        keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
        keyboardActions: KeyboardActions = KeyboardActions.Default,
        singleLine: Boolean = false,
        maxLines: Int = Int.MAX_VALUE,
        visualTransformation: VisualTransformation = VisualTransformation.None,
        onTextLayout: (TextLayoutResult) -> Unit = {},
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
        cursorBrush: Brush = SolidColor(Color.Black),
        decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
            @Composable { innerTextField -> innerTextField() }
    ) 

    TextField输入框案例如下:

    kotlin 复制代码
    @Composable
        fun TextFieldSample(){
            var text by remember {
                mutableStateOf("")
            }
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color(0xfd3d3d3)),
                contentAlignment = Alignment.Center
            ) {
                BasicTextField(
                    modifier = Modifier
                        .padding(10.dp)
                        .background(Color.White, CircleShape)
                        .height(30.dp)
                        .fillMaxSize(),
                    value = text,
                    onValueChange = {text = it},
                    decorationBox = {
                        Row(
                            verticalAlignment = Alignment.CenterVertically,
                            modifier = Modifier.padding(vertical = 2.dp, horizontal = 8.dp)
                        ) {
                            Icon(imageVector = Icons.Filled.Search, contentDescription = null)
                            Box(modifier = Modifier
                                .padding(horizontal = 10.dp)
                                .width(100.dp), contentAlignment = Alignment.CenterStart) {
                                if(text.isEmpty()){
                                    Text(
                                        text = "输入点东西看看吧~",
                                        style = TextStyle(
                                            color = Color(0,0,0,128)
                                        )
                                    )
                                }
                                it()
                            }
                           Box(contentAlignment = Alignment.CenterEnd){
                               if(text.isNotEmpty()){
                                   IconButton(
                                       onClick = { text="" },
                                       modifier = Modifier.size(16.dp)) {
                                       Icon(imageVector = Icons.Filled.Close, contentDescription = null, tint = Color.Red)
                                   }
                               }
                           }
                        }
                    })
            }
    
        }
  • 图片组件

    1. Icon图标
      Icon组件用于显示一系列小图标。Icon组件支持三种不同类型的图片设置:
    kotlin 复制代码
    @Composable
    fun Icon(
        imageVector: ImageVector,//矢量图对象,可以显示SVG格式的图标
        contentDescription: String?,
        modifier: Modifier = Modifier,
        tint: Color = LocalContentColor.current
    )
    @Composable
    fun Icon(
        bitmap: ImageBitmap,//位图对象,可以显示JPG、PNG等格式的图标
        contentDescription: String?,
        modifier: Modifier = Modifier,
        tint: Color = LocalContentColor.current
    )
    @Composable
    fun Icon(
        painter: Painter,//代表一个自定义画笔,可以使用画笔在Canvas上直接绘制图标
        contentDescription: String?,
        modifier: Modifier = Modifier,
        tint: Color = LocalContentColor.current
    )
    1. Image图片
      Image组件用来显示一张图片。它和Icon一样也支持三种类型的图片设置。
  • 按钮组件

    1. Button按钮
      Button也是最常用的组件之一,它也是按照MD风格来实现的。Button并非唯一可点击组件,理论上任何Composable组件都可以通过Modifier.clickable修饰符化身为可点击组件。
      案例如下:
    kotlin 复制代码
        @Composable
        fun ButtonSample(){
            val interactionSource = remember {
                MutableInteractionSource()
            }
            val pressState = interactionSource.collectIsPressedAsState()
            val borderColor = if (pressState.value) Color.Green else Color.White
            Button(
             modifier = Modifier.clickable {
             //该方法在button组件会失效
                Log.e("MainActivity","点击Butttonclickable")
            },
                interactionSource = interactionSource,
                border = BorderStroke(2.dp, color = borderColor),
                onClick = {
                    Log.e("MainActivity","点击Buttton")
                }) {
                Icon(imageVector = Icons.Filled.Done, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
                Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
                Text(text = "确认")
            }
        }
    1. IconButton图标按钮
      IconButton组件实际上只是Button组件的简单封装(一个可点击的图标),它一般用于应用栏中的导航或者其他行为。一般来说,需要在IconButton组件里提供一个图标组件,这个图标的默认尺寸一般为24*24dp。
    kotlin 复制代码
     IconButton(
        onClick = { text="" },
        modifier = Modifier.size(16.dp)) {
        Icon(imageVector = Icons.Filled.Close, contentDescription = null, tint = Color.Red)
    1. FloatingActionButton悬浮按钮
    kotlin 复制代码
        @Composable
        fun FloatButton(){
            FloatingActionButton(
                onClick = {}) {
                Row() {
                    Text(text = "添加到我的喜欢")
                    Icon(imageVector = Icons.Filled.Done, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
                }
            }
            ExtendedFloatingActionButton(
                onClick = { /*TODO*/ }) {
                Row() {
                    Text(text = "添加到我的喜欢")
                    Icon(imageVector = Icons.Filled.Done, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
                }
            }
        }
  • 选择器

    1. CheckBox复选框
    kotlin 复制代码
        @Composable
        fun ChecBoxDemo(){
            val checkedState = remember {
                mutableStateOf(true)
            }
            Checkbox(
                checked = checkedState.value,
                onCheckedChange ={checkedState.value = it},
            colors = CheckboxDefaults.colors(checkedColor =  Color(0xff0079d3)))
        }
    1. TriStateCheckBox三态选择框
      很多时候,我们的复选框会有很多个,并且希望能够统一选择或者取消,这个时候就可以使用三态选择框
    kotlin 复制代码
     @Composable
        fun ChecBoxDemo(){
            val (state,onStateChange) = remember { mutableStateOf(true) }
            val (state2,onStateChange2) = remember { mutableStateOf(true) }
    
            val parentState = remember(state,state2) {
                if (state&&state2) ToggleableState.On
                else if (!state&&!state2) ToggleableState.Off
                else ToggleableState.Indeterminate
            }
    
            val onParentClick = {
                val s = parentState != ToggleableState.On
                onStateChange(s)
                onStateChange2(s)
            }
    
            TriStateCheckbox(
                colors = CheckboxDefaults.colors(checkedColor = Color.Red),
                state = parentState,
                onClick = onParentClick,
            )
    
            Column(Modifier.padding(10.dp,0.dp,0.dp,0.dp)) {
                Checkbox(checked = state, onCheckedChange = onStateChange )
                Checkbox(checked = state2, onCheckedChange = onStateChange2 )
    
            }
        }
    1. Switch单选开关------控制单个项目的开启或关闭状态
    kotlin 复制代码
      @Composable
        fun ChecBoxDemo(){
            val checkedState = remember {
                mutableStateOf(true)
            }
            Switch(checked = checkedState.value, onCheckedChange = {checkedState.value = it})
        }
    1. Slider滑竿组件------可用来做音量、亮度之类的数值调整或进度条
    kotlin 复制代码
     var sliderPosition by remember {
                mutableStateOf(0f)
            }
            Slider(value = sliderPosition, onValueChange = {sliderPosition = it})
    1. 进度条
    kotlin 复制代码
      var progress by remember {
                        mutableStateOf(0.1f)
                    }
    
                    val animatedProgress by animateFloatAsState(targetValue = progress, animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec)
    
                    Column {
                        CircularProgressIndicator(progress = animatedProgress)
                        Spacer(modifier = Modifier.requiredHeight(30.dp))
                        OutlinedButton(onClick = {
                            if(progress<1f)progress +=.1f
                        }) {
                            Text(text = "增加进度")
                        }
    
                    }
    1. 对话框
    kotlin 复制代码
        @Composable
        fun DialogDemo(){
            val openDialog = remember {
                mutableStateOf(true)
            }
            val dialogWidth = 200.dp
            val dialogHeight = 50.dp
            if(openDialog.value){
                Dialog(
                    onDismissRequest = { openDialog.value = false },
                properties = DialogProperties(
                    dismissOnBackPress = true,//允许通过点击物理返回键取消对话框
                    dismissOnClickOutside = true//允许点击对话框外部取消对话框
                )
                ) {
                    Box(modifier = Modifier
                        .size(dialogWidth, dialogHeight)
                        .background(Color.Blue))
                }
            }
        }

常用的布局组件

  • 线性布局

    线性布局对应于传统视图中的linearLayout,Compose根据orientation的不同又分为Column和Row。

    1. Column
      Column是一个垂直线性布局组件,它能够将子项按照从上到下的顺序垂直排列。verticalArrangement和horizontalAlignment参数分别可以帮助我们安排子项的垂直/水平位置,在默认的情况下,子项会以垂直方向上靠上,水平方向上靠左来布置。
    2. Row
      Row组件能够将内部子项按照从左到右的方向水平排列。用法Column类似。
  • 帧布局

    1. Box
      Box组件是一个能够将里面的子项依次按照顺序堆叠的布局组件,在使用上类似于传统视图中的Framelayout。
    2. Surface
      Surface是一个平面,在MD设计准则中也同样如此,我们可以将很多的组件摆放在这个平面之上,可以设置这个平面的边框、圆角、颜色等。
  • Spacer留白

    在很多时候,需要让两个组件之间留有空白的间隔,这个时候就可以使用该组件。其实当Box组件没有content时,完全可以用Spacer替换。可以给Spacer做如下封装,可以更方便地用在水平或垂直布局中。

    kotlin 复制代码
        @Composable
        fun WidthSpacer(value:Dp){
            Spacer(modifier = Modifier.width(value))
        }
    
        @Composable
        fun HeightSpacer(value:Dp){
            Spacer(modifier = Modifier.height(value))
        }
  • ConstraintLayout约束布局

    各位读者可以参考下面这篇博客:
    Compose ConstraintLayout 详讲

  • Scaffold脚手架

    Scaffold组件实现了MD的布局结构,通过配合其他MD组件可以轻松构建MD风格的页面。

    kotlin 复制代码
        @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
        @OptIn(ExperimentalMaterial3Api::class)
        @Composable
        fun Sample(){
            var selectedItem by remember {
                mutableStateOf(0)
            }
            val items = listOf(
                Item("主页",R.drawable.ic_launcher),
                Item("列表",R.drawable.ic_launcher),
                Item("设置",R.drawable.ic_launcher),
            )
    
            Scaffold(
    
                topBar ={
                   TopAppBar(
                       title = { Text(text = "主页")},
                       navigationIcon = {
                           IconButton(onClick = { /*TODO*/ }) {
                               Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
                           }
                   })
                }){
                Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center){
                    Text(text = "主页面")
                }
            }
        }

列表

很多产品中都有展示一组数据的需求场景,如果数据数量是可以枚举的,则仅需通过Column组件来枚举列出。然而很多时候,列表中的项目会非常多,我们需要滑动列表来查看所有内容,可以通过给Column的modifier添加verticalScroll方法来让列表实现滑动。

kotlin 复制代码
    @Composable
    fun ListDemo(){
        val initialCapacity = 1000 // 设置你想要的初始容量
        val items = ArrayList<String>(initialCapacity)
        for (i in 1..initialCapacity) {
            items.add("我是text$i")
        }
        Column(
            modifier = Modifier
                .verticalScroll(state = ScrollState(5), enabled = true)
        ) {
            items.forEach{
                Text(text = "$it")
            }

        }
    }

给Column的Modifier添加verticalScroll方法可以让列表实现滑动。但是如果列表过长,众多的内容会占用大量的内存。然而更多的内容对于用户其实都是不可见的,没有必要加载到内存。所以compose提供了专门用于处理长列表的组件,LazyColumn和LazyRow,其作用类似于传统视图中的Listview和RecyclerView。

kotlin 复制代码
  LazyColumn(
    modifier = Modifier
            .fillMaxSize()
            .background(Color.Cyan),
        contentPadding = PaddingValues(30.dp),//为内容设置外边距
    verticalArrangement = Arrangement.spacedBy(10.dp)//为每个item设置间隔
    ) {
        items(1000){
            Text(text = "$it")
        }

    }
相关推荐
氤氲息2 小时前
Android 底部tab,使用recycleview实现
android
Clockwiseee3 小时前
PHP之伪协议
android·开发语言·php
小林爱3 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
小何开发4 小时前
Android Studio 安装教程
android·ide·android studio
开发者阿伟4 小时前
Android Jetpack LiveData源码解析
android·android jetpack
weixin_438150994 小时前
广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
android·单片机
CheungChunChiu5 小时前
Android10 rk3399 以太网接入流程分析
android·framework·以太网·eth·net·netd
木头没有瓜5 小时前
ruoyi 请求参数类型不匹配,参数[giftId]要求类型为:‘java.lang.Long‘,但输入值为:‘orderGiftUnionList
android·java·okhttp
键盘侠0075 小时前
springboot 上传图片 转存成webp
android·spring boot·okhttp
江上清风山间明月6 小时前
flutter bottomSheet 控件详解
android·flutter·底部导航·bottomsheet