手把手教你使用Jetpack Compose完成你的自定义Layout

Posted Android开发骆驼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你使用Jetpack Compose完成你的自定义Layout相关的知识,希望对你有一定的参考价值。

不会吧,不会吧,现在还有不了解 Jetpack Compose 的移动开发?

Jetpack Compose是谷歌在2019Google i/o大会上发布的新的库,是用于构建原生android UI的现代工具包。他有强大的工具和直观的Kotlin API,简化并加速了Android上的UI开发。可以帮助开发者用更少更直观的代码创建View,还有更强大的功能,以及还能提高开发速度。

今天主要是和大家分享一篇教大家使用Jetpack Compose完成你的自定义Layout的技术文。
原文地址:https://juejin.cn/post/6963262309699420173
ps:本文示例代码与图片来自Jetpack Compose代码实验室

目前有一个正在进行的Jetpack Compose中文手册项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家进行关注与加入!
项目地址:https://github.com/compose-museum/jetpack-compose-tutorial
这篇文章由本人撰写,目前已经发布到该手册中,欢迎进行查阅。

概述

Compose已经内置了许多组件,诸如Column,Row,Box等。开发者可以通过这些组合这些已有的组件来定制自己的专属组件。

就像在传统View系统中,当LinearLayout等基础布局无法满足你的需求时,你可以通过重写measure与layout来达成你的期望。Compose沿用了这一理念,在一些场景下如果Compose内置组件可能无法满足你的需求,可以尝试通过定制测量与布局过程来完成需求。事实上,Compose内置组件也是通过定制Layout来达成的,只是一个更高层次的封装。

在学习如何定制Layout之前,我们需要先了解下Compose的布局原理。

Compose布局原理

composable被调用时会将自身包含的UI元素添加到UI树中并在屏幕上被渲染出来。每个UI元素都有一个父元素,可能会包含零至多个子元素。每个元素都有一个相对其父元素的内部位置和尺寸。

每个元素都会被要求根据父元素的约束来进行自我测量(类似传统View中的MeasureSpec),约束中包含了父元素允许子元素的最大宽度与高度和最小宽度与高度,当父元素想要强制子元素宽高为固定值时,其对应的最大值与最小值就是相同的。

对于一些包含多个子元素的UI元素,需要测量每一个子元素从而确定当前UI元素自身的大小。并且在每个子元素自我测量后,当前UI元素可以根据其所需要的宽度与高度进行在自己内部进行放置

**Compose UI 不允许多次测量,当前UI元素的每一个子元素均不能被重复进行测量,换句话说就是每个子元素只允许被测量一次。**这样做的好处是什么?这样做的好处是为了提高性能。在传统View系统中一个UI元素允许多次测量子元素,我们假设对子元素测量两次,而该子元素可能又对其子元素又测量了两次,总体上当前UI元素重新测量一次,则孙子元素就需要测量四次,测量次数会随着深度而指数级上升。以此类推,那么一次布局整颗UI树都需要做大量的工作,很难保持应用程序的良好性能。 为避免传统View系统测量布局的性能陷阱,Compose限制了每个子元素的测量次数,可以高效处理深度比较大的UI树(极端情况是退化成链表的树形结构)。但是在有些场景下,多次测量子元素是有意义的,我们是需要获取到子元素多次测量的信息的。对于这些情况,有办法做到这一点,我们将在后面讨论。

使用Layout Modifier

使用 Modifier.layout() 手动控制元素的测量和布局。通常layout修饰符的使用方法像下面这样。

fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
  ...
})

当使用layout修饰符时,你传入的回调lambda需要包含两个参数:measurable、constraints

measurable:子元素的测量句柄,通过提供的api完成测量与布局过程

constraints: 子元素的测量约束,包括宽度与高度的最大值与最小值。

Layout Modifier使用示例

有时你想在屏幕上展示一段文本信息,通常你会使用到Compose内置的Text组件。单单显示文本是不够的,你希望指定Text顶部到文本基线的高度,让文本看的更自然一些。使用内置的padding修饰符是无法满足你的需求的,他只能指定Text顶部到文本顶部的高度,此时你就需要使用到layout修饰符了。

我们首先创建一个 firstBaselineToTop 修饰符

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  ...
}

正如我们在Compose布局原理中所提到的,每个子元素只允许被测量一次。

通过使用 measurable.measure(constraints)完成子元素的测量,如果将lambda的constraints直接传入则意味着你将父元素给当前元素的限制直接提供了当前元素的子元素,自身没有增加任何额外的限制。子元素测量的结果被包装在一个 Placeable 实例中,可通过该Placeable 实例获取子元素测量结果。

在我们的示例中当前Text元素也不对子元素进行额外限制。

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  val placeable = measurable.measure(constraints)
  ...
}

现在子元素已经完成了测量流程,你需要计算当前元素的打算并通过 layout(width, height) 方法对当前元素的宽度与高度进行指定。并将子元素的布局流程写入在 layout(width, height)的lambda参数中。

在我们的示例中当前Text元素的宽度则是文本宽度,而高度则是我们指定的Text顶部到文本基线高度与文本基线到Text底部的高度之和。

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  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(x, y) 来完成子元素的布局流程,这是必要的。placeRelative 会根据当前 layoutDirection 自动调整子元素的位置。

在我们的示例中,当前子元素的横向坐标相对当前元素为零,而纵向坐标则为Text组件顶部到文本顶部的距离。

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  ...
  val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
  val height = placeable.height + placeableY
  layout(placeable.width, height) {
    placeable.placeRelative(0, placeableY)
  }
}

为预览布局结果,我们创建了两个预览视图。

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.firstBaselineToTop(24.dp))
  }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.padding(top = 24.dp))
  }
}

预览效果

使用Layout Composable

Layout Modifier会将当前元素的所有子元素视作为整体进行统一的测量与布局,多适用于统一处理的场景。然而我们有时是需要精细化测量布局每一个子组件,这需要我们进行完全的自定义Layout。这类似于传统View系统中定制View与ViewGroup测量布局流程的区别。对于定制“ViewGroup”的场景,我们应该使用Layout Composable了。首先我们需要创建一个Layout Composable。

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    // custom layout attributes 
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

可以看到,Layout需要填写三个参数:modifier,content,measurePolicy

modifier:由外部传入的修饰符,会决定该UI元素的constraints

content:在content中声明所有子元素信息

measurePolicy:默认场景下只实现measure即可,上面示例中最后传入的lambda就是measure的实现。当你想要为你的Layout Composable适配Intrinsics时(官方中文翻译为固有特性测量),则需要重写 minIntrinsicWidthminIntrinsicHeightmaxIntrinsicWidthmaxIntrinsicHeight 方法,有关于固有特性测量的文章后续会更新,请持续关注。

Layout Composable使用示例

我们可以通过Layout Composable定制一个自己专属的Column,首先我们需要声明这个Composable。

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

和Layout Modifier一样,我们需要对所有子组件进行一次测量。切记,每个子元素只允许被测量一次。

与Layout Modifier不同的是,这里的measurables是一个List,而Layout Modifier则只是一个measurable,因为他将所有子元素看作了一个整体。

在我们的示例中仍然不对子元素进行额外限制,最终将每次测量的结果保存到placeables这个List中。

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        val placeables = measurables.map { measurable ->
            // Measure each child
            measurable.measure(constraints)
        }
    }
}

现在在将这些子元素布局之前,你需要计算当前定制column所应该占用的屏幕宽度与高度。这样为了出于简单考虑,选择将宽度与高度设置为其父元素所允许的最大高度与宽度。与Layout Modifier一样通过 layout(width, height) 方法对当前元素的宽度与高度进行指定。

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        ...
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Place children
        }
    }
}

具体子元素的布局也与Layout Modifier是相同的。作为Column是需要将子元素进行垂直排列的,所以我们仅需指定每一个子元素的顶部相对位置即可。

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { 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
            }
        }
    }
}

为预览布局结果,我们创建了预览视图,创建自己定制的Column,并添加了一些子元素。

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    MyOwnColumn(modifier.padding(8.dp)) {
        Text("MyOwnColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

预览效果

Jetpack Compose 完全开发手册

接下来,我将会给大家介绍一份2021年最新整理出来的《Jetpack Compose 完全开发手册》,手把手教大家Jetpack Compose从入门到精通。

这份资料旨在给希望了解、学习、应用Android Jetpack Compose的小伙伴一个参考资料。

有需要的朋友可以【点击此处】或者通过下方代码块,找我免费领取。

//wechat number:
study5233

资料详情

第一章 初识 Jetpack Compose

  1. 为什么我们需要一个新的UI 工具?

  2. Jetpack Compose的着重点
    加速开发
    强大的UI工具
    直观的Kotlin API

  3. API 设计

  4. Compose API 的原则
    一切都是函数
    顶层函数(Top-level function)
    组合优于继承
    信任单一来源

  5. 深入了解Compose
    Core
    Foundation
    Material

  6. 插槽API

第二章 Jetpack Compose构建Android UI

  1. Android Jetpack Compose 最全上手指南
    Jetpack Compose 环境准备和Hello World
    布局
    使用Material design 设计
    Compose 布局实时预览
    ……

  2. 深入详解 Jetpack Compose | 优化 UI 构建
    Compose 所解决的问题
    Composable 函数剖析
    声明式 UI
    组合 vs 继承
    封装
    重组
    ……

  3. 深入详解 Jetpack Compose | 实现原理
    @Composable 注解意味着什么?
    执行模式
    Positional Memoization (位置记忆化)
    存储参数
    重组
    ……

第三章 Jetpack Compose 项目实战演练(附Demo)

  1. Jetpack Compose应用1
    开始前的准备
    创建DEMO
    遇到的问题

  2. Jetpack Compose应用2

  3. Jetpack Compose应用做一个倒计时器
    数据结构
    倒计时功能
    状态模式
    Compose 布局
    绘制时钟

  4. 用Jetpack Compose写一个玩安卓App
    准备工作
    引入依赖
    新建 Activity
    创建 Compose
    PlayTheme
    画页面
    底部导航栏
    管理状态
    添加页面

  5. 用Compose Android 写一个天气应用
    开篇
    画页面
    画背景
    画内容
    ……

  6. 用Compose快速打造一个“电影App”
    成品
    实现方案
    实战
    不足
    ……

有需要的朋友可以【点击此处】或者通过下方代码块找我免费领取。

//wechat number:
study5233

希望这份资料可以给希望了解、学习、应用Android Jetpack Compose的小伙伴一个参考。

以上是关于手把手教你使用Jetpack Compose完成你的自定义Layout的主要内容,如果未能解决你的问题,请参考以下文章

手把手教你如何通过docker-compose部署Zabbix监控

使用 Jetpack Compose 完成自定义绘制

什么是Jetpack Compose?带你走进Jetpack Compose~

使用Jetpack Compose完成自定义手势处理

Jetpack Compose实践:完成自定义手势处理

Jetpack All In Compose ?看各种Jetpack库在Compose中的使用