美文网首页
Jetpack Compose 自定义布局以及固有特性测量

Jetpack Compose 自定义布局以及固有特性测量

作者: 今日Android | 来源:发表于2021-05-18 10:57 被阅读0次

    这篇文章我们会讲解下,Compose中如何去自定义布局,以及Intrinsic Measurement(官方翻译为固有特性测量)的理解跟使用。

    一:自定义布局

    自定义布局可以通过两种方式去处理,一种是使用布局修饰符Modifier.layout,一种是使用Layout去创建自定义布局。我们先来讲下Modifier.layout的方式

    1.1 使用布局修饰符来实现自定义布局

    我们自定义一个Modifier的扩展函数,Modifier.customCornerPosLayout。该方法的作用就是可以按我们传入的CornerPosition,去把view放置在左上角,左下角,右上角,右下角。代码如下:

    enum class CornerPosition{
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight
    }
    
    fun Modifier.customCornerPosLayout(pos:CornerPosition) = layout { measurable, constraints ->
        // Measure the composable
        val placeable = measurable.measure(constraints)
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Where the composable gets placed
            when(pos){
                CornerPosition.TopLeft->{
                    placeable.placeRelative(0, 0)
                }
                CornerPosition.TopRight->{
                    placeable.placeRelative(constraints.maxWidth-placeable.width, 0)
                }
                CornerPosition.BottomLeft->{
                    placeable.placeRelative(0, constraints.maxHeight-placeable.height)
                }
                CornerPosition.BottomRight->{
                    placeable.placeRelative(constraints.maxWidth-placeable.width, constraints.maxHeight-placeable.height)
                }
            }
        }
    }
    复制代码
    
    • 首先我们声明一个枚举。叫CornerPosition,枚举有四种取值,TopLeft表示左上角,TopRight右上角,BottomLeft左下角,BottomRight又下角。
    • 接着我们定义一个Modifier的扩展函数customCornerPosLayout,入参是pos:CornerPosition我们的枚举类
    • customCornerPosLayout的实现是通过layout的lamda去实现。其实是调用Modifier.layout去实现自定义布局。有两个参数一个是measurable,一个是constraints父控件的约束
    • 自定义布局第一步val placeable = measurable.measure(constraints) 是根据measurable跟constraints父控件的约束,通过方法measurable.measure(constraints)去生成一个placeable类。该类拥有子控件的宽width跟高height
    • 自定义布局的第二部 需要调用layout(w,h)的方法去设置当前view能使用的宽高。我们这里是用layout(constraints.maxWidth, constraints.maxHeight) 传入的是constraints.maxWidth父控件提供能使用的最大宽,constraints.maxHeight父控件提供能使用的最大高
    • 接着第三部就是在layout{}里通过 placeable.placeRelative 去放置该元素的位置。我们这里是根据传入的参数是左上角CornerPosition.TopLeft,还是左下角CornerPosition.BottomLeft,还是右上角CornerPosition.TopRight,还是右下角CornerPosition.BottomRight去放置该界面元素的位置。
    • placeable.placeRelative(0, 0) 左上角是0,0
    • placeable.placeRelative(constraints.maxWidth-placeable.width, 0) 右上角 x是需要拿最大宽度减去本身view的宽度,y是0
    • placeable.placeRelative(0, constraints.maxHeight-placeable.height) 左下角x是0,y是需要拿最大的高度减去本身的高度
    • placeable.placeRelative(constraints.maxWidth-placeable.width, constraints.maxHeight-placeable.height) x是需要拿最大宽度减去本身view的宽度,y需要拿最大的高度减去本身的高度

    举例:自定义完成之后,我们举个放置在右下角的例子

    @Preview
    @Composable
    fun customCornerPosLayoutTest(){
        Box(modifier = Modifier.size(100.dp)
            .background(color = Color.Red)) {
            Box(modifier = Modifier
                .customCornerPosLayout(CornerPosition.BottomRight)
                .size(10.dp)
                .background(color = Color.Blue, shape = CircleShape))
        }
    }
    复制代码
    
    效果如下: Screenshot_bottomright.jpg

    1.2 Layout创建自定义布局

    Modifier.layout修饰符仅更改调用的可组合项。如需测量和布置多个可组合项,请改用 Layout。在 View 系统中,创建自定义布局我们是会扩展 ViewGroup 并实现onMeasure和onLayout函数。在 Compose 中,我们只需使用 Layout 可组合项编写一个函数即可。
    下面我们通过Layout去自定义一个有方向的Column布局。直接上代码

    @Composable
    fun CustomColumnView(
        layoutDirection:LayoutDirection,
        modifier: Modifier = Modifier,
        content: @Composable() () -> Unit){
        Layout(
            modifier = modifier,
            content = content
        ) { measurables, constraints ->
           var totalHeight = 0
           var maxWidth = 0
           val placeables = measurables.map {
               val placeable = it.measure(constraints)
               totalHeight+=placeable.height
               if(placeable.width>maxWidth){
                   maxWidth = placeable.width
               }
               placeable
           }
           layout(maxWidth,totalHeight){
               if(layoutDirection == LayoutDirection.Ltr){
                   var y = 0
                   placeables.forEach {
                       it.place(0,y)
                       y+=it.height
                   }
               }else{
                   var y = totalHeight
                   placeables.forEach {
                       y-=it.height
                       it.place(0,y)
                   }
               }
           }
        }
    }
    复制代码
    
    • 我们定义了一个CustomColumnView的可组合函数

    • 定义了三个参数,layoutDirection去控制方向,modifier修饰符,内容可组合函数content。

    • 内部的实现我们是通过Layout去自定义布局

    • 测量每个子view的宽高 val placeable = it.measure(constraints) 自定义布局第一步需要通过父控件给的约束条件constraints跟measurable去执行measure测量。获得Placeable对象。该对象里可以获取到测量完成的每个子item的宽width跟高height。

    • 接着我们根据测量好的所有的子view的宽高,可以去计算出我们自定义CustomColumnView所需要的宽高,高我们用totalHeight变量先存储,宽我们用maxWidth变量存储

    • totalHeight+=placeable.height 我们把每个子View的高累加起来就是CustomColumnView控件的高。

    • if(placeable.width>maxWidth){maxWidth = placeable.width} 我们把子View最大的宽作为CustomColumnView的宽。

    • 计算完成CustomColumnView的宽高之后,我们需要调用layout(maxWidth,totalHeight)方法去设置CustomColumnView的宽高

    • 并且需要在layout方法里去调用placeable.place(x, y) 去放置子View的位置

      layout(maxWidth,totalHeight){
               if(layoutDirection == LayoutDirection.Ltr){
                   var y = 0
                   placeables.forEach {
                       it.place(0,y)
                       y+=it.height
                   }
               }else{
                   var y = totalHeight
                   placeables.forEach {
                       y-=it.height
                       it.place(0,y)
                   }
               }
           }
        }
      复制代码
      
    • 比如上面的例子中我们是根据方向layoutDirection == LayoutDirection.Ltr 我们就从上到下放置子View。it.place(0,y) y初始值是0,并且y是一直累加上上一个子View的高度

    • 否则我们就从下到上去放置子View,it.place(0,y) y初始值是totalHeight即CustomColumnView的高度也就是在最底部,并且y是一直去减去当前子View的高度。这样就能从底部往上放置了

    封装完成自定义的CustomColumnView后,我们举例使用 从上往下排列的CustomColumnView

    @Preview
    @Composable
    fun bottomToTopCustomColumnTest(){
        CustomColumnView(
            layoutDirection = LayoutDirection.Ltr,
            modifier = Modifier.background(color = Color.Red)
        ){
            Text(text = "第一个Text第一个Text第一个Text第一个Text")
            Text(text = "第二个Text")
            Text(text = "第三个Text")
            Text(text = "第四个Text")
            Text(text = "第五个Text第五个Text")
        }
    }
    复制代码
    
    效果如下: Screenshot_top_bottom.jpg

    从下往上排列的CustomColumnView

    @Preview
    @Composable
    fun bottomToTopCustomColumnTest(){
        CustomColumnView(
            layoutDirection = LayoutDirection.Rtl,
            modifier = Modifier.background(color = Color.Red)
        ){
            Text(text = "第一个Text第一个Text第一个Text第一个Text")
            Text(text = "第二个Text")
            Text(text = "第三个Text")
            Text(text = "第四个Text")
            Text(text = "第五个Text第五个Text")
        }
    }
    复制代码
    
    Screenshot_bottom_top.jpg

    二:Intrinsic Measurement(固有特性测量)

    以前在Android的View系统中,存在多次测量。所以在View系统中我们为了优化性能,要求减少布局层次的嵌套。而在Compose中,子项只能测量一次,测量两次就会引发运行时异常,所以在Compose中各种嵌套子项是不会影响性能的。Compose中为啥能做到不需要测量多次呢,就是因为有了这个Intrinsic Measurement(固有特性测量)。

    Intrinsic Measurement 是允许父项对子项测量之前,先让子项测量下自己的最大最小的尺寸。 使用官网的例子讲解:比如我们想要达到下面的效果 test.jpg

    我们的代码如下去写:

    @Preview
    @Composable
    fun twoViewTest() {
        Row() {
            Text(
                modifier = Modifier
                    .weight(1f)
                    .padding(start = 4.dp)
                    .wrapContentWidth(Alignment.Start),
                text = "Hi"
            )
    
            Divider(color = Color.Black, modifier = Modifier
                .fillMaxHeight()
                .width(1.dp))
    
            Text(
                modifier = Modifier
                    .weight(1f)
                    .padding(end = 4.dp)
                    .wrapContentWidth(Alignment.End),
    
                text = "there"
            )
        }
    }
    复制代码
    
    我们以为row的高度会自动的是Text的高度。而出来的效果却是如下: test.jpg

    之所以出现这种情况,是因为 Row 会逐个测量每个子项,并且 Text 的高度不能用于限制 Divider。为此,我们可以使用 height(IntrinsicSize.Min) 修饰符。我们使用IntrinsicSize.Min去告知使用的高度是子项本身的最小高度就可以了。代码如下修改:

    @Preview
    @Composable
    fun twoViewTest() {
        Row(modifier = Modifier.height(IntrinsicSize.Min)) {
            Text(
                modifier = Modifier
                    .weight(1f)
                    .padding(start = 4.dp)
                    .wrapContentWidth(Alignment.Start),
                text = "Hi"
            )
    
            Divider(color = Color.Black, modifier = Modifier
                .fillMaxHeight()
                .width(1.dp))
    
            Text(
                modifier = Modifier
                    .weight(1f)
                    .padding(end = 4.dp)
                    .wrapContentWidth(Alignment.End),
    
                text = "there"
            )
        }
    }
    复制代码
    
    这样就达到了我们所要的效果 test.jpg

    这就是固有特性的使用,他们可以提供IntrinsicSize.Min跟IntrinsicSize.Max。子项本身自己的最大最小的尺寸

    Jetpack compose在开源项目:https://github.com/Android-Alvin/Android-LearningNotes 中已收录,里面还包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中...

    相关文章

      网友评论

          本文标题:Jetpack Compose 自定义布局以及固有特性测量

          本文链接:https://www.haomeiwen.com/subject/zouejltx.html