diff options
| author | 2025-02-17 10:10:49 -0800 | |
|---|---|---|
| committer | 2025-02-17 10:10:49 -0800 | |
| commit | 1c2633f9af68fc1671d13d617708f5532c18f07e (patch) | |
| tree | 164f959a2b3f506cc56faa9037b27818de6bc2e4 | |
| parent | 4e16555d2b90fa0090d3018a772d298567b4fa0b (diff) | |
| parent | cad8f822d132b4bafa003061d44977ad40761f6f (diff) | |
Merge changes If0c48e56,Ib4541b47 into main
* changes:
Clean-up ExpandableController
Extract Expandable related functions to be used by ag/31718343
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 }) } |