禁用 BottomNavigationItem 的涟漪效应
Disable ripple effect of BottomNavigationItem
我的应用程序需要一个底部导航栏。要显示我使用的项目 BottomNavigationItem
:
BottomNavigation(
...
) {
...
BottomNavigationItem(
...
onClick = {onItemSelect(item)},
...
)
}
但是,这些带有连锁反应,我想禁用它,但我不知道如何禁用。 BottomNavigationItem
需要属性 onClick
,因此不能使用 .clickable()
修饰符。
编辑:
来自 Gabriele Mariotti 的 建议将 MutableInteractionSource
传递给 .clickable
函数,以及将 null
传递给 indication
。尽管 BottomNavigationItem
接受 MutableInteractionSource
,但它似乎不接受 indication
。
在这种情况下,您可以提供自定义 LocalRippleTheme
来覆盖默认行为。
类似于:
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
BottomNavigation {
BottomNavigationItem(
//...
)
}
}
}
与:
private object NoRippleTheme : RippleTheme {
@Composable
override fun defaultColor() = Color.Unspecified
@Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f,0.0f,0.0f,0.0f)
}
为了绕过这个,我从 material 库中复制了所有 BottomNavigationItem.kt 文件并更改了我想要的行。
在框范围内,您可以在可选修饰符中看到指示参数。我用 null 传递这个。但是我无法在 class.
之外更改此参数
@Composable
fun RowScope.NoRippleEffectBottomNavigationItem(
selected: Boolean,
onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
selectedContentColor: Color = LocalContentColor.current,
unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
) {
val styledLabel: @Composable (() -> Unit)? = label?.let {
@Composable {
val style = MaterialTheme.typography.caption.copy(textAlign = TextAlign.Center)
ProvideTextStyle(style, content = label)
}
}
// The color of the Ripple should always the selected color, as we want to show the color
// before the item is considered selected, and hence before the new contentColor is
// provided by BottomNavigationTransition.
val ripple = rememberRipple(bounded = false, color = selectedContentColor)
Box(
modifier
.selectable(
selected = selected,
onClick = onClick,
enabled = enabled,
role = Role.Tab,
interactionSource = interactionSource,
indication = null //Changed line.
)
.weight(1f),
contentAlignment = Alignment.Center
) {
BottomNavigationTransition(
selectedContentColor,
unselectedContentColor,
selected
) { progress ->
val animationProgress = if (alwaysShowLabel) 1f else progress
BottomNavigationItemBaselineLayout(
icon = icon,
label = styledLabel,
iconPositionAnimationProgress = animationProgress
)
}
}
}
private val BottomNavigationAnimationSpec = TweenSpec<Float>(
durationMillis = 300,
easing = FastOutSlowInEasing
)
@Composable
private fun BottomNavigationTransition(
activeColor: Color,
inactiveColor: Color,
selected: Boolean,
content: @Composable (animationProgress: Float) -> Unit
) {
val animationProgress by animateFloatAsState(
targetValue = if (selected) 1f else 0f,
animationSpec = BottomNavigationAnimationSpec
)
val color = lerp(inactiveColor, activeColor, animationProgress)
CompositionLocalProvider(
LocalContentColor provides color.copy(alpha = 1f),
LocalContentAlpha provides color.alpha,
) {
content(animationProgress)
}
}
private val BottomNavigationItemHorizontalPadding = 12.dp
@Composable
private fun BottomNavigationItemBaselineLayout(
icon: @Composable () -> Unit,
label: @Composable (() -> Unit)?,
/*@FloatRange(from = 0.0, to = 1.0)*/
iconPositionAnimationProgress: Float
) {
Layout(
{
Box(Modifier.layoutId("icon")) { icon() }
if (label != null) {
Box(
Modifier
.layoutId("label")
.alpha(iconPositionAnimationProgress)
.padding(horizontal = BottomNavigationItemHorizontalPadding)
) { label() }
}
}
) { measurables, constraints ->
val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints)
val labelPlaceable = label?.let {
measurables.first { it.layoutId == "label" }.measure(
// Measure with loose constraints for height as we don't want the label to take up more
// space than it needs
constraints.copy(minHeight = 0)
)
}
// If there is no label, just place the icon.
if (label == null) {
placeIcon(iconPlaceable, constraints)
} else {
placeLabelAndIcon(
labelPlaceable!!,
iconPlaceable,
constraints,
iconPositionAnimationProgress
)
}
}
}
private fun MeasureScope.placeIcon(
iconPlaceable: Placeable,
constraints: Constraints
): MeasureResult {
val height = constraints.maxHeight
val iconY = (height - iconPlaceable.height) / 2
return layout(iconPlaceable.width, height) {
iconPlaceable.placeRelative(0, iconY)
}
}
private val CombinedItemTextBaseline = 12.dp
private fun MeasureScope.placeLabelAndIcon(
labelPlaceable: Placeable,
iconPlaceable: Placeable,
constraints: Constraints,
/*@FloatRange(from = 0.0, to = 1.0)*/
iconPositionAnimationProgress: Float
): MeasureResult {
val height = constraints.maxHeight
// TODO: consider multiple lines of text here, not really supported by spec but we should
// have a better strategy than overlapping the icon and label
val baseline = labelPlaceable[LastBaseline]
val baselineOffset = CombinedItemTextBaseline.roundToPx()
// Label should be [baselineOffset] from the bottom
val labelY = height - baseline - baselineOffset
val unselectedIconY = (height - iconPlaceable.height) / 2
// Icon should be [baselineOffset] from the text baseline, which is itself
// [baselineOffset] from the bottom
val selectedIconY = height - (baselineOffset * 2) - iconPlaceable.height
val containerWidth = max(labelPlaceable.width, iconPlaceable.width)
val labelX = (containerWidth - labelPlaceable.width) / 2
val iconX = (containerWidth - iconPlaceable.width) / 2
// How far the icon needs to move between unselected and selected states
val iconDistance = unselectedIconY - selectedIconY
// When selected the icon is above the unselected position, so we will animate moving
// downwards from the selected state, so when progress is 1, the total distance is 0, and we
// are at the selected state.
val offset = (iconDistance * (1 - iconPositionAnimationProgress)).roundToInt()
return layout(containerWidth, height) {
if (iconPositionAnimationProgress != 0f) {
labelPlaceable.placeRelative(labelX, labelY + offset)
}
iconPlaceable.placeRelative(iconX, selectedIconY + offset)
}
}
我的应用程序需要一个底部导航栏。要显示我使用的项目 BottomNavigationItem
:
BottomNavigation(
...
) {
...
BottomNavigationItem(
...
onClick = {onItemSelect(item)},
...
)
}
但是,这些带有连锁反应,我想禁用它,但我不知道如何禁用。 BottomNavigationItem
需要属性 onClick
,因此不能使用 .clickable()
修饰符。
编辑:
来自 Gabriele Mariotti 的MutableInteractionSource
传递给 .clickable
函数,以及将 null
传递给 indication
。尽管 BottomNavigationItem
接受 MutableInteractionSource
,但它似乎不接受 indication
。
在这种情况下,您可以提供自定义 LocalRippleTheme
来覆盖默认行为。
类似于:
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
BottomNavigation {
BottomNavigationItem(
//...
)
}
}
}
与:
private object NoRippleTheme : RippleTheme {
@Composable
override fun defaultColor() = Color.Unspecified
@Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f,0.0f,0.0f,0.0f)
}
为了绕过这个,我从 material 库中复制了所有 BottomNavigationItem.kt 文件并更改了我想要的行。
在框范围内,您可以在可选修饰符中看到指示参数。我用 null 传递这个。但是我无法在 class.
之外更改此参数@Composable
fun RowScope.NoRippleEffectBottomNavigationItem(
selected: Boolean,
onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
selectedContentColor: Color = LocalContentColor.current,
unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
) {
val styledLabel: @Composable (() -> Unit)? = label?.let {
@Composable {
val style = MaterialTheme.typography.caption.copy(textAlign = TextAlign.Center)
ProvideTextStyle(style, content = label)
}
}
// The color of the Ripple should always the selected color, as we want to show the color
// before the item is considered selected, and hence before the new contentColor is
// provided by BottomNavigationTransition.
val ripple = rememberRipple(bounded = false, color = selectedContentColor)
Box(
modifier
.selectable(
selected = selected,
onClick = onClick,
enabled = enabled,
role = Role.Tab,
interactionSource = interactionSource,
indication = null //Changed line.
)
.weight(1f),
contentAlignment = Alignment.Center
) {
BottomNavigationTransition(
selectedContentColor,
unselectedContentColor,
selected
) { progress ->
val animationProgress = if (alwaysShowLabel) 1f else progress
BottomNavigationItemBaselineLayout(
icon = icon,
label = styledLabel,
iconPositionAnimationProgress = animationProgress
)
}
}
}
private val BottomNavigationAnimationSpec = TweenSpec<Float>(
durationMillis = 300,
easing = FastOutSlowInEasing
)
@Composable
private fun BottomNavigationTransition(
activeColor: Color,
inactiveColor: Color,
selected: Boolean,
content: @Composable (animationProgress: Float) -> Unit
) {
val animationProgress by animateFloatAsState(
targetValue = if (selected) 1f else 0f,
animationSpec = BottomNavigationAnimationSpec
)
val color = lerp(inactiveColor, activeColor, animationProgress)
CompositionLocalProvider(
LocalContentColor provides color.copy(alpha = 1f),
LocalContentAlpha provides color.alpha,
) {
content(animationProgress)
}
}
private val BottomNavigationItemHorizontalPadding = 12.dp
@Composable
private fun BottomNavigationItemBaselineLayout(
icon: @Composable () -> Unit,
label: @Composable (() -> Unit)?,
/*@FloatRange(from = 0.0, to = 1.0)*/
iconPositionAnimationProgress: Float
) {
Layout(
{
Box(Modifier.layoutId("icon")) { icon() }
if (label != null) {
Box(
Modifier
.layoutId("label")
.alpha(iconPositionAnimationProgress)
.padding(horizontal = BottomNavigationItemHorizontalPadding)
) { label() }
}
}
) { measurables, constraints ->
val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints)
val labelPlaceable = label?.let {
measurables.first { it.layoutId == "label" }.measure(
// Measure with loose constraints for height as we don't want the label to take up more
// space than it needs
constraints.copy(minHeight = 0)
)
}
// If there is no label, just place the icon.
if (label == null) {
placeIcon(iconPlaceable, constraints)
} else {
placeLabelAndIcon(
labelPlaceable!!,
iconPlaceable,
constraints,
iconPositionAnimationProgress
)
}
}
}
private fun MeasureScope.placeIcon(
iconPlaceable: Placeable,
constraints: Constraints
): MeasureResult {
val height = constraints.maxHeight
val iconY = (height - iconPlaceable.height) / 2
return layout(iconPlaceable.width, height) {
iconPlaceable.placeRelative(0, iconY)
}
}
private val CombinedItemTextBaseline = 12.dp
private fun MeasureScope.placeLabelAndIcon(
labelPlaceable: Placeable,
iconPlaceable: Placeable,
constraints: Constraints,
/*@FloatRange(from = 0.0, to = 1.0)*/
iconPositionAnimationProgress: Float
): MeasureResult {
val height = constraints.maxHeight
// TODO: consider multiple lines of text here, not really supported by spec but we should
// have a better strategy than overlapping the icon and label
val baseline = labelPlaceable[LastBaseline]
val baselineOffset = CombinedItemTextBaseline.roundToPx()
// Label should be [baselineOffset] from the bottom
val labelY = height - baseline - baselineOffset
val unselectedIconY = (height - iconPlaceable.height) / 2
// Icon should be [baselineOffset] from the text baseline, which is itself
// [baselineOffset] from the bottom
val selectedIconY = height - (baselineOffset * 2) - iconPlaceable.height
val containerWidth = max(labelPlaceable.width, iconPlaceable.width)
val labelX = (containerWidth - labelPlaceable.width) / 2
val iconX = (containerWidth - iconPlaceable.width) / 2
// How far the icon needs to move between unselected and selected states
val iconDistance = unselectedIconY - selectedIconY
// When selected the icon is above the unselected position, so we will animate moving
// downwards from the selected state, so when progress is 1, the total distance is 0, and we
// are at the selected state.
val offset = (iconDistance * (1 - iconPositionAnimationProgress)).roundToInt()
return layout(containerWidth, height) {
if (iconPositionAnimationProgress != 0f) {
labelPlaceable.placeRelative(labelX, labelY + offset)
}
iconPlaceable.placeRelative(iconX, selectedIconY + offset)
}
}