Merge changes from topics "expandable-border", "expandable-interactive-size" into tm-qpr-dev am: 2ea532629c
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20745108 Change-Id: I032f37dbe16131d3917e479d943e3e697e333edb Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
committed by
Automerger Merge Worker
commit
fd5549daa1
@@ -20,13 +20,19 @@ import android.content.Context
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroupOverlay
|
import android.view.ViewGroupOverlay
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||||
import androidx.compose.material3.contentColorFor
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
@@ -34,45 +40,70 @@ import androidx.compose.runtime.DisposableEffect
|
|||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCompositionContext
|
import androidx.compose.runtime.rememberCompositionContext
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.PathOperation
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.drawOutline
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.drawscope.scale
|
import androidx.compose.ui.graphics.drawscope.scale
|
||||||
import androidx.compose.ui.layout.boundsInRoot
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewTreeLifecycleOwner
|
import androidx.lifecycle.ViewTreeLifecycleOwner
|
||||||
import androidx.lifecycle.ViewTreeViewModelStoreOwner
|
import androidx.lifecycle.ViewTreeViewModelStoreOwner
|
||||||
|
import com.android.systemui.animation.Expandable
|
||||||
import com.android.systemui.animation.LaunchAnimator
|
import com.android.systemui.animation.LaunchAnimator
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an expandable shape that can launch into an Activity or a Dialog.
|
* Create an expandable shape that can launch into an Activity or a Dialog.
|
||||||
*
|
*
|
||||||
|
* If this expandable should be expanded when it is clicked directly, then you should specify a
|
||||||
|
* [onClick] handler, which will ensure that this expandable interactive size and background size
|
||||||
|
* are consistent with the M3 components (48dp and 40dp respectively).
|
||||||
|
*
|
||||||
|
* If this expandable should be expanded when a children component is clicked, like a button inside
|
||||||
|
* the expandable, then you can use the Expandable parameter passed to the [content] lambda.
|
||||||
|
*
|
||||||
* Example:
|
* Example:
|
||||||
* ```
|
* ```
|
||||||
* Expandable(
|
* Expandable(
|
||||||
* color = MaterialTheme.colorScheme.primary,
|
* color = MaterialTheme.colorScheme.primary,
|
||||||
* shape = RoundedCornerShape(16.dp),
|
* shape = RoundedCornerShape(16.dp),
|
||||||
* ) { controller ->
|
*
|
||||||
* Row(
|
|
||||||
* Modifier
|
|
||||||
* // For activities:
|
* // For activities:
|
||||||
* .clickable { activityStarter.startActivity(intent, controller.forActivity()) }
|
* onClick = { expandable ->
|
||||||
|
* activityStarter.startActivity(intent, expandable.activityLaunchController())
|
||||||
|
* },
|
||||||
*
|
*
|
||||||
* // For dialogs:
|
* // For dialogs:
|
||||||
* .clickable { dialogLaunchAnimator.show(dialog, controller.forDialog()) }
|
* onClick = { expandable ->
|
||||||
* ) { ... }
|
* dialogLaunchAnimator.show(dialog, controller.dialogLaunchController())
|
||||||
|
* },
|
||||||
|
* ) {
|
||||||
|
* ...
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -85,11 +116,14 @@ fun Expandable(
|
|||||||
shape: Shape,
|
shape: Shape,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentColor: Color = contentColorFor(color),
|
contentColor: Color = contentColorFor(color),
|
||||||
content: @Composable (ExpandableController) -> Unit,
|
borderStroke: BorderStroke? = null,
|
||||||
|
onClick: ((Expandable) -> Unit)? = null,
|
||||||
|
content: @Composable (Expandable) -> Unit,
|
||||||
) {
|
) {
|
||||||
Expandable(
|
Expandable(
|
||||||
rememberExpandableController(color, shape, contentColor),
|
rememberExpandableController(color, shape, contentColor, borderStroke),
|
||||||
modifier,
|
modifier,
|
||||||
|
onClick,
|
||||||
content,
|
content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -118,11 +152,13 @@ fun Expandable(
|
|||||||
* @sample com.android.systemui.compose.gallery.ActivityLaunchScreen
|
* @sample com.android.systemui.compose.gallery.ActivityLaunchScreen
|
||||||
* @sample com.android.systemui.compose.gallery.DialogLaunchScreen
|
* @sample com.android.systemui.compose.gallery.DialogLaunchScreen
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Expandable(
|
fun Expandable(
|
||||||
controller: ExpandableController,
|
controller: ExpandableController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable (ExpandableController) -> Unit,
|
onClick: ((Expandable) -> Unit)? = null,
|
||||||
|
content: @Composable (Expandable) -> Unit,
|
||||||
) {
|
) {
|
||||||
val controller = controller as ExpandableControllerImpl
|
val controller = controller as ExpandableControllerImpl
|
||||||
val color = controller.color
|
val color = controller.color
|
||||||
@@ -136,13 +172,23 @@ fun Expandable(
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalContentColor provides contentColor,
|
LocalContentColor provides contentColor,
|
||||||
) {
|
) {
|
||||||
content(controller)
|
// We make sure that the content itself (wrapped by the background) is at least
|
||||||
|
// 40.dp, which is the same as the M3 buttons. This applies even if onClick is
|
||||||
|
// null, to make it easier to write expandables that are sometimes clickable and
|
||||||
|
// sometimes not. There shouldn't be any Expandable smaller than 40dp because if
|
||||||
|
// the expandable is not clickable directly, then something in its content should
|
||||||
|
// be (and with a size >= 40dp).
|
||||||
|
val minSize = 40.dp
|
||||||
|
Box(
|
||||||
|
Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
content(controller.expandable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val thisExpandableSize by remember {
|
var thisExpandableSize by remember { mutableStateOf(Size.Zero) }
|
||||||
derivedStateOf { controller.boundsInComposeViewRoot.value.size }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we don't read animatorState directly here to avoid recomposition every time the
|
// Make sure we don't read animatorState directly here to avoid recomposition every time the
|
||||||
// state changes (i.e. every frame of the animation).
|
// state changes (i.e. every frame of the animation).
|
||||||
@@ -152,22 +198,42 @@ fun Expandable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this expandable is expanded when it's being directly clicked on, let's ensure that it has
|
||||||
|
// the minimum interactive size followed by all M3 components (48.dp).
|
||||||
|
val minInteractiveSizeModifier =
|
||||||
|
if (onClick != null && LocalMinimumTouchTargetEnforcement.current) {
|
||||||
|
// TODO(b/242040009): Replace this by Modifier.minimumInteractiveComponentSize() once
|
||||||
|
// http://aosp/2305511 is available.
|
||||||
|
val minTouchSize = LocalViewConfiguration.current.minimumTouchTargetSize
|
||||||
|
Modifier.layout { measurable, constraints ->
|
||||||
|
// Copied from androidx.compose.material3.InteractiveComponentSize.kt
|
||||||
|
val placeable = measurable.measure(constraints)
|
||||||
|
val width = maxOf(placeable.width, minTouchSize.width.roundToPx())
|
||||||
|
val height = maxOf(placeable.height, minTouchSize.height.roundToPx())
|
||||||
|
layout(width, height) {
|
||||||
|
val centerX = ((width - placeable.width) / 2f).roundToInt()
|
||||||
|
val centerY = ((height - placeable.height) / 2f).roundToInt()
|
||||||
|
placeable.place(centerX, centerY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
isAnimating -> {
|
isAnimating -> {
|
||||||
// Don't compose the movable content during the animation, as it should be composed only
|
// Don't compose the movable content during the animation, as it should be composed only
|
||||||
// once at all times. We make this spacer exactly the same size as this Expandable when
|
// once at all times. We make this spacer exactly the same size as this Expandable when
|
||||||
// it is visible.
|
// it is visible.
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier
|
modifier.requiredSize(with(controller.density) { thisExpandableSize.toDpSize() })
|
||||||
.clip(shape)
|
|
||||||
.requiredSize(with(controller.density) { thisExpandableSize.toDpSize() })
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The content and its animated background in the overlay. We draw it only when we are
|
// The content and its animated background in the overlay. We draw it only when we are
|
||||||
// animating.
|
// animating.
|
||||||
AnimatedContentInOverlay(
|
AnimatedContentInOverlay(
|
||||||
color,
|
color,
|
||||||
thisExpandableSize,
|
controller.boundsInComposeViewRoot.value.size,
|
||||||
controller.animatorState,
|
controller.animatorState,
|
||||||
controller.overlay.value
|
controller.overlay.value
|
||||||
?: error("AnimatedContentInOverlay shouldn't be composed with null overlay."),
|
?: error("AnimatedContentInOverlay shouldn't be composed with null overlay."),
|
||||||
@@ -181,6 +247,8 @@ fun Expandable(
|
|||||||
controller.isDialogShowing.value -> {
|
controller.isDialogShowing.value -> {
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
|
.onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
|
||||||
|
.then(minInteractiveSizeModifier)
|
||||||
.drawWithContent { /* Don't draw anything when the dialog is shown. */}
|
.drawWithContent { /* Don't draw anything when the dialog is shown. */}
|
||||||
.onGloballyPositioned {
|
.onGloballyPositioned {
|
||||||
controller.boundsInComposeViewRoot.value = it.boundsInRoot()
|
controller.boundsInComposeViewRoot.value = it.boundsInRoot()
|
||||||
@@ -188,11 +256,29 @@ fun Expandable(
|
|||||||
) { wrappedContent(controller) }
|
) { wrappedContent(controller) }
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Box(
|
val clickModifier =
|
||||||
modifier.clip(shape).background(color, shape).onGloballyPositioned {
|
if (onClick != null) {
|
||||||
controller.boundsInComposeViewRoot.value = it.boundsInRoot()
|
Modifier.clickable { onClick(controller.expandable) }
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier
|
||||||
|
.onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
|
||||||
|
.then(minInteractiveSizeModifier)
|
||||||
|
// Note that clip() *must* be above the clickModifier to properly clip the
|
||||||
|
// ripple.
|
||||||
|
.clip(shape)
|
||||||
|
.then(clickModifier)
|
||||||
|
.background(color, shape)
|
||||||
|
.border(controller)
|
||||||
|
.onGloballyPositioned {
|
||||||
|
controller.boundsInComposeViewRoot.value = it.boundsInRoot()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
wrappedContent(controller)
|
||||||
}
|
}
|
||||||
) { wrappedContent(controller) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +290,7 @@ private fun AnimatedContentInOverlay(
|
|||||||
sizeInOriginalLayout: Size,
|
sizeInOriginalLayout: Size,
|
||||||
animatorState: State<LaunchAnimator.State?>,
|
animatorState: State<LaunchAnimator.State?>,
|
||||||
overlay: ViewGroupOverlay,
|
overlay: ViewGroupOverlay,
|
||||||
controller: ExpandableController,
|
controller: ExpandableControllerImpl,
|
||||||
content: @Composable (ExpandableController) -> Unit,
|
content: @Composable (ExpandableController) -> Unit,
|
||||||
composeViewRoot: View,
|
composeViewRoot: View,
|
||||||
onOverlayComposeViewChanged: (View?) -> Unit,
|
onOverlayComposeViewChanged: (View?) -> Unit,
|
||||||
@@ -254,24 +340,7 @@ private fun AnimatedContentInOverlay(
|
|||||||
return@drawWithContent
|
return@drawWithContent
|
||||||
}
|
}
|
||||||
|
|
||||||
val topRadius = animatorState.topCornerRadius
|
drawBackground(animatorState, color, controller.borderStroke)
|
||||||
val bottomRadius = animatorState.bottomCornerRadius
|
|
||||||
if (topRadius == bottomRadius) {
|
|
||||||
// Shortcut to avoid Outline calculation and allocation.
|
|
||||||
val cornerRadius = CornerRadius(topRadius)
|
|
||||||
drawRoundRect(color, cornerRadius = cornerRadius)
|
|
||||||
} else {
|
|
||||||
val shape =
|
|
||||||
RoundedCornerShape(
|
|
||||||
topStart = topRadius,
|
|
||||||
topEnd = topRadius,
|
|
||||||
bottomStart = bottomRadius,
|
|
||||||
bottomEnd = bottomRadius,
|
|
||||||
)
|
|
||||||
val outline = shape.createOutline(size, layoutDirection, this)
|
|
||||||
drawOutline(outline, color = color)
|
|
||||||
}
|
|
||||||
|
|
||||||
drawContent()
|
drawContent()
|
||||||
},
|
},
|
||||||
// We center the content in the expanding container.
|
// We center the content in the expanding container.
|
||||||
@@ -360,3 +429,107 @@ private fun getOverlayViewGroup(context: Context, overlay: ViewGroupOverlay): Vi
|
|||||||
overlay.remove(view)
|
overlay.remove(view)
|
||||||
return current as ViewGroup
|
return current as ViewGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Modifier.border(controller: ExpandableControllerImpl): Modifier {
|
||||||
|
return if (controller.borderStroke != null) {
|
||||||
|
this.border(controller.borderStroke, controller.shape)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ContentDrawScope.drawBackground(
|
||||||
|
animatorState: LaunchAnimator.State,
|
||||||
|
color: Color,
|
||||||
|
border: BorderStroke?,
|
||||||
|
) {
|
||||||
|
val topRadius = animatorState.topCornerRadius
|
||||||
|
val bottomRadius = animatorState.bottomCornerRadius
|
||||||
|
if (topRadius == bottomRadius) {
|
||||||
|
// Shortcut to avoid Outline calculation and allocation.
|
||||||
|
val cornerRadius = CornerRadius(topRadius)
|
||||||
|
|
||||||
|
// Draw the background.
|
||||||
|
drawRoundRect(color, cornerRadius = cornerRadius)
|
||||||
|
|
||||||
|
// Draw the border.
|
||||||
|
if (border != null) {
|
||||||
|
// Copied from androidx.compose.foundation.Border.kt
|
||||||
|
val strokeWidth = border.width.toPx()
|
||||||
|
val halfStroke = strokeWidth / 2
|
||||||
|
val borderStroke = Stroke(strokeWidth)
|
||||||
|
|
||||||
|
drawRoundRect(
|
||||||
|
brush = border.brush,
|
||||||
|
topLeft = Offset(halfStroke, halfStroke),
|
||||||
|
size = Size(size.width - strokeWidth, size.height - strokeWidth),
|
||||||
|
cornerRadius = cornerRadius.shrink(halfStroke),
|
||||||
|
style = borderStroke
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val shape =
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = topRadius,
|
||||||
|
topEnd = topRadius,
|
||||||
|
bottomStart = bottomRadius,
|
||||||
|
bottomEnd = bottomRadius,
|
||||||
|
)
|
||||||
|
val outline = shape.createOutline(size, layoutDirection, this)
|
||||||
|
|
||||||
|
// Draw the background.
|
||||||
|
drawOutline(outline, color = color)
|
||||||
|
|
||||||
|
// Draw the border.
|
||||||
|
if (border != null) {
|
||||||
|
// Copied from androidx.compose.foundation.Border.kt.
|
||||||
|
val strokeWidth = border.width.toPx()
|
||||||
|
val path =
|
||||||
|
createRoundRectPath(
|
||||||
|
(outline as Outline.Rounded).roundRect,
|
||||||
|
strokeWidth,
|
||||||
|
)
|
||||||
|
|
||||||
|
drawPath(path, border.brush)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method that creates a round rect with the inner region removed by the given stroke width.
|
||||||
|
*
|
||||||
|
* Copied from androidx.compose.foundation.Border.kt.
|
||||||
|
*/
|
||||||
|
private fun createRoundRectPath(
|
||||||
|
roundedRect: RoundRect,
|
||||||
|
strokeWidth: Float,
|
||||||
|
): Path {
|
||||||
|
return Path().apply {
|
||||||
|
addRoundRect(roundedRect)
|
||||||
|
val insetPath =
|
||||||
|
Path().apply { addRoundRect(createInsetRoundedRect(strokeWidth, roundedRect)) }
|
||||||
|
op(this, insetPath, PathOperation.Difference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copied from androidx.compose.foundation.Border.kt. */
|
||||||
|
private fun createInsetRoundedRect(widthPx: Float, roundedRect: RoundRect) =
|
||||||
|
RoundRect(
|
||||||
|
left = widthPx,
|
||||||
|
top = widthPx,
|
||||||
|
right = roundedRect.width - widthPx,
|
||||||
|
bottom = roundedRect.height - widthPx,
|
||||||
|
topLeftCornerRadius = roundedRect.topLeftCornerRadius.shrink(widthPx),
|
||||||
|
topRightCornerRadius = roundedRect.topRightCornerRadius.shrink(widthPx),
|
||||||
|
bottomLeftCornerRadius = roundedRect.bottomLeftCornerRadius.shrink(widthPx),
|
||||||
|
bottomRightCornerRadius = roundedRect.bottomRightCornerRadius.shrink(widthPx)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to shrink the corner radius by the given value, clamping to 0 if the resultant
|
||||||
|
* corner radius would be negative.
|
||||||
|
*
|
||||||
|
* Copied from androidx.compose.foundation.Border.kt.
|
||||||
|
*/
|
||||||
|
private fun CornerRadius.shrink(value: Float): CornerRadius =
|
||||||
|
CornerRadius(max(0f, this.x - value), max(0f, this.y - value))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroupOverlay
|
import android.view.ViewGroupOverlay
|
||||||
import android.view.ViewRootImpl
|
import android.view.ViewRootImpl
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.material3.contentColorFor
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -62,6 +63,7 @@ fun rememberExpandableController(
|
|||||||
color: Color,
|
color: Color,
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
contentColor: Color = contentColorFor(color),
|
contentColor: Color = contentColorFor(color),
|
||||||
|
borderStroke: BorderStroke? = null,
|
||||||
): ExpandableController {
|
): ExpandableController {
|
||||||
val composeViewRoot = LocalView.current
|
val composeViewRoot = LocalView.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -87,11 +89,20 @@ fun rememberExpandableController(
|
|||||||
val isComposed = remember { mutableStateOf(true) }
|
val isComposed = remember { mutableStateOf(true) }
|
||||||
DisposableEffect(Unit) { onDispose { isComposed.value = false } }
|
DisposableEffect(Unit) { onDispose { isComposed.value = false } }
|
||||||
|
|
||||||
return remember(color, contentColor, shape, composeViewRoot, density, layoutDirection) {
|
return remember(
|
||||||
|
color,
|
||||||
|
contentColor,
|
||||||
|
shape,
|
||||||
|
borderStroke,
|
||||||
|
composeViewRoot,
|
||||||
|
density,
|
||||||
|
layoutDirection,
|
||||||
|
) {
|
||||||
ExpandableControllerImpl(
|
ExpandableControllerImpl(
|
||||||
color,
|
color,
|
||||||
contentColor,
|
contentColor,
|
||||||
shape,
|
shape,
|
||||||
|
borderStroke,
|
||||||
composeViewRoot,
|
composeViewRoot,
|
||||||
density,
|
density,
|
||||||
animatorState,
|
animatorState,
|
||||||
@@ -109,6 +120,7 @@ internal class ExpandableControllerImpl(
|
|||||||
internal val color: Color,
|
internal val color: Color,
|
||||||
internal val contentColor: Color,
|
internal val contentColor: Color,
|
||||||
internal val shape: Shape,
|
internal val shape: Shape,
|
||||||
|
internal val borderStroke: BorderStroke?,
|
||||||
internal val composeViewRoot: View,
|
internal val composeViewRoot: View,
|
||||||
internal val density: Density,
|
internal val density: Density,
|
||||||
internal val animatorState: MutableState<LaunchAnimator.State?>,
|
internal val animatorState: MutableState<LaunchAnimator.State?>,
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 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.systemui.compose.modifiers
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.DrawModifier
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||||
|
import androidx.compose.ui.platform.InspectorInfo
|
||||||
|
import androidx.compose.ui.platform.InspectorValueInfo
|
||||||
|
import androidx.compose.ui.platform.debugInspectorInfo
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a fading [shape] with a solid [color] and [alpha] behind the content.
|
||||||
|
*
|
||||||
|
* @param color color to paint background with
|
||||||
|
* @param alpha alpha of the background
|
||||||
|
* @param shape desired shape of the background
|
||||||
|
*/
|
||||||
|
fun Modifier.background(
|
||||||
|
color: Color,
|
||||||
|
alpha: () -> Float,
|
||||||
|
shape: Shape = RectangleShape,
|
||||||
|
) =
|
||||||
|
this.then(
|
||||||
|
FadingBackground(
|
||||||
|
brush = SolidColor(color),
|
||||||
|
alpha = alpha,
|
||||||
|
shape = shape,
|
||||||
|
inspectorInfo =
|
||||||
|
debugInspectorInfo {
|
||||||
|
name = "background"
|
||||||
|
value = color
|
||||||
|
properties["color"] = color
|
||||||
|
properties["alpha"] = alpha
|
||||||
|
properties["shape"] = shape
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class FadingBackground
|
||||||
|
constructor(
|
||||||
|
private val brush: Brush,
|
||||||
|
private val shape: Shape,
|
||||||
|
private val alpha: () -> Float,
|
||||||
|
inspectorInfo: InspectorInfo.() -> Unit
|
||||||
|
) : DrawModifier, InspectorValueInfo(inspectorInfo) {
|
||||||
|
// naive cache outline calculation if size is the same
|
||||||
|
private var lastSize: Size? = null
|
||||||
|
private var lastLayoutDirection: LayoutDirection? = null
|
||||||
|
private var lastOutline: Outline? = null
|
||||||
|
|
||||||
|
override fun ContentDrawScope.draw() {
|
||||||
|
if (shape === RectangleShape) {
|
||||||
|
// shortcut to avoid Outline calculation and allocation
|
||||||
|
drawRect()
|
||||||
|
} else {
|
||||||
|
drawOutline()
|
||||||
|
}
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ContentDrawScope.drawRect() {
|
||||||
|
drawRect(brush, alpha = alpha())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ContentDrawScope.drawOutline() {
|
||||||
|
val outline =
|
||||||
|
if (size == lastSize && layoutDirection == lastLayoutDirection) {
|
||||||
|
lastOutline!!
|
||||||
|
} else {
|
||||||
|
shape.createOutline(size, layoutDirection, this)
|
||||||
|
}
|
||||||
|
drawOutline(outline, brush = brush, alpha = alpha())
|
||||||
|
lastOutline = outline
|
||||||
|
lastSize = size
|
||||||
|
lastLayoutDirection = layoutDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = brush.hashCode()
|
||||||
|
result = 31 * result + alpha.hashCode()
|
||||||
|
result = 31 * result + shape.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
val otherModifier = other as? FadingBackground ?: return false
|
||||||
|
return brush == otherModifier.brush &&
|
||||||
|
alpha == otherModifier.alpha &&
|
||||||
|
shape == otherModifier.shape
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "FadingBackground(brush=$brush, alpha = $alpha, shape=$shape)"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user