标题写的好像有点笼统了,上个效果图看看

Jetpack Compose 实现下拉动态渐变切换布局

总的来说,便是 A 页面下拉拖动时,B 页面开端从某一方位淡出,直至掩盖全屏。最终完结的布局,格式化后的代码不超越 70 行,一定程度上也体现出 Jetpack Compose 中自界说布局的简洁性。

您能够到 这里 下载 APK 体会

细节

如图所见,我的小运用《译站》 最近做了次 UI 升级,期望参阅谷歌翻译完结 “下拉打开历史记录” 的操作。详细来说,页面的变化如下:

开端时:

Jetpack Compose 实现下拉动态渐变切换布局

页面有一个 Main,其中又分为了 Upper(上面的布景)和 Lower(下面的功用栏) 两个部分,当开端拖动时,远景的初始巨细为 MainUpper 的巨细(与它堆叠),跟着用户的拖动逐步打开,渐渐淡出;一起布景渐渐隐去,直到最终完结切换。

完结

经过考虑要求,咱们发现,要知道远景的巨细,需要先知道 MainUpper 的巨细。这种“子布局巨细需要依赖其他子布局来确定”的情形,意味着咱们需要运用 SubcomposeLayout 。假如你对此不了解,能够参阅 SubcomposeLayout | 你好 Compose

那么首要,写个大致的结构出来:

@Composable
fun SwipeCrossFadeLayout(
    modifier: Modifier = Modifier,
    mainUpper: @Composable () -> Unit,
    mainLower: @Composable () -> Unit,
    foreground: @Composable () -> Unit,
) {
    SubcomposeLayout(modifier = modifier) { constraints ->
        layout(..., ...) {
        }
    }
}

丈量

接下来便是丈量。因为我的特别需求,我期望 MainLower 先被丈量,之后 MainUpper 再填满剩余的空间。最终才是依据 MainUpper 的巨细丈量 Foreground 。写几个变量用于保存几个巨细,编写代码如下:

@Composable
fun SwipeCrossFadeLayout(
    modifier: Modifier = Modifier,
    mainUpper: @Composable () -> Unit,
    mainLower: @Composable () -> Unit,
    foreground: @Composable () -> Unit,
) {
    var containerHeight by remember { mutableStateOf(100) } // 容器的高度,开端设为 100
    var mainUpperHeight by remember { mutableStateOf(0) }   // 布景的上半部分的高度,开端设为 0
    var lowerPartHeight by remember { mutableStateOf(100) } // 布景的下半部分的高度,开端设为 100
    SubcomposeLayout(modifier = modifier) { constraints ->
        containerHeight = constraints.maxHeight // 获取容器的最大高度作为 containerHeight
        // 先经过 subcompose 和 measure 办法对布景的下半部分进行丈量
        val mainLowerPlaceable = subcompose(MainLowerKey, mainLower).first().measure(constraints.copy(
            minWidth = 0,
            minHeight = 0
        ))
        lowerPartHeight = mainLowerPlaceable.height // 记录布景的下半部分的高度
        // 再经过 subcompose 和 measure 办法对布景的上半部分进行丈量
        val mainUpperPlaceable = subcompose(MainUpperKey, mainUpper).first().measure(constraints.copy(
            minWidth = 0,
            minHeight = 0,
            maxHeight = constraints.maxHeight - lowerPartHeight
            // 高度设为容器最大高度减去下半部分的高度
        ))
        mainUpperHeight = mainUpperPlaceable.height // 记录布景的上半部分的高度
        layout(..., ...) {
        }
    }
}

到这里,Main 的巨细就算完了,接下来便是核算 Foreground 部分的巨细了,而这,就需要结合当时拖动的方位动态核算。

滑动

走运的是,优秀的 Jetpack Compose 现已为咱们提供了 swipeable 修饰符,利用它就能够轻松完结“带有回弹和动画的拖动”效果。假如你不了解,能够参阅:滑动(Swipeable) | 你好 Compose。
考虑到外部应该能够控制远景的显现与封闭,咱们把对应的状况放到参数上:

@Composable
fun SwipeCrossFadeLayout(
    modifier: Modifier = Modifier,
    state: SwipeableState<SwipeShowType> = rememberSwipeableState(SwipeShowType.Main),
    mainUpper: @Composable () -> Unit,
    mainLower: @Composable () -> Unit,
    foreground: @Composable () -> Unit,
)

然后增加 swipeable 修饰符:

enum class SwipeShowType {
    Main,
    Foreground
}
SubcomposeLayout( 
    modifier = modifier.swipeable( 
        state = state, // 运用 SwipeableState 管理可滑动状况 
        // anchors 参数界说了滑动到各个方位时触发哪些状况 
        anchors = mapOf( 
            0f to SwipeShowType.Main, // 滑动到 0 时显现布景的上半部分 
            lowerPartHeight.toFloat() to SwipeShowType.Foreground // 滑动到 lowerPartHeight 时显现远景 
        ), 
        orientation = Orientation.Vertical, 
        thresholds = { _, _ -> FractionalThreshold(0.5f) } // 设置触发阈值为 0.5f 
    ) 
) { constraints ->
}

然后便是依据 SwipeableStatecurrentOffset 动态核算远景的高度了,这部分的代码如下:

    SubcomposeLayout(modifier = modifier) { constraints ->
        // ...
        val progress = (state.offset.value / lowerPartHeight).coerceIn(0f, 1f)  // 核算当时滑动进展,progress 的值在 0 到 1 之间
        // 依据滑动进展核算远景的高度
        val foregroundHeight = mainUpperHeight + progress * lowerPartHeight
        // 丈量时固定高度
        val foregroundPlaceable = subcompose(ForegroundKey, foreground).first().measure(
            constraints.copy(
                minWidth = constraints.minWidth,
                minHeight = foregroundHeight.toInt(),
                maxWidth = constraints.maxWidth,
                maxHeight = foregroundHeight.toInt()
            )
        )
        layout(..., ...) {
        }
    }    

摆放

最终便是 layout代码的完结,因为要改变 alpha ,因而选用 placeWithLayer 完结。有了上面的 progress,摆放时只需要将 Main 的透明度从 1->0Foreground0.5->1 (选择 0.5 而不是 0 开端,是因为 0 开端的话,开端的阶段真实看不见)

layout(constraints.maxWidth, constraints.maxHeight) {
    if (progress != 1f) {
        // 假如滑动进展不为 1,则突变消失布景的上半部分和下半部分
        mainUpperPlaceable.placeRelativeWithLayer(0, 0) {
            alpha = 1f - progress
        }
        mainLowerPlaceable.placeRelativeWithLayer(0, containerHeight - lowerPartHeight) {
            alpha = 1f - progress
        }
    }
    if (progress > 0.01f) {
        // 假如滑动进展大于 0.01,则突变显现远景
        foregroundPlaceable.placeRelativeWithLayer(0, 0) {
            alpha = lerp(0.5f, 1f, progress)
            // shadowElevation = if (progress == 1f) 0f else 8f
        }
    }
}

你可能注意到,上面的代码加了 if 作为判别,这是因为我期望当切换完结后,整个 Composable 只显现 远景 或者 布景 之一

处理嵌套滑动

其实到这里,这个布局现已根本可用了。但是,因为我的远景为列表,因而当时景打开后,因为滑动事件被列表消费了,因而无法上滑封闭。因而这里咱们要手动处理下嵌套滑动的问题。

Jetpack Compose 为嵌套滑动提供了特别的修饰符 .nestedScroll(nestedScrollConnection),详细介绍能够参阅 嵌套滑动(NestedScroll) | 你好 Compose。在这里,因为咱们要将“列表滑动后剩余的偏移量给父布局消费”,因而重写 postScroll 办法,代码如下:

val nestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            // 因为 HistoryScreen 是列表,假如滑到底部仍然有剩余的滑动间隔,就封闭
            // Log.d("NestedScrollConnection", "onPostScroll: $available")
            if (!swipeableState.isAnimationRunning 
                && source == NestedScrollSource.Drag 
                && available.y < -30.0f
            ) {
                scope.launch {
                    swipeableState.animateTo(SwipeShowType.Main)
                }
                return Offset(0f, available.y)
            }
            return super.onPostScroll(consumed, available, source)
        }
    }
}

然后就完结了~

代码和其他

本文的代码能够在 FunnyTranslation/SwipeCrossFadeLayout.kt 找到。作为我个人继续保护的 Jetpack Compose 开源项目,译站最近完结了一次 UI 大更新,除了本文提到的,还有其他有趣的效果。比方下面这个文字动画:

Jetpack Compose 实现下拉动态渐变切换布局

有用户说,它有一种上世纪的美

代码里还有一些花里胡哨的效果,比方 运用 Jetpack Compose 做一个年度报告页面 里展现的。

假如您对 Jetpack Compose 感兴趣,能够去那里翻翻,说不定有帮助。假如觉得哪里代码写的不好想吐槽的(这样的当地可能还不少?),也欢迎 issue 交流。

本文的部分代码注释由 ChatGPT 完结