Jetpack Compose 到底优秀在哪里?

作者:业志陈
转载地址:https://mp.weixin.qq.com/s?__biz=Mzk0NDIwMTExNw==&mid=2247523991&idx=1&sn=84df0fbff7efb77a83a385d3785254f3&utm_source=tuicool&utm_medium=referral

单纯看官方的介绍或者是网络上的文章,开发者也许已经对 Jetpack Compose 有这么一个印象了: 使用 Jetpack Compose 时我们可以深层次地嵌套布局而不用担心会影响性能。这是 Google 在介绍 Jetpack Compose 时经常拿来和原生 View 体系进行比较的一个特性,也是介绍其优势时的一个着重点,本文就来介绍这一方面的相关知识点,涉及到的内容有:

  • 原生 View 体系下,我们一直强调要减少布局的嵌套层次,那这么做的意义是什么呢;

  • Jetpack Compose 的布局模型;

  • Jetpack Compose 如何实现自定义布局;

  • Jetpack Compose 是如何避免多次测量的;

  • Jetpack Compose 固有特性测量的作用,以及如何进行适配。

减少布局嵌套的意义

进行应用性能优化最基本的一个方向就是减少布局的嵌套层次,这是很多开发者都知道的一个知识点,这里先来讲下这么做的意义所在。

以 FrameLayout 嵌套多个 TextView 为例,此时只有一层嵌套。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:orientation="vertical"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:padding="20dp"
        android:text="Jetpack Compose" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="业志" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="字节" />

</FrameLayout>

该布局有以下特点:

  • FrameLayout 的宽度是 wrap_content,即打算宽度由其子项来决定,子项的最大宽度是多少,FrameLayout 的宽度即是多少;
  • textView2 和 textView3 的宽度是 match_parent,即希望宽度占满整个 FrameLayout,FrameLayout 的宽度是多少,这两个 TextView 的宽度即是多少。

此时就出现了一个矛盾点: FrameLayout 的宽度由三个子项的最大宽度来决定,因此需要先测量出所有子项的最大宽度后才能确定自身的宽度。而 textView2 和 textView3 又希望宽度和 FrameLayout 保持一致,因此只有先测量出 FrameLayout 的宽度后才能决定自身的宽度。这就相当于陷入了一个死循环中。当然,FrameLayout 在进行 measure 时也已经考虑到这种情况了,应对策略就是进行两次 measure 操作,其 onMeasure 方法在逻辑上可以分为两步:第一步,FrameLayout 会按照正常流程来测量每一个子项。在这个过程中,如果 FrameLayout 的宽或高使用的是 wrap_content 的话,那么就将使用了 match_parent 的子项都保存到 mMatchParentChildren 中:

@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
    int count = getChildCount();
    //layout_width 或 layout_height 是否设置了 wrap_content
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //第一次测量子项的宽高
            measureChildWithMargins(child,widthMeasureSpec,heightMeasureSpec,0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState,child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    //将使用了 match_parent 的子项保存起来
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    ···

}

第二步,FrameLayout 会构建出新的 MeasureSpec 来重新测量 mMatchParentChildren,也即 textView2 和 textView3。此时这两个 TextView 就会得到一个新的 MeasureSpec,size 即第一步得到的 measuredWidth (需要减去 padding 和 margin),mode 变成了 EXACTLY,对应的也即 match_parent 这个属性值的含义:

@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {

    ···

    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0,getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width,MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,lp.width);
            }

            ···
            //第二次测量子项的宽高
            child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
        }
    }
}

所以说,在这种简单的嵌套结构中,父布局和子项之间的宽度是相互影响并通过双方来一起确定的,导致了需要先后执行多次 measure 操作才能完成布局要求: FrameLayout 一次,textView1 一次,textView2 和 textView3 各两次,总计六次。

而在实际开发中面对的情况其实会更加复杂:

  • 即使使用的不是 FrameLayout,改为 LinearLayout 一样会有这个问题。此外,虽然我们一般也不会用 wrap_content 来嵌套 match_parent,但如果搭配使用 LinearLayout 和 layout_weight 的话,测量次数也不止一次;

  • 如果将 textView2 和 textView3 替换为其它 ViewGroup 的话,将连锁导致 ViewGroup 内嵌的其它子项也要跟着一起重新测量;

  • 如果该布局是用于 Activity 的话,由于 ViewRootImpl 的 performTraversals() 会在 Activity 启动时被调用两次 (API Leave 31),因此 FrameLayout 需要测量两次,连锁导致 textView1 测量两次,textView2 和 textView3 各测量四次,总计十二次。

所以说,布局结构的嵌套层次越深,布局的测量次数和时间是会呈指数级增长的,从而连锁导致整个视图绘制到屏幕上的时间就越长,对用户来说应用的流畅度就越低,用户体验就越差。

布局模型

Jetpack Compose 作为一个全新的现代 Android 原生 UI 开发框架,从设计之初自然就会考虑如何避免原生 View 体系的弊端,因此从底层就直接限制了不允许进行多次测量,布局组件不能为了尝试不同的测量配置而多次测量任何子元素,否则将直接抛出 IllegalStateException。这使得我们可以根据需要进行深层次嵌套,此时测量次数也只会是线性增长,而不用担心会影响性能。

引用官方的例子来说明: Compose 布局基础知识https://developer.android.google.cn/jetpack/compose/layouts/basics

Jetpack Compose 的布局模型通过单次传递即可完成界面树的布局,其传递方式和原生 View 体系一样是通过递归的方式来实现。首先,系统会要求每个节点对自身进行测量,然后以递归方式完成所有子节点的测量,并将尺寸约束条件沿着树向下传递给子节点。再然后,确定叶节点的尺寸和放置位置,并将经过解析的尺寸和放置指令沿着树向上回传。简而言之,父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整。

举个例子,以下的SearchResult() 函数会生成相对应的界面树:

@Composable
fun SearchResult(...) {
  Row(...) {
    Image(...)
    Column(...) {
      Text(...)
      Text(..)
    }
  }
}

SearchResult
  Row
    Image
    Column
      Text
      Text

在 SearchResult 示例中,界面树布局遵循以下顺序:

  1. 系统要求根节点 Row 对自身进行测量;
  2. 根节点 Row 要求其第一个子节点 (即 Image) 进行测量;
  3. Image 是一个叶节点 (也就是说,它没有子节点),因此该节点会报告尺寸并返回放置指令;
  4. 根节点 Row 要求其第二个子节点 (即 Column) 进行测量;
  5. 节点 Column 要求其第一个子节点 Text 进行测量;
  6. 由于第一个节点 Text 是叶节点,因此该节点会报告尺寸并返回放置指令;
  7. 节点 Column 要求其第二个子节点 Text 进行测量;
  8. 由于第二个节点 Text 是叶节点,因此该节点会报告尺寸并返回放置指令;
  9. 现在,节点 Column 已测量其子节点,并已确定其子节点的尺寸和放置位置,接下来它可以确定自己的尺寸和放置位置了;
  10. 现在,根节点 Row 已测量其子节点,并已确定其子节点的尺寸和放置位置,接下来它可以确定自己的尺寸和放置位置了。

所以说,Jetpack Compose 在通过 Composition、Layout、Drawing 三个步骤 (也即组合、布局、绘制) 将状态转换为界面元素的过程中,Layout 阶段就用来确定每个节点的尺寸信息和位置信息。每个节点均需要先完成包含的所有子项的测量工作后才能确定自己的尺寸,在确定自身尺寸之后才能来决定子项的位置信息;而如果某个子项同时又内嵌了其它子项的话,该子项就需要根据以上步骤递归完成自身的测量工作。

SearchResult() 函数的 measure 和 place 顺序就如下图所示:

自定义布局

这里来实现一个自定义布局以便加强理解,就叫它 CustomLayout 吧,在其内部的每个子项都会按照声明顺序逐次往右下角放置,且边界线对齐。

CustomLayout 的使用方式和 Row、Column 等组件完全一样,随意嵌套即可。

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeCustomLayoutTheme {
                Surface(
                    modifier = Modifier
                        .fillMaxSize()
                        .wrapContentSize(align = Alignment.Center),color = MaterialTheme.colors.background
                ) {
                    CustomLayout()
                }
            }
        }
    }

}

@Composable
private fun CustomLayout() {
    CustomLayout(
        modifier = Modifier
            .background(color = Color.Yellow)
    ) {
        Spacer(
            modifier = Modifier
                .background(color = Color.Green)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Cyan)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Magenta)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Red)
                .size(size = 40.dp)
        )
    }
}

Jetpack Compose 的自定义布局是通过 Layout 方法来实现的:

@Composable
inline fun Layout(
    content: @Composable () -> Unit,modifier: Modifier = Modifier,measurePolicy: MeasurePolicy
)
  • content。布局组件所包含的所有子项;
  • modifier。对布局组件声明的 Modifier,可以通过该值来声明自定义布局的尺寸;
  • measurePolicy。测量策略,布局组件包含的所有子项的尺寸和位置都是在当中进行设置。

我们要实现各种自定义布局,最重要的一点就是要来计算各个子项应该获得的尺寸和位置,对应的逻辑都通过 MeasurePolicy 接口来实现,该接口一共包含五个方法,这里只要关注 measure 方法即可。

fun interface MeasurePolicy {

    fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult

}
  • measurables。每一个 Measurable 都对应自定义布局中的一个子项,同时也是子项的测量句柄,通过调用其内部的 measure(constraints: Constraints) 方法来对子项进行测量;
  • constraints。自定义的布局组件的父级对其的约束条件。

要实现 CustomLayout,基本的步骤有:

  1. measure 方法传入的 constraints 参数,代表的是 CustomLayout 的上一级对其的布局约束条件,包含的约束条件有 minWidth、maxWidth、minHeight、maxHeight,由于这里不想让上一级布局设定的 minWidth 和 minHeight 对子项产生影响,因此要将其重置为 0,或者说根据实际需要来构建一个新的 Constraints;
  2. CustomLayout 的整体宽高是通过累加内部所有子项的宽高而得来的,因此要先测量出每个子项的宽高后才能确定 CustomLayout 自身的宽高;
  3. 当确定了 CustomLayout 的宽高大小后,通过 MeasureScope.layout 方法来传递宽高信息;
  4. MeasureScope.layout 方法会提供一个 Placeable.PlacementScope 作用域,在这个作用域内部通过 placeable.place、placeable.placeRelative 等方法来放置每一个子项,即在这里计算每个子项在坐标系中的位置。

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,content: @Composable () -> Unit
) {
    Layout(
        content = content,modifier = modifier,measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,constraints: Constraints
            ): MeasureResult {
                if (measurables.isEmpty()) {
                    return layout(
                        constraints.minWidth,constraints.minHeight
                    ) {}
                }
                //第一步
                val contentConstraints = constraints.copy(minWidth = 0,minHeight = 0)
                val placeables = arrayOfNulls<Placeable>(measurables.size)
                var layoutWidth = 0
                var layoutHeight = 0
                //第二步,测量所有子项,累加所有子项的宽高值
                measurables.forEachIndexed { index,measurable ->
                    val placeable = measurable.measure(contentConstraints)
                    placeables[index] = placeable
                    layoutWidth += placeable.width
                    layoutHeight += placeable.height
                }
                //第三步,传递布局自身所占据的宽高
                return layout(layoutWidth,layoutHeight) {
                    var top = 0
                    var left = 0
                    //第四步,计算每个子项应该放置的坐标值
                    placeables.forEach { placeable ->
                        if (placeable != null) {
                            placeable.place(position = IntOffset(x = left,y = top))
                            top += placeable.height
                            left += placeable.width
                        }
                    }
                }
            }
        }
    )
}

固有特性测量

看完以上内容后,其实依然还是不知道 Jetpack Compose 是如何来避免多次测量的。需要进行多次测量,往往是因为父布局和子项的宽高需要两者来共同确定,这算作是比较常见的场景,这是由业务需求来决定的,无关乎我们使用的是什么 UI 框架。而在 Android 原生的 View 体系下,系统一开始的设计思路也决定了在 XML 声明的视图结构不可避免地需要进行多次测量。Jetpack Compose 作为现代的新型 Android 原生 UI 开发框架,在设计之初也自然会想着怎么用更高效的方式来实现这种业务场景。以 CustomLayout 为例,假设现在想要在 CustomLayout 之间插入一个分隔线 Divider。

Divider(
    color = Color.Black,modifier = Modifier
  .width(10.dp)
        .fillMaxHeight()
)

Divider 希望自身高度和父布局保持一致,这种情况就类似于文章开头给出的例子了: CustomLayout 的高度需要通过累加所有子项 (除了 Divider) 的高度得到,因此需要测量完所有的子项后才能决定自身高度,而 Divider 在测量阶段又希望直接指定为父布局的高度。

如果不对这种需求进行适配的话,Divider 的高度将等于 CustomLayout 能获得的最大高度,也即屏幕高度,因此 CustomLayout 整体视图最终就会超出整个屏幕。

为了解决这种问题,并避免需要多次测量,Jetpack Compose 提供的解决方案就是: 固有特性测量。官方对固有特性测量这个特性描述得不是很清晰,以下内容属于我自己的个人理解,也许会有点理解偏差,读者要有自己的判断。

还是以 CustomLayout 为例,先来看下固有特性测量的使用方式,这里主要就做了两点改动:

  • CustomLayout 通过 IntrinsicSize.Min 来声明自己期望按最小固有高度来进行显示,相对应的还有一个 IntrinsicSize.Max;
  • 为 CustomLayout 新增了一个扩展函数 matchParentHeight(),交给 Divider 使用,用于表明该子项想要直接占满父布局高度。
@Composable
private fun CustomLayout() {
    CustomLayout(
        modifier = Modifier
            .height(intrinsicSize = IntrinsicSize.Min)
            .background(color = Color.Yellow)
    ) {
        Spacer(
            modifier = Modifier
                .background(color = Color.Green)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Cyan)
                .size(size = 40.dp)
        )
        Divider(
            modifier = Modifier
                .width(width = 6.dp)
                .matchParentHeight(),color = Color.Gray
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Magenta)
                .size(size = 40.dp)
        )
        Spacer(
            modifier = Modifier
                .background(color = Color.Red)
                .size(size = 40.dp)
        )
    }
}

固有特性测量就用于满足父布局和子项的宽高需要两者来共同确定这种需求,其提供了一种机制用于在正式测量前能够让布局组件预先获取到子项对尺寸的期望值,这些期望值包括: 在特定的宽度下,子项能够正常显示的最小高度和最大高度分别是多少?以及在特定的高度下,子项能够正常显示的最小宽度和最大宽度又分别是多少?在拿到这些期望值后,也就知道了父布局在特定宽度或高度下能够正常显示的尺寸大小范围。

MeasurePolicy 中的四个 Intrinsic 方法就代表这些期望值的获取途径。例如,当我们使用了 height(intrinsicSize = IntrinsicSize.Min) 后,布局阶段就会来调用 minIntrinsicHeight 方法;使用了 height(intrinsicSize = IntrinsicSize.Max) 后,就会来调用 maxIntrinsicHeight 方法。


fun interface MeasurePolicy {

    fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult

    fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

    fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int

    fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

    fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int

}

这四个方法都有默认实现,但默认实现未必能满足我们的需求,我们需要根据实际需求来重写特定方法。例如,我们可以通过重写 minIntrinsicHeight 方法来计算出 CustomLayout 在特定 width 下能够正常显示的最小高度是多少。此时,除了调用了 matchParentHeight() 方法的子项之外,其它子项的 minIntrinsicHeight 累加之后的值即是 CustomLayout 能够接受的最小高度。

override fun IntrinsicMeasureScope.minIntrinsicHeight(
    measurables: List<IntrinsicMeasurable>,width: Int
): Int {
    var maxHeight = 0
    measurables.forEach {
        if (!it.matchParentHeight()) {
            maxHeight += it.minIntrinsicHeight(width)
        }
    }
    return maxHeight
}

重写这些方法后,就会影响 CustomLayout 获取到的 Constraints 中 minWidth、maxWidth、minHeight、maxHeight 等值的大小,使得 CustomLayout 能够感知到调用方允许自己显示的最小尺寸和最大尺寸。

在适配固有特性测量之前,由于 CustomLayout 是根布局,所以 Constraints 对应的最大尺寸即屏幕宽高,类似于 Constraints(minWidth = 0,maxWidth = 1080,minHeight = 0,maxHeight = 1776),因此 Divider 采用了 fillMaxHeight() 修饰后就会直接撑满整个屏幕高度。

在适配固有特性测量之后,由于 CustomLayout 使用了 IntrinsicSize.Min 来进行修饰,在语义上就是希望 CustomLayout 能够按照最小高度来进行显示,因此 Constraints 对应的最小和最大高度就变成了 minIntrinsicHeight 方法的返回值,类似于 Constraints(minWidth = 0,minHeight = 480,maxHeight = 480),此时 Divider 的高度就会直接固定为 480 px 了,而不会越界。

所以说,在适配了固有特性测量机制后,四个 Intrinsic 方法相当于是在正式开始 measure 之前进行的一次粗略测量,一次性计算出能接受的尺寸范围,CustomLayout 就无需在 measure 阶段来多次测量子项了,而是改为依靠 Intrinsic 方法来影响子项的测量结果,从而避免了多次测量。

适配固有特性测量

再来介绍下如何让 CustomLayout 适配固有特性测量。

为了能够识别出哪一个子项希望占满布局高度,就需要将这种 “期望” 传递给 CustomLayout,也即子项需要能够传参给到父布局。Jetpack Compose 通过 ParentDataModifier 来实现参数传递,我们在使用 Column 时声明的 weight 参数也是通过 ParentDataModifier 来传递的,IntrinsicMeasurable 接口内部就包含一个 Any? 类型的 parentData 参数。

CustomLayout 通过声明自己专属的 CustomLayoutParentData 类来作为参数载体,并通过扩展函数 matchParentHeight() 来传递参数值。

@LayoutScopeMarker
@Immutable
interface CustomLayoutScope {

    @Stable
    fun Modifier.matchParentHeight(): Modifier

}

private object CustomLayoutScopeInstance : CustomLayoutScope {

    override fun Modifier.matchParentHeight(): Modifier {
        return this.then(
            LayoutMatchParentHeightImpl(
                matchParentHeight = true,inspectorInfo = debugInspectorInfo {
                    name = "matchParentHeight"
                    value = true
                    properties["matchParentHeight"] = true
                }
            )
        )
    }

}

internal data class CustomLayoutParentData(
    val matchParentHeight: Boolean = false
)

internal class LayoutMatchParentHeightImpl(
    val matchParentHeight: Boolean,inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier,InspectorValueInfo(inspectorInfo) {

    override fun Density.modifyParentData(parentData: Any?): Any {
        return (parentData as? CustomLayoutParentData)
            ?: CustomLayoutParentData(matchParentHeight = matchParentHeight)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as LayoutMatchParentHeightImpl
        if (matchParentHeight != other.matchParentHeight) return false
        return true
    }

    override fun hashCode(): Int {
        return matchParentHeight.hashCode()
    }

    override fun toString(): String {
        return "LayoutMatchParentHeightImpl(matchParentHeight=$matchParentHeight)"
    }

}

之后只要判断 IntrinsicMeasurable 包含的 parentData 成员变量是否是 CustomLayoutParentData 即可获取到传参值 matchParentHeight。

private fun IntrinsicMeasurable.matchParentHeight(): Boolean {
    return (parentData as? CustomLayoutParentData)?.matchParentHeight ?: false
}

最终 CustomLayout 相对之前主要有以下改动:

  1. 为子项提供一个专属的特定布局作用域 CustomLayoutScope,确保 matchParentHeight() 方法只有 CustomLayout 的子项才能使用;

  2. Divider 会影响 CustomLayout 占据的整体宽度,但不会影响 CustomLayout 的整体高度,因此在进行 measure 时就要分情况来判断是否需要累加 layoutHeight;

  3. 在计算子项的坐标值时,除了 Divider 的 Y 坐标需要固定为 0 外,其它子项照旧。

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,content: @Composable CustomLayoutScope.() -> Unit
) {
    Layout(
        content = { CustomLayoutScopeInstance.content() },measurePolicy = object : MeasurePolicy {

            private fun IntrinsicMeasurable.matchParentHeight(): Boolean {
                return (parentData as? CustomLayoutParentData)?.matchParentHeight ?: false
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,width: Int
            ): Int {
                var maxHeight = 0
                measurables.forEach {
                    if (!it.matchParentHeight()) {
                        maxHeight += it.minIntrinsicHeight(width)
                    }
                }
                return maxHeight
            }

            override fun MeasureScope.measure(
                measurables: List<Measurable>,constraints.minHeight
                    ) {}
                }
                val contentConstraints = constraints.copy(minWidth = 0,minHeight = 0)
                val dividerConstraints = constraints.copy(minWidth = 0)
                val placeables = arrayOfNulls<Placeable>(measurables.size)
                val matchParentHeightChildren = mutableListOf<Placeable>()
                var layoutWidth = 0
                var layoutHeight = 0
                measurables.forEachIndexed { index,measurable ->
                    val placeable = if (measurable.matchParentHeight()) {
                        measurable.measure(dividerConstraints).apply {
                            layoutWidth += width
                            matchParentHeightChildren.add(this)
                        }
                    } else {
                        measurable.measure(contentConstraints).apply {
                            layoutWidth += width
                            layoutHeight += height
                        }
                    }
                    placeables[index] = placeable
                }
                return layout(layoutWidth,layoutHeight) {
                    var top = 0
                    var left = 0
                    placeables.forEach { placeable ->
                        placeable as Placeable
                        if (matchParentHeightChildren.contains(placeable)) {
                            placeable.place(position = IntOffset(x = left,y = 0))
                        } else {
                            placeable.place(position = IntOffset(x = left,y = top))
                            top += placeable.height
                        }
                        left += placeable.width
                    }
                }
            }
        }
    )
}

最终 CustomLayout 就可以正常显示 Divider 了。

结尾

Jetpack Compose 除了通过固有特性测量机制避免多次测量外,也少了将 XML 文件反射实例化为 View 的步骤,减少了 I/O 操作,这也是 Jetpack Compose 的一个性能优势点。

此外,对于我们的开发体验也有很大提升:

  • 从命令式转向了声明式,使得我们可以专注于状态管理,减少了出现问题的概率;
  • 少了很多割裂感,无需在各个 Java、Kotlin、XML 文件之间来回切换,不管是 UI 还是业务逻辑,都是直接 Kotlin 搞定 (但现阶段 Preview 功能感觉还是好慢);
  • 由于 Android 各个版本之间的差异性,同一套 View 体系代码经常会在不同系统版本上有着不同的风格,导致我们经常需要定义各种 style 和 theme 来保证 UI 统一,采用 Jetpack Compose 后就没有这种烦恼了,由其来抹平各个系统版本的差异性。

Jetpack Compose 目前更新得很快,随着后期的不断优化,相信也是会越来越好 ~

推荐

之前在学习这块内容的时候,随手整理了一份 Compose的学习笔记,有需要参考可以直接 点击这里查看获取方式传送门直达! 如下图中所示:

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具