不久前,Jetpack Compose 发布了 1.3.0 正式版。经过一年多的发展,再回头去看,Compose 总算带来了缺失已久的瀑布流布局以及DrawScope.drawText方法。本文就简单介绍一下。
截止此文写作时,Jetpack Compose 的最新 stable 版别为 1.3.1,而查阅 Compose 与 Kotlin 的兼容性对应关系 文档可知,此版别对应的 Kotlin 版别为 1.7.10。如需尝试部分代码,请保证对应版别设置正确。

BOM

Compose Bill of Materials 是 Compose 最近带来的新东西,它能帮你指定 Compose 各种库的版别,保证各个 Compose 相关的库是项目兼容的(但并不引进对应的库)。具体来说,当你在 build.gradle 中引进 BOM

// Import the Compose BOM
implementation platform('androidx.compose:compose-bom:2022.10.00')

再引进其它 Compose 相关的库就不需求手动指定版别号了,它们会由 BOM 指定

implementation "androidx.compose.ui:ui"
implementation "androidx.compose.material:material"
implementation "androidx.compose.ui:ui-tooling-preview"

BOM 指定的版别都是稳定版,你也能够选择覆写部分版别到 alpha 版别,如下:

// Override Material Design 3 library version with a pre-release version
implementation 'androidx.compose.material3:material3:1.1.0-alpha01'

需求留意的是,这样或许会使部分其它的 Compose 库也晋级为对应的 alpha 版别,以保证兼容性。
BOM 和 库版别 的映射能够在 Quick start | Jetpack Compose | Android Developers 找到,现在的两个版别对应如下

Library group Version in 2022.10.00 Version in 2022.11.00
androidx.compose.animation:animation 1.3.0 1.3.1
androidx.compose.animation:animation-core 1.3.0 1.3.1
androidx.compose.animation:animation-graphics 1.3.0 1.3.1
androidx.compose.foundation:foundation 1.3.0 1.3.1
androidx.compose.foundation:foundation-layout 1.3.0 1.3.1
androidx.compose.material:material 1.3.0 1.3.1
androidx.compose.material:material-icons-core 1.3.0 1.3.1
androidx.compose.material:material-icons-extended 1.3.0 1.3.1
androidx.compose.material:material-ripple 1.3.0 1.3.1
androidx.compose.material3:material3 1.0.0 1.0.1
androidx.compose.material3:material3-window-size-class 1.0.0 1.0.1
androidx.compose.runtime:runtime 1.3.0 1.3.1
androidx.compose.runtime:runtime-livedata 1.3.0 1.3.1
androidx.compose.runtime:runtime-rxjava2 1.3.0 1.3.1
androidx.compose.runtime:runtime-rxjava3 1.3.0 1.3.1
androidx.compose.runtime:runtime-saveable 1.3.0 1.3.1
androidx.compose.ui:ui 1.3.0 1.3.1
androidx.compose.ui:ui-geometry 1.3.0 1.3.1
androidx.compose.ui:ui-graphics 1.3.0 1.3.1
androidx.compose.ui:ui-test 1.3.0 1.3.1
androidx.compose.ui:ui-test-junit4 1.3.0 1.3.1
androidx.compose.ui:ui-test-manifest 1.3.0 1.3.1
androidx.compose.ui:ui-text 1.3.0 1.3.1
androidx.compose.ui:ui-text-google-fonts 1.3.0 1.3.1
androidx.compose.ui:ui-tooling 1.3.0 1.3.1
androidx.compose.ui:ui-tooling-data 1.3.0 1.3.1
androidx.compose.ui:ui-tooling-preview 1.3.0 1.3.1
androidx.compose.ui:ui-unit 1.3.0 1.3.1
androidx.compose.ui:ui-util 1.3.0 1.3.1
androidx.compose.ui:ui-viewbinding 1.3.0 1.3.1

瀑布流布局

在 Jetpack Compose 1.0 正式版发布一年多后,瀑布流组件总算是姗姗来迟。现在,此组件的用法与 LazyGrid 保持了高度一致,而后者我现已在 Jetpack Compose LazyGrid运用全解 做过具体演示。此处不做过多赘述,示例如下:

