Skip to content

Commit 8ec4599

Browse files
CUJ related snippets
1 parent 0c9024b commit 8ec4599

File tree

4 files changed

+594
-1
lines changed

4 files changed

+594
-1
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.example.compose.snippets.animations
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.offset
6+
import androidx.compose.foundation.lazy.LazyColumn
7+
import androidx.compose.material3.MaterialTheme
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.getValue
11+
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.runtime.remember
13+
import androidx.compose.runtime.setValue
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.geometry.Offset
16+
import androidx.compose.ui.graphics.Color
17+
import androidx.compose.ui.graphics.painter.ColorPainter
18+
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
19+
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
20+
import androidx.compose.ui.input.nestedscroll.nestedScroll
21+
import androidx.compose.ui.layout.layout
22+
import androidx.compose.ui.tooling.preview.Preview
23+
import androidx.compose.ui.unit.Dp
24+
import androidx.compose.ui.unit.IntOffset
25+
import androidx.compose.ui.unit.dp
26+
27+
@Composable
28+
fun AnimatedImageContent(
29+
nestedScrollConnection: NestedScrollConnection,
30+
currentImgSize: Dp,
31+
modifier: Modifier = Modifier,
32+
) {
33+
Box(
34+
modifier = modifier.nestedScroll(nestedScrollConnection)
35+
) {
36+
LazyColumn(
37+
modifier = Modifier.offset {
38+
IntOffset(0, currentImgSize.roundToPx())
39+
}
40+
) {
41+
items(100, key = { it }) {
42+
Text(
43+
text = "Scroll down to see the image size animating...",
44+
style = MaterialTheme.typography.bodyLarge
45+
)
46+
}
47+
}
48+
Image(
49+
modifier = Modifier
50+
.layout { measurable, constraints ->
51+
val heightPx = currentImgSize
52+
.coerceAtLeast(0.dp)
53+
val placeable = measurable.measure(
54+
constraints.copy(
55+
maxHeight = heightPx.roundToPx()
56+
)
57+
)
58+
layout(placeable.width, placeable.height) {
59+
placeable.place(0, 0)
60+
}
61+
},
62+
painter = ColorPainter(Color.Blue),
63+
contentDescription = "Animated Image",
64+
)
65+
}
66+
}
67+
68+
@Composable
69+
fun AnimatedImageSizeOnScroll(
70+
modifier: Modifier = Modifier,
71+
maxImgSize: Dp = 300.dp,
72+
minImgSize: Dp = 100.dp
73+
) {
74+
var currentImgSize by remember { mutableStateOf(maxImgSize) }
75+
val nestedScrollConnection = remember {
76+
object : NestedScrollConnection {
77+
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
78+
val delta = available.y
79+
val newImgSize = currentImgSize + delta.dp
80+
val previousImgSize = currentImgSize
81+
currentImgSize = newImgSize.coerceIn(minImgSize, maxImgSize)
82+
val consumed = currentImgSize - previousImgSize
83+
84+
return Offset(0f, consumed.value)
85+
}
86+
}
87+
}
88+
AnimatedImageContent(nestedScrollConnection, currentImgSize, modifier)
89+
}
90+
91+
@Preview
92+
@Composable
93+
private fun AnimatedImageSizeOnScrollPreview() {
94+
AnimatedImageSizeOnScroll()
95+
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package com.example.compose.snippets.components
2+
3+
import android.util.Log
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.interaction.MutableInteractionSource
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.Row
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.width
15+
import androidx.compose.foundation.layout.wrapContentSize
16+
import androidx.compose.foundation.text.KeyboardOptions
17+
import androidx.compose.material.icons.Icons
18+
import androidx.compose.material.icons.filled.MoreVert
19+
import androidx.compose.material.icons.filled.Phone
20+
import androidx.compose.material3.DropdownMenu
21+
import androidx.compose.material3.DropdownMenuItem
22+
import androidx.compose.material3.ExperimentalMaterial3Api
23+
import androidx.compose.material3.ExposedDropdownMenuBox
24+
import androidx.compose.material3.ExposedDropdownMenuDefaults
25+
import androidx.compose.material3.HorizontalDivider
26+
import androidx.compose.material3.Icon
27+
import androidx.compose.material3.IconButton
28+
import androidx.compose.material3.MenuAnchorType
29+
import androidx.compose.material3.OutlinedTextField
30+
import androidx.compose.material3.Text
31+
import androidx.compose.material3.TextField
32+
import androidx.compose.material3.TopAppBar
33+
import androidx.compose.material3.VerticalDivider
34+
import androidx.compose.runtime.Composable
35+
import androidx.compose.runtime.LaunchedEffect
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.runtime.mutableStateOf
38+
import androidx.compose.runtime.remember
39+
import androidx.compose.runtime.setValue
40+
import androidx.compose.ui.Alignment
41+
import androidx.compose.ui.Modifier
42+
import androidx.compose.ui.text.font.FontWeight
43+
import androidx.compose.ui.text.input.KeyboardType
44+
import androidx.compose.ui.text.style.TextAlign
45+
import androidx.compose.ui.tooling.preview.Preview
46+
import androidx.compose.ui.unit.dp
47+
import androidx.compose.ui.unit.sp
48+
49+
@OptIn(ExperimentalMaterial3Api::class)
50+
@Composable
51+
fun ActionBarMenu(modifier: Modifier = Modifier) {
52+
var mDisplayMenu by remember { mutableStateOf(false) }
53+
54+
TopAppBar(
55+
title = { Text(text = "Compose") },
56+
actions = {
57+
IconButton(onClick = { mDisplayMenu = !mDisplayMenu }) {
58+
Icon(Icons.Default.MoreVert, "")
59+
}
60+
DropdownMenu(expanded = mDisplayMenu, onDismissRequest = { mDisplayMenu = false }) {
61+
DropdownMenuItem(text = { Text("Refresh") }, onClick = { /* Handle refresh! */ })
62+
DropdownMenuItem(text = { Text("Settings") }, onClick = { /* Handle settings! */ })
63+
}
64+
}
65+
)
66+
}
67+
68+
@Preview
69+
@Composable
70+
private fun ActionBarMenuPreview() {
71+
ActionBarMenu()
72+
}
73+
74+
@Composable
75+
fun OptionsMenu(modifier: Modifier = Modifier) {
76+
var expanded by remember { mutableStateOf(false) }
77+
Box(
78+
modifier = modifier
79+
.fillMaxSize()
80+
.wrapContentSize(Alignment.TopStart)
81+
) {
82+
IconButton(onClick = { expanded = true }) {
83+
Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
84+
}
85+
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
86+
DropdownMenuItem(text = { Text("Refresh") }, onClick = { /* Handle refresh! */ })
87+
DropdownMenuItem(text = { Text("Settings") }, onClick = { /* Handle settings! */ })
88+
HorizontalDivider()
89+
DropdownMenuItem(text = { Text("Send Feedback") }, onClick = { /* Handle send feedback! */ })
90+
}
91+
}
92+
}
93+
94+
@Preview
95+
@Composable
96+
private fun DropDownMenuPreview() {
97+
OptionsMenu()
98+
}
99+
100+
@Composable
101+
fun PhoneNumberCodePicker(
102+
countryCodes: Map<String, String>,
103+
selectedCountry: String,
104+
modifier: Modifier = Modifier,
105+
value: String = "",
106+
onValueChange: (String) -> Unit = {},
107+
onCountryCodeChange: (String) -> Unit
108+
) {
109+
val interactionSource by remember { mutableStateOf(MutableInteractionSource()) }
110+
val numericRegex = Regex("[^0-9]")
111+
var expandedOptions by remember { mutableStateOf(false) }
112+
113+
114+
Box(
115+
modifier = Modifier
116+
.fillMaxWidth()
117+
.padding(16.dp)
118+
.wrapContentSize(Alignment.TopCenter)
119+
) {
120+
OutlinedTextField(
121+
modifier = modifier,
122+
singleLine = true,
123+
value = value,
124+
onValueChange = {
125+
val stripped = numericRegex.replace(it, "")
126+
val phoneNumber = if (stripped.length >= 10) {
127+
stripped.substring(0..9)
128+
} else {
129+
stripped
130+
}
131+
onValueChange(phoneNumber)
132+
},
133+
leadingIcon = {
134+
Row(
135+
modifier = Modifier
136+
.height(48.dp)
137+
.clickable(
138+
interactionSource = interactionSource,
139+
indication = null,
140+
onClick = { expandedOptions = !expandedOptions }
141+
),
142+
verticalAlignment = Alignment.CenterVertically
143+
) {
144+
Column(
145+
modifier = Modifier
146+
.width(64.dp)
147+
.padding(start = 4.dp),
148+
horizontalAlignment = Alignment.CenterHorizontally
149+
) {
150+
Text(
151+
text = countryCodes[selectedCountry] ?: "",
152+
fontWeight = FontWeight.Bold,
153+
fontSize = 14.sp,
154+
lineHeight = 8.sp
155+
)
156+
Text(
157+
text = selectedCountry,
158+
textAlign = TextAlign.Center,
159+
fontSize = 12.sp,
160+
lineHeight = 14.sp
161+
)
162+
}
163+
VerticalDivider(modifier = Modifier.padding(start = 2.dp, end = 8.dp))
164+
}
165+
},
166+
trailingIcon = {
167+
Icon(imageVector = Icons.Filled.Phone, contentDescription = "Phone icon")
168+
},
169+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
170+
)
171+
DropdownMenu(
172+
expanded = expandedOptions, onDismissRequest = { expandedOptions = false }
173+
) {
174+
countryCodes.forEach { selectionOption ->
175+
DropdownMenuItem(
176+
text = {
177+
Row(
178+
modifier = Modifier.fillMaxSize(),
179+
horizontalArrangement = Arrangement.SpaceBetween
180+
) {
181+
Text(selectionOption.key)
182+
Text(selectionOption.value)
183+
}
184+
},
185+
onClick = {
186+
onCountryCodeChange(selectionOption.key)
187+
expandedOptions = !expandedOptions
188+
},
189+
)
190+
}
191+
}
192+
}
193+
}
194+
195+
@Preview
196+
@Composable
197+
private fun PhoneNumberCodePickerPreview() {
198+
val countryCodes = mapOf(
199+
"United States" to "+1",
200+
"Canada" to "+1",
201+
)
202+
203+
var selectedCountry by remember { mutableStateOf("United States") }
204+
var phoneNumber by remember { mutableStateOf("") }
205+
206+
PhoneNumberCodePicker(
207+
countryCodes = countryCodes,
208+
selectedCountry = selectedCountry,
209+
value = phoneNumber,
210+
onValueChange = { phoneNumber = it },
211+
onCountryCodeChange = { country -> selectedCountry = country }
212+
)
213+
214+
LaunchedEffect(selectedCountry, phoneNumber) {
215+
Log.i("PhoneNumber", "${countryCodes[selectedCountry]} $phoneNumber")
216+
}
217+
}
218+
219+
@OptIn(ExperimentalMaterial3Api::class)
220+
@Composable
221+
fun ExposedDropdownMenu(options: List<String>, modifier: Modifier = Modifier) {
222+
var expanded by remember { mutableStateOf(false) }
223+
var selectedOptionText by remember { mutableStateOf(options[0]) }
224+
// We want to react on tap/press on TextField to show menu
225+
ExposedDropdownMenuBox(
226+
expanded = expanded,
227+
onExpandedChange = { expanded = it }
228+
) {
229+
TextField(
230+
readOnly = true,
231+
value = selectedOptionText,
232+
onValueChange = { },
233+
label = { Text("Label") },
234+
trailingIcon = {
235+
ExposedDropdownMenuDefaults.TrailingIcon(
236+
expanded = expanded
237+
)
238+
},
239+
colors = ExposedDropdownMenuDefaults.textFieldColors(),
240+
modifier = Modifier
241+
.menuAnchor(MenuAnchorType.PrimaryNotEditable, true) // This is the key that enables the anchor for the dropdown menu
242+
.clickable { expanded = true }
243+
)
244+
ExposedDropdownMenu(
245+
expanded = expanded,
246+
onDismissRequest = {
247+
expanded = false
248+
}
249+
) {
250+
options.forEach { selectionOption ->
251+
DropdownMenuItem(
252+
text = { Text(text = selectionOption) },
253+
onClick = {
254+
selectedOptionText = selectionOption
255+
expanded = false
256+
}
257+
)
258+
}
259+
}
260+
}
261+
}
262+
263+
@Preview
264+
@Composable
265+
private fun ExposedDropdownMenuBoxPreview() {
266+
val options = listOf("Option 1", "Option 2", "Option 3")
267+
ExposedDropdownMenu(options = options)
268+
}
269+

0 commit comments

Comments
 (0)