• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.mechanics.behavior
18 
19 import androidx.compose.ui.Modifier
20 import androidx.compose.ui.draw.drawWithCache
21 import androidx.compose.ui.geometry.CornerRadius
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.geometry.Rect
24 import androidx.compose.ui.geometry.Size
25 import androidx.compose.ui.graphics.Color
26 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
27 import androidx.compose.ui.graphics.drawscope.clipRect
28 import androidx.compose.ui.graphics.layer.GraphicsLayer
29 import androidx.compose.ui.graphics.layer.drawLayer
30 import androidx.compose.ui.node.DrawModifierNode
31 import androidx.compose.ui.node.ModifierNodeElement
32 import androidx.compose.ui.node.requireGraphicsContext
33 import androidx.compose.ui.util.fastCoerceAtLeast
34 import androidx.compose.ui.util.fastCoerceIn
35 import androidx.compose.ui.util.lerp
36 import kotlin.math.min
37 
38 /**
39  * Draws the background of a vertically container, and applies clipping to it.
40  *
41  * Intended to be used with a [VerticalExpandContainerSpec] motion.
42  */
verticalExpandContainerBackgroundnull43 fun Modifier.verticalExpandContainerBackground(
44     backgroundColor: Color,
45     spec: VerticalExpandContainerSpec,
46 ): Modifier =
47     this.then(
48         if (spec.isFloating) {
49             Modifier.verticalFloatingExpandContainerBackground(backgroundColor, spec)
50         } else {
51             Modifier.verticalEdgeExpandContainerBackground(backgroundColor, spec)
52         }
53     )
54 
55 /**
56  * Draws the background of an floating container, and applies clipping to it.
57  *
58  * Intended to be used with a [VerticalExpandContainerSpec] motion.
59  */
verticalFloatingExpandContainerBackgroundnull60 internal fun Modifier.verticalFloatingExpandContainerBackground(
61     backgroundColor: Color,
62     spec: VerticalExpandContainerSpec,
63 ): Modifier =
64     this.drawWithCache {
65         val targetRadiusPx = spec.radius.toPx()
66         val currentRadiusPx = min(targetRadiusPx, min(size.width, size.height) / 2f)
67         val horizontalInset = targetRadiusPx - currentRadiusPx
68         val shapeTopLeft = Offset(horizontalInset, 0f)
69         val shapeSize = Size(size.width - (horizontalInset * 2f), size.height)
70 
71         val layer =
72             obtainGraphicsLayer().apply {
73                 clip = true
74                 setRoundRectOutline(shapeTopLeft, shapeSize, cornerRadius = currentRadiusPx)
75 
76                 record { drawContent() }
77             }
78 
79         onDrawWithContent {
80             drawRoundRect(
81                 color = backgroundColor,
82                 topLeft = shapeTopLeft,
83                 size = shapeSize,
84                 cornerRadius = CornerRadius(currentRadiusPx),
85             )
86 
87             drawLayer(layer)
88         }
89     }
90 
91 /**
92  * Draws the background of an edge container, and applies clipping to it.
93  *
94  * Intended to be used with a [VerticalExpandContainerSpec] motion.
95  */
verticalEdgeExpandContainerBackgroundnull96 internal fun Modifier.verticalEdgeExpandContainerBackground(
97     backgroundColor: Color,
98     spec: VerticalExpandContainerSpec,
99 ): Modifier = this.then(EdgeContainerExpansionBackgroundElement(backgroundColor, spec))
100 
101 internal class EdgeContainerExpansionBackgroundNode(
102     var backgroundColor: Color,
103     var spec: VerticalExpandContainerSpec,
104 ) : Modifier.Node(), DrawModifierNode {
105 
106     private var graphicsLayer: GraphicsLayer? = null
107     private var lastOutlineSize = Size.Zero
108 
109     fun invalidateOutline() {
110         lastOutlineSize = Size.Zero
111     }
112 
113     override fun onAttach() {
114         graphicsLayer = requireGraphicsContext().createGraphicsLayer().apply { clip = true }
115     }
116 
117     override fun onDetach() {
118         requireGraphicsContext().releaseGraphicsLayer(checkNotNull(graphicsLayer))
119     }
120 
121     override fun ContentDrawScope.draw() {
122         val height = size.height
123 
124         // The width is growing between visibleHeight and detachHeight
125         val visibleHeight = spec.visibleHeight.toPx()
126         val widthFraction =
127             ((height - visibleHeight) / (spec.detachHeight.toPx() - visibleHeight)).fastCoerceIn(
128                 0f,
129                 1f,
130             )
131         val width = size.width - lerp(spec.widthOffset.toPx(), 0f, widthFraction)
132         val horizontalInset = (size.width - width) / 2f
133 
134         // The radius is growing at the beginning of the transition
135         val radius = height.fastCoerceIn(spec.minRadius.toPx(), spec.radius.toPx())
136 
137         // Draw (at most) the bottom half of the rounded corner rectangle, aligned to the bottom.
138         val upperHeight = height - radius
139 
140         // The rounded rect is drawn at 2x the radius height, to avoid smaller corner radii.
141         // The clipRect limits this to the relevant part (-1 to avoid a hairline gap being visible
142         // between this and the fill below.
143         clipRect(top = (upperHeight - 1).fastCoerceAtLeast(0f)) {
144             drawRoundRect(
145                 color = backgroundColor,
146                 cornerRadius = CornerRadius(radius),
147                 size = Size(width, radius * 2f),
148                 topLeft = Offset(horizontalInset, size.height - radius * 2f),
149             )
150         }
151 
152         if (upperHeight > 0) {
153             // Fill the space above the bottom shape.
154             drawRect(
155                 color = backgroundColor,
156                 topLeft = Offset(horizontalInset, 0f),
157                 size = Size(width, upperHeight),
158             )
159         }
160 
161         // Draw the node's content in a separate layer.
162         val graphicsLayer = checkNotNull(graphicsLayer)
163         graphicsLayer.record { this@draw.drawContent() }
164 
165         if (size != lastOutlineSize) {
166             // The clip outline is a rounded corner shape matching the bottom of the shape.
167             // At the top, the rounded corner shape extends by radiusPx above top.
168             // This clipping thus would not prevent the containers content to overdraw at the top,
169             // however this is off-screen anyways.
170             val top = min(-radius, height - radius * 2f)
171 
172             val rect = Rect(left = horizontalInset, top = top, right = width, bottom = height)
173             graphicsLayer.setRoundRectOutline(rect.topLeft, rect.size, radius)
174             lastOutlineSize = size
175         }
176 
177         this.drawLayer(graphicsLayer)
178     }
179 }
180 
181 private data class EdgeContainerExpansionBackgroundElement(
182     val backgroundColor: Color,
183     val spec: VerticalExpandContainerSpec,
184 ) : ModifierNodeElement<EdgeContainerExpansionBackgroundNode>() {
createnull185     override fun create(): EdgeContainerExpansionBackgroundNode =
186         EdgeContainerExpansionBackgroundNode(backgroundColor, spec)
187 
188     override fun update(node: EdgeContainerExpansionBackgroundNode) {
189         node.backgroundColor = backgroundColor
190         if (node.spec != spec) {
191             node.spec = spec
192             node.invalidateOutline()
193         }
194     }
195 }
196