// 纵向,横向的对应 Horizontal...
LazyVerticalStaggeredGrid(
    // columns 参数类似于 LazyVerticalGrid
    columns = StaggeredGridCells.Fixed(2),
    // 全体内边距
    contentPadding = PaddingValues(8.dp, 8.dp),
    // item 和 item 之间的纵向距离
    verticalArrangement = Arrangement.spacedBy(4.dp),
    // item 和 item 之间的横向距离
    horizontalArrangement = Arrangement.spacedBy(8.dp)
){
    itemsIndexed(pages, key = { _, p -> p.first }){ i, pair ->
        ...
    }
}

效果如下

Jetpack Compose 上新:瀑布流布局、下拉加载、DrawScope.drawText

上面的完整代码能够在我的项目 FunnySaltyFish/JetpackComposeStudy: 自己 Jetpack Compose 主题文章所包括的示例,包括自定义布局、部分组件用法等 找到,也是乘此机会整理了下之前文章出现的比如,便利检查

下拉改写

运用

新增的 Modifier.pullRefresh 能够用于下拉改写的完成。它的签名如下:

fun Modifier.pullRefresh(
    state: PullRefreshState,
    enabled: Boolean = true
) 

第一个参数用于存储下拉的进展,第二个代表是否启用。相关联的这个 State 天然也有对应的 remember 方法用于创立

/**
 * 创立一个被 remember 的[PullRefreshState
 *
 * 对 [refreshing] 的更改会更新 [PullRefreshState].
 *
 * @sample androidx.compose.material.samples.PullRefreshSample
 *
 * @param refreshing 布尔值,代表当时是否正在改写
 * @param onRefresh 改写时的回调
 * @param refreshThreshold 若超越此阈值,则放手后会触发 [onRefresh]
 * @param refreshingOffset 改写时指示器的底部位置
 */
@Composable
@ExperimentalMaterialApi
fun rememberPullRefreshState(
    refreshing: Boolean,
    onRefresh: () -> Unit,
    refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold, // 80.dp
    refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset, // 56.dp
): PullRefreshState

归纳运用,示例代码如下

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeToRefreshTest(
    modifier: Modifier = Modifier
) {
    val list = remember {
        List(4){ "Item $it" }.toMutableStateList()
    }
    var refreshing by remember {
        mutableStateOf(false)
    }
    // 用协程模拟一个耗时加载
    val scope = rememberCoroutineScope()
    val state = rememberPullRefreshState(refreshing = refreshing, onRefresh = {     
        scope.launch {
            refreshing = true
            delay(1000) // 模拟数据加载
            list+="Item ${list.size+1}"
            refreshing = false
        }
    })
    Box(modifier = modifier
        .fillMaxSize()
        .pullRefresh(state)
    ){
        LazyColumn(Modifier.fillMaxWidth()){
            // ...
        }
        PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
    }
}

Jetpack Compose 上新:瀑布流布局、下拉加载、DrawScope.drawText

上面的代码并不难理解,用 modifier.pullRefresh 将下拉的相关数值存在 state 中,之后 PullRefreshIndicator 再运用就行了。二者用 Box 堆叠。

完成原理

这个控件的源代码也异常简单,最终是根据 nestedScrollConnection(嵌套滑动)完成的

@ExperimentalMaterialApi
fun Modifier.pullRefresh(
    onPull: (pullDelta: Float) -> Float,
    onRelease: suspend (flingVelocity: Float) -> Unit,
    enabled: Boolean = true
) = Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))

关于嵌套滑动,RugerMc 佬在很早前就写过文章,能够前往 嵌套滑动(NestedScroll) | 你好 Compose 阅读。这篇文章里也完成了下拉改写,并给出了伸缩 ToolBar 的完成。
假如你懒得跳过去,简而言之,经过 NestedScrollConnection ,咱们能够在滑动开端前/后拿到当时的偏移量、速度等信息,按状况提早消费或放着不管他。针对下拉改写的状况,咱们首要干这两件事:

  1. 当咱们手指向下滑时,咱们期望滑动手势首要交给子布局中的列表进行处理,假如列表现已滑到顶部阐明此刻滑动手势事件没有被消费,此刻再交由父布局进行消费。父布局能够消费列表消费剩下的滑动手势事件(为加载动画增加偏移)。
  2. 当咱们手指向上滑时,咱们期望滑动手势首要被父布局消费(为加载动画减小偏移),假如加载动画本身仍未出现时,则不进行消费。然后将剩下的滑动手势交给子布局列表进行消费。

完成起来并不难

private class PullRefreshNestedScrollConnection(
    private val onPull: (pullDelta: Float) -> Float,
    private val onRelease: suspend (flingVelocity: Float) -> Unit,
    private val enabled: Boolean
) : NestedScrollConnection {
    override fun onPreScroll(
        available: Offset,
        source: NestedScrollSource
    ): Offset = when {
        !enabled -> Offset.Zero
        // 向上滑动,父布局先处理(收回偏移),走 onPull 回调,并根据处理成果返回被消费掉的 Offset
        source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
        else -> Offset.Zero
    }
    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset = when {
        !enabled -> Offset.Zero
        // 向下滑动,假如子布局处理完了还有剩余(拉到顶了还往下拉),就展示偏移
        source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
        else -> Offset.Zero
    }
    override suspend fun onPreFling(available: Velocity): Velocity {
        onRelease(available.y)
        return Velocity.Zero
    }
}

DrawScope.drawText

从前 Compose 的 Canvas 内部,假如需求画文字,就需求 canvas.nativeCanvas 先获取到原生的 android.graphics.Canvas 再调用对应方法。现在总算有 drawText 方法了。
现在给出了两种共四个 API (别离对应 textLayoutResult 和 textMeasurer 两类参数)

Jetpack Compose 上新:瀑布流布局、下拉加载、DrawScope.drawText

接下来让咱们尝试运用一下,先试试 textMeasurer 参数的

@OptIn(ExperimentalTextApi::class)
@Composable
fun DrawTextTest() {
    val textMeasurer = rememberTextMeasurer(cacheSize = 8)
    Canvas(modifier = Modifier.fillMaxSize()){
        drawText(textMeasurer, "Hello World\n This is a simple text", style = TextStyle(color = Color.Black))
    }
}

效果很直接

Jetpack Compose 上新:瀑布流布局、下拉加载、DrawScope.drawText

读称号能够知道,TextMeasurer 担任对文本进行丈量,此类的注释大致如下:

TextMeasurer担任丈量整个文本,以便准备制作。
应经过 androidx.compose.ui.rememberTextMeasurer 在 @Composable 中创立 TextMeasurer 实例,以便从 Composable 上下文中接收到默认值
文本布局是一项计算成本高昂的任务。因此,该类运用内部的 LRU 缓存保存 layout 输入和输出对,以优化运用相同输入参数时的重复调用。
尽管大多数输入参数对布局有直接影响,但部分能够在布局过程中被忽略,如色彩、笔刷和暗影,并在最后进行设置。将 TextMeasurer 与恰当的 cacheSize 一起运用,在为不影响布局的属性(如色彩)设置动画时,应该会有显著的改进。
此外,假如需求出现多个静态文本,您能够按cacheSize供给文本的数量,并缓存它们的layout以供重复调用。请留意,即使对输入参数(如fontSize、maxLines、文本中的一个附加字符)进行轻微更改,也会创立一组不同的输入参数。这将计算新的layout,并将一组新的输入和输出对放置在 LRU 缓存中。旧成果或许会被遗弃。
……

读读注释,能感觉到这个类存在的含义:丈量文本并做恰当的缓存。那么丈量出来的成果天然便是 TextLayoutResult 了。事实上,textMeasurer 参数对应的函数内部便是帮助丈量了下,得到 textLayoutResult 再制作。

@ExperimentalTextApi
fun DrawScope.drawText(
    textMeasurer: TextMeasurer,
    text: String,
    topLeft: Offset = Offset.Zero,
    ...
) {
    val textLayoutResult = textMeasurer.measure(
        text = AnnotatedString(text),
        style = style,
        ...
    )
    withTransform({
        translate(topLeft.x, topLeft.y)
        clip(textLayoutResult)
    }) {
        textLayoutResult.multiParagraph.paint(drawContext.canvas)
    }
}

因此,对于杂乱的制作,咱们能够先手动丈量得到成果后,再根据需求做相关制作,以完成花里胡哨的效果。Halifax 佬在Compose把Text组件玩出新高度 做了大量骚操作,我就不赘述了。

参阅

  • Android Developers Blog: What’s new in Jetpack Compose (googleblog.com)
  • 其他链接文中已给出

本文涉及到的代码见 FunnySaltyFish/JetpackComposeStudy: 自己 Jetpack Compose 主题文章所包括的示例,包括自定义布局、部分组件用法等

本文正在参加「金石方案 . 分割6万现金大奖」