Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(上)

简介: Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(上)

在上一篇中,我们不仅了解了 Compose 中的 Column、Row、Box 等几种常见的布局方式 还学习了 CompositionLocal 类在 Compose 中进行传值的方法;还有可快速搭建 App 结构的 Scaffold 脚手架组件,顺便学习了 Surface、Modifier 的一些使用,还有 ConstraintLayout 在Compose 中的使用方法。虽然官方提供了这么多 Compose 组件,但在实际需求开发中,定制化组件仍然必不可少。

在传统的 View 体系中,系统为开发者提供了许多可以直接使用的组件 View,比如:TextView、ImageView、RelativeLayout等。我们也可以通过自定义 View 来创建一些系统没有提供给我们的、具有特殊功能的 View。Compose 当然也不甘落后,在 Compose 中我们可以使用 Layout 组件来自定义我们自己的 Composable 组件。实际上,所有类似于 Column、Row 等组件底层都是用 Layout 进行扩展实现的。

在 View 体系中,自定义 View 最为常见的两种情况是:1)继承已有 View 进行功能扩展,例如继承 TextView 或直接继承 View 进行改写;2)继承 ViewGroup,并重写父类的 onMeasure 和 onLayout 方法。而在 Compose 中我们只需要简单地使用 Layout 组件自定义就可以了。

在开始之前,我们需要先了解一下 Layout Composable 组件的一些基础知识。


1. Compose 自定义 Layout 的基本原则


在 Compose 中,一个 Composable 方法被执行时,会被添加到 UI 树中,然后会被渲染展示在屏幕上。这个 Composable 方法我们可以看成是一个 View 系统中的布局,在 Compose 中称为 Layout。每个 Layout 都有一个 parent Layout 和 0 个或多个 children,这跟 View 体系很像。当然,这个 Layout 自身含有在它的 parent Layout 中的位置信息,包括位置坐标(x, y)和它的尺寸大小 widthheight

Layout 中的 children Layout 子元素会被调用去测量它们自身的大小,同时需要满足规定的 Constraints 约束。这些 Constraints 约束限制了 widthheight的最大值和最小值。当 Layout 把自己的 children Layout 测量完成之后,它自己的尺寸才会确定下来,又是递归。。。一旦一个 Layout 元素完成自身的测量,它就可以将自己的 children 根据 Constraints 约束在自己的空间中进行摆放了。是不是跟 View 体系一样?先测量后摆放。

OK,最重要的来了!Compose UI 不允许多次测量。 Layout 元素为了尝试不同的测量设置,它不能多次测量其任何子元素。单次测量(Single-pass measurement)当然会提升渲染效率,尤其是在 Compose 处理深度较大的 UI 树时。如果一个 Layout 元素需要测量两次它的所有子元素,子元素中的子元素就会被测量四次,以此类推,测量的次数就会随着布局深度成指数级增长!其实 View 体系就是这样的,所以在 View 体系中开发一定要减少布局的层数!不然在需要重复测量的情况下,渲染效率将会及其低下。所以 Compose 中才做了不允许多次测量的限制,然而,在有些场景下,我们又是需要获取到子元素多次测量并获取信息的。对于这些情况,还是有方法做到多次测量的,限于篇幅原因,后面有空再说~

Compose 中自定义一个控件(官方称之为 Layout)也有两种情况:

  1. 自定义 Layout 没有其他子元素,就只是它自己本身,类似于 View 体系中的 “自定义View”;
  2. 自定义 Layout 有子元素,需要考虑子元素的摆放位置,类似于 View 体系中的 “自定义ViewGroup”。

我们先来看第一种情况。


2. Compose 自定义一个 “View”


Compose 中的自定义 Layout 跟 View 体系是很不同的。我们需要自定义的 Layout 居然就是自定义一个 Modifier 属性!就是去自己实现 Modifier 中 Layout 方法,去实现如何测量以及放置它自己本身即可。一个常见的自定义 Layout Modifier 的结构代码如下:

// code 1
fun Modifier.customLayoutModifier(...) {    // 可以自定义一些属性
    Modifier.layout { measurable, constraints ->
        ...    // 在这里需要自己实现 测量 和 放置的方法
    }
}

可以看出来,关键就是 Modifier.layout 方法,它有两个 lambda 表达式:

  1. measurable:用于子元素的测量和位置放置的;
  2. constraints:用于约束子元素 width 和 height 的最大值和最小值。

举个简单的栗子进行说明。一个普通的 Text 组件只能调整文案的边缘离 Text 组件上下左右四边缘的距离,例如图1所示。这个 Text 只能设置四周的 padding 值,上下我设置的 15dp,左右设置的 30dp。

image.png

如果我想控制文案的底部 baseline 离 Text 上边距的距离呢?啥是底部 baseline?这就需要了解一下 Android 在绘制文案时的算法了。

image.png

从图 2 可以看出,Android 绘制文案时,baseline 决定了文案主体的底部位置。Compose 中的 Text 只能通过 Modifier.padding 设置 leading 离 Text 组件顶部的距离。而这里我们自定义的 Layout 需要满足可设置 Baseline 离 Text 顶部的距离。即下图图 3 中上方的效果,怎么做呢?

image.png

首先当然就是测量啦,记住 Layout 只能测量它的子元素一次。在 code1 中调用 measure 方法,就可以测量了:

// code 2
fun Modifier.firstBaselineToTop(  // firstBaselineToTop 就是你自定义的 modifier 的方法名
    firstBaselineToTop: Dp    // 自定义 modifier 方法中的参数,这里就是一个
) = this.then(
    layout { measurable, constraints -> // 调用 layout 方法去测量和放置子元素组件
        val placeable = measurable.measure(constraints) // 首先是测量
        ...
    }
)

当调用 measurable 的 measure 方法后,就会返回一个 Placeable 对象。在这里,我们可以将 layout 中的 constraints 约束条件传递给 measure 方法,或者传入我们自定义的约束条件的 lambda。因为在这个场景下我们不需要再去对测量进行任何的限制,所以直接传入 layout 中给的 constraints 即可。总之,这一步就是为了得到这个 Placeable 对象,拿到这个之后就可以在后面调用 Placeable 对象的 placeRelative 方法对子元素进行位置的摆放了!

OK,现在已经对 Composable 组件进行了测量,然后我们就可以调用 layout(width, height) 方法去根据测量的尺寸来放置内容。width 不用求,直接用测量得来的 width 就行,关键就是如何求出传入 layout 方法的 height 值,看代码再来说吧:

// code 3
fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        // 检查这个 Composable 组件是否存在 FirstBaseline
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        // 存在的情况下,获取 FirstBaseline 离 Composable 组件顶部的距离
        val firstBaseline = placeable[FirstBaseline]
        // 计算 Y 轴方向上 Composable 组件的放置位置
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        // 计算得出此 Composable 组件真正的 height 值
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            ...
        }
    }
)

说实话最初看到这段代码也是懵逼了好久。。。 首先 check 方法类似于一个 assert 断言,如果里面的结果是 false 则会抛出一个 IllegalStateException 异常。这里是检查下被我们自定义的 Modifier 修饰的 Composable 组件是否存在 FirstBaseline 属性,Text 组件里是存在 baseline 的,如果不存在当然就不能用我们自定义的这个 firstBaselineToTop Modifier了。

存在的情况下,再去获取这个 Baseline 与 此组件顶部的距离,也就是图4 中 c 的长度。图中蓝色框代表的是普通的 Text 组件所占的空间位置;黑色框代表的是屏幕边缘;红色虚线代表的是 Text 中的 Baseline。a 表示的就是我们自定义的 Modifier.firstBaselineToTop 方法的 firstBaselintToTop 参数。我们的目标就是可以根据传入的 firstBaselintToTop 参数计算出 Text 组件在 Y 轴上的摆放位置,以及真正的 width 和 height 值大小。

image.png

之前在 layout 方法中调用了 measurable 的 measure 方法测量的是普通 Text 组件的宽高,即图4 中蓝色框的宽高,而我们自定义的 Layout 的宽高则是图中用橙色和绿色标注的宽高尺寸。width 直接由 Placeable 对象就可获得(placeable.width),而高度由示意图可以得出计算方法:height = placeable.height + d,即普通 Text 的高度再加上 d,d = a - c,即 d = firstBaselintToTop  - baseline。所以,d 就是 placeableY 参数。终于看懂 code 3 了,原来就是为了算出自定义 Layout 的 width 和 height,然后通过 layout 方法进行设置啊!

接下来就是位置的放置了。调用 Placeable 对象的 placeRelative 方法即可:

// code 4
fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    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(0, placeableY)
        }
    }
)

注意,自定义 Layout 必须调用 placeRelative 方法,否则该自定义 Layout 将不可见。 placeRelative 方法会根据当前的 layoutDirection 布局方向对自定义 Layout 自动进行位置调整。在这里我们自定义的 Layout 摆放比较简单,就是 Y 轴上有个偏移量,X 轴上没有偏移,看图2 也可直观得知。

那么如何使用呢?想必你们也猜到了,就跟之前使用其他 Modifier 方法修饰 Text 或其他 Composable 组件一样使用就好:

// code 5
@Composable
fun CustomLayoutDemo() {
    Row {
        Text(
            text = "我是栗子1",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 20.sp
        )
        Spacer(modifier = Modifier.width(20.dp))
        Text(
            text = "我是栗子2",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 15.sp
        )
        Spacer(modifier = Modifier.width(20.dp))
        Text(
            text = "我是栗子3",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 30.sp
        )
    }
}

image.png

在 code 5 中分别展示了 3 个 Text,都使用了我们自定义的 Modifier 修饰符 firstBaselineToTop,且设置的参数都是 40dp,不同的是字号。从图 5 的显示效果来看,达到了我们想要的自定义 Layout 的效果,即虽然字号大小不同,但是每个 Text 中文案的 Baseline 离自定义 Layout 的顶部距离是一样的。

目录
相关文章
|
11天前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
26 4
|
2月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
3月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
3月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
3月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
3月前
深入了解 Jetpack Compose 中的 Modifier
深入了解 Jetpack Compose 中的 Modifier
|
3月前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
|
4月前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。
|
3月前
|
数据管理 API 数据库
探索Android Jetpack:现代安卓开发的利器
Android Jetpack是谷歌为简化和优化安卓应用开发而推出的一套高级组件库。本文深入探讨了Jetpack的主要构成及其在应用开发中的实际运用,展示了如何通过使用这些工具来提升开发效率和应用性能。
|
2月前
|
存储 数据库 Android开发
🔥Android Jetpack全解析!拥抱Google官方库,让你的开发之旅更加顺畅无阻!🚀
【7月更文挑战第28天】在Android开发中追求高效稳定的路径?Android Jetpack作为Google官方库集合,是你的理想选择。它包含多个独立又协同工作的库,覆盖UI到安全性等多个领域,旨在减少样板代码,提高开发效率与应用质量。Jetpack核心组件如LiveData、ViewModel、Room等简化了数据绑定、状态保存及数据库操作。引入Jetpack只需在`build.gradle`中添加依赖。例如,使用Room进行数据库操作变得异常简单,从定义实体到实现CRUD操作,一切尽在掌握之中。拥抱Jetpack,提升开发效率,构建高质量应用!
50 4