Loading packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt +155 −4 Original line number Diff line number Diff line Loading @@ -31,14 +31,13 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.contentColorFor import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.State import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf Loading @@ -62,9 +61,17 @@ 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.translate import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Density Loading @@ -75,6 +82,8 @@ import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.FullScreenComposeViewInOverlay import com.android.systemui.animation.Expandable import com.android.systemui.animation.TransitionAnimator import kotlin.math.max Loading Loading @@ -122,6 +131,9 @@ fun Expandable( borderStroke: BorderStroke? = null, onClick: ((Expandable) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, // TODO(b/285250939): Default this to true then remove once the Compose QS expandables have // proven that the new implementation is robust. useModifierBasedImplementation: Boolean = false, content: @Composable (Expandable) -> Unit, ) { Expandable( Loading @@ -129,6 +141,7 @@ fun Expandable( modifier, onClick, interactionSource, useModifierBasedImplementation, content, ) } Loading Loading @@ -157,16 +170,26 @@ fun Expandable( * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen * @sample com.android.systemui.compose.gallery.DialogLaunchScreen */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun Expandable( controller: ExpandableController, modifier: Modifier = Modifier, onClick: ((Expandable) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, // TODO(b/285250939): Default this to true then remove once the Compose QS expandables have // proven that the new implementation is robust. useModifierBasedImplementation: Boolean = false, content: @Composable (Expandable) -> Unit, ) { val controller = controller as ExpandableControllerImpl if (useModifierBasedImplementation) { Box(modifier.expandable(controller, onClick, interactionSource)) { WrappedContent(controller.expandable, controller.contentColor, content) } return } val color = controller.color val contentColor = controller.contentColor val shape = controller.shape Loading Loading @@ -277,6 +300,133 @@ private fun WrappedContent( } } @Composable @Stable private fun Modifier.expandable( controller: ExpandableController, onClick: ((Expandable) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, ): Modifier { val controller = controller as ExpandableControllerImpl val isAnimating = controller.isAnimating val drawInOverlayModifier = if (isAnimating) { val graphicsLayer = rememberGraphicsLayer() FullScreenComposeViewInOverlay { view -> Modifier.then(DrawExpandableInOverlayElement(view, controller, graphicsLayer)) } Modifier.drawWithContent { graphicsLayer.record { this@drawWithContent.drawContent() } } } else { null } return this.thenIf(onClick != null) { Modifier.minimumInteractiveComponentSize() } .thenIf(!isAnimating) { Modifier.border(controller) .then(clickModifier(controller, onClick, interactionSource)) .background(controller.color, controller.shape) } .thenIf(drawInOverlayModifier != null) { drawInOverlayModifier!! } .onPlaced { controller.boundsInComposeViewRoot = it.boundsInRoot() } .thenIf(!isAnimating && controller.isDialogShowing) { Modifier.layout { measurable, constraints -> measurable.measure(constraints).run { layout(width, height) { /* Do not place/draw. */ } } } } } private data class DrawExpandableInOverlayElement( private val overlayComposeView: ComposeView, private val controller: ExpandableControllerImpl, private val contentGraphicsLayer: GraphicsLayer, ) : ModifierNodeElement<DrawExpandableInOverlayNode>() { override fun create(): DrawExpandableInOverlayNode { return DrawExpandableInOverlayNode(overlayComposeView, controller, contentGraphicsLayer) } override fun update(node: DrawExpandableInOverlayNode) { node.update(overlayComposeView, controller, contentGraphicsLayer) } } private class DrawExpandableInOverlayNode( composeView: ComposeView, controller: ExpandableControllerImpl, private var contentGraphicsLayer: GraphicsLayer, ) : Modifier.Node(), DrawModifierNode { private var controller = controller set(value) { resetCurrentNodeInOverlay() field = value setCurrentNodeInOverlay() } private var composeViewLocationOnScreen = composeView.locationOnScreen fun update( composeView: ComposeView, controller: ExpandableControllerImpl, contentGraphicsLayer: GraphicsLayer, ) { this.controller = controller this.composeViewLocationOnScreen = composeView.locationOnScreen this.contentGraphicsLayer = contentGraphicsLayer } override fun onAttach() { setCurrentNodeInOverlay() } override fun onDetach() { resetCurrentNodeInOverlay() } private fun setCurrentNodeInOverlay() { controller.currentNodeInOverlay = this } private fun resetCurrentNodeInOverlay() { if (controller.currentNodeInOverlay == this) { controller.currentNodeInOverlay = null } } override fun ContentDrawScope.draw() { val state = controller.animatorState ?: return val topOffset = state.top.toFloat() - composeViewLocationOnScreen[1] val leftOffset = state.left.toFloat() - composeViewLocationOnScreen[0] translate(top = topOffset, left = leftOffset) { // Background. this@draw.drawBackground( state, controller.color, controller.borderStroke, size = Size(state.width.toFloat(), state.height.toFloat()), ) // Content, scaled & centered w.r.t. the animated state bounds. val contentSize = controller.boundsInComposeViewRoot.size val contentWidth = contentSize.width val contentHeight = contentSize.height val scale = min(state.width / contentWidth, state.height / contentHeight) scale(scale, pivot = Offset(state.width / 2f, state.height / 2f)) { translate( left = (state.width - contentWidth) / 2f, top = (state.height - contentHeight) / 2f, ) { drawLayer(contentGraphicsLayer) } } } } } private fun clickModifier( controller: ExpandableControllerImpl, onClick: ((Expandable) -> Unit)?, Loading Loading @@ -447,6 +597,7 @@ private fun ContentDrawScope.drawBackground( animatorState: TransitionAnimator.State, color: Color, border: BorderStroke?, size: Size = this.size, ) { val topRadius = animatorState.topCornerRadius val bottomRadius = animatorState.bottomCornerRadius Loading @@ -455,7 +606,7 @@ private fun ContentDrawScope.drawBackground( val cornerRadius = CornerRadius(topRadius) // Draw the background. drawRoundRect(color, cornerRadius = cornerRadius) drawRoundRect(color, cornerRadius = cornerRadius, size = size) // Draw the border. if (border != null) { Loading packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt +18 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,8 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf Loading @@ -36,6 +38,8 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Shape import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalView Loading @@ -50,6 +54,7 @@ import com.android.systemui.animation.TransitionAnimator import kotlin.math.roundToInt /** A controller that can control animated launches from an [Expandable]. */ @Stable interface ExpandableController { /** The [Expandable] controlled by this controller. */ val expandable: Expandable Loading Loading @@ -146,6 +151,11 @@ internal class ExpandableControllerImpl( /** The [ActivityTransitionAnimator.Controller] to be cleaned up [onDispose]. */ private var activityControllerForDisposal: ActivityTransitionAnimator.Controller? = null /** * The current [DrawModifierNode] in the overlay, drawing the expandable during a transition. */ internal var currentNodeInOverlay: DrawModifierNode? = null override val expandable: Expandable = object : Expandable { override fun activityTransitionController( Loading Loading @@ -204,6 +214,10 @@ internal class ExpandableControllerImpl( override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { animatorState = null // Force invalidate the drawing done in the overlay whenever the animation state // changes. currentNodeInOverlay?.invalidateDraw() } override fun onTransitionAnimationProgress( Loading @@ -227,6 +241,10 @@ internal class ExpandableControllerImpl( // Force measure and layout the ComposeView in the overlay whenever the animation // state changes. currentComposeViewInOverlay?.let { measureAndLayoutComposeViewInOverlay(it, state) } // Force invalidate the drawing done in the overlay whenever the animation state // changes. currentNodeInOverlay?.invalidateDraw() } override fun createAnimatorState(): TransitionAnimator.State { Loading Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt +155 −4 Original line number Diff line number Diff line Loading @@ -31,14 +31,13 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.contentColorFor import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.State import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf Loading @@ -62,9 +61,17 @@ 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.translate import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Density Loading @@ -75,6 +82,8 @@ import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.FullScreenComposeViewInOverlay import com.android.systemui.animation.Expandable import com.android.systemui.animation.TransitionAnimator import kotlin.math.max Loading Loading @@ -122,6 +131,9 @@ fun Expandable( borderStroke: BorderStroke? = null, onClick: ((Expandable) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, // TODO(b/285250939): Default this to true then remove once the Compose QS expandables have // proven that the new implementation is robust. useModifierBasedImplementation: Boolean = false, content: @Composable (Expandable) -> Unit, ) { Expandable( Loading @@ -129,6 +141,7 @@ fun Expandable( modifier, onClick, interactionSource, useModifierBasedImplementation, content, ) } Loading Loading @@ -157,16 +170,26 @@ fun Expandable( * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen * @sample com.android.systemui.compose.gallery.DialogLaunchScreen */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun Expandable( controller: ExpandableController, modifier: Modifier = Modifier, onClick: ((Expandable) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, // TODO(b/285250939): Default this to true then remove once the Compose QS expandables have // proven that the new implementation is robust. useModifierBasedImplementation: Boolean = false, content: @Composable (Expandable) -> Unit, ) { val controller = controller as ExpandableControllerImpl if (useModifierBasedImplementation) { Box(modifier.expandable(controller, onClick, interactionSource)) { WrappedContent(controller.expandable, controller.contentColor, content) } return } val color = controller.color val contentColor = controller.contentColor val shape = controller.shape Loading Loading @@ -277,6 +300,133 @@ private fun WrappedContent( } } @Composable @Stable private fun Modifier.expandable( controller: ExpandableController, onClick: ((Expandable) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, ): Modifier { val controller = controller as ExpandableControllerImpl val isAnimating = controller.isAnimating val drawInOverlayModifier = if (isAnimating) { val graphicsLayer = rememberGraphicsLayer() FullScreenComposeViewInOverlay { view -> Modifier.then(DrawExpandableInOverlayElement(view, controller, graphicsLayer)) } Modifier.drawWithContent { graphicsLayer.record { this@drawWithContent.drawContent() } } } else { null } return this.thenIf(onClick != null) { Modifier.minimumInteractiveComponentSize() } .thenIf(!isAnimating) { Modifier.border(controller) .then(clickModifier(controller, onClick, interactionSource)) .background(controller.color, controller.shape) } .thenIf(drawInOverlayModifier != null) { drawInOverlayModifier!! } .onPlaced { controller.boundsInComposeViewRoot = it.boundsInRoot() } .thenIf(!isAnimating && controller.isDialogShowing) { Modifier.layout { measurable, constraints -> measurable.measure(constraints).run { layout(width, height) { /* Do not place/draw. */ } } } } } private data class DrawExpandableInOverlayElement( private val overlayComposeView: ComposeView, private val controller: ExpandableControllerImpl, private val contentGraphicsLayer: GraphicsLayer, ) : ModifierNodeElement<DrawExpandableInOverlayNode>() { override fun create(): DrawExpandableInOverlayNode { return DrawExpandableInOverlayNode(overlayComposeView, controller, contentGraphicsLayer) } override fun update(node: DrawExpandableInOverlayNode) { node.update(overlayComposeView, controller, contentGraphicsLayer) } } private class DrawExpandableInOverlayNode( composeView: ComposeView, controller: ExpandableControllerImpl, private var contentGraphicsLayer: GraphicsLayer, ) : Modifier.Node(), DrawModifierNode { private var controller = controller set(value) { resetCurrentNodeInOverlay() field = value setCurrentNodeInOverlay() } private var composeViewLocationOnScreen = composeView.locationOnScreen fun update( composeView: ComposeView, controller: ExpandableControllerImpl, contentGraphicsLayer: GraphicsLayer, ) { this.controller = controller this.composeViewLocationOnScreen = composeView.locationOnScreen this.contentGraphicsLayer = contentGraphicsLayer } override fun onAttach() { setCurrentNodeInOverlay() } override fun onDetach() { resetCurrentNodeInOverlay() } private fun setCurrentNodeInOverlay() { controller.currentNodeInOverlay = this } private fun resetCurrentNodeInOverlay() { if (controller.currentNodeInOverlay == this) { controller.currentNodeInOverlay = null } } override fun ContentDrawScope.draw() { val state = controller.animatorState ?: return val topOffset = state.top.toFloat() - composeViewLocationOnScreen[1] val leftOffset = state.left.toFloat() - composeViewLocationOnScreen[0] translate(top = topOffset, left = leftOffset) { // Background. this@draw.drawBackground( state, controller.color, controller.borderStroke, size = Size(state.width.toFloat(), state.height.toFloat()), ) // Content, scaled & centered w.r.t. the animated state bounds. val contentSize = controller.boundsInComposeViewRoot.size val contentWidth = contentSize.width val contentHeight = contentSize.height val scale = min(state.width / contentWidth, state.height / contentHeight) scale(scale, pivot = Offset(state.width / 2f, state.height / 2f)) { translate( left = (state.width - contentWidth) / 2f, top = (state.height - contentHeight) / 2f, ) { drawLayer(contentGraphicsLayer) } } } } } private fun clickModifier( controller: ExpandableControllerImpl, onClick: ((Expandable) -> Unit)?, Loading Loading @@ -447,6 +597,7 @@ private fun ContentDrawScope.drawBackground( animatorState: TransitionAnimator.State, color: Color, border: BorderStroke?, size: Size = this.size, ) { val topRadius = animatorState.topCornerRadius val bottomRadius = animatorState.bottomCornerRadius Loading @@ -455,7 +606,7 @@ private fun ContentDrawScope.drawBackground( val cornerRadius = CornerRadius(topRadius) // Draw the background. drawRoundRect(color, cornerRadius = cornerRadius) drawRoundRect(color, cornerRadius = cornerRadius, size = size) // Draw the border. if (border != null) { Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt +18 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,8 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf Loading @@ -36,6 +38,8 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Shape import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalView Loading @@ -50,6 +54,7 @@ import com.android.systemui.animation.TransitionAnimator import kotlin.math.roundToInt /** A controller that can control animated launches from an [Expandable]. */ @Stable interface ExpandableController { /** The [Expandable] controlled by this controller. */ val expandable: Expandable Loading Loading @@ -146,6 +151,11 @@ internal class ExpandableControllerImpl( /** The [ActivityTransitionAnimator.Controller] to be cleaned up [onDispose]. */ private var activityControllerForDisposal: ActivityTransitionAnimator.Controller? = null /** * The current [DrawModifierNode] in the overlay, drawing the expandable during a transition. */ internal var currentNodeInOverlay: DrawModifierNode? = null override val expandable: Expandable = object : Expandable { override fun activityTransitionController( Loading Loading @@ -204,6 +214,10 @@ internal class ExpandableControllerImpl( override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { animatorState = null // Force invalidate the drawing done in the overlay whenever the animation state // changes. currentNodeInOverlay?.invalidateDraw() } override fun onTransitionAnimationProgress( Loading @@ -227,6 +241,10 @@ internal class ExpandableControllerImpl( // Force measure and layout the ComposeView in the overlay whenever the animation // state changes. currentComposeViewInOverlay?.let { measureAndLayoutComposeViewInOverlay(it, state) } // Force invalidate the drawing done in the overlay whenever the animation state // changes. currentNodeInOverlay?.invalidateDraw() } override fun createAnimatorState(): TransitionAnimator.State { Loading