开发者

Jetpack Compose重写TopAppBar实现标题多行折叠详解

目录
  • 前言
  • MediumTopAppBar
    • 阅读源码
      • 核心
  • 解决方法
    • 重写TopAppBarLayout
      • 完整代码

        前言

        想用composes实现类似掘金的文章详细页面的标题栏

        上滑隐藏标题后标题栏显示标题

        Jetpack Compose重写TopAppBar实现标题多行折叠详解

        compose.material3下的TopAppBar不能嵌套滚动

        MediumTopAppBar

        便使用了MediumTopAppBar一开始用着没什么问题,但是标题字数多了,MediumTopAppBar就不支持了,最多就两行,进入源码一看就明白了php

        @ExperimentalMaterial3Api
        @Composable
        fun MediumTopAppBar(
           ...
        ) {
            TwoRowsTopAppBar(
               ...
            )
        }
        

        TwoRowsTopAppBar 官方就是告诉你我就两行,要是不服你就自己写,自己写就自己写,当然我才不自己写呢,直接抄,把TwoRowsTopAppBarcopy过来改改就行,开始想着改TextmaxLines就行,后来才发现TwoRowsTopAppBar是用最大heignt限制的

        阅读源码

        Jetpack Compose重写TopAppBar实现标题多行折叠详解

        理解源码可以知道MediumTopAppBar布局可以分为两块

        上标题栏(TopAppBa) 和下标题(bottomTitle)分别设置了固定高度

        Jetpack Compose重写TopAppBar实现标题多行折叠详解

        布局高度
        上标题栏122.dp
        下标题64.dp

        这个就是TwoRowsTopAppBar命名的TwoRows的原因

        Jetpack Compose重写TopAppBar实现标题多行折叠详解

        高度是固定在我们改不了

        核心

        首先限制嵌套滑动的Y轴最大的偏移量也就是高度,目的就是仅隐藏底部标题区域并保留顶部标题

        手指上滑后计算上滑偏移量

        //官方源码
        SideEffect {
            if (scrollBehavior?.state?.heightOffsetLimjsit != pinnedHeightPx - maxHeightPx) {
                scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx
            }
        }
        

        接着scrollBehavior.state.collapsedFraction获取折叠高度百分比(0.0表示完全展开,1.0表示完全折叠)

        在利用三阶贝塞尔曲线+百分比设置titleText的Alpha值实现滑动渐显效果

        最后实现自定义布局,下标题的高度-上滑偏移量实现折叠标题 并且利用Alpha显示上标题

        Column {
           //上标题
            TopAppBarLayout(
               ...
            )
            //下标题
            TopAppBarLayout(
              ...
                heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset
                    ?: 0f)
              ...
            )
        }
        ......
        val layoutHeight = heightPx.roundToInt()
        layout(constraints.maxWidth, layoutHeight) {
            // Title
            titlePlaceable.placeRelative(...)
        }
        

        解决方法

        先计算下布局高度

        var bottomLayoutViewSize: IntSize by remember { mutableStateOf(IntSize(0,0)) }
        val bottomLayoutBox = @Composable {
            Box(
                modifier= Modifier.onSizeChanged { bottomLayoutViewSize = it },
                content = bottomLayout
            )
        }
        

        保留上标题的固定高度,动态计算最大高度

        LocalDensity.current.run {
            maxHeightPx = 上布局的高度 + 下布局的高度
        }
        

        重写TopAppBarLayout

        为下布局重写TopAppBarLayout,去除里面的无用代码

        使用方法和MediumTopAppBar一样,只不过

        title变成了topLayoutbottomLayout两个Composable

        为了方便实现不同的字体风格其他布局,可以像掘金一样显示头像关注

        KnowledgeTopAppBar(
            topLayout = {
                Text(
                    modifier = Modifier.padding(6.dp),
                    text = "九狼JIULANG",
                    color = CustomTheme.colors.textPrimary,
                    fontSize = 21.sp,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    fontWeight = FontWeight.Bold
                )
            },
            bottomLayout = {
                Text(
                    modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
                    text = "关注 点赞 ",
                    color = CustomTheme.colors.textPrimary,
                    fontSize = 19.sp,
                    fontWeight = FontWeight.Bold
                )
            },
            navigationIcon = {
          },
            actions = {
            },
            scrollBehavior = scrollBehavior
        )
        

        完整代码

        import androidx.compose.animation.core.*
        import androidx.compose.foundation.gestures.Orientation
        import androidx.compose.foundation.gestures.draggable
        import androidx.compose.foundation.gestures.rememberDraggableState
        import androidx.compose.foundation.layout.*
        import androidx.compose.material3.*
        import androidx.compose.runtime.*
        import androidx.compose.ui.Alignment
        import androidx.compose.ui.Modifier
        import androidx.compose.ui.draw.clipToBounds
        import androidx.compose.ui.graphics.Color
        import androidx.compose.ui.graphics.graphicsLayer
        import androidx.compose.ui.platform.LocalDensity
        imhttp://www.devze.comport androidx.compose.ui.semantics.clearAndSetSemantics
        import androidx.compose.ui.text.TextStyle
        import com.jiulang.wordsfairy.ui.theme.CustomTheme
        import kotlin.math.abs
        import kotlin.math.max
        import kotlin.math.roundToInt
        import androidx.compose.ui.layout.*
        import androidx.compose.ui.unit.*
        import com.google.accompanist.insets.statusBarsPadding
        @ExperimentalMaterial3Api
        @Composable
        fun KnowledgeTopAppBar(
            modifier: Modifier = Modifier,
            titleBottomPadding: Dp = 28.dp,
            navigationIcon: @Composable () -> Unit,
            actions: @Composable RowScope.() -> Unit,
            topLayout: @Composable () -> Unit,
            bottomLayout: @Composable BoxScope.() -> Unit,
            pinnedHeight: Dp = 46.0.dp,
            scroll编程客栈Behavior: TopAppBarScrollBehavior
        ){
            val pinnedHeightPx: Float
            val maxHeightPx: Float
            val titleBottomPaddingPx: Int
            var bottomLayoutViewSize: IntSize by remember { mutableStateOf(IntSize(0,0)) }
            //计算布局高度
            val bottomLayoutBox = @Composable {
                Box(
                    modifier= Modifier.onSizeChanged { bottomLayoutViewSize = it },
                    content = bottomLayout
                )
            }
            LocalDensity.current.run {
                pinnedHeightPx = pinnedHeight.toPx()
                maxHeightPx = bottomLayoutViewSize.height.toFloat() +pinnedHeightPx
                titleBottomPaddingPx = titleBottomPadding.roundToPx()
            }
            // 设置应用程序栏的高度偏移限制以仅隐藏底部标题区域并保留顶部标题
            // 折叠时可见。
            SideEffect {
                if (scrollBehavior.state.heightOffsetLimit != pinnedHeightPx - maxHeightPx) {
                    scrollBehavior.state.heightOffsetLimit = pinnedHeightPx - maxHeightPx
                }
            }
            val colorTransitionFraction = scrollBehavior.state.collapsedFraction
            val appBarContainerColor by rememberUpdatedState(CustomTheme.colors.statusBarColor)
            val actionsRow = @Composable {
                Row(
                    horizontalArrangement = Arrangement.End,
                    verticalAlignment = Alignment.CenterVertically,
                    content = actions
                )
            }
            val topLayoutAlpha = CubicBezierEasing(.8f, 0f, .8f, .15f).transform(colorTransitionFraction)
            val bottomLayoutAlpha = 1f - colorTransitionFraction
            // Hide the top row title semantics when its alpha value goes below 0.5 threshold.
            // Hide the bottom row title semantics when the top title semantics are active.
            val hideTopRowSemantics = colorTransitionFraction < 0.5f
            val hideBottomRowSemantics = !hideTopRowSemantics
            // Set up support for resizing the top app bar when vertically dragging the bar itself.
            val appBarDragModifier = if (!scrollBehavior.isPinned) {
                Modifier.draggable(
                    orientation = Orientation.Vertical,
                    state = rememberDraggableState { delta ->
                        scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffset + delta
                    },
                    onDragStopped = { velocity ->
                        settleAppBar(
                            scrollBehavior.state,
                            velocity,
                            scrollBehavior.flingAnimationSpec,
                            scrollBehavior.snapAnimationSpec
                        )
                    }
                )
            } else {
                Modifier
            }
            Surface(modifier = modifier.then(appBarDragModifier), color = appBarContainerColor) {
                Column {
                    TopAppBarLayout(
                        modifier = Modifier
                            .statusBarsPadding()
                            // 在填充后剪辑,这样不会在插入区域上显示标题
                            .clipToBounds(),
                        heightPx = pinnedHeightPx,
                        navigationIconContentColor =
                        CustomTheme.colors.mainColor,
                        actionIconContentColor =
                        CustomTheme.colors.mainColor,
                        title = topLayout,
                        titleTextStyle = TextStyle.Default,
                        titleAlpha = topLayoutAlpha,
                        titleVerticalArrangement = Arrangement.Center,
                        titleHorizontalArrangement = Arrangement.Start,
                        titleBottomPadding = 0,
                        hideTitleSemantics = hideTopRowSemantics,
                        navigationIcon = navigationIcon,
                        actions = actionsRow,
                    )
                    KnowledgeTitleLayout(
                        modifier = Modifier.clipToBounds(),
                        heightPx =  maxHeightPx - pinnedHeightPx + scrollBehavior.state.heightOffset,
                        title = bottomLayoutBox,
                        titleTextStyle = TextStyle.Default,
                        titleAlpha = bottomLayoutAlpha,
                        titleVerticalArrangement = Arrangement.Bottom,
                        titleHorizontalArrangement = Arrangement.Start,
                        titleBottomPadding = titleBottomPaddingPx,
                        hideTitleSemantics = hideBottomRowSemantics,
                    )
                }
            }
        }
        @OptIn(ExperimentalMaterial3Api::class)
        private suspend fun settleAppBar(
            state: TopAppBarState,
            velocity: Float,
            flingAnimationSpec: DecayAnimationSpec<Float>?,
            snapAnimationSpec: AnimationSpec<Float>?
        ): Velocity {
            //检查应用程序栏是否完全折叠/展开。如果是,则无需结算应用程序栏,
            //然后返回零速度。
            //请注意,由于collapsedFraction的浮点精度,不用检查 0f
            if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
                return Velocity.Zero
            }
            var remainingVelocity = velocity
            //如果有一个初始速度是在前一次用户投掷后留下的,则设置动画以
            // 继续运动以展开或折叠应用程序栏。
            if (flingAnimationSpec != null && abs(velocity) > 1f) {
                var lastValue = 0f
                AnimationState(
                    initialValue = 0f,
                    initialVelocity = velocity,
                )
                    .animateDecay(flingAnimationSpec) {
                        val delta = value - lastValue
                        val initialHeightOffset = state.heightOffset
                        state.heightOffset = initialHeightOffset + delta
                        val consumed = abs(initialHeightOffset - state.heightOffset)
                        lastValue = value
                        remainingVelocity = this.velocity
                        // 避免舍入错误,如果有任何内容未被使用,则停止
                        if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
                    }
            }
            // 如果提供了动画规格,则捕捉。
            if (snapAnimationSpec != null) {
                if (state.heightOffset < 0 &&
                    state.heightOffset > state.heightOffsetLimit
                ) {
                    AnimationState(initialValue = state.heightOffset).animateTo(
                        if (state.collapsedFraction < 0.5f) {
                            0f
                        } else {
                            state.heightOffsetLimit
                        },
                        animationSpec = snapAnimationSpec
                    ) { state.heightOffset = value }
                }
            }
            return Velocity(0f, remainingVelocity)
        }
        @Composable
        private fun TopAppBarLayout(
            modifier: Modifier,
            heightPx: Float,
            navigationIconContentColor: Color,
            actionIconContentColor: Color,
            title: @Composable () -> Unit,
            titleTextStyle: TextStyle,
            titleAlpha: Float,
            titleVerticalArrangement: Arrangement.Vertical,
            titleHorizontalArrangement: Arrangement.Horizontal,
            titleBottomPadding: Int,
            hideTitleSemantics: Boolean,
            navigationIcon: @Composable () -> Unit,
            actions: @Composable () -> Unit,
        ) {
            Layout(
                {
                    Box(
                        Modifier
                            .layoutId("navigationIcon")
                            .padding(start = TopAppBarHorizontalPadding)
                    ) {
                        CompositionLocalProvider(
                            LocalContentColor provides navigationIconContentColor,
                            content = navigationIcon
                        )
                    }
                    Box(
                        Modifier
                            .layoutId("title")
                  编程客栈          .padding(horizontal = TopAppBarHorizontalPadding)
                            .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier)
                            .graphicsLayer(alpha = titleAlpha)
                    ) {
                        ProvideTextStyle(value = titleTextStyle) {
                            CompositionLocalProvider(
                                content = title
                            )
                        }
                    }
                    Box(
                        Modifier
                            .layoutId("actionIcons")
                            .padding(end = TopAppBarHorizontalPadding)
                    ) {
                        CompositionLocalProvider(
                            LocalContentColor provides actionIconContentColor,
                            content = actions
                        )
                    }
                },
                modifier = modifier
            ) { measurables, constraints ->
                val navigationIconPlaceable =
                    measurables.first { it.layoutId == "navigationIcon" }
                        .measure(constraints.copy(minWidth = 0))
                val actionIconsPlaceable =
                    measurables.first { it.layoutId == "actionIcons" }
                        .measure(constraints.copy(minWidth = 0))
                val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) {
                    constraints.maxWidth
                } else {
                    (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
                        .coerceAtLeast(0)
                }
                val titlePlaceable =
                    measurables.first { it.layoutId == "title" }
                        .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
                // Locate the title's baseline.
                val titleBaseline =
                    if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) {
                        titlePlaceable[LastBaseline]
                    } else {
                        0
                    }
                val layoutHeight = heightPx.roundToInt()
                layout(constraints.maxWidth, layoutHeight) {
                    // Navigation icon
                    navigationIconPlaceable.placeRelative(
                        x = 0,
                        y = (layoutHeight - navigationIconPlaceable.height) / 2
                    )
                    // Title
                    titlePlaceable.placeRelative(
                        x = when (titleHorizontalArrangement) {
                            Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2
                            Arrangement.End ->
                                constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width
                            // Arrangement.Start.
                            // An TopAppBarTitl开发者_Go学习eInset will make sure the title is offset in case the
                            // navigation icon is missing.
                            else -> max(12.dp.roundToPx(), navigationIconPlaceable.width)
                        },
                        y = when (titleVerticalArrangement) {
                            Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
                            // Apply bottom padding from the title's baseline only when the Arrangement is
                            // "Bottom".
                            Arrangement.Bottom ->
                                if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
                                else layoutHeight - titlePlaceable.height - max(
                                    0,
                                    titleBottomPadding - titlePlaceable.height + titleBaseline
                                )
                            // Arrangement.Top
                            else -> 0
                        }
                    )
                    // Action icons
                    actionIconsPlaceable.placeRelative(
                        x = constraints.maxWidth - actionIconsPlaceable.width,
                        y = (layoutHeight - actionIconsPlaceable.height) / 2
                    )
                }
            }
        }
        @Composable
        private fun KnowledgeTitleLayout(
            modifier: Modifier,
            heightPx: Float,
            title: @Composable () -> Unit,
            titleTextStyle: TextStyle,
            titleAlpha: Float,
            titleVerticalArrangement: Arrangement.Vertical,
            titleHorizontalArrangement: Arrangement.Horizontal,
            titleBottomPadding: Int,
            hideTitleSemantics: Boolean,
        ) {
            Layout(
                {
                    Box(
                        Modifier
                            .layoutId("title")
                            .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier)
                            .graphicsLayer(alpha = titleAlpha)
                    ) {
                        ProvideTextStyle(value = titleTextStyle) {
                            CompositionLocalProvider(
                                content = title
                            )
                        }
                    }
                },
                modifier = modifier
            ) { measurables, constraints ->
                val maxTitleWidth =  constraints.maxWidth
                val titlePlaceable =
                    measurables.first { it.layoutId == "title" }
                        .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
                val layoutHeight =heightPx.roundToInt()
                layout(maxTitleWidth, layoutHeight) {
                    // Title
                    titlePlaceable.placeRelative(
                        x = when (titleHorizontalArrangement) {
                            Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2
                            Arrangement.End ->
                                constraints.maxWidth - titlePlaceable.width
                            else -> max(0.dp.roundToPx(), 0.dp.roundToPx())
                        },
                        y = when (titleVerticalArrangement) {
                            Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
                            // Apply bottom padding from the title's baseline only when the Arrangement is
                            // "Bottom".
                            Arrangement.Bottom ->
                                if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
                                else layoutHeight - titlePlaceable.height - max(
                                    0,
                                    titleBottomPadding - titlePlaceable.height
                                )
                            // Arrangement.Top
                            else -> 0
                        }
                    )
                }
            }
        }
        private val TopAppBarHorizontalPadding = 4.dp

        以上就是Jetpack Compose重写TopAppBar实现标题多行折叠详解的详细内容,更多关于Jetpack Compose TopAppBar的资料请关注我们其它相关文章!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