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)然我才不自己寫呢,直接抄,把TwoRowsTopAppBarcopy過來改改就行,開始想著改Text的maxLines就行,后來才發(fā)現(xiàn)TwoRowsTopAppBar是用最大heignt限制的
閱讀源碼

理解源碼可以知道MediumTopAppBar布局可以分為兩塊
上標(biāo)題欄(TopAppBa) 和下標(biāo)題(bottomTitle)分別設(shè)置了固定高度

| 布局 | 高度 |
|---|---|
| 上標(biāo)題欄 | 122.dp |
| 下標(biāo)題 | 64.dp |
這個就是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兩個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)題
// 折疊時可見。
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
//如果有一個初始速度是在前一次用戶投擲后留下的,則設(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 四種動畫效果的調(diào)用實(shí)現(xiàn)代碼
在這里, 我將每種動畫分別應(yīng)用于四個按鈕為例,需要的朋友可以參考下2013-01-01
android編程開發(fā)之全屏和退出全屏的實(shí)現(xiàn)方法
這篇文章主要介紹了android編程開發(fā)之全屏和退出全屏的實(shí)現(xiàn)方法,以實(shí)例形式較為詳細(xì)的分析了Android全屏及退出全屏的頁面布局與功能實(shí)現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11
uniapp打包Android的apk(原生APP-云打包)及發(fā)布測試全過程
uni-app本地打包apk需要提前做非常多的準(zhǔn)備工作,而且可能會勸退一些開發(fā)者,下面這篇文章主要給大家介紹了關(guān)于uniapp打包Android的apk(原生APP-云打包)及發(fā)布測試的相關(guān)資料,需要的朋友可以參考下2023-02-02
android非RxJava環(huán)境下使用Handler實(shí)現(xiàn)預(yù)加載
這篇文章主要介紹了android非RxJava環(huán)境下使用Handler實(shí)現(xiàn)預(yù)加載的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01
Android開發(fā)實(shí)現(xiàn)抽屜菜單
這篇文章主要為大家詳細(xì)介紹了Android開發(fā)實(shí)現(xiàn)抽屜菜單,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
Android 提交或者上傳數(shù)據(jù)時的dialog彈框動畫效果
我們在使用支付寶支付的時候會看到類似這種彈框動畫效果,下面通過實(shí)例代碼給大家分享android 提交或者上傳數(shù)據(jù)時的彈框動畫效果,感興趣的的朋友參考下2017-07-07

