MotionLayout 动画无法与 Jetpack Compose 中的 LazyColumn 一起正常工作的问题

问题描述 投票:0回答:3

我正在尝试使用 Jetpack Compose 中的 androidx.constraintlayout.compose 版本 1.1.0-alpha09 库来实现带有滑动手势的 MotionLayout 动画。但是,我面临一个问题,即在 LayoutMation 内使用 LazyColumn 并在 LazyColumn 上滑动时,动画无法按预期工作。

这是我的作文

@OptIn(ExperimentalMotionApi::class)
@Composable
fun HomeView(
    navController: NavController,
    viewModel: AuthViewModel = hiltViewModel()
) {

    val context = LocalContext.current

    val motionScene = remember { context.resources.openRawResource(R.raw.home_main_scene).readBytes().decodeToString() }

    val tabIndex = remember { mutableStateOf(0) }

    val motionState = rememberMotionLayoutState()

    val systemUiController = rememberSystemUiController()

    SideEffect {
        systemUiController.setNavigationBarColor(
            color = LightColors.lightDebianColor,
            darkIcons = true
        )
        systemUiController.setStatusBarColor(
            color = PrimaryDebianColors.primary,
        )
    }

    MotionLayout(
        motionScene = MotionScene(content = motionScene),
        motionLayoutState = motionState,
        modifier = Modifier.fillMaxSize(),
    ) {
        Box(
            modifier = Modifier.layoutId("hero"),
            contentAlignment = Alignment.Center
        ) {
            ShopHighLights()
        }

        Box(
            modifier = Modifier
                .layoutId("tabs")
                .padding(top = 16.dp, bottom = 8.dp)
        ) {
            UziaDefaultScrollableTabs(
                selectedTabIndex = tabIndex.value,
                onClick = { tabIndex.value = it })
        }

        Surface(
            modifier = Modifier
                .layoutId("back")
                .clip(RoundedCornerShape(topStart = 42.dp, topEnd = 42.dp)),
            color = LightColors.lightTealColor
        ) {
            Box(
                modifier = Modifier.padding(horizontal = 16.dp, vertical = 24.dp),
                contentAlignment = Alignment.TopCenter
            ) {
                ActionStatusComponent()
            }
        }

        Surface(
            modifier = Modifier
                .layoutId("front")
                .fillMaxSize()
                .clip(RoundedCornerShape(topStart = 42.dp, topEnd = 42.dp)),
            color = LightColors.lightDebianColor
        ) {
            FoodGridList(columns = 2)
        }
    }
}

这是运动场景

{
  ConstraintSets: {
    start: {
      hero: {
        width: "spread",
        top: ["parent", "top"],
        start: ["parent", "start"],
        end: ["parent", "end"],
        translationY: 0,
        alpha: 1,
        scaleX: 1,
        scaleY: 1,
      },
      content: {
        width: "spread",
        height: "spread",
        start: ["parent", "start"],
        end: ["parent", "end"],
        top: ["hero","bottom"],
        bottom: ["parent","bottom"],
      },
    },
    end: {
      hero: {
        width: "spread",
        top: ["parent", "top"],
        start: ["parent", "start"],
        end: ["parent", "end"],
        translationY: -50,
        alpha: 0,
        scaleX: 0.8,
        scaleY: 0.8,
      },
      content: {
        width: "spread",
        height: "spread",
        start: ["parent", "start"],
        end: ["parent", "end"],
        top: ["parent","top"],
        bottom: ["parent","bottom"],
      },
    },
  },
  Transitions: {
    default: {
      from: "start",
      to: "end",
      onSwipe: {
        anchor: "content",
        direction: "bottom",
        side: "top",
      },
    }
  }
}

该问题似乎与 Jetpack Compose 如何优先考虑滚动事件和滑动事件有关。我尝试禁用和启用 LazyColumn 的 userScrollEnabled 属性,但它没有提供所需的行为。我想要实现的是一个动画,其中 LazyColumn 在动画完成之前不会滚动。动画完成后,如果用户继续滑动,LazyColumn 应恢复滚动。

我怀疑 LazyColumn 和 MotionLayout 之间的触摸事件被拦截或冲突,导致动画无法在 LazyColumn 中按预期工作。

我正在寻找有关如何正确处理 MotionLayout 和 LazyColumn 之间的交互以实现所需的动画行为的建议或见解。我希望 LazyColumn 在动画过程中暂停滚动,如果用户在动画完成后继续滑动,则恢复滚动。

任何帮助或指导将不胜感激。谢谢!

android-jetpack-compose android-constraintlayout android-motionlayout android-jetpack-compose-lazy-column
3个回答
1
投票

不幸的是,这些解决方法意味着我们无法使用 onSwipe 选项(例如自动完成)。如果您想在折叠状态下优先考虑向下滚动(这是 xml 视图世界中的自然行为),那么您需要共享内部可滚动的滚动状态,并在nestedScrollConnection 中包含一个片段来检查它是否可以滚动后退。 请注意,标题完全折叠,即我的

minPx
= 0 但我认为如果您简单地正确定义
minPx
(即最大高度 - 最小高度),您可以避免使用
headerScrollMaxPx

// Can put in utils file fun collapsingHeaderNestedScrollConnection( headerExpandedPx: MutableState<Float>, headerScrollMaxPx: Float, nestedCanScrollBack: State<Boolean> ): NestedScrollConnection = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val expandedPx = headerExpandedPx.value val allowNestedScrollBack = available.y > 0 && nestedCanScrollBack.value if (allowNestedScrollBack) { return Offset(0f, 0f) } val scrollBeyondMax = expandedPx + available.y > headerScrollMaxPx if (scrollBeyondMax) { headerExpandedPx.value = headerScrollMaxPx return Offset(0f, headerScrollMaxPx - expandedPx) } val scrollBeyondCollapsed = expandedPx + available.y < 0 if (scrollBeyondCollapsed) { headerExpandedPx.value = 0f return Offset(0f, -expandedPx) } // Else use all scroll to collapse/expand header (block nested scroll by consuming all y delta) headerExpandedPx.value += available.y return Offset(0f, available.y) } } @Composable fun MotionLayoutComposeView( items: List<NotificationSettingItem> ) { val scrollMaxPx = with(LocalDensity.current) { dimensionResource(id = R.dimen.collapsing_sweep_view_header_scroll_span_default) .roundToPx() .toFloat() } val headerExpandedPx = remember { mutableFloatStateOf(scrollMaxPx) } ManageNotificationsMotionLayout( headerExpandedPx = headerExpandedPx, headerScrollMaxPx = scrollMaxPx ) ... @Composable fun ManageNotificationsMotionLayout( headerExpandedPx: State<Float>, headerScrollMaxPx: Float, content: @Composable (MotionLayoutScope.() -> Unit) ) { val progress = 1 - headerExpandedPx.value / headerScrollMaxPx MotionLayout( progress = progress... // Inner scroll content composable scope... val settingsScrollState = rememberLazyListState() val canScrollBackState = remember { derivedStateOf { settingsScrollState.canScrollBackward } } val nestedScrollConnection = remember { collapsingHeaderNestedScrollConnection( headerExpandedPx = headerExpandedPx, headerScrollMaxPx = headerScrollMaxPx, nestedCanScrollBack = canScrollBackState ) } CompositionLocalProvider(LocalOverscrollConfiguration provides null) { LazyColumn( state = settingsScrollState, modifier = modifier.nestedScroll(nestedScrollConnection)...
    

0
投票
您需要让滑动驱动motionLayout的进度。 这些连接需要是程序化的并且在组合上更加明确。 关键代码是 NestedScrollConnection 和包含 LazyColumn 的 Box:

val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val height = toolbarHeight.value; if (height + available.y > maxPx) { toolbarHeight.value = maxPx return Offset(0f, maxPx - height) } if (height + available.y < minPx) { toolbarHeight.value = minPx return Offset(0f, minPx - height) } toolbarHeight.value += available.y return Offset(0f, available.y) } } } val progress = = 1 - (toolbarHeight.value - minPx) / (maxPx - minPx); MotionLayout( ... progress = progress ) Box( Modifier .nestedScroll(nestedScrollConnection)) { LazyColumn() { } } }
工作代码可以在这里找到:

https://github.com/androidx/constraintlayout/tree/main/demoProjects/ExamplesComposeMotionLayout#motion-layout-as-collapsing-toolbar-for-lazy-column


0
投票
在 Jetpack Compose 中,您全权负责通过修改其

progress

 参数来驱动 MotionLayout 的动画。

要使用 LazyColumn 在 Compose 中重新创建开箱即用的 RecyclerView 行为,我们需要做两件事:

    实现
  1. NestedScrollConnection接口。该接口提供了 onPreScroll
    onPostScroll
     方法,使我们能够使用所谓的滚动增量。通过访问这些增量,我们只需要弄清楚消耗多少和忽略多少。
  2. 当滚动停止且工具栏未处于静止状态(折叠或展开)时对事件做出反应。使用
  3. LazyListState
     即可轻松完成此操作。
这里是官方文档中“Motion Layout as Collapseing Toolbar for Lazy column”示例的稍加修改的代码:

package com.example.examplescomposemotionlayout import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.EaseInOut import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.ExperimentalMotionApi import androidx.constraintlayout.compose.MotionLayout import androidx.constraintlayout.compose.MotionScene import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.launch @OptIn(ExperimentalMotionApi::class) @Preview(group = "scroll", device = "spec:shape=Normal,width=480,height=800,unit=dp,dpi=440") @Composable fun ToolBarLazyExampleDsl() { val big = 250.dp val small = 50.dp val scene = MotionScene { val title = createRefFor("title") val image = createRefFor("image") val icon = createRefFor("icon") val start1 = constraintSet { constrain(title) { bottom.linkTo(image.bottom) start.linkTo(image.start) } constrain(image) { width = Dimension.matchParent height = Dimension.value(big) top.linkTo(parent.top) customColor("cover", Color(0x000000FF)) } constrain(icon) { top.linkTo(image.top, 16.dp) start.linkTo(image.start, 16.dp) alpha = 0f } } val end1 = constraintSet { constrain(title) { bottom.linkTo(image.bottom) start.linkTo(icon.end) centerVerticallyTo(image) scaleX = 0.7f scaleY = 0.7f } constrain(image) { width = Dimension.matchParent height = Dimension.value(small) top.linkTo(parent.top) customColor("cover", Color(0xFF0000FF)) } constrain(icon) { top.linkTo(image.top, 16.dp) start.linkTo(image.start, 16.dp) } } transition(start1, end1, "default") {} } val maxHeightInPx = with(LocalDensity.current) { big.roundToPx().toFloat() } val minHeightInPx = with(LocalDensity.current) { small.roundToPx().toFloat() } var toolbarHeight by remember { mutableStateOf(maxHeightInPx) } val progress = remember { Animatable(initialValue = 0f) } val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() val nestedConnection = remember(listState) { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // If the list can scroll backward, then we need to allow it to consume the delta. // Otherwise, we consume it to update the toolbar height & progress. return if (listState.canScrollBackward) { Offset.Zero } else { consume(available) } } override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { // We need to handle the onPostScroll in order to consume the leftover delta after a user // flings a list from the bottom to the top so that the toolbar could expand automatically. return if (!listState.canScrollBackward && available.y > 0) { consume(available) } else { Offset.Zero } } private fun consume(available: Offset): Offset { val currentHeight = toolbarHeight return when { currentHeight + available.y > maxHeightInPx -> { onUpdateValues(maxHeightInPx) Offset(0f, maxHeightInPx - currentHeight) } currentHeight + available.y < minHeightInPx -> { onUpdateValues(minHeightInPx) Offset(0f, minHeightInPx - currentHeight) } else -> { onUpdateValues(toolbarHeight + available.y) Offset(0f, available.y) } } } private fun onUpdateValues(newToolbarHeight: Float) { toolbarHeight = newToolbarHeight val newProgress = calculateProgressGivenToolbarHeight( toolbarHeight = newToolbarHeight, minHeight = minHeightInPx, maxHeight = maxHeightInPx, ) coroutineScope.launch { progress.snapTo(newProgress) } } } } // Effect that takes care of listening when scrolling stops and if toolbar is not // at resting state, then it animates it to the closest state LaunchedEffect(Unit) { snapshotFlow { listState.isScrollInProgress } .distinctUntilChanged() .filterNot { isScrolling -> isScrolling } .collect { val currentProgress = progress.value if (currentProgress != 0f && currentProgress != 1f) { val newProgress = if (currentProgress < 0.5f) 0f else 1f // We calculate the duration here in a specific way to recreate 1 to 1 // RecyclerView's OnSwipe behavior val duration = calculateAutoTransitionDuration(currentProgress) launch { progress.animateTo( targetValue = newProgress, animationSpec = tween(durationMillis = duration, easing = EaseInOut), block = { // Also, when we are animating, we need to recalculate the // toolbarHeight as well to keep it in sync with the progress toolbarHeight = calculateToolbarHeightGivenProgress( progress = value, minHeight = minHeightInPx, maxHeight = maxHeightInPx, ) }, ) } } } } Column { MotionLayout( motionScene = scene, progress = progress.value, ) { Image( modifier = Modifier .layoutId("image") .background(customColor("image", "cover")), painter = painterResource(R.drawable.bridge), contentDescription = null, contentScale = ContentScale.Crop ) Image( modifier = Modifier.layoutId("icon"), painter = painterResource(R.drawable.menu), contentDescription = null ) Text( modifier = Modifier.layoutId("title"), text = "San Francisco", fontSize = 30.sp, color = Color.White ) } LazyColumn( modifier = Modifier .fillMaxWidth() .nestedScroll(nestedConnection), state = listState, ) { items(100) { Text(text = "item $it", modifier = Modifier.padding(4.dp)) } } } } private fun calculateToolbarHeightGivenProgress( progress: Float, minHeight: Float, maxHeight: Float, ): Float { return minHeight + (1 - progress) * (maxHeight - minHeight) } private fun calculateProgressGivenToolbarHeight( toolbarHeight: Float, minHeight: Float, maxHeight: Float, ): Float { return 1 - (toolbarHeight - minHeight) / (maxHeight - minHeight) } /** * Calculates a duration for the auto transition in the following way: * - for progress that is zero, the duration is minimal (0f -> min) * - for progress that is half way, the duration is maximal (0.5f -> max) * - for progress that is one, the duration is minimal (1f -> min) **/ private fun calculateAutoTransitionDuration(progress: Float): Int { val minDuration = 300 val maxDuration = 1200 return (minDuration + (maxDuration - minDuration) * 4 * progress * (1 - progress)).toInt() }
我在上面的代码中添加了一些注释来帮助理解。如果您还有任何疑问,请随时在评论中提问。

© www.soinside.com 2019 - 2024. All rights reserved.