• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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