1 /*
<lambda>null2 * Copyright (C) 2024 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.systemui.communal.ui.compose
18
19 import androidx.compose.foundation.Canvas
20 import androidx.compose.foundation.gestures.AnchoredDraggableState
21 import androidx.compose.foundation.gestures.Orientation
22 import androidx.compose.foundation.gestures.anchoredDraggable
23 import androidx.compose.foundation.layout.Arrangement
24 import androidx.compose.foundation.layout.Box
25 import androidx.compose.foundation.layout.BoxScope
26 import androidx.compose.foundation.layout.PaddingValues
27 import androidx.compose.foundation.layout.fillMaxSize
28 import androidx.compose.foundation.layout.fillMaxWidth
29 import androidx.compose.foundation.layout.height
30 import androidx.compose.foundation.lazy.grid.GridItemSpan
31 import androidx.compose.foundation.lazy.grid.LazyGridState
32 import androidx.compose.material3.MaterialTheme
33 import androidx.compose.runtime.Composable
34 import androidx.compose.runtime.LaunchedEffect
35 import androidx.compose.runtime.derivedStateOf
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.remember
38 import androidx.compose.runtime.rememberUpdatedState
39 import androidx.compose.runtime.snapshotFlow
40 import androidx.compose.ui.Alignment
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.geometry.CornerRadius
43 import androidx.compose.ui.geometry.Offset
44 import androidx.compose.ui.geometry.Size
45 import androidx.compose.ui.graphics.Brush
46 import androidx.compose.ui.graphics.Color
47 import androidx.compose.ui.graphics.SolidColor
48 import androidx.compose.ui.graphics.drawscope.Stroke
49 import androidx.compose.ui.graphics.graphicsLayer
50 import androidx.compose.ui.platform.LocalDensity
51 import androidx.compose.ui.unit.Dp
52 import androidx.compose.ui.unit.dp
53 import androidx.compose.ui.util.fastIsFinite
54 import androidx.compose.ui.zIndex
55 import com.android.compose.modifiers.thenIf
56 import com.android.systemui.communal.ui.viewmodel.DragHandle
57 import com.android.systemui.communal.ui.viewmodel.ResizeInfo
58 import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
59 import kotlinx.coroutines.flow.collectLatest
60 import kotlinx.coroutines.flow.combine
61
62 @Composable
63 private fun UpdateGridLayoutInfo(
64 viewModel: ResizeableItemFrameViewModel,
65 key: String,
66 gridState: LazyGridState,
67 gridContentPadding: PaddingValues,
68 verticalArrangement: Arrangement.Vertical,
69 minHeightPx: Int,
70 maxHeightPx: Int,
71 resizeMultiple: Int,
72 currentSpan: GridItemSpan,
73 ) {
74 val density = LocalDensity.current
75 LaunchedEffect(
76 density,
77 viewModel,
78 key,
79 gridState,
80 gridContentPadding,
81 verticalArrangement,
82 minHeightPx,
83 maxHeightPx,
84 resizeMultiple,
85 currentSpan,
86 ) {
87 val verticalItemSpacingPx = with(density) { verticalArrangement.spacing.toPx() }
88 val verticalContentPaddingPx =
89 with(density) {
90 (gridContentPadding.calculateTopPadding() +
91 gridContentPadding.calculateBottomPadding())
92 .toPx()
93 }
94
95 combine(
96 snapshotFlow { gridState.layoutInfo.maxSpan },
97 snapshotFlow { gridState.layoutInfo.viewportSize.height },
98 snapshotFlow {
99 gridState.layoutInfo.visibleItemsInfo.firstOrNull { it.key == key }
100 },
101 ::Triple,
102 )
103 .collectLatest { (maxItemSpan, viewportHeightPx, itemInfo) ->
104 viewModel.setGridLayoutInfo(
105 verticalItemSpacingPx = verticalItemSpacingPx,
106 currentRow = itemInfo?.row,
107 maxHeightPx = maxHeightPx,
108 minHeightPx = minHeightPx,
109 currentSpan = currentSpan.currentLineSpan,
110 resizeMultiple = resizeMultiple,
111 totalSpans = maxItemSpan,
112 viewportHeightPx = viewportHeightPx,
113 verticalContentPaddingPx = verticalContentPaddingPx,
114 )
115 }
116 }
117 }
118
119 @Composable
DragHandlenull120 private fun BoxScope.DragHandle(
121 handle: DragHandle,
122 dragState: AnchoredDraggableState<Int>,
123 outlinePadding: Dp,
124 brush: Brush,
125 alpha: () -> Float,
126 modifier: Modifier = Modifier,
127 ) {
128 val directionalModifier = if (handle == DragHandle.TOP) -1 else 1
129 val alignment = if (handle == DragHandle.TOP) Alignment.TopCenter else Alignment.BottomCenter
130 Box(
131 modifier
132 .align(alignment)
133 .graphicsLayer {
134 translationY =
135 directionalModifier * (size.height / 2 + outlinePadding.toPx()) +
136 (dragState.offset.takeIf { it.fastIsFinite() } ?: 0f)
137 }
138 .anchoredDraggable(dragState, Orientation.Vertical)
139 ) {
140 Canvas(modifier = Modifier.fillMaxSize()) {
141 if (dragState.anchors.size > 1) {
142 drawCircle(
143 brush = brush,
144 radius = outlinePadding.toPx(),
145 center = Offset(size.width / 2, size.height / 2),
146 alpha = alpha(),
147 )
148 }
149 }
150 }
151 }
152
153 /**
154 * Draws a frame around the content with drag handles on the top and bottom of the content.
155 *
156 * @param key The unique key of this element, must be the same key used in the [LazyGridState].
157 * @param currentSpan The current span size of this item in the grid.
158 * @param gridState The [LazyGridState] for the grid containing this item.
159 * @param gridContentPadding The content padding used for the grid, needed for determining offsets.
160 * @param verticalArrangement The vertical arrangement of the grid items.
161 * @param modifier Optional modifier to apply to the frame.
162 * @param enabled Whether resizing is enabled.
163 * @param outlinePadding The padding to apply around the entire frame, in [Dp]
164 * @param outlineColor Optional color to make the outline around the content.
165 * @param cornerRadius Optional radius to give to the outline around the content.
166 * @param strokeWidth Optional stroke width to draw the outline with.
167 * @param minHeightPx Optional minimum height in pixels that this widget can be resized to.
168 * @param maxHeightPx Optional maximum height in pixels that this widget can be resized to.
169 * @param resizeMultiple Optional number of spans that we allow resizing by. For example, if set to
170 * 3, then we only allow resizing in multiples of 3 spans.
171 * @param alpha Optional function to provide an alpha value for the outline. Can be used to fade the
172 * outline in and out. This is wrapped in a function for performance, as the value is only
173 * accessed during the draw phase.
174 * @param onResize Optional callback which gets executed when the item is resized to a new span.
175 * @param content The content to draw inside the frame.
176 */
177 @Composable
ResizableItemFramenull178 fun ResizableItemFrame(
179 key: String,
180 currentSpan: GridItemSpan,
181 gridState: LazyGridState,
182 gridContentPadding: PaddingValues,
183 verticalArrangement: Arrangement.Vertical,
184 modifier: Modifier = Modifier,
185 enabled: Boolean = true,
186 outlinePadding: Dp = 8.dp,
187 outlineColor: Color = MaterialTheme.colorScheme.primary,
188 cornerRadius: Dp = 37.dp,
189 strokeWidth: Dp = 3.dp,
190 minHeightPx: Int = 0,
191 maxHeightPx: Int = Int.MAX_VALUE,
192 resizeMultiple: Int = 1,
193 alpha: () -> Float = { 1f },
194 viewModel: ResizeableItemFrameViewModel,
<lambda>null195 onResize: (info: ResizeInfo) -> Unit = {},
196 content: @Composable () -> Unit,
197 ) {
198 val brush = SolidColor(outlineColor)
199 val onResizeUpdated by rememberUpdatedState(onResize)
200 val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
201 val isDragging by
<lambda>null202 remember(viewModel) {
203 derivedStateOf {
204 val topOffset = viewModel.topDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
205 val bottomOffset =
206 viewModel.bottomDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
207 topOffset > 0 || bottomOffset > 0
208 }
209 }
210
211 // Draw content surrounded by drag handles at top and bottom. Allow drag handles
212 // to overlap content.
<lambda>null213 Box(modifier.thenIf(isDragging) { Modifier.zIndex(1f) }) {
214 content()
215
216 if (enabled) {
217 DragHandle(
218 handle = DragHandle.TOP,
219 dragState = viewModel.topDragState,
220 outlinePadding = outlinePadding,
221 brush = brush,
222 alpha = alpha,
223 modifier = Modifier.fillMaxWidth().height(dragHandleHeight),
224 )
225
226 DragHandle(
227 handle = DragHandle.BOTTOM,
228 dragState = viewModel.bottomDragState,
229 outlinePadding = outlinePadding,
230 brush = brush,
231 alpha = alpha,
232 modifier = Modifier.fillMaxWidth().height(dragHandleHeight),
233 )
234
235 // Draw outline around the element.
<lambda>null236 Canvas(modifier = Modifier.matchParentSize()) {
237 val paddingPx = outlinePadding.toPx()
238 val topOffset = viewModel.topDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
239 val bottomOffset =
240 viewModel.bottomDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
241 drawRoundRect(
242 brush,
243 alpha = alpha(),
244 topLeft = Offset(-paddingPx, topOffset + -paddingPx),
245 size =
246 Size(
247 width = size.width + paddingPx * 2,
248 height = -topOffset + bottomOffset + size.height + paddingPx * 2,
249 ),
250 cornerRadius = CornerRadius(cornerRadius.toPx()),
251 style = Stroke(width = strokeWidth.toPx()),
252 )
253 }
254
255 UpdateGridLayoutInfo(
256 viewModel = viewModel,
257 key = key,
258 gridState = gridState,
259 currentSpan = currentSpan,
260 gridContentPadding = gridContentPadding,
261 verticalArrangement = verticalArrangement,
262 minHeightPx = minHeightPx,
263 maxHeightPx = maxHeightPx,
264 resizeMultiple = resizeMultiple,
265 )
<lambda>null266 LaunchedEffect(viewModel) {
267 viewModel.resizeInfo.collectLatest { info -> onResizeUpdated(info) }
268 }
269 }
270 }
271 }
272