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知识。

相关推荐
五味香17 分钟前
Java学习,List 元素替换
android·java·开发语言·python·学习·golang·kotlin
十二测试录1 小时前
【自动化测试】—— Appium使用保姆教程
android·经验分享·测试工具·程序人生·adb·appium·自动化
allanGold1 小时前
【flutter版本升级】【Nativeshell适配】nativeshell需要做哪些更改
flutter·nativeshell
Couvrir洪荒猛兽2 小时前
Android实训九 数据存储和访问
android
aloneboyooo3 小时前
Android Studio安装配置
android·ide·android studio
Jacob程序员3 小时前
leaflet绘制室内平面图
android·开发语言·javascript
昆仑道长4 小时前
ARM64平台Flutter环境搭建
flutter
sunly_4 小时前
Flutter:自定义Tab切换,订单列表页tab,tab吸顶
开发语言·javascript·flutter
2401_897907864 小时前
10天学会flutter DAY2 玩转dart 类
android·flutter
前端没钱4 小时前
flutter入门系列教程<一>:tab组件的灵活妙用
flutter