summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jordan Demeulenaere <jdemeulenaere@google.com> 2025-02-17 10:10:49 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-02-17 10:10:49 -0800
commit1c2633f9af68fc1671d13d617708f5532c18f07e (patch)
tree164f959a2b3f506cc56faa9037b27818de6bc2e4
parent4e16555d2b90fa0090d3018a772d298567b4fa0b (diff)
parentcad8f822d132b4bafa003061d44977ad40761f6f (diff)
Merge changes If0c48e56,Ib4541b47 into main
* changes: Clean-up ExpandableController Extract Expandable related functions to be used by ag/31718343
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt111
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt100
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt13
3 files changed, 115 insertions, 109 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
index ae75e6c089ca..a1d362a4a11d 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
@@ -39,7 +39,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
@@ -175,21 +174,7 @@ fun Expandable(
val wrappedContent =
remember(content) {
movableContentOf { expandable: Expandable ->
- CompositionLocalProvider(LocalContentColor provides contentColor) {
- // 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(expandable)
- }
- }
+ WrappedContent(expandable, contentColor, content)
}
}
@@ -209,11 +194,7 @@ fun Expandable(
// Make sure we don't read animatorState directly here to avoid recomposition every time the
// state changes (i.e. every frame of the animation).
- val isAnimating by remember {
- derivedStateOf {
- controller.animatorState.value != null && controller.overlay.value != null
- }
- }
+ val isAnimating = controller.isAnimating
// 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).
@@ -237,58 +218,36 @@ fun Expandable(
// animating.
AnimatedContentInOverlay(
color,
- controller.boundsInComposeViewRoot.value.size,
- controller.animatorState,
- controller.overlay.value
+ controller.boundsInComposeViewRoot.size,
+ controller.overlay
?: error("AnimatedContentInOverlay shouldn't be composed with null overlay."),
controller,
wrappedContent,
controller.composeViewRoot,
- { controller.currentComposeViewInOverlay.value = it },
+ { controller.currentComposeViewInOverlay = it },
controller.density,
)
}
- controller.isDialogShowing.value -> {
+ controller.isDialogShowing -> {
Box(
modifier
.updateExpandableSize()
.then(minInteractiveSizeModifier)
.drawWithContent { /* Don't draw anything when the dialog is shown. */ }
- .onGloballyPositioned {
- controller.boundsInComposeViewRoot.value = it.boundsInRoot()
- }
+ .onGloballyPositioned { controller.boundsInComposeViewRoot = it.boundsInRoot() }
) {
wrappedContent(controller.expandable)
}
}
else -> {
- val clickModifier =
- if (onClick != null) {
- if (interactionSource != null) {
- // If the caller provided an interaction source, then that means that they
- // will draw the click indication themselves.
- Modifier.clickable(interactionSource, indication = null) {
- onClick(controller.expandable)
- }
- } else {
- // If no interaction source is provided, we draw the default indication (a
- // ripple) and make sure it's clipped by the expandable shape.
- Modifier.clip(shape).clickable { onClick(controller.expandable) }
- }
- } else {
- Modifier
- }
-
Box(
modifier
.updateExpandableSize()
.then(minInteractiveSizeModifier)
- .then(clickModifier)
+ .then(clickModifier(controller, onClick, interactionSource))
.background(color, shape)
.border(controller)
- .onGloballyPositioned {
- controller.boundsInComposeViewRoot.value = it.boundsInRoot()
- }
+ .onGloballyPositioned { controller.boundsInComposeViewRoot = it.boundsInRoot() }
) {
wrappedContent(controller.expandable)
}
@@ -296,12 +255,55 @@ fun Expandable(
}
}
+@Composable
+private fun WrappedContent(
+ expandable: Expandable,
+ contentColor: Color,
+ content: @Composable (Expandable) -> Unit,
+) {
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ // 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(expandable)
+ }
+ }
+}
+
+private fun clickModifier(
+ controller: ExpandableControllerImpl,
+ onClick: ((Expandable) -> Unit)?,
+ interactionSource: MutableInteractionSource?,
+): Modifier {
+ if (onClick == null) {
+ return Modifier
+ }
+
+ if (interactionSource != null) {
+ // If the caller provided an interaction source, then that means that they will draw the
+ // click indication themselves.
+ return Modifier.clickable(interactionSource, indication = null) {
+ onClick(controller.expandable)
+ }
+ }
+
+ // If no interaction source is provided, we draw the default indication (a ripple) and make sure
+ // it's clipped by the expandable shape.
+ return Modifier.clip(controller.shape).clickable { onClick(controller.expandable) }
+}
+
/** Draw [content] in [overlay] while respecting its screen position given by [animatorState]. */
@Composable
private fun AnimatedContentInOverlay(
color: Color,
sizeInOriginalLayout: Size,
- animatorState: State<TransitionAnimator.State?>,
overlay: ViewGroupOverlay,
controller: ExpandableControllerImpl,
content: @Composable (Expandable) -> Unit,
@@ -324,7 +326,7 @@ private fun AnimatedContentInOverlay(
// so that its content is laid out exactly the same way.
.requiredSize(with(density) { sizeInOriginalLayout.toDpSize() })
.drawWithContent {
- val animatorState = animatorState.value ?: return@drawWithContent
+ val animatorState = controller.animatorState ?: return@drawWithContent
// Scale the content with the background while keeping its aspect ratio.
val widthRatio =
@@ -348,7 +350,8 @@ private fun AnimatedContentInOverlay(
setContent {
Box(
Modifier.fillMaxSize().drawWithContent {
- val animatorState = animatorState.value ?: return@drawWithContent
+ val animatorState =
+ controller.animatorState ?: return@drawWithContent
if (!animatorState.visible) {
return@drawWithContent
}
@@ -385,7 +388,7 @@ private fun AnimatedContentInOverlay(
overlay.add(composeViewInOverlay)
val startState =
- animatorState.value
+ controller.animatorState
?: throw IllegalStateException(
"AnimatedContentInOverlay shouldn't be composed with null animatorState."
)
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
index c5d2802c8941..377ea96c5723 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
@@ -25,10 +25,11 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
@@ -53,6 +54,9 @@ interface ExpandableController {
/** The [Expandable] controlled by this controller. */
val expandable: Expandable
+ /** Whether this controller is currently animating a launch. */
+ val isAnimating: Boolean
+
/** Called when the [Expandable] stop being included in the composition. */
fun onDispose()
}
@@ -73,24 +77,9 @@ fun rememberExpandableController(
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
- // The current animation state, if we are currently animating a dialog or activity.
- val animatorState = remember { mutableStateOf<TransitionAnimator.State?>(null) }
-
- // Whether a dialog controlled by this ExpandableController is currently showing.
- val isDialogShowing = remember { mutableStateOf(false) }
-
- // The overlay in which we should animate the launch.
- val overlay = remember { mutableStateOf<ViewGroupOverlay?>(null) }
-
- // The current [ComposeView] being animated in the [overlay], if any.
- val currentComposeViewInOverlay = remember { mutableStateOf<View?>(null) }
-
- // The bounds in [composeViewRoot] of the expandable controlled by this controller.
- val boundsInComposeViewRoot = remember { mutableStateOf(Rect.Zero) }
-
// Whether this composable is still composed. We only do the dialog exit animation if this is
// true.
- val isComposed = remember { mutableStateOf(true) }
+ var isComposed by remember { mutableStateOf(true) }
val controller =
remember(
@@ -109,19 +98,14 @@ fun rememberExpandableController(
borderStroke,
composeViewRoot,
density,
- animatorState,
- isDialogShowing,
- overlay,
- currentComposeViewInOverlay,
- boundsInComposeViewRoot,
layoutDirection,
- isComposed,
+ { isComposed },
)
}
DisposableEffect(Unit) {
onDispose {
- isComposed.value = false
+ isComposed = false
if (TransitionAnimator.returnAnimationsEnabled()) {
controller.onDispose()
}
@@ -138,14 +122,27 @@ internal class ExpandableControllerImpl(
internal val borderStroke: BorderStroke?,
internal val composeViewRoot: View,
internal val density: Density,
- internal val animatorState: MutableState<TransitionAnimator.State?>,
- internal val isDialogShowing: MutableState<Boolean>,
- internal val overlay: MutableState<ViewGroupOverlay?>,
- internal val currentComposeViewInOverlay: MutableState<View?>,
- internal val boundsInComposeViewRoot: MutableState<Rect>,
private val layoutDirection: LayoutDirection,
- private val isComposed: State<Boolean>,
+ private val isComposed: () -> Boolean,
) : ExpandableController {
+ /** The current animation state, if we are currently animating a dialog or activity. */
+ var animatorState by mutableStateOf<TransitionAnimator.State?>(null)
+ private set
+
+ /** Whether a dialog controlled by this ExpandableController is currently showing. */
+ var isDialogShowing by mutableStateOf(false)
+ private set
+
+ /** The overlay in which we should animate the launch. */
+ var overlay by mutableStateOf<ViewGroupOverlay?>(null)
+ private set
+
+ /** The current [ComposeView] being animated in the [overlay], if any. */
+ var currentComposeViewInOverlay by mutableStateOf<View?>(null)
+
+ /** The bounds in [composeViewRoot] of the expandable controlled by this controller. */
+ var boundsInComposeViewRoot by mutableStateOf(Rect.Zero)
+
/** The [ActivityTransitionAnimator.Controller] to be cleaned up [onDispose]. */
private var activityControllerForDisposal: ActivityTransitionAnimator.Controller? = null
@@ -158,7 +155,7 @@ internal class ExpandableControllerImpl(
returnCujType: Int?,
isEphemeral: Boolean,
): ActivityTransitionAnimator.Controller? {
- if (!isComposed.value) {
+ if (!isComposed()) {
return null
}
@@ -174,7 +171,7 @@ internal class ExpandableControllerImpl(
override fun dialogTransitionController(
cuj: DialogCuj?
): DialogTransitionAnimator.Controller? {
- if (!isComposed.value) {
+ if (!isComposed()) {
return null
}
@@ -182,6 +179,8 @@ internal class ExpandableControllerImpl(
}
}
+ override val isAnimating: Boolean by derivedStateOf { animatorState != null && overlay != null }
+
override fun onDispose() {
activityControllerForDisposal?.onDispose()
activityControllerForDisposal = null
@@ -204,7 +203,7 @@ internal class ExpandableControllerImpl(
override val isLaunching: Boolean = true
override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
- animatorState.value = null
+ animatorState = null
}
override fun onTransitionAnimationProgress(
@@ -214,7 +213,7 @@ internal class ExpandableControllerImpl(
) {
// We copy state given that it's always the same object that is mutated by
// ActivityTransitionAnimator.
- animatorState.value =
+ animatorState =
TransitionAnimator.State(
state.top,
state.bottom,
@@ -227,13 +226,11 @@ internal class ExpandableControllerImpl(
// Force measure and layout the ComposeView in the overlay whenever the animation
// state changes.
- currentComposeViewInOverlay.value?.let {
- measureAndLayoutComposeViewInOverlay(it, state)
- }
+ currentComposeViewInOverlay?.let { measureAndLayoutComposeViewInOverlay(it, state) }
}
override fun createAnimatorState(): TransitionAnimator.State {
- val boundsInRoot = boundsInComposeViewRoot.value
+ val boundsInRoot = boundsInComposeViewRoot
val outline =
shape.createOutline(
Size(boundsInRoot.width, boundsInRoot.height),
@@ -285,7 +282,7 @@ internal class ExpandableControllerImpl(
private fun rootLocationOnScreen(): Offset {
composeViewRoot.getLocationOnScreen(rootLocationOnScreen)
- val boundsInRoot = boundsInComposeViewRoot.value
+ val boundsInRoot = boundsInComposeViewRoot
val x = rootLocationOnScreen[0] + boundsInRoot.left
val y = rootLocationOnScreen[1] + boundsInRoot.top
return Offset(x, y)
@@ -319,14 +316,14 @@ internal class ExpandableControllerImpl(
override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
delegate.onTransitionAnimationStart(isExpandingFullyAbove)
- overlay.value = transitionContainer.overlay as ViewGroupOverlay
+ overlay = transitionContainer.overlay as ViewGroupOverlay
cujType?.let { InteractionJankMonitor.getInstance().begin(composeViewRoot, it) }
}
override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
cujType?.let { InteractionJankMonitor.getInstance().end(it) }
delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
- overlay.value = null
+ overlay = null
}
}
}
@@ -339,14 +336,14 @@ internal class ExpandableControllerImpl(
override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
val newOverlay = viewGroup.overlay as ViewGroupOverlay
- if (newOverlay != overlay.value) {
- overlay.value = newOverlay
+ if (newOverlay != overlay) {
+ overlay = newOverlay
}
}
override fun stopDrawingInOverlay() {
- if (overlay.value != null) {
- overlay.value = null
+ if (overlay != null) {
+ overlay = null
}
}
@@ -357,7 +354,7 @@ internal class ExpandableControllerImpl(
delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
// Make sure we don't draw this expandable when the dialog is showing.
- isDialogShowing.value = true
+ isDialogShowing = true
}
}
}
@@ -367,16 +364,17 @@ internal class ExpandableControllerImpl(
return object : TransitionAnimator.Controller by delegate {
override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
- isDialogShowing.value = false
+ isDialogShowing = false
}
}
}
- override fun shouldAnimateExit(): Boolean =
- isComposed.value && composeViewRoot.isAttachedToWindow && composeViewRoot.isShown
+ override fun shouldAnimateExit(): Boolean {
+ return isComposed() && composeViewRoot.isAttachedToWindow && composeViewRoot.isShown
+ }
override fun onExitAnimationCancelled() {
- isDialogShowing.value = false
+ isDialogShowing = false
}
override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt
index f5c3a834a8d7..089da4b932b2 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt
@@ -42,13 +42,19 @@ import androidx.savedstate.setViewTreeSavedStateRegistryOwner
@Composable
fun Modifier.drawInOverlay(): Modifier {
val containerState = remember { ContainerState() }
+ FullScreenComposeViewInOverlay { Modifier.container(containerState) }
+ return this.drawInContainer(containerState, enabled = { true })
+}
+
+@Composable
+internal fun FullScreenComposeViewInOverlay(modifier: (ComposeView) -> Modifier = { Modifier }) {
val context = LocalContext.current
val localView = LocalView.current
val compositionContext = rememberCompositionContext()
val displayMetrics = context.resources.displayMetrics
val displaySize = IntSize(displayMetrics.widthPixels, displayMetrics.heightPixels)
- DisposableEffect(containerState, context, localView, compositionContext, displaySize) {
+ DisposableEffect(context, localView, compositionContext, displaySize) {
val overlay = localView.rootView.overlay as ViewGroupOverlay
val view =
ComposeView(context).apply {
@@ -59,7 +65,8 @@ fun Modifier.drawInOverlay(): Modifier {
setViewTreeViewModelStoreOwner(localView.findViewTreeViewModelStoreOwner())
setViewTreeSavedStateRegistryOwner(localView.findViewTreeSavedStateRegistryOwner())
- setContent { Box(Modifier.fillMaxSize().container(containerState)) }
+ val view = this
+ setContent { Box(modifier(view).fillMaxSize()) }
}
overlay.add(view)
@@ -74,6 +81,4 @@ fun Modifier.drawInOverlay(): Modifier {
onDispose { overlay.remove(view) }
}
-
- return this.drawInContainer(containerState, enabled = { true })
}