标题写的好像有点笼统了,上个效果图看看
总的来说,便是 A 页面下拉拖动时,B 页面开端从某一方位淡出,直至掩盖全屏。最终完结的布局,格式化后的代码不超越 70 行,一定程度上也体现出 Jetpack Compose 中自界说布局的简洁性。
您能够到 这里 下载 APK 体会
细节
如图所见,我的小运用《译站》 最近做了次 UI 升级,期望参阅谷歌翻译完结 “下拉打开历史记录” 的操作。详细来说,页面的变化如下:
开端时:
页面有一个 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 ->
}
然后便是依据 SwipeableState
的 currentOffset
动态核算远景的高度了,这部分的代码如下:
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->0
,Foreground
从 0.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 感兴趣,能够去那里翻翻,说不定有帮助。假如觉得哪里代码写的不好想吐槽的(这样的当地可能还不少?),也欢迎 issue 交流。
本文的部分代码注释由 ChatGPT 完结