likes
comments
collection
share

Compose编程思想 -- 初识Compose

作者站长头像
站长
· 阅读数 7

前言

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

1 Compose入门

1.1 声明式UI

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

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

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

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

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

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

@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
@Composable
fun showLayout() {

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

Compose编程思想 -- 初识Compose

  • 线性布局 - Column / Row
@Composable
fun showLayout() {

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

Compose编程思想 -- 初识Compose

Columnvertical属性;Rowhorizontal属性。

  • 相对布局 - Box

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

@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显示在父容器的中心位置。

Compose编程思想 -- 初识Compose

  • ConstraintLayout

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

  • RecyclerView - LazyColumn
@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。

@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来实现。

<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参数变量。

@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属性设置的。

@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组件来设置。

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

}

看下效果:

Compose编程思想 -- 初识Compose

2.2 match_parent和wrap_content如何实现

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

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

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

}

Compose编程思想 -- 初识Compose

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

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构造方法中的参数设置。

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中,因为属于通用的能力。

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,如下所示:

Compose编程思想 -- 初识Compose

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

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

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

Compose编程思想 -- 初识Compose

3 Compose架构分层设计

我截取了官方文档中的一张图,从上到下代表了Compose架构的层级由高到低。

Compose编程思想 -- 初识Compose

其实在架构设计模式中,也会遵循这个原则,层级高的组件需要依赖层级低的组件,层级低的组件提供的更多的是基础能力,我们挨个分析下:

Compose编程思想 -- 初识Compose

  • compose.runtime

提供了Compose运行时的基本组件,常用的有:remembermutableStateof@ComposableSideEffect,他们单独使用无法实现界面,但是却是界面中的必不可少的元素。通过@Composable构建可组合函数,用于将数据转换为界面;使用remembermutableStateof存储界面数据,并完成订阅能力实现自动刷新。

  • compose.foundation

属于Compose中的基础层,用于提供页面搭建的基础框架,例如RowColumnLazyColumn和手势识别等,可以理解为Android View系统中的页面布局,内部的细节需要更上层的组件来配合实现

  • compose.ui

这是Compose中组件库,常用的分包有ui-androidui-graphicsui-previewui-toolsui-android用于提供更小颗粒度的组件,例如Text、Image、Button等;ui-graphics则是提供绘制能力,常用的有Paint、Canvas等,与传统View体系类似;ui-preview则是提供预览能力。

  • compose.material

这一层则是为Compose提供了MD的系统实现,开发者可以直接使用提供的组件进行界面的构建。

那么Compose这种架构设计的好处是啥?其实很简单,在国内的公司里面,对于MD并不感冒,更多的时间我们是需要自定义view来完成UI的需求,因此通过分层的方式,能够提供多种能力,开发者选择其中某个点完成自定义Composable,满足UI的需求。

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