前语

日常开发中我们不免遇到嵌套翻滚的需求,那么在Compose中又是怎么完结的呢?本文将用最简单的代码完结一个嵌套翻滚页面。

NestedScrollConnection

Compose中能够运用 nestedScroll 修饰符定义嵌套翻滚层次结构来行进灵活性。

object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        return super.onPreScroll(available, source)
    }
    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        return super.onPostScroll(consumed, available, source)
    }
    override suspend fun onPreFling(available: Velocity): Velocity {
        return super.onPreFling(available)
    }
    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
        return super.onPostFling(consumed, available)
    }
}

onPreScroll:预处理滑动工作,先交给父组件消费后再交由子组件 available:其时可用滑动偏移量 source:滑动类型 回来值:其时消费的滑动偏移量,假设不想消费可回来Offset.Zero

onPostScroll:子组件滑动后的回调 consumed:之前件消费滑动偏移量 available:其时剩下可用滑动偏移量 source:滑动工作的类型 回来值:其时消费的滑动偏移量,假设不想消费可回来Offset.Zero,则剩下偏移量会继续交由父组件进行处理

onPreFling 惯性翻滚工作预处理。 available:开始时的速度 回来值:其时组件消费的速度,假设不想消费可回来 Velocity.Zero

onPostFling 惯性翻滚工作处理 consumed:之前消费的一切速度 available:其时剩下可用的速度 回来值:其时组件消费的速度,假设不想消费可回来Velocity.Zero,则剩下速度会继续交由父组件进行处理。

接下来我们来完结下图作用:

Jetpack Compose : 一文学会嵌套翻滚NestedScrollConnection

我们先来完结布局,能够分为头部和列表伪代码如下:

Column{
	Box{}
	LazyColumn{}
}

接下来让头部和列表进行联动,我的主意是动态改动头部高度伪代码如下:

val titleBarSize = 45.dp
val targetHeight = 100.dp
val targetPercent by remember { mutableStateOf(1f) }
Column{
	Box(            
		modifier = Modifier
                .fillMaxWidth()
                .height(titleBarSize + targetHeight * targetPercent.value)
		){}
	LazyColumn(
		modifier = Modifier.fillMaxSize()
	){}
}

NestedScrollConnection提供onPreScroll来预处理滑动工作,所以我们在此处理targetPercent的逻辑,伪代码如下:

val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            var dyConsumed = 0f //记载消费距离
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                val delta = available.y // delta<0向上翻滚,反之向下翻滚
                dyConsumed += delta
                dyConsumed = dyConsumed.coerceAtMost(0f)
                val percent = dyConsumed / targetHeightPx // 计算翻滚距离百分比
                coroutineScope.launch {
                    targetPercent = 1 - abs(percent.coerceIn(-1f, 0f))
                }
                if (percent > -1 && percent < 0) { //向上翻滚时先交给父组件消费
                    return Offset(0f, delta)
                }
                return Offset.Zero
            }
        }
    }

以上便是思路和核心代码,最终依照常规贴上完好代码:

@Composable
fun UserScreen(
    userId: String,
    onNavigateToLogin: () -> Unit = {},
    onNavigateToSystem: (cid: String) -> Unit = {},
    onNavigateToWeb: (url: String) -> Unit = {},
) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()
    val sw = context.getScreenWidth()
    val titleBarSize = 45.dp
    val titleBarSizePx = with(LocalDensity.current) { titleBarSize.roundToPx().toFloat() }
    val avatarOffsetXPx = (sw - titleBarSizePx) / 2
    val avatarOffsetX = Dp(context.px2dp(avatarOffsetXPx))
    val targetHeight = 100.dp
    val targetHeightPx = with(LocalDensity.current) { targetHeight.roundToPx().toFloat() }
    val targetPercent by remember { mutableStateOf(Animatable(1f)) }
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            var dyConsumed = 0f
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                val delta = available.y
                dyConsumed += delta
                dyConsumed = dyConsumed.coerceAtMost(0f)
                val percent = dyConsumed / targetHeightPx
                coroutineScope.launch {
                    targetPercent.animateTo(1 - abs(percent.coerceIn(-1f, 0f)))
                }
                if (percent > -1 && percent < 0) {
                    return Offset(0f, delta)
                }
                return Offset.Zero
            }
        }
    }
    val viewModel: UserViewModel = viewModel(
        factory = UserViewModel.provideFactory(userId)
    )
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    Column(
        modifier = Modifier
                .systemBarsPadding()
                .nestedScroll(nestedScrollConnection)
    ) {
        Box(
            modifier = Modifier
                    .background(colorResource(R.color.theme))
                    .fillMaxWidth()
                    .height(titleBarSize + targetHeight * targetPercent.value)
        ) {
            IconButton(
                modifier = Modifier.height(titleBarSize),
                onClick = {
                    if (context is AppCompatActivity) {
                        context.onBackPressedDispatcher.onBackPressed()
                    }
                }
            ) {
                Icon(
                    Icons.Filled.ArrowBack,
                    contentDescription = null,
                    tint = colorResource(R.color.white)
                )
            }
            Image(
                painter = painterResource(id = uiState.coinResult.getAvatarId()),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                        .size(titleBarSize * targetPercent.value.coerceAtLeast(0.75f))
                        .align(Alignment.Center)
                        .offset(x = -(avatarOffsetX - titleBarSize) * (1 - targetPercent.value))
                        .clip(CircleShape),
            )
            Text(
                text = uiState.coinResult.nickname,
                modifier = Modifier
                        .align(Alignment.Center)
                        .offset(
                            x = -(avatarOffsetX - (titleBarSize * 2)) * (1 - targetPercent.value),
                            y = 35.dp * targetPercent.value
                        ),
                fontSize = 16.sp,
                color = colorResource(R.color.text_fff),
            )
            Text(
                text = "积分:${uiState.coinResult.coinCount}",
                modifier = Modifier
                        .align(Alignment.Center)
                        .offset(x = 0.dp, y = 55.dp * targetPercent.value)
                        .graphicsLayer {
                            alpha = targetPercent.value
                        },
                fontSize = 12.sp,
                color = colorResource(R.color.text_fff),
            )
        }
        BoxLayout(uiState.refreshing && !uiState.loading) {
            SwipeRefresh(
                modifier = Modifier
                        .fillMaxSize()
                        .background(colorResource(R.color.white)),
                contentPadding = PaddingValues(10.dp),
                verticalArrangement = Arrangement.spacedBy(10.dp),
                refreshing = uiState.refreshing,
                loading = uiState.loading,
                onRefresh = { viewModel.getShareArticlesHome() },
                onLoad = { viewModel.getShareArticlesNext() },
                onRetry = { viewModel.getShareArticlesHome() },
                data = uiState.articleResult,
            ) { _, item ->
                ArticleCard(
                    item = item,
                    onNavigateToLogin = onNavigateToLogin,
                    onNavigateToSystem = onNavigateToSystem,
                    onNavigateToWeb = onNavigateToWeb
                )
            }
        }
    }
}

Thanks

以上便是本篇文章的全部内容,如有问题欢迎指出,我们一起行进。
假设觉得本篇文章对您有协助的话请点个赞让更多人看到吧,您的鼓动是我行进的动力。
谢谢~~

源代码地址

  • UserScreen.kt miaowmiaow/fragmject