Jetpack?Compose重寫TopAppBar實(shí)現(xiàn)標(biāo)題多行折疊詳解
前言
想用composes實(shí)現(xiàn)類似掘金的文章詳細(xì)頁面的標(biāo)題欄
上滑隱藏標(biāo)題后標(biāo)題欄顯示標(biāo)題
compose.material3
下的TopAppBar
不能嵌套滾動
MediumTopAppBar
便使用了MediumTopAppBar
一開始用著沒什么問題,但是標(biāo)題字?jǐn)?shù)多了,MediumTopAppBar
就不支持了,最多就兩行,進(jìn)入源碼一看就明白了
@ExperimentalMaterial3Api @Composable fun MediumTopAppBar( ... ) { TwoRowsTopAppBar( ... ) }
TwoRowsTopAppBar 官方就是告訴你我就兩行,要是不服你就自己寫,自己寫就自己寫,當(dāng)然我才不自己寫呢,直接抄,把TwoRowsTopAppBar
copy過來改改就行,開始想著改Text
的maxLines
就行,后來才發(fā)現(xiàn)TwoRowsTopAppBar
是用最大heignt限制的
閱讀源碼
理解源碼可以知道MediumTopAppBar
布局可以分為兩塊
上標(biāo)題欄(TopAppBa) 和下標(biāo)題(bottomTitle)分別設(shè)置了固定高度
布局 | 高度 |
---|---|
上標(biāo)題欄 | 122.dp |
下標(biāo)題 | 64.dp |
這個(gè)就是TwoRowsTopAppBar
命名的TwoRows
的原因
高度是固定在我們改不了
核心
首先限制嵌套滑動的Y軸最大的偏移量也就是高度,目的就是僅隱藏底部標(biāo)題區(qū)域并保留頂部標(biāo)題
手指上滑后計(jì)算上滑偏移量
//官方源碼 SideEffect { if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx) { scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx } }
接著scrollBehavior.state.collapsedFraction
獲取折疊高度百分比(0.0表示完全展開,1.0表示完全折疊)
在利用三階貝塞爾曲線+百分比設(shè)置titleText的Alpha值
實(shí)現(xiàn)滑動漸顯效果
最后實(shí)現(xiàn)自定義布局,下標(biāo)題的高度-上滑偏移量實(shí)現(xiàn)折疊標(biāo)題 并且利用Alpha顯示上標(biāo)題
Column { //上標(biāo)題 TopAppBarLayout( ... ) //下標(biāo)題 TopAppBarLayout( ... heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset ?: 0f) ... ) } ...... val layoutHeight = heightPx.roundToInt() layout(constraints.maxWidth, layoutHeight) { // Title titlePlaceable.placeRelative(...) }
解決方法
先計(jì)算下布局高度
var bottomLayoutViewSize: IntSize by remember { mutableStateOf(IntSize(0,0)) } val bottomLayoutBox = @Composable { Box( modifier= Modifier.onSizeChanged { bottomLayoutViewSize = it }, content = bottomLayout ) }
保留上標(biāo)題的固定高度,動態(tài)計(jì)算最大高度
LocalDensity.current.run { maxHeightPx = 上布局的高度 + 下布局的高度 }
重寫TopAppBarLayout
為下布局重寫TopAppBarLayout
,去除里面的無用代碼
使用方法和MediumTopAppBar一樣,只不過
title變成了topLayout
和bottomLayout
兩個(gè)Composable
為了方便實(shí)現(xiàn)不同的字體風(fēng)格和其他布局,可以像掘金一樣顯示頭像和關(guān)注。
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 = "關(guān)注 點(diǎn)贊 ", 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 import 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, scrollBehavior: TopAppBarScrollBehavior ){ val pinnedHeightPx: Float val maxHeightPx: Float val titleBottomPaddingPx: Int var bottomLayoutViewSize: IntSize by remember { mutableStateOf(IntSize(0,0)) } //計(jì)算布局高度 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() } // 設(shè)置應(yīng)用程序欄的高度偏移限制以僅隱藏底部標(biāo)題區(qū)域并保留頂部標(biāo)題 // 折疊時(shí)可見。 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() // 在填充后剪輯,這樣不會在插入?yún)^(qū)域上顯示標(biāo)題 .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 { //檢查應(yīng)用程序欄是否完全折疊/展開。如果是,則無需結(jié)算應(yīng)用程序欄, //然后返回零速度。 //請注意,由于collapsedFraction的浮點(diǎn)精度,不用檢查 0f if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { return Velocity.Zero } var remainingVelocity = velocity //如果有一個(gè)初始速度是在前一次用戶投擲后留下的,則設(shè)置動畫以 // 繼續(xù)運(yùn)動以展開或折疊應(yīng)用程序欄。 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 // 避免舍入錯誤,如果有任何內(nèi)容未被使用,則停止 if (abs(delta - consumed) > 0.5f) this.cancelAnimation() } } // 如果提供了動畫規(guī)格,則捕捉。 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 TopAppBarTitleInset 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實(shí)現(xiàn)標(biāo)題多行折疊詳解的詳細(xì)內(nèi)容,更多關(guān)于Jetpack Compose TopAppBar的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android surfaceView實(shí)現(xiàn)播放視頻功能
這篇文章主要為大家詳細(xì)介紹了android surfaceView實(shí)現(xiàn)播放視頻功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05Android實(shí)現(xiàn)標(biāo)題上顯示隱藏進(jìn)度條效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)標(biāo)題上顯示隱藏進(jìn)度條效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android矢量圖之VectorDrawable類自由填充色彩
這篇文章主要介紹了Android矢量圖之VectorDrawable類自由填充色彩的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-05-05android app跳轉(zhuǎn)應(yīng)用商店實(shí)現(xiàn)步驟
這篇文章主要為大家介紹了android app跳轉(zhuǎn)應(yīng)用商店實(shí)現(xiàn)步驟詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11Android使用ViewPager實(shí)現(xiàn)自動輪播
這篇文章主要介紹了Android使用ViewPager實(shí)現(xiàn)自動輪播的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-07-07