Android全新UI框架之Compose组件渲染流程

在传统View体系中,组件渲染分为三步:测量、布局和绘制。Compose也遵循这样的分层设计,将组件渲染流程划分为组合、布局与绘制三个阶段。

  • 组合:执行Composable函数体,生成并维护LayoutNode视图树。
  • 布局:对于视图树中的每个LayoutNode进行宽高尺寸测量并完成位置摆放。
  • 绘制:将所有LayoutNode实际绘制到屏幕之上。

对于一般的组件都是正常经历组合->布局->绘制三个阶段来生成帧画面的,当然也存在特例,LazyColumn、LazyRow、等组件的子项合成可以延迟到这类组件的布局阶段进行,这是由于这类组件的子项组合需要依赖这类组件在布局阶段所能提供的一些信息。

组合

组合阶段的主要目标是生成并维护LayoutNode视图树,当我们在Activity中使用setContent时,会开始首次组合,此时会执行代码块中涉及的所有Composable函数体,生成与之对应的layoutNode视图树。与之相对应的是,在传统View系统中也是在setContentView中首次构建View视图树的。在Compose中如果Composable依赖了某个可变状态,当该状态发生更新时,会触发当前Composable重新进行组合阶段,故也被称为重组。在当前组件发生重组时,子Composable被依次重新调用:

  • 被调用的子Composable将当前传入的参数与前次重组中的参数做比较,若参数变化,则Composable函数发生重组,更新LayoutNode视图树上对应节点,UI发生更新。
  • 被调用的子Composable,对参数比较后,如果无任何变化,则跳过本次执行,即所谓的智能重组。LayoutNode视图树对应的节点保持不变,UI无变化。
  • 如果子Composable在重组中没有再被调用到,其对应的节点及其子节点会从LayoutNode视图树中被删除,UI从屏幕移除。反之新增也是同理。

综上所述,重组过程可以自动维护LayoutNode视图树,使其永远保持在最新的视图状态。而在传统View系统中,只能手动对ViewGroup进行add/remove等操作来维护View视图树,这是两种视图体系本质的区别。
注意:子Composable是否跳过重组除了取决于参数是否变化,也取决于参数类型是否是Stable。

布局

布局阶段用来对视图树中每个LayoutNode进行宽高尺寸测量并完成位置摆放。当Compose的内置组件无法满足我们的需求时,可以在定制组件的布局阶段实现满足自己需求的组件。

