summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt159
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt18
2 files changed, 173 insertions, 4 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 a1d362a4a11d..873991923e51 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
@@ -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
@@ -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
@@ -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
@@ -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(
@@ -129,6 +141,7 @@ fun Expandable(
modifier,
onClick,
interactionSource,
+ useModifierBasedImplementation,
content,
)
}
@@ -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
@@ -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)?,
@@ -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
@@ -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) {
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 377ea96c5723..a03c89626cd7 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,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
@@ -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
@@ -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
@@ -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(
@@ -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(
@@ -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 {