Compose编程思想 -- 初识Compose

前言

相较于传统的XML布局构建方式,Compose旨在使用更少的代码实现一种新型的布局方式,为了迎合Google官方的首选开发语言Kotlin,Compose只能使用Kotlin语言开发,从《Compose编程思想》这个专题开始,借助Google官方开发文档,带领大家从Compose入门到高手的进阶。

1 Compose入门

1.1 声明式UI

如果对flutter有了解,或者有开发经验的伙伴,对于声明式UI可能就非常熟悉,它的核心思想在于使用代码(Kotlin或者Dart)实现布局细节,而且不需要手动更新UI,什么意思呢?

先从传统的Java实现方式,布局文件是在xml布局文件中声明,如下:

xml 复制代码
<TextView
    android:id="@+id/tv_ui"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="UI绘制"/>

通过android:text属性可以预先设置文本展示,也可以通过findViewById的方式获取TextView实例对象,通过动态设置文案的方式刷新页面。

java 复制代码
val view = findViewById<TextView>(R.id.tv_ui)
view.text = "动态修改";

而声明式UI(以Compose为例)则是通过可组合函数构建UI,如下:

kotlin 复制代码
@Composable
fun showText() {
    val name by remember { mutableStateOf("Alex") }
    Text(text = name)
}

例如我们要展示文本文案,那么可以使用Text组件,类似于Java中的TextView,例如name设置为Text需要展示的文案,当name的值发生变化的时候,Text展示的文案也会发生变化,但是这个变化是因为值发生变化从而导致组件自动刷新,而不是Java中手动设置值(即主动调用setText方法)刷新。

1.2 Compose组件思想

一个app基本的元素,就是文本和图片,在Compose中对应的组件为TextImage,为什么不用TextView或者ImageView这种我们常见的Android平台中的控件命名方式?

其实这里就涉及到了Compose组件的思想 - 独立于Android平台 。独立于Android平台并不意味着脱离了Android平台,像Text最终底层还是调用了Android原生的drawTextdrawTextRun函数,但是并没有使用任何Android中的组件,这样做的好处就是能够实现多平台(multi-platform)。

多平台,意味着可以在桌面版(Windows、linux)、WEB、IOS中使用同一套代码,只需要区分平台即可。

因此Compose开始对所有的组件起新的名字,Text对应TextViewImage对应ImageViewRecyclerView对应LazyColumn等等,与传统的Android组件并没有任何的关系,但是底层依然采用了Android原生的API,为什么Compose还要使用Android原生的API,是因为Compose需要跟原生的View交互,因为它绕不开原生。

如果不想跟原生的View有一丝的牵连,那么就是Flutter了,它是直接在NDK的层面与Skia渲染器打交道了,既然深入了底层,自然跟Android原生组件没有任何关系了。

所以总结一下,Compose组件的思想就是绕开了原生的Android组件,直接调用Android底层API渲染,并没有完全脱离原生。

1.3 Compose中那些原生布局的平替

在Android的原生布局中,常用的有约束布局(ConstrainLayout)、线性布局(LinearLayout)、帧布局(FrameLayout)、ScrollView、RecyclerView、ViewPager。

那么在Compose中对应的组件是什么呢?

  • 帧布局 - Box
kotlin 复制代码
@Composable
fun showLayout() {

    Box {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            modifier = Modifier.size(100.dp)
        )
        Text(text = "Alex")
    }
}
  • 线性布局 - Column / Row
kotlin 复制代码
@Composable
fun showLayout() {

    Column {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            modifier = Modifier.size(100.dp)
        )
        Text(text = "Alex")
    }
}

Columnvertical属性;Rowhorizontal属性。

  • 相对布局 - Box

这里需要提一点的就是,相对布局在传统UI中用于定位子View的相对位置,以及View之间的位置关系,在Compose中依然可以使用Box作为平替,而位置关系则是使用Modifier来完成对应关系。

kotlin 复制代码
@Composable
fun textRelativeLayout() {

    Box {

        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            modifier = Modifier.size(100.dp)
        )

        Text(
            text = "Alex",
            modifier = Modifier.align(Alignment.Center)
        )
    }

}

例如,使用Alignment.Center值就可以让Text显示在父容器的中心位置。

  • ConstraintLayout

在Compose中,拥有与ConstraintLayout同名的组件,用于处理比RelativeLayout更多的功能,这里不做赘述,会有专题介绍如何在Compose中使用ConstraintLayout。

  • RecyclerView - LazyColumn
kotlin 复制代码
@Composable
fun testRecyclerView() {

    val datas = mutableListOf("A", "B", "C", "D")
    LazyColumn {
        items(datas) { item ->
            Box(modifier = Modifier.size(50.dp)) {
                Text(text = item,
                    modifier = Modifier.align(Alignment.Center))
            }
        }
    }
}

LazyColumn需要一个动态的列表数据,添加到items中,就可以展示全部的列表数据,不需要AdapterViewHolderLayoutManager

而且当我们需要给列表加上head或者foot的时候,如果使用RecyclerView,那么就需要在数据项的第0个位置和最后一个位置加上view,那么使用LazyColumn则不需要,直接通过item就可以添加一个ItemView。

kotlin 复制代码
@Composable
fun testRecyclerView() {

    val datas = mutableListOf("A", "B", "C", "D")
    LazyColumn {
        // 添加一个头部
        item {
            Text(text = "这是列表的头部")
        }
        items(datas) { item ->
            Box(modifier = Modifier.size(50.dp)) {
                Text(text = item,
                    modifier = Modifier.align(Alignment.Center))
            }
        }
    }
}

当然除了LazyColumn,还有LazyRow,属于横向的滑动。除此之外,LazyColumn/LazyRow也有类似于RecyclerView的缓存机制,保证列表的顺畅滑动。

除此之外,像ScrollerView、ViewPager,在Compose中平替的组件还在孵化中,像ViewPager对应的Pager(VerticalPager/HorizontalPager)组件,目前还在测试阶段,如果在项目中想要实现ViewPager的功能,可以自行实现,或者使用原生的ViewPager。

2 Modifier详解

Modifier在Compose中是一个非常重要的组成,像我们在传统的UI中,需要声明每个组件的间隔或者相对于父容器的间隔,通常使用margin或者padding来实现。

xml 复制代码
<TextView
    android:id="@+id/tv_ui"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="UI绘制"
    android:layout_marginBottom="20dp"
    android:paddingTop="20dp"/>

而在Compose当中,会使用Modifier来完成,在Compose当中的每个组件中,都有一个Modifier参数变量。

kotlin 复制代码
@Composable
fun Text(
    text: String,
    // 可以设置Modifier
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    // ......
) {

2.1 设置组件padding 和 margin

如下一个可组合函数,垂直线性布局中,有一个Image和Text,其中通过Mofifier设置了Image的padding为12dp,整个线性布局的背景是蓝色的,也是通过Modifier的background属性设置的。

kotlin 复制代码
@Composable
fun testModifier() {

    Column(modifier = Modifier.background(Color.Blue)){
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            modifier = Modifier.padding(12.dp)
        )
        Text(text = "测试Modifier")
    }

}

如果设置margin属性,我们发现Modifier没有margin这个函数,那么在Compose中如何设置margin呢?可以通过Spacer组件来设置。

kotlin 复制代码
@Composable
fun testModifier() {

    Column(modifier = Modifier.background(Color.Blue)) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            modifier = Modifier.padding(12.dp)
        )

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

        Text(
            text = "测试Modifier",
            modifier = Modifier.background(Color.Red)
        )
    }

}

看下效果:

2.2 match_parent和wrap_content如何实现

其实Modifier除了设置paddingbackground之外,还可以设置组件的大小,当然与传统的XML布局必须要设置layout_width和layout_height不同的是,Compose默认宽高均为wrap_content,不需要强制设置。

如果想要组件实现match_parent,那么可以使用Modifier的fillMaxHeightfillMaxWidthfillMaxSize函数实现宽高的配置。

kotlin 复制代码
@Composable
fun testModifier() {

    Column(
        modifier = Modifier
            .background(Color.Blue)
            .fillMaxHeight()
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            modifier = Modifier.padding(12.dp)
        )

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

        Text(
            text = "测试Modifier",
            modifier = Modifier.background(Color.Red)
        )
    }

}

如果要设置一个固定的值,那么可以使用Modifier的widthheightsize函数,那么就可以配置组件的具体宽高,如果组件是一个正方形,可以直接调用size函数。

kotlin 复制代码
Image(
    painter = painterResource(id = R.drawable.ic_launcher_background),
    contentDescription = null,
    modifier = Modifier
        .padding(12.dp)
        .width(100.dp)
        .height(80.dp)
)

2.3 什么情况下会使用Modifier

假如,我们要设置Text的文字大小,和文字颜色,我们可以使用Modifier来设置吗?其实不能,这两个属性其实需要通过Text构造方法中的参数设置。

kotlin 复制代码
Text(
    text = "测试Modifier",
    modifier = Modifier.background(Color.Red),
    fontSize = 20.sp,
    fontWeight = FontWeight(400),
    color = Color.White
)

像文字的颜色、字重、字号都只能通过Text的构造函数中的参数设置,而不能使用Modifier。那么什么情况下会使用Modifier呢?

其实Modifier属于公共属性的集合。其实不难理解,对于Text来说它是展示文本的组件,字号、字重属于其独有的属性,而如果在Modifier中维护这些属性,对于布局、Image这些来说根本用不到的属性,反而会导致Modifier变得越来越臃肿。

所以之后在自定义Compose组件的时候,对于组件的特有属性,需要放在构造参数中。

2.4 点击事件

在Android传统UI中,每一个View都可以被设置点击事件监听器,无论View还是ViewGroup都是可被点击的,因此Compose中点击事件也是放在了Modifier中,因为属于通用的能力。

kotlin 复制代码
Text(
    text = "测试Modifier",
    fontSize = 20.sp,
    modifier = Modifier.background(Color.Red)
        .padding(12.dp)
        .clickable {
            
        },
    fontWeight = FontWeight(400),
    color = Color.White
)

在Modifier中提供了clickable函数,这里需要注意一点,clickable的摆放顺序是有讲究的,因为我们给按钮加了一个padding,所以clickable放在了padding后面,那么点击时热区不包括padding,如下所示:

如果想要热区包括padding,那么在声明clickable时,需要放在padding的前面,可以这么理解,想要响应点击事件的区域,需要放在clickable之后。

kotlin 复制代码
Text(
    text = "测试Modifier",
    fontSize = 20.sp,
    modifier = Modifier.background(Color.Red)
        .clickable {

        }
        .padding(12.dp),
    fontWeight = FontWeight(400),
    color = Color.White
)

经过前面的这些介绍,相信伙伴们对于Compose有了一些基础的了解,相信掌握了前面的这些知识,通过Compose能够写出一些页面了吧,而且相较于传统的UI,这种声明式的框架显得更加灵活且高效,当然这也是Compose的冰山一角,后续我将会介绍更加高阶的Compose知识。

相关推荐
Dnelic-1 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen3 小时前
MTK Android12 user版本MtkLogger
android·framework
比格丽巴格丽抱11 小时前
flutter项目苹果编译运行打包上线
flutter·ios
长亭外的少年11 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
SoaringHeart11 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter
建群新人小猿13 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神14 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛15 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
AiFlutter15 小时前
Flutter通过 Coap发送组播
flutter
Y多了个想法15 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526