在compose中,每个LayoutNode都会根据来自父LayoutNode的布局约束进行自我测量(类似传统View中的MeasureSpec)。布局约束中包含了父LayoutNode允许子LayoutNode的最大宽高和最小宽高,当父LayoutNode希望子LayoutNode测量的宽高为某个具体值时,约束中的最大宽高与最小宽高就是相同的。LayoutNode不允许被多次测量,在Compose中多次测量会抛异常。需要注意的是,有些需求场景需要多次测量LayoutNode,Compose为我们提供了固有特性测量与SubcomposeLayout作为解决方案

  • LayoutModifier

    如果想着定Text顶部到文本基线的高度,使用内置的padding修饰符是无法满足需求的,因为padding只能指定Text顶部到文本顶部的高度,虽然Compose提供了paddingFromBaseline修饰符可以用来解决该问题,但是不妨使用layout修饰符来重新实现一个。
    layout修饰符是用来修饰LayoutNode的宽高与原有内容在新宽高下摆放位置的。

    kotlin 复制代码
    @Preview
    @Composable
    fun Text1(){
        Column {
            Text(text = "Hello World", modifier = Modifier.firstBaselineToTop(24.dp))
            Text(text = "Hello World", modifier = Modifier.padding(top = 24.dp))
        }
    }
    
    fun Modifier.firstBaselineToTop(
        firstBaselineToTop:Dp
    ) = Modifier.layout { measurable, constraints ->
        // measurable表示被修饰LayoutNode的测量句柄,通过内部measure方法完成layoutNode的测量,constraints表示来自父LayoutNode的布局约束。
        //采用布局约束对该组件完成测量,测量结果保存在Placeable实例中
        val placeable = measurable.measure(constraints)
        //保证该组件是存在内容基线的
        check(placeable[FirstBaseline]!=AlignmentLine.Unspecified)
        //获取基线的高度
        val firstBaseline = placeable[FirstBaseline]
        //应摆放的顶部高度为所设置的顶部到基线的高度减去实际组件内容顶部到基线的高度
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        //该组件占有的高度为应摆放的高度加上实际内容的高度
        val height = placeable.height + placeableY
        //仅是高度发生变化
        layout(placeable.width,height){
            placeable.placeRelative(0,placeableY)//指定原有应该绘制的内容在新的高度下所应该摆放的相对位置
        }
    
    }
  • LayoutComposable

    LayoutModifier可以类比于定制单元View。如果想在Compose中类似定制"ViewGroup",就需要使用LayoutComposable了。

    kotlin 复制代码
    fun Layout(
        //contents为我们声明的子组件信息
        contents: List<@Composable @UiComposable () -> Unit>,
        //外部传入的修饰符
        modifier: Modifier = Modifier,
        //表示测量策略,默认场景下只实现measure即可,如果还想实现固有特性测量,还需要重写Intrinsic系列方法
        measurePolicy: MultiContentMeasurePolicy
    )
    kotlin 复制代码
    @Composable
    fun MyColumn(
        modifier: Modifier = Modifier,
        content:@Composable ()->Unit
    ){
        Layout(content = content, modifier = modifier){measurables, constraints ->
            val placeables = measurables.map { measurable ->
                measurable.measure(constraints)
            }
            var yPosition = 0
            layout(constraints.maxWidth, constraints.maxHeight) {
                placeables.forEach { placeable ->
                    placeable.placeRelative(x = 0, y = yPosition)
                    yPosition += placeable.height
                }
            }
        }
    }
    
    @Preview
    @Composable
    fun BodyContent(modifier: Modifier = Modifier) {
        MyColumn(modifier.padding(8.dp)) {
            Text("MyOwnColumn")
            Text("places items")
            Text("vertically.")
            Text("We've done it by hand!")
        }
    }

    注意:如果我们在LayoutModifier的measure方法或LayoutComposable中读取了某个可变状态,当该状态更新时,会导致当前组件重新进行布局阶段,故也被称为重排。如果组件的大小或位置发生了更新,则还会重新进行接下来的绘制阶段。

  • 固有特性测量Intrinsic

    固有特性测量为我们提供了预先测量所有子组件确定自身constraints的能力,并在正式测量阶段对子组件的测量产生影响。其本质就是允许父组件预先获取到每个子组件宽高信息后,影响自身在测量阶段获取到的constraints宽高信息,从而间接影响子组件的测量过程。

    1. 使用内置组件的固有特性测量
      使用固有特性测量的前提是组件需要适配固有特性测量,目前许多内置组件已经实现了固有特性测量,可以直接使用。绝大多数内置组件都是用LayoutComposable实现的,LayoutComposable中需要传入一个measurePolicy,默认只需实现measure,但如果要实现固有特性测量,就需要额外重写Intrinsic系列方法。
      注意:只能对已经适配固有特性测量的内置组件使用IntrinsicSize.Min或IntrinsicSize.Max,否则程序运行会crash。
    kotlin 复制代码
    @Composable
    fun TwoTexts(modifier: Modifier=Modifier,text1:String,text2:String){
        Row(modifier = modifier
            .height(IntrinsicSize.Min)
            //为高度设置固有特性测量,宽度并没有设置。
            //此时就表示当宽度不限定时,根据子组件预先测量的宽高信息所能计算的当前组件的高度最小可以是多少。
            //当然也可以设置宽度,也就是表示当宽度收到限制时,根据子组件测量的宽高信息所能计算当前组件的高度最小可以是多少
        ) {
            Text(
                modifier= Modifier
                    .weight(1f)
                    .padding(start = 4.dp)
                    .wrapContentSize(align = Alignment.CenterStart),
                text = text1)
            Divider(color = Color.Black, modifier = Modifier
                .fillMaxHeight()
                .width(1.dp))
            Text(
                modifier= Modifier
                    .weight(1f)
                    .padding(start = 4.dp)
                    .wrapContentSize(align = Alignment.CenterEnd),
                text = text2)
        }
    }
    
    @Preview
    @Composable
    fun TwoTextsPreview(){
        Surface() {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
    1. 自定义固有特性测量
    kotlin 复制代码
    @Preview
    @Composable
    fun TwoTextsPreview(){
        Surface() {
        //由于声明了Modifier.fillMaxWidth(),导致自定义layout宽度是确定的(constraints参数中minWidth与maxWidth相等),又因为使用了固有特性属性,高度也为一个固定值
            IntrinsicRow(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(IntrinsicSize.Min)) {
                Text(
                    modifier= Modifier
                        .wrapContentSize(align = Alignment.CenterStart)
                        .layoutId("main"),
                    text = "Left")
                Divider(
                    color = Color.Black,
                    modifier = Modifier
                        .fillMaxHeight()
                        .width(4.dp)
                        .layoutId("divider"))
                Text(
                    modifier= Modifier
                        .wrapContentSize(align = Alignment.CenterEnd)
                        .layoutId("main"),
                    text = "Right")
            }
        }
    }
    
    @Composable
    fun IntrinsicRow(modifier: Modifier,content:@Composable () -> Unit){
        Layout(
            content = content,
            modifier = modifier,
            measurePolicy = object :MeasurePolicy{
                override fun MeasureScope.measure(
                    measurables: List<Measurable>,
                    constraints: Constraints
                ): MeasureResult {
                //将constraints的最小宽度设置为0,此时宽度将不会作为一个确定的值影响Divider的测量过程。可以理解为使IntrinsicRow的fillMaxWidth失效,进而使用Divider组件设置的width属性
                    var devideConstraints = constraints.copy(minWidth = 0)
                    var mainPlaceables = measurables.filter {
                        it.layoutId =="main"
                    }.map {
                        it.measure(constraints)
                    }
                    var devidePlaceable = measurables.first { it.layoutId =="divider" }.measure(devideConstraints)
                    var minPos = constraints.maxWidth/2
                    return layout(constraints.maxWidth,constraints.maxHeight){
                        mainPlaceables.forEach {
                            it.placeRelative(0,0)
                        }
                        devidePlaceable.placeRelative(minPos,0)
                    }
                }
    
                override fun IntrinsicMeasureScope.minIntrinsicHeight(
                    measurables: List<IntrinsicMeasurable>,
                    width: Int
                ): Int {
                    var maxHeight = 0
                    measurables.forEach {
                        maxHeight = it.maxIntrinsicHeight(width).coerceAtLeast(maxHeight)
                    }
                    //可以拿到子组件预先测量句柄measurables。在预先测量所有子组件后,就可以根据子组件的高度计算其中的高度最大值,此值将会影响到正式测量是父组件获取到的constraints的高度信息。此时constraints的maxHeight与minHeight都被设置为返回的高度值maxHeight
                    return maxHeight
                }
    
                override fun IntrinsicMeasureScope.maxIntrinsicHeight(
                    measurables: List<IntrinsicMeasurable>,
                    width: Int
                ): Int {
                    TODO("Not yet implemented")
                }
    
                override fun IntrinsicMeasureScope.minIntrinsicWidth(
                    measurables: List<IntrinsicMeasurable>,
                    height: Int
                ): Int {
                    TODO("Not yet implemented")
                }
    
                override fun IntrinsicMeasureScope.maxIntrinsicWidth(
                    measurables: List<IntrinsicMeasurable>,
                    height: Int
                ): Int {
                    TODO("Not yet implemented")
                }
            })
    }
  • SubcomposeLayout

    SubcomposeLayout允许子组件的组合阶段延迟到父组件的布局阶段进行,为我们提供了更强的测量定制能力。SubcomposeLayout可以做到将某个子组件的组合阶段延迟至其所依赖的同级子组件测量结束后进行,从而可以定制子组件将的组合、布局阶段顺序,以取代固有特性测量

    kotlin 复制代码
    @Preview
    @Composable
    fun TwoTextsPreview(){
        Column() {
            SubcomposeRow(
                modifier = Modifier
                    .fillMaxWidth(),
                text = {
                    Text(
                        modifier= Modifier
                            .wrapContentSize(align = Alignment.CenterStart),
                        text = "Left")
                    Text(
                        modifier= Modifier
                            .wrapContentSize(align = Alignment.CenterEnd),
                        text = "Right")
                }) {
                var heightDp = with(LocalDensity.current){it.toDp()}
                Divider(
                    color = Color.Black,
                    modifier = Modifier
                        .height(heightDp)
                        .width(4.dp))
            }
        }
    }
    
    @Composable
    fun SubcomposeRow(
        modifier: Modifier,
        text:@Composable ()->Unit,
        divider:@Composable (Int)->Unit //传入高度
    ){
        SubcomposeLayout(modifier=modifier){constraints ->
            var maxHeight = 0
            var placeables = subcompose("text",text).map {
                var placeable = it.measure(constraints)
                maxHeight = placeable.height.coerceAtLeast(maxHeight)
                placeable
            }
            var dividerPlaceable = subcompose("divider") {
               divider(maxHeight)
            }.map {
                it.measure(constraints.copy(minWidth = 0))//
            }
            assert(dividerPlaceable.size==1,{"DividerScope Error"})
            var minPos = constraints.maxWidth/2
            layout(constraints.maxWidth,constraints.maxHeight){
                placeables.forEach {
                    it.placeRelative(0,0)
                }
                dividerPlaceable.forEach {
                    it.placeRelative(minPos,0)
                }
            }
    
        }
    }

    SubcomposeLayout具有更强的灵活性,然而性能上不如常规Layout,因为子组件的组合阶段需要延迟到父组件布局阶段才能进行,因此还需要额外创建一个子Composition。

绘制

绘制阶段主要是将所有LayoutNode实际绘制到屏幕之上,也可以对绘制阶段进行定制。

  • Canvas Composable

    官方提供的一个专门用来自定义绘制的单元组件,该组件不可以包含任何子组件,可以看作是传统View体系中的一个单元View。

    kotlin 复制代码
    @Preview
    @Composable
    fun LoadingProgressBar(){
    
        Box(modifier = Modifier.size(375.dp), contentAlignment = Alignment.Center){
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Text(
                    text = "Loading",
                    fontSize = 40.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.White
                )
                Text(
                    text = "45%",
                    fontSize = 40.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.White
                )
            }
            Canvas(modifier = Modifier.fillMaxSize().padding(30.dp)){
                drawCircle(
                    color = Color(0xff1e7171),
                    center = Offset(drawContext.size.width/2f,drawContext.size.height/2f),
                    style = Stroke(width = 20.dp.toPx())
                )
                drawArc(
                    color = Color(0xff3bdcce),
                    startAngle = -90f,
                    sweepAngle = 162F,
                    useCenter = true,
                    style = Stroke(width = 20.dp.toPx(), cap = StrokeCap.Round)
                )
            }
        }
    }
  • DrawModifier

    DrawModifier修饰符共有三个,每个都有其各自的使命。

    • drawWithContent:允许开发者可以在绘制时自定义绘制层级。
      越先绘制的内容z轴越小,后面绘制的内容可能会遮盖前面绘制的内容,这样就产生了绘制的层级关系。
    • drawWithCache:允许开发者在绘制时携带缓存。
      有时在绘制时,会用到一些与绘制有关的对象(如ImageBitmap、Paint、Path等),当组件发生重绘时,由于DrawScope会反复执行,其中声明的对象也会随之重新创建。为了解决该问题官方提供了该方法。
    • drawBehind:用来定制组件背景。
      先绘制拓展的内容,再绘制组件本身,也就是用来自定义绘制组件背景的。
    kotlin 复制代码
    @Preview
    @Composable
    fun DrawBefore(){
        Box(
            modifier = Modifier.fillMaxSize(), 
            contentAlignment = Alignment.Center) {
            Card(
                shape = RoundedCornerShape(8.dp),
                modifier = Modifier
                    .size(100.dp)
                    .drawWithContent {
                    //drawContext提供了绘制所需的信息,包括size和canvas
                        drawContent()
                        drawCircle(
                            Color(0xffe7614e),
                            18.dp.toPx() / 2,
                            center = Offset(drawContext.size.width, 0f)
                        )
    
                    }) {
                Image(painter = painterResource(id = R.drawable.ic_light_logo), contentDescription =null )
            }
        }
    }
    
    @Preview
    @Composable
    fun DrawBehind(){
        var content = LocalContext.current
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center) {
            Card(
                shape = RoundedCornerShape(8.dp),
                modifier = Modifier
                    .size(100.dp)
    //                .drawWithCache {
    //                    val image = ImageBitmap.imageResource(content.resources,id = R.drawable.ic_light_logo)
    //                    onDrawBehind {
    //
    //                    }
    //                    onDrawWithContent {
    //
    //                    }
    //                }
                    .drawBehind {
                        drawCircle(
                            Color(0xffe7614e),
                            18.dp.toPx() / 2,
                            center = Offset(drawContext.size.width, 0f)
                        )
    
                    }) {
                Image(painter = painterResource(id = R.drawable.ic_light_logo), contentDescription =null )
            }
        }
    }

    注意:如果在drawWithContent或drawBehind方法中依赖了某个可变状态,当该状态更新时,会导致当前组件重新进行绘制阶段,故也称重绘。

  • 使用平台原生Canvas

    drawContext.canvas.nativeCanvas可以直接获取具体平台canvas实例。一般DrawScope作用域中提供的API也只是对原生平台canvas的封装,底层仍然使用原生平台canvas绘制的。如果非要使用原生canvas,那就要注意通用性。

相关推荐
雨白6 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk6 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING7 小时前
RN容器启动优化实践
android·react native
恋猫de小郭10 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker15 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴15 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农2 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos