一文了解Compose

一文了解Compose

简介

Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。阅读官方介绍可以了解到,Compose 大概是这么个东西:

  1. Compose 是一个声明性界面框架,使用更少的代码、强大的工具和直观的 Kotlin API。
  2. 抛弃了原有安卓view的体系,完全重新实现了一套新的ui体系
  3. 使用可组合函数来替换view构建UI界面,只允许一次测量,避免了布局嵌套多次测量问题,从根本上解决了布局层级对布局性能的影响。

看完介绍后第一反应便有了以下几个疑问。

  1. Compose完全摒弃了View吗?ComposeUI结构是什么样?
  2. 可组合函数与View有什么不同?函数最终有没有转换成了view
  3. ComposeUI是如何解多次测量问题问题的?

这篇文章将会介绍Compose的整体结构并一个一个探索这些问题的答案。

基础概念

compose编程思想

Compose是用Kotlin写的,Kotlin版本不低于1.5.10;kotlin支持函数式编程是Compose实现的关键。简介里说了 Compose 是一个声明性框架,也就是声明式或者说函数式编程。他通过函数刷新屏幕上的内容,而不需要拿到组件的具体实例,UI是关于状态的函数,一切都是函数。

Composable注解

Compose函数都加了Composable注解修饰。Composable注解的函数只能被另一个Composable注解的函数调用。此注解可告诉编译器,被修饰的函数是Compose函数,用来描述UI界面的。这些函数不返回任何内容,他们旨在描述当前的UI状态。

微件

Text()、Image()、Row()、Coulm()等这些描述屏幕元素的函数都被称为微件,类似TextView、ImageView、LinearLayout。从代码上看 view 之间的关系是继承的关系,LinearLayout 继承ViewGroup,ViewGroup继承 View。微件之间没有任何关系,通过组合嵌套自由搭配。在继承关系中有些不必要的属性也会一并继承,这就显的多余。在这一点上,组合组合函数明显更好。

重组

在view 体系中如需更改某个组件,可以在该该件上调用 setter 以更改其内部状态。在 Compose 中,则是使用新数据再次调用可组合函数。在Compose中UI刷新的唯一方法就是重组,决定是否重组的条件就是与@Composable元素绑定的数据是否发生了变化。

简单使用

实现一个recycleView

@Composable
fun showList() {
    //生产 100条数据
    var datalist: MutableList<String> = mutableListOf()
    for (i: Int in 0..100) {
        datalist.add("$i")
    }
    //纵向滑动容器
    LazyColumn {
        //顶部吸顶布局
        stickyHeader {
            Text(text = "顶部吸顶布局",
                 color = Color.Cyan,
                 modifier = Modifier
                     .fillMaxWidth()
                     .background(Color.White),
                 fontSize = 20.sp,
                 textAlign = TextAlign.Center
            )
        }
        //构建列表
        items(datalist) { name ->
            //构建一个item
            BindItemView(name = name)
        }
    }
}

@Composable
fun BindItemView(name: String) {
    var imageURL =
        "https://img0.baidu.com/it/u=1766591091,2326601705&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=674"
    //横向布局 包含文本和图片
    Row(modifier = Modifier
        .padding(10.dp)
        //Item点击事件
        .clickable {
            Toast
                .makeText(mContext, name, Toast.LENGTH_LONG)
                .show()
        }) {
        //纵向文本布局
        Column(
            modifier = Modifier
                .background(Color.White)
                .weight(1f)
        ) {
            Text(text = "我是个标题$name", fontSize = 16.sp, maxLines = 1)
            Text(
                color = Purple200,
                modifier = Modifier.padding(top =6.dp, bottom = 4.dp)
                    .background(Color.White),
                text = "我是个内容,我可能很长,但是我只能显示最多两行",
                fontSize = 14.sp,
                maxLines = 2
            )
            Text(text = "userName", fontSize = 10.sp)
            Spacer(modifier = Modifier.padding(top = 4.dp).fillMaxWidth().height(1.dp).background(Color.Gray))
        }
        //加载一个网络图片
        Image(
            modifier = Modifier
                .size(80.dp, 60.dp)
                .align(Alignment.CenterVertically),
            contentScale = ContentScale.Crop,
            painter = rememberImagePainter(imageURL),
            contentDescription = ""
        )
    }
}

上述60行代码中的Ui,包含了一个纵向滚动的list布局,item有文本有图片,有点击事件,还有顶部吸顶效果。没有xml没有adapter,相比xml-view体系,代码简洁度确实高了不少。


image.png

Compose 视图结构

Comepose抛弃了view体系,那Compose的视图结构是什么样的。

打开“显示布局边界”可以看到Compose的组件显示了布局边界,我们知道,Flutter与WebView H5内的组件都是不会显示布局边界的,难道Compose最终还是把函数变成了View?

边框.jpg

通过android studio 的LayoutInspector看到ComposeActivity的布局结构

compose_view_layout.png

最上层还是DecorView、FrameLayout,然后就看到有一个AndroidComposeView,没有TextView或者其他的View了。ComposeActivity 的onCreate方法里setContent替换了原来的setContentView,点击去我们看到

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    //decorView 的第一个子View如果是 ComposeView 直接用,如果不是就创建一个ComposeView ,然后添加到跟布局
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
       ...
        setContent(content)
    } else ComposeView(this).apply {
       ...
        setContent(content)
       ...
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

decorView 的第一个子View如果不是ComposeView就创建一个 ,然后添加到跟布局。而ComposeView 里又通过 Wrapper_android.kt 创建了一个 AndroidComposeView,我们前面见过。ComposeView 和 AndroidComposeView 都是继承ViewGroup,都是在setContent时添加进去的。除此之外我们写的Column,Row,Text并没有出现在布局层级中,也就是说Compose 并没有把函数转成View。AndroidComposeView 可以理解是一个连接view 和 compose 的入口。

我们知道,View系统通过一个View树的数据结构来存储TextView,ImageView等屏幕元素,渲染的时候去遍历View树。Compose里是怎么管理它的布局元素的呢。

  1. LayoutNote

我们点开任何一个微件函数,一系列调用最终都会到了Layout.kt的Layout()方法,Layout() 核心是调用ReusableComposeNode ()方法。这里有个参数 factory,factory 是一个构造器函数, factory 被调用就会创建一个LayoutNote,定义的布局属性modifier也在这里设置给了LayoutNode。每个微件最终都是一个LayoutNote。

@Composable inline fun Layout(
    content: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    ...
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        //1. factory 指向 ComposeUiNode的构造器,创建一个LayoutNode
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
        },
        //2. modifier设置给LayoutNode
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}

ComposeUiNode #{
     val Constructor: () -> ComposeUiNode = LayoutNode.Constructor
}

LayoutNode #{
    internal val Constructor: () -> LayoutNode = { LayoutNode() }
}
  1. Composables :ReusableComposeNode方法里又调用Composer 来插入或者更新LayoutNote。
inline fun <T, reified E : Applier<*>> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit,
    noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
    content: @Composable () -> Unit
) {

    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startReusableNode()

    //插入一个新节点
    if (currentComposer.inserting) {
        currentComposer.createNode(factory)
    } else {
     //复用节点
        currentComposer.useNode()
    }
    currentComposer.disableReusing()
    //更新节点
    Updater<T>(currentComposer).update()
    currentComposer.enableReusing()
    SkippableUpdater<T>(currentComposer).skippableUpdate()
    currentComposer.startReplaceableGroup(0x7ab4aae9)
    content()
    currentComposer.endReplaceableGroup()
    currentComposer.endNode()
}

  1. Composer :这里 factory方法才被执行创建了一个note,获取插入节点的位置,调用nodeApplier插入到root里。
override fun <T> createNode(factory: () -> T) {
        ...
        //1. 获取插入节点的位置
        val insertIndex = nodeIndexStack.peek()
        val groupAnchor = writer.anchor(writer.parent)
        groupNodeCount++
         recordFixup { applier, slots, _ ->
            @Suppress("UNCHECKED_CAST")
            //2. 在这里才执行了 factory方法, 创建了一个note
            val node = factory()
            slots.updateNode(groupAnchor, node)
            @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<T>
            //3. 调用nodeApplier插入到root里。
            nodeApplier.insertTopDown(insertIndex, node)
            applier.down(node)
        }
       ... 
    }

  1. Applier
    Composer 里的 nodeApplier 是一个Applier接口,实现类是UiApplier,note 最终是在这里add到 root 节点 (LayoutNote) 的 _foldedChildren 列表里。

总结一下,compose 构建了一个LayoutNode树,每一个微件函数会生成一个LayoutNode,Composition 作为起点,发起首次的 构图,通过 Composer 的执行填充 NodeTree。渲染引擎基于 LayoutNote 渲染 UI, 每当重构发生时,都会通过 Applier 对 NodeTree 进行更新。简单来说就是 Composer 和 Applier 一起维护了一个 LayoutNote 树。

Compose在渲染时不会转化成View,而是有一个AndroidComposeView作为入口View,我们声明的Compose布局在渲染时会转化成LayoutNode,所有的屏幕元素都存储在这颗树上。AndroidComposeView 中会触发LayoutNode的布局与绘制。

compose界面只允许一次测量。

我们知道View体系有个问题, 如果父布局的布局属性是wrap_content、子布局是match_parent ,父布局先以0为强制宽度测量子View、然后继续测量剩下的其他子View,再用其他子View里最宽的宽度,二次测量这个match_parent的子 View。子View测量了两次,子view也会对下面的子view进行两次测量 。并且随着布局的增长测量次数成指数增长,Compose是如何处理的?

开头里的例子里我们用了一个线性视图容器Row,我们先试着自定义个AutoRow,实现自动换行的横向布局。以Row为样板,照葫芦画瓢。

定义一个@Composable 方法:AutoRow

@Composable
fun AutoRow(modifier: Modifier, content: @Composable () -> Unit) {
    //实例一个 measurePolicy 定义测量和布局策略
    var measurePolicy = object : MeasurePolicy{
        //实现 measure 方法,
        // Measurable : 是一个接口,通过断点可以看到,它的实例就是一个 LayoutNote,childs 就是子节点列表。
        // constraints : 是父布局的约束条件,有点像View 的 MeasureSpec
        override fun MeasureScope.measure(
            childs: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {
            var placeables = mutableListOf<Placeable>()
            childs.forEach {
                //1.遍历测量子项,得到一个可放置项 Placeable
                // measure()方法传入一个Constraints:约束。 测量完成后返回 一个 Placeable
                // 一次测量过程中,多次调用一个子项的measure()方法会抛异常
               var constraints = Constraints(0, constraints.maxWidth, 0, constraints.maxHeight)
                val placeable = it.measure(constraints)
                placeables.add(placeable)
            }
            var layoutX = 0
            var layoutY = 0
            var padding = 20
            //2.layout 方法确定布局的大小,把测量得到的Placeable摆放到指定位置
            val measureResult = layout(width = constraints.minWidth, height = constraints.minHeight,placementBlock={
                placeables.forEach { placeable ->
                    if (layoutX + placeable.width > constraints.maxWidth) {
                        layoutX = 0
                        layoutY += placeable.height
                    }
                    // 3.定位子项在父布局位置的位置
                    placeable.placeRelative(layoutX, layoutY)
                    layoutX += placeable.width + padding
                }
            })
            return measureResult
        }

    }
    //4.布局
    //上面这些事定义了measure()、layout() 的策略,Layout这里才触发执行的。
    Layout(content = content, modifier = modifier, measurePolicy = measurePolicy)
}
自动换行.png

测量的 measure () 方法最终调用了 outerMeasurablePlaceable.measure(),其中check()方法里判断了当前节点是否正在被父布局测量,如果正在测量就会抛出异常。

    override fun measure(constraints: Constraints) =
        outerMeasurablePlaceable.measure(constraints)

    //  OuterMeasurablePlaceable ##
    override fun measure(constraints: Constraints): Placeable {
        // when we measure the root it is like the virtual parent is currently laying out
        val parent = layoutNode.parent
        if (parent != null) {
            check(
                layoutNode.measuredByParent == LayoutNode.UsageByParent.NotUsed ||
                    @Suppress("DEPRECATION") layoutNode.canMultiMeasure
            ) {
                "measure() may not be called multiple times on the same Measurable. Current " +
                    "state ${layoutNode.measuredByParent}. Parent state ${parent.layoutState}."
            }
            ...
    }

其实Measurable的注释讲的很清楚

Measures the layout with constraints, returning a Placeable layout that has its new size. A Measurable can only be measured once inside a layout pass.

使用约束测量布局,返回具有新大小的可放置布局。一个Measurable 只能测量一次在一次layout 过程里。
这是Compose的特性,就只允许一次测量,从根本上解决了布局嵌套多次测量的问题。

测量过程分析

Modifier 链

我们构造一个compose 微件时,有一个参数很重要,他描述了这个微件的一些特性。比如:Text()通过 Modifier设置了size,padding,background等等。

   Text( modifier = Modifier
           .size(80.dp, 20.dp)
           .padding(10.dp)
           .background(Color.White),
         text = "我是个内容,我可能很长,但是我只能显示最多两行",
   )

    fun Modifier.size(width: Dp, height: Dp) = this.then(
        SizeModifier(
            minWidth = width,
            maxWidth = width,
            minHeight = height,
            maxHeight = height,
            enforceIncoming = true,
            inspectorInfo = debugInspectorInfo {
                name = "size"
                properties["width"] = width
                properties["height"] = height
            }
        )
    )
    
    infix fun then(other: Modifier): Modifier =
        if (other === Modifier) this else CombinedModifier(this, other)

class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
) : Modifier {...}

size(),padding()可以理解成操作符,类似rxjava 里的那样。点进去会看到在这些方法里创建了对应的 SizeModifier,PaddingModifier,他们都继承自LayoutModifier(表示Layout相关的属性),background 里创建了Background 继承DrawModifier(表示绘制相关的属性),LayoutModifier、DrawModifier 继承Modifier.Element。SizeModifier,PaddingModifier、Background被包进CombinedModifier,形成了一条Modifier链。

Modifier链.png

LayoutNodeWrapper链

我们在前面Layout 里看到,modifier(链)被传入materializerOf()方法,最终被赋值给LayoutNote.modifier这个成员变量。
当被set()方法设置给LayoutNote时 ,

  1. 调用foldOut()方法遍历modifier链,把不同的Modifier类型包装成Node,LayoutModifier包装成ModifiedLayoutNode,
  2. ModifiedLayoutNode 挂在初始节点 innerLayoutNodeWrapper:LayoutNodeWrapper 上,形成一条Wrapper链,innerLayoutNodeWrapper在链尾。
  3. 遍历完成后把链头设置给outerMeasurablePlaceable.outerWrapper。
override var modifier: Modifier = Modifier
        set(value) {// 成员变量modifier 的set()方法
           ...
            //1. foldOut()遍历 modifier 每个节点
            val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
              
               // 2.  类型判断,构建Node节点,挂在初始节innerLayoutNodeWrapper上
                if (mod is DrawModifier) {
                    val drawEntity = DrawEntity(toWrap, mod)
                    drawEntity.next = toWrap.drawEntityHead
                    toWrap.drawEntityHead = drawEntity
                    drawEntity.onInitialize()
                }
                //toWrap = innerLayoutNodeWrapper
                var wrapper = toWrap
               ... 
              //省略了一些Modifier类型
                
               // 不同的Modifier类型被包装成不同类型的Node,
               //LayoutModifier被包装成了ModifiedLayoutNode,
              if (mod is FocusEventModifier) {
                    wrapper = ModifiedFocusEventNode(wrapper, mod)
                        .initialize()
                        .assignChained(toWrap)
                }
                if (mod is FocusRequesterModifier) {
                    wrapper = ModifiedFocusRequesterNode(wrapper, mod)
                        .initialize()
                        .assignChained(toWrap)
                }
                if (mod is FocusOrderModifier) {
                    wrapper = ModifiedFocusOrderNode(wrapper, mod)
                        .initialize()
                        .assignChained(toWrap)
                }
                if (mod is LayoutModifier) {
                    wrapper = ModifiedLayoutNode(wrapper, mod)
                        .initialize()
                        .assignChained(toWrap)
                }
               ...
               //3. 返回链头
                wrapper
            }

            outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
           //outerWrapper是链头,innerLayoutNodeWrapper在链尾
            outerMeasurablePlaceable.outerWrapper = outerWrapper
             ...
        }

画成图如下:


measure.png

测量入口

Layout()里把modifier设置给了LayoutNode,还把modifier转成了LayoutNodeWrapper链,那什么时候使用这些Modifier的呢。我们开始的地方知道了ComposeActivity布局的顶层还是View,那就是说视图的最顶层的渲染还是依赖view的。在 AndroidComposeView 看到了 dispatchDraw(),这就是Compose 渲染的入口。这里会触发布局测量。

    override fun dispatchDraw(canvas: android.graphics.Canvas) {
        //Compose 测量与布局入口
        measureAndLayout()
        
        //Compose 绘制入口
        canvasHolder.drawInto(canvas) { root.draw(this) }
        //...
    }

测量布局过程

measureAndLayout()遍历所有需要测量的 LayoutNote,最终到MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded()方法,测量并布局每一个LayoutNote。

private fun remeasureAndRelayoutIfNeeded(layoutNode: LayoutNode): Boolean {
        var sizeChanged = false
        if (layoutNode.isPlaced ||
            layoutNode.canAffectParent ||
            layoutNode.alignmentLines.required
        ) {
            if (layoutNode.layoutState == NeedsRemeasure) {
                //1. 测量 layoutNode
                sizeChanged = doRemeasure(layoutNode)
            }
            if (layoutNode.layoutState == NeedsRelayout && layoutNode.isPlaced) {
                 //2. 设置 layoutNode 位置
                if (layoutNode === root) {
                    layoutNode.place(0, 0)
                } else {
                    layoutNode.replace()
                }
                onPositionedDispatcher.onNodePositioned(layoutNode)
                consistencyChecker?.assertConsistent()
            }
            。。。
        }
        return sizeChanged
    }
    
  1. 测量
    doRemeasure() 调用了 outerMeasurablePlaceabl.remeasure(),
    outerMeasurablePlaceabl 是 LayoutNote 的一个成员,outerMeasurablePlaceabl 又调用outerWrapper.measure()。上面说到过,outerWrapper 是头节点 LayoutModifierNode 。
internal class ModifiedLayoutNode(
    wrapped: LayoutNodeWrapper,
    modifier: LayoutModifier
) : DelegatingLayoutNodeWrapper<LayoutModifier>(wrapped, modifier) {

    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
        with(modifier) { 
            measureResult = measureScope.measure(wrapped, constraints)
            this@ModifiedLayoutNode
        }
    }

根据我们写的代码,此时modifier 为 SizeModifier, 类型 LayoutModifier.kt

SizeModifier.kt

  private class SizeModifier(
    private val minWidth: Dp = Dp.Unspecified,
    private val minHeight: Dp = Dp.Unspecified,
    private val maxWidth: Dp = Dp.Unspecified,
    private val maxHeight: Dp = Dp.Unspecified,
    private val enforceIncoming: Boolean,
    inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    override fun MeasureScope.measure(
        measurable: Measurable,/*下一个LayoutNodeWrapper*/
        constraints: Constraints/* 父容器或者上一个节点的约束 */
    ): MeasureResult {
        val wrappedConstraints = targetConstraints.let { targetConstraints ->
            if (enforceIncoming) {//当我们给控件指定大小时,这个值就为true
                //结合父容器或者上一个节点的约束 和我们指定约束进行结合生成一个新的约束
                constraints.constrain(targetConstraints)
            } else {
                ……
            }
        }
        // 进行下一个 LayoutNodeWrapper 节点测量
        val placeable = measurable.measure(wrappedConstraints)

        //测量完,开始摆放节点的位置
        return layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }

SizeModifier.measure自己测量完,进行下一个 LayoutNodeWrapper 节点的测量,一直到最后 InnerPlaceable 节点。InnerPlaceable触发LayoutNode子项的测量,这里就是Layout 的MeasurePolicy 的measure 执行的地方了

class InnerPlaceable{   
    override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
        val measureResult = with(layoutNode.measurePolicy) {
            // 触发LayoutNode子项的测量,这里就是Layout 的MeasurePolicy 的measure 执行的地方了
            layoutNode.measureScope.measure(layoutNode.children, constraints)
        }
        layoutNode.handleMeasureResult(measureResult)
        return this
    }
}

  1. 布局
internal fun replace() {
        try {
            relayoutWithoutParentInProgress = true
            outerMeasurablePlaceable.replace()
        } finally {
            relayoutWithoutParentInProgress = false
        }
    }

Layout. replace()方法,看到布局过程也是由outerMeasurablePlaceable完成的。和测量过程一样,过程就不一步步的分析了,经过一系列的链式调用,最终到 InnerPlaaceable.placeAt()。

  1. InnerPlaaceable.placeAt(),设置 当前节点的位置。

  2. onNodePlaced()最终会调用 MeasureResult.placeChildren(),触发子项位置的摆放。

    override fun placeAt(
        position: IntOffset,
        zIndex: Float,
        layerBlock: (GraphicsLayerScope.() -> Unit)?
    ) { 
        //1.往下掉用LayoutNoteWrapper.placeAt(),设置当前节点的 的位置
        super.placeAt(position, zIndex, layerBlock)

       //2. 最终会调用 MeasureResult.placeChildren(),设置子项的位置。
        layoutNode.onNodePlaced()
    }
  1. 绘制
    这些布局是怎么绘制到屏幕上的呢?在AndroidComposeView里看到绘制的入口是
 canvasHolder.drawInto(canvas) { root.draw(this) }

root是LayoutNode的根节点。调用了LayoutNode.draw()方法,还传了个参数:canvas。LayoutNode继续调用outerLayoutNodeWrapper.draw(canvas),看到这应该能猜到了,绘制应该也是LayoutNodeWrapper完成的。还记得backgroung方法创建了BackGround继承DrawModifier,在LayoutNode里被包装成了DrawEntity

internal class DrawEntity(
    val layoutNodeWrapper: LayoutNodeWrapper,//下一个节点
    val modifier: DrawModifier
) : OwnerScope {

最终还是调用到了LayoutNodeWrapper.draw()

    fun draw(canvas: Canvas) {
        val layer = layer
        if (layer != null) {
            layer.drawLayer(canvas)
        } else {
            val x = position.x.toFloat()
            val y = position.y.toFloat()
            canvas.translate(x, y)
            drawContainedDrawModifiers(canvas)
            canvas.translate(-x, -y)
        }
    }

我们看到绘制还是基于canvas的。

总结:

  1. 在setContent的过程中,会创建ComposeView与AndroidComposeView,其中AndroidComposeView是Compose的入口,连接View体系和新的Compose体系。
  2. Compose虽然没有了view 树,但也需要一个数据结构来存储Text、Image等屏幕元素,那就是LayoutNote树。
  3. 微件里设置的modifier会构建一个LayoutModifier链,在设置进LayoutNode时转换成了LayoutNodeWrapper链,测量和布局是由一个个LayoutNodeWrapper节点按顺序完成的,最后一个节点InnerPlaaceable触发子项的测量和布局。
  4. Compose 特性禁止在一次Layout 过程中多次测量一个子项,这从根本上解决了布局嵌套多次测量的问题。
  5. Compose绘制虽然脱离了View,但还是依赖canvas。

文章到这里结束了,开头的那些问题也都有了答案。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容