1 /*
<lambda>null2  * Copyright 2023 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 @file:Suppress("DEPRECATION")
18 
19 package androidx.compose.foundation.layout.samples
20 
21 import androidx.annotation.Sampled
22 import androidx.compose.foundation.Canvas
23 import androidx.compose.foundation.background
24 import androidx.compose.foundation.clickable
25 import androidx.compose.foundation.layout.Arrangement
26 import androidx.compose.foundation.layout.Box
27 import androidx.compose.foundation.layout.ExperimentalLayoutApi
28 import androidx.compose.foundation.layout.FlowRow
29 import androidx.compose.foundation.layout.FlowRowOverflow
30 import androidx.compose.foundation.layout.FlowRowOverflowScope
31 import androidx.compose.foundation.layout.fillMaxSize
32 import androidx.compose.foundation.layout.fillMaxWidth
33 import androidx.compose.foundation.layout.height
34 import androidx.compose.foundation.layout.padding
35 import androidx.compose.foundation.layout.width
36 import androidx.compose.foundation.layout.wrapContentHeight
37 import androidx.compose.material.Text
38 import androidx.compose.runtime.Composable
39 import androidx.compose.runtime.getValue
40 import androidx.compose.runtime.mutableStateOf
41 import androidx.compose.runtime.remember
42 import androidx.compose.runtime.setValue
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.geometry.Offset
46 import androidx.compose.ui.graphics.Color
47 import androidx.compose.ui.layout.layout
48 import androidx.compose.ui.text.TextLayoutResult
49 import androidx.compose.ui.text.TextStyle
50 import androidx.compose.ui.text.drawText
51 import androidx.compose.ui.text.font.FontWeight
52 import androidx.compose.ui.text.rememberTextMeasurer
53 import androidx.compose.ui.unit.Constraints
54 import androidx.compose.ui.unit.dp
55 import androidx.compose.ui.unit.sp
56 import kotlin.random.Random
57 
58 @OptIn(ExperimentalLayoutApi::class)
59 @Sampled
60 @Composable
61 fun SimpleFlowRow() {
62     Text(
63         modifier =
64             Modifier.fillMaxWidth(1f).padding(20.dp).wrapContentHeight(align = Alignment.Top),
65         text = "Flow Row with weights",
66         fontWeight = FontWeight.Bold
67     )
68 
69     FlowRow(
70         Modifier.fillMaxWidth(1f).padding(20.dp).wrapContentHeight(align = Alignment.Top),
71         horizontalArrangement = Arrangement.spacedBy(10.dp),
72         verticalArrangement = Arrangement.spacedBy(20.dp),
73         maxItemsInEachRow = 3,
74     ) {
75         repeat(20) {
76             Box(
77                 Modifier.align(Alignment.CenterVertically)
78                     .width(50.dp)
79                     .height(50.dp)
80                     .weight(1f, true)
81                     .background(Color.Green)
82             ) {
83                 Text(text = it.toString(), fontSize = 18.sp, modifier = Modifier.padding(3.dp))
84             }
85         }
86     }
87 }
88 
89 @OptIn(ExperimentalLayoutApi::class)
90 @Sampled
91 @Composable
SimpleFlowRowMaxLinesWithSeeMorenull92 fun SimpleFlowRowMaxLinesWithSeeMore() {
93     val totalCount = 20
94     var maxLines by remember { mutableStateOf(2) }
95 
96     Text(
97         modifier =
98             Modifier.fillMaxWidth(1f).padding(20.dp).wrapContentHeight(align = Alignment.Top),
99         text =
100             "Flow Row with total items of 40, " +
101                 "with Max Lines of 2 and See More, " +
102                 "which when clicked, increases max lines by two",
103         fontWeight = FontWeight.Bold
104     )
105     FlowRow(
106         Modifier.fillMaxWidth(1f).padding(20.dp).wrapContentHeight(align = Alignment.Top),
107         horizontalArrangement = Arrangement.spacedBy(10.dp),
108         verticalArrangement = Arrangement.spacedBy(20.dp),
109         maxLines = maxLines,
110         overflow = FlowRowOverflow.expandIndicator { Ellipsis(text = "...") { maxLines += 2 } }
111     ) {
112         repeat(totalCount) {
113             Box(
114                 Modifier.align(Alignment.CenterVertically)
115                     .width(50.dp)
116                     .height(50.dp)
117                     .background(Color.Green)
118             ) {
119                 Text(
120                     text = it.toString(),
121                     fontSize = 18.sp,
122                     modifier = Modifier.padding(3.dp).align(Alignment.Center)
123                 )
124             }
125         }
126     }
127 }
128 
129 @OptIn(ExperimentalLayoutApi::class)
130 @Composable
SimpleFlowRowWithMaxHeightnull131 fun SimpleFlowRowWithMaxHeight() {
132     var initialHeight = 200.dp
133     var height by remember { mutableStateOf(initialHeight) }
134 
135     Text(
136         modifier =
137             Modifier.fillMaxWidth(1f).padding(20.dp).wrapContentHeight(align = Alignment.Top),
138         text =
139             "Flow Row with total items of 40, " +
140                 "with Max height of 200.dp and See More/collapse button, " +
141                 "which when clicked, increases height by 200.dp",
142         fontWeight = FontWeight.Bold
143     )
144 
145     FlowRow(
146         Modifier.fillMaxWidth(1f)
147             .padding(20.dp)
148             .height(height)
149             .wrapContentHeight(align = Alignment.Top),
150         horizontalArrangement = Arrangement.spacedBy(10.dp),
151         verticalArrangement = Arrangement.spacedBy(20.dp),
152         overflow =
153             FlowRowOverflow.expandOrCollapseIndicator(
154                 minHeightToShowCollapse = 200.dp,
155                 expandIndicator = { Ellipsis(text = "...") { height += 200.dp } },
156                 collapseIndicator = { Ellipsis(text = "^") { height = 100.dp } }
157             )
158     ) {
159         repeat(40) {
160             Box(
161                 Modifier.align(Alignment.CenterVertically)
162                     .width(50.dp)
163                     .height(50.dp)
164                     .background(Color.Green)
165             ) {
166                 Text(text = it.toString(), fontSize = 18.sp, modifier = Modifier.padding(3.dp))
167             }
168         }
169     }
170 }
171 
172 @OptIn(ExperimentalLayoutApi::class)
173 @Sampled
174 @Composable
SimpleFlowRowMaxLinesDynamicSeeMorenull175 fun SimpleFlowRowMaxLinesDynamicSeeMore() {
176     val totalCount = 40
177     var maxLines by remember { mutableStateOf(2) }
178 
179     Text(
180         modifier =
181             Modifier.fillMaxWidth(1f).padding(20.dp).wrapContentHeight(align = Alignment.Top),
182         text =
183             "Flow Row with total items of 40, " +
184                 "with Max Lines of 2 and See More/collapse button, " +
185                 "which when clicked, increases max lines by two",
186         fontWeight = FontWeight.Bold
187     )
188     val moreOrLessIndicator =
189         @Composable { scope: FlowRowOverflowScope ->
190             DynamicSeeMoreForDrawText(
191                 isHorizontal = true,
192                 totalCount = totalCount,
193                 { scope.shownItemCount },
194                 onExpand = { maxLines += 2 },
195                 onShrink = { maxLines = 2 }
196             )
197         }
198     FlowRow(
199         Modifier.fillMaxWidth(1f)
200             .padding(20.dp)
201             .wrapContentHeight()
202             .padding(20.dp)
203             .fillMaxWidth(1f),
204         horizontalArrangement = Arrangement.spacedBy(10.dp),
205         verticalArrangement = Arrangement.spacedBy(20.dp),
206         maxLines = maxLines,
207         overflow =
208             FlowRowOverflow.expandOrCollapseIndicator(
209                 minRowsToShowCollapse = 4,
210                 expandIndicator = moreOrLessIndicator,
211                 collapseIndicator = moreOrLessIndicator
212             )
213     ) {
214         repeat(totalCount) {
215             Box(
216                 Modifier.align(Alignment.CenterVertically)
217                     .width(50.dp)
218                     .height(50.dp)
219                     .background(Color.Green)
220             ) {
221                 Text(
222                     text = it.toString(),
223                     fontSize = 18.sp,
224                     modifier = Modifier.padding(3.dp).align(Alignment.Center)
225                 )
226             }
227         }
228     }
229 }
230 
231 @Composable
DynamicSeeMoreForDrawTextnull232 internal fun DynamicSeeMoreForDrawText(
233     isHorizontal: Boolean,
234     totalCount: Int,
235     shownItemCount: () -> Int?,
236     onExpand: () -> Unit,
237     onShrink: () -> Unit,
238 ) {
239     Box(
240         Modifier.clickable(
241                 onClick = {
242                     val remainingItems = shownItemCount()?.let { totalCount - it }
243                     if (remainingItems == 0) {
244                         onShrink()
245                     } else {
246                         onExpand()
247                     }
248                 }
249             )
250             .width(50.dp)
251             .height(50.dp)
252             .background(Color.Green)
253     ) {
254         val textMeasurer = rememberTextMeasurer()
255         Canvas(
256             Modifier.fillMaxSize().layout { measurable, constraints ->
257                 // TextLayout can be done any time prior to its use in draw, including in a
258                 // background thread.
259                 // In this sample, text layout is measured in layout modifier. This way the layout
260                 // call can be restarted when async font loading completes due to the fact that
261                 // `.measure` call is executed in `.layout`.
262                 val result =
263                     textMeasurer.measure(
264                         text = "+2000",
265                         style = TextStyle(fontSize = 24.sp),
266                         constraints = constraints
267                     )
268                 val placeable =
269                     measurable.measure(
270                         Constraints(
271                             minWidth = result.size.width,
272                             maxWidth = result.size.width,
273                             minHeight = result.size.height,
274                             maxHeight = result.size.height
275                         )
276                     )
277                 layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
278             }
279         ) {
280             // This happens during draw phase.
281             val collapseText = if (isHorizontal) "^" else "<"
282             val remainingItems = shownItemCount()?.let { totalCount - it }
283             var textLayoutResult: TextLayoutResult =
284                 textMeasurer.measure(
285                     text = if (remainingItems == 0) collapseText else "+$remainingItems",
286                     style = TextStyle(fontSize = 18.sp)
287                 )
288             drawText(
289                 textLayoutResult,
290                 topLeft =
291                     Offset(
292                         (size.width - textLayoutResult.size.width) / 2,
293                         (size.height - textLayoutResult.size.height) / 2,
294                     )
295             )
296         }
297     }
298 }
299 
300 @Composable
Ellipsisnull301 internal fun Ellipsis(text: String, onClick: () -> Unit) {
302     Box(Modifier.clickable(onClick = onClick).width(50.dp).height(50.dp).background(Color.Green)) {
303         Text(
304             modifier = Modifier.align(Alignment.Center).padding(3.dp),
305             text = text,
306             fontSize = 18.sp
307         )
308     }
309 }
310 
311 @OptIn(ExperimentalLayoutApi::class)
312 @Sampled
313 @Composable
SimpleFlowRow_EqualHeightnull314 fun SimpleFlowRow_EqualHeight() {
315     FlowRow(
316         Modifier.fillMaxWidth(1f).padding(20.dp).wrapContentHeight(align = Alignment.Top),
317         horizontalArrangement = Arrangement.spacedBy(10.dp),
318         verticalArrangement = Arrangement.spacedBy(20.dp),
319         maxItemsInEachRow = 3,
320     ) {
321         repeat(9) {
322             Box(Modifier.width(100.dp).background(Color.Green).fillMaxRowHeight(1f)) {
323                 val text = generateRandomString(IntRange(10, 50).random())
324                 Text(text = text, fontSize = 18.sp, modifier = Modifier.padding(3.dp))
325             }
326         }
327     }
328 }
329 
generateRandomStringnull330 fun generateRandomString(length: Int): String {
331     val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
332     val random = Random.Default
333 
334     val randomString = StringBuilder(length)
335     repeat(length) {
336         val randomIndex = random.nextInt(0, charPool.size)
337         val randomChar = charPool[randomIndex]
338         randomString.append(randomChar)
339     }
340     return randomString.toString()
341 }
342