Merge "Add CustomizedLargeTopAppBar for Spa"
This commit is contained in:
@@ -0,0 +1,604 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spa.widget.scaffold
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.AnimationState
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.animateDecay
|
||||
import androidx.compose.animation.core.animateTo
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.TopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
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.graphics.lerp
|
||||
import androidx.compose.ui.layout.AlignmentLine
|
||||
import androidx.compose.ui.layout.LastBaseline
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.settingslib.spa.framework.compose.horizontalValues
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.framework.theme.SettingsTheme
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun CustomizedTopAppBar(
|
||||
title: @Composable () -> Unit,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
SingleRowTopAppBar(
|
||||
title = title,
|
||||
titleTextStyle = MaterialTheme.typography.titleMedium,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
windowInsets = TopAppBarDefaults.windowInsets,
|
||||
colors = topAppBarColors(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The customized LargeTopAppBar for Settings.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun CustomizedLargeTopAppBar(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
) {
|
||||
TwoRowsTopAppBar(
|
||||
title = { Title(title = title, maxLines = 2) },
|
||||
titleTextStyle = MaterialTheme.typography.displaySmall,
|
||||
smallTitleTextStyle = MaterialTheme.typography.titleMedium,
|
||||
titleBottomPadding = LargeTitleBottomPadding,
|
||||
smallTitle = { Title(title = title, maxLines = 1) },
|
||||
modifier = modifier,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
colors = topAppBarColors(),
|
||||
windowInsets = TopAppBarDefaults.windowInsets,
|
||||
maxHeight = 176.dp,
|
||||
pinnedHeight = ContainerHeight,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
WindowInsets.navigationBars
|
||||
.asPaddingValues()
|
||||
.horizontalValues()
|
||||
)
|
||||
.padding(horizontal = SettingsDimension.itemPaddingAround),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = maxLines,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun topAppBarColors() = TopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents the colors used by a top app bar in different states.
|
||||
* This implementation animates the container color according to the top app bar scroll state. It
|
||||
* does not animate the leading, headline, or trailing colors.
|
||||
*/
|
||||
@Stable
|
||||
private class TopAppBarColors(
|
||||
val containerColor: Color,
|
||||
val scrolledContainerColor: Color,
|
||||
val navigationIconContentColor: Color,
|
||||
val titleContentColor: Color,
|
||||
val actionIconContentColor: Color,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Represents the container color used for the top app bar.
|
||||
*
|
||||
* A [colorTransitionFraction] provides a percentage value that can be used to generate a color.
|
||||
* Usually, an app bar implementation will pass in a [colorTransitionFraction] read from
|
||||
* the [TopAppBarState.collapsedFraction] or the [TopAppBarState.overlappedFraction].
|
||||
*
|
||||
* @param colorTransitionFraction a `0.0` to `1.0` value that represents a color transition
|
||||
* percentage
|
||||
*/
|
||||
@Composable
|
||||
fun containerColor(colorTransitionFraction: Float): Color {
|
||||
return lerp(
|
||||
containerColor,
|
||||
scrolledContainerColor,
|
||||
FastOutLinearInEasing.transform(colorTransitionFraction)
|
||||
)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other !is TopAppBarColors) return false
|
||||
|
||||
if (containerColor != other.containerColor) return false
|
||||
if (scrolledContainerColor != other.scrolledContainerColor) return false
|
||||
if (navigationIconContentColor != other.navigationIconContentColor) return false
|
||||
if (titleContentColor != other.titleContentColor) return false
|
||||
if (actionIconContentColor != other.actionIconContentColor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = containerColor.hashCode()
|
||||
result = 31 * result + scrolledContainerColor.hashCode()
|
||||
result = 31 * result + navigationIconContentColor.hashCode()
|
||||
result = 31 * result + titleContentColor.hashCode()
|
||||
result = 31 * result + actionIconContentColor.hashCode()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single-row top app bar that is designed to be called by the small and center aligned top app
|
||||
* bar composables.
|
||||
*/
|
||||
@Composable
|
||||
private fun SingleRowTopAppBar(
|
||||
title: @Composable () -> Unit,
|
||||
titleTextStyle: TextStyle,
|
||||
navigationIcon: @Composable () -> Unit,
|
||||
actions: @Composable (RowScope.() -> Unit),
|
||||
windowInsets: WindowInsets,
|
||||
colors: TopAppBarColors,
|
||||
) {
|
||||
// Wrap the given actions in a Row.
|
||||
val actionsRow = @Composable {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = actions
|
||||
)
|
||||
}
|
||||
|
||||
// Compose a Surface with a TopAppBarLayout content.
|
||||
Surface(color = colors.scrolledContainerColor) {
|
||||
val height = LocalDensity.current.run { ContainerHeight.toPx() }
|
||||
TopAppBarLayout(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(windowInsets)
|
||||
// clip after padding so we don't show the title over the inset area
|
||||
.clipToBounds(),
|
||||
heightPx = height,
|
||||
navigationIconContentColor = colors.navigationIconContentColor,
|
||||
titleContentColor = colors.titleContentColor,
|
||||
actionIconContentColor = colors.actionIconContentColor,
|
||||
title = title,
|
||||
titleTextStyle = titleTextStyle,
|
||||
titleAlpha = 1f,
|
||||
titleVerticalArrangement = Arrangement.Center,
|
||||
titleHorizontalArrangement = Arrangement.Start,
|
||||
titleBottomPadding = 0,
|
||||
hideTitleSemantics = false,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actionsRow,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A two-rows top app bar that is designed to be called by the Large and Medium top app bar
|
||||
* composables.
|
||||
*
|
||||
* @throws [IllegalArgumentException] if the given [maxHeight] is equal or smaller than the
|
||||
* [pinnedHeight]
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TwoRowsTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: @Composable () -> Unit,
|
||||
titleTextStyle: TextStyle,
|
||||
titleBottomPadding: Dp,
|
||||
smallTitle: @Composable () -> Unit,
|
||||
smallTitleTextStyle: TextStyle,
|
||||
navigationIcon: @Composable () -> Unit,
|
||||
actions: @Composable RowScope.() -> Unit,
|
||||
windowInsets: WindowInsets,
|
||||
colors: TopAppBarColors,
|
||||
maxHeight: Dp,
|
||||
pinnedHeight: Dp,
|
||||
scrollBehavior: TopAppBarScrollBehavior?
|
||||
) {
|
||||
if (maxHeight <= pinnedHeight) {
|
||||
throw IllegalArgumentException(
|
||||
"A TwoRowsTopAppBar max height should be greater than its pinned height"
|
||||
)
|
||||
}
|
||||
val pinnedHeightPx: Float
|
||||
val maxHeightPx: Float
|
||||
val titleBottomPaddingPx: Int
|
||||
LocalDensity.current.run {
|
||||
pinnedHeightPx = pinnedHeight.toPx()
|
||||
maxHeightPx = maxHeight.toPx()
|
||||
titleBottomPaddingPx = titleBottomPadding.roundToPx()
|
||||
}
|
||||
|
||||
// Sets the app bar's height offset limit to hide just the bottom title area and keep top title
|
||||
// visible when collapsed.
|
||||
SideEffect {
|
||||
if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx) {
|
||||
scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the
|
||||
// bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or
|
||||
// collapse.
|
||||
// This will potentially animate or interpolate a transition between the container color and the
|
||||
// container's scrolled color according to the app bar's scroll state.
|
||||
val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction ?: 0f
|
||||
val appBarContainerColor by rememberUpdatedState(colors.containerColor(colorTransitionFraction))
|
||||
|
||||
// Wrap the given actions in a Row.
|
||||
val actionsRow = @Composable {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = actions
|
||||
)
|
||||
}
|
||||
val topTitleAlpha = TopTitleAlphaEasing.transform(colorTransitionFraction)
|
||||
val bottomTitleAlpha = 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 != null && !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
|
||||
.windowInsetsPadding(windowInsets)
|
||||
// clip after padding so we don't show the title over the inset area
|
||||
.clipToBounds(),
|
||||
heightPx = pinnedHeightPx,
|
||||
navigationIconContentColor = colors.navigationIconContentColor,
|
||||
titleContentColor = colors.titleContentColor,
|
||||
actionIconContentColor = colors.actionIconContentColor,
|
||||
title = smallTitle,
|
||||
titleTextStyle = smallTitleTextStyle,
|
||||
titleAlpha = topTitleAlpha,
|
||||
titleVerticalArrangement = Arrangement.Center,
|
||||
titleHorizontalArrangement = Arrangement.Start,
|
||||
titleBottomPadding = 0,
|
||||
hideTitleSemantics = hideTopRowSemantics,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actionsRow,
|
||||
)
|
||||
TopAppBarLayout(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset
|
||||
?: 0f),
|
||||
navigationIconContentColor = colors.navigationIconContentColor,
|
||||
titleContentColor = colors.titleContentColor,
|
||||
actionIconContentColor = colors.actionIconContentColor,
|
||||
title = title,
|
||||
titleTextStyle = titleTextStyle,
|
||||
titleAlpha = bottomTitleAlpha,
|
||||
titleVerticalArrangement = Arrangement.Bottom,
|
||||
titleHorizontalArrangement = Arrangement.Start,
|
||||
titleBottomPadding = titleBottomPaddingPx,
|
||||
hideTitleSemantics = hideBottomRowSemantics,
|
||||
navigationIcon = {},
|
||||
actions = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The base [Layout] for all top app bars. This function lays out a top app bar navigation icon
|
||||
* (leading icon), a title (header), and action icons (trailing icons). Note that the navigation and
|
||||
* the actions are optional.
|
||||
*
|
||||
* @param heightPx the total height this layout is capped to
|
||||
* @param navigationIconContentColor the content color that will be applied via a
|
||||
* [LocalContentColor] when composing the navigation icon
|
||||
* @param titleContentColor the color that will be applied via a [LocalContentColor] when composing
|
||||
* the title
|
||||
* @param actionIconContentColor the content color that will be applied via a [LocalContentColor]
|
||||
* when composing the action icons
|
||||
* @param title the top app bar title (header)
|
||||
* @param titleTextStyle the title's text style
|
||||
* @param modifier a [Modifier]
|
||||
* @param titleAlpha the title's alpha
|
||||
* @param titleVerticalArrangement the title's vertical arrangement
|
||||
* @param titleHorizontalArrangement the title's horizontal arrangement
|
||||
* @param titleBottomPadding the title's bottom padding
|
||||
* @param hideTitleSemantics hides the title node from the semantic tree. Apply this
|
||||
* boolean when this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics
|
||||
* from accessibility services. This is needed to avoid having multiple titles visible to
|
||||
* accessibility services at the same time, when animating between collapsed / expanded states.
|
||||
* @param navigationIcon a navigation icon [Composable]
|
||||
* @param actions actions [Composable]
|
||||
*/
|
||||
@Composable
|
||||
private fun TopAppBarLayout(
|
||||
modifier: Modifier,
|
||||
heightPx: Float,
|
||||
navigationIconContentColor: Color,
|
||||
titleContentColor: 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(
|
||||
LocalContentColor provides titleContentColor,
|
||||
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.
|
||||
// A TopAppBarTitleInset will make sure the title is offset in case the
|
||||
// navigation icon is missing.
|
||||
else -> max(TopAppBarTitleInset.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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
|
||||
* after the fling settles.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private suspend fun settleAppBar(
|
||||
state: TopAppBarState,
|
||||
velocity: Float,
|
||||
flingAnimationSpec: DecayAnimationSpec<Float>?,
|
||||
snapAnimationSpec: AnimationSpec<Float>?
|
||||
): Velocity {
|
||||
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
|
||||
// and just return Zero Velocity.
|
||||
// Note that we don't check for 0f due to float precision with the collapsedFraction
|
||||
// calculation.
|
||||
if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
|
||||
return Velocity.Zero
|
||||
}
|
||||
var remainingVelocity = velocity
|
||||
// In case there is an initial velocity that was left after a previous user fling, animate to
|
||||
// continue the motion to expand or collapse the app bar.
|
||||
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
|
||||
// avoid rounding errors and stop if anything is unconsumed
|
||||
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
|
||||
}
|
||||
}
|
||||
// Snap if animation specs were provided.
|
||||
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)
|
||||
}
|
||||
|
||||
// An easing function used to compute the alpha value that is applied to the top title part of a
|
||||
// Medium or Large app bar.
|
||||
private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
|
||||
|
||||
private val ContainerHeight = 56.dp
|
||||
private val LargeTitleBottomPadding = 28.dp
|
||||
private val TopAppBarHorizontalPadding = 4.dp
|
||||
|
||||
// A title inset when the App-Bar is a Medium or Large one. Also used to size a spacer when the
|
||||
// navigation icon is missing.
|
||||
private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding
|
||||
@@ -24,17 +24,14 @@ import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -136,7 +133,6 @@ private fun SearchableTopAppBar(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SearchTopAppBar(
|
||||
query: TextFieldValue,
|
||||
@@ -144,20 +140,16 @@ private fun SearchTopAppBar(
|
||||
onClose: () -> Unit,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
Surface(color = SettingsTheme.colorScheme.surfaceHeader) {
|
||||
TopAppBar(
|
||||
title = { SearchBox(query, onQueryChange) },
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
navigationIcon = { CollapseAction(onClose) },
|
||||
actions = {
|
||||
if (query.text.isNotEmpty()) {
|
||||
ClearAction { onQueryChange(TextFieldValue()) }
|
||||
}
|
||||
actions()
|
||||
},
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
CustomizedTopAppBar(
|
||||
title = { SearchBox(query, onQueryChange) },
|
||||
navigationIcon = { CollapseAction(onClose) },
|
||||
actions = {
|
||||
if (query.text.isNotEmpty()) {
|
||||
ClearAction { onQueryChange(TextFieldValue()) }
|
||||
}
|
||||
actions()
|
||||
},
|
||||
)
|
||||
BackHandler { onClose() }
|
||||
}
|
||||
|
||||
|
||||
@@ -17,24 +17,9 @@
|
||||
package com.android.settingslib.spa.widget.scaffold
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.android.settingslib.spa.framework.compose.horizontalValues
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import com.android.settingslib.spa.framework.theme.SettingsTheme
|
||||
import com.android.settingslib.spa.framework.theme.rememberSettingsTypography
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -43,20 +28,12 @@ internal fun SettingsTopAppBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
actions: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
// TODO: Remove MaterialTheme() after top app bar color fixed in AndroidX.
|
||||
MaterialTheme(
|
||||
colorScheme = remember { colorScheme.copy(surface = colorScheme.background) },
|
||||
typography = rememberSettingsTypography(),
|
||||
) {
|
||||
LargeTopAppBar(
|
||||
title = { Title(title) },
|
||||
navigationIcon = { NavigateBack() },
|
||||
actions = actions,
|
||||
colors = largeTopAppBarColors(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
CustomizedLargeTopAppBar(
|
||||
title = title,
|
||||
navigationIcon = { NavigateBack() },
|
||||
actions = actions,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -65,22 +42,3 @@ internal fun TopAppBarScrollBehavior.collapse() {
|
||||
heightOffset = heightOffsetLimit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.padding(WindowInsets.navigationBars.asPaddingValues().horizontalValues())
|
||||
.padding(SettingsDimension.itemPaddingAround),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun largeTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user