mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
New plugin covering mobile (iOS, Android, React Native) and web applications with modern design patterns, accessibility, and design systems. Components: - 9 skills: design-system-patterns, accessibility-compliance, responsive-design, mobile-ios-design, mobile-android-design, react-native-design, web-component-design, interaction-design, visual-design-foundations - 4 commands: design-review, create-component, accessibility-audit, design-system-setup - 3 agents: ui-designer, accessibility-expert, design-system-architect Marketplace updated: - Version bumped to 1.3.4 - 102 agents (+3), 116 skills (+9)
797 lines
23 KiB
Markdown
797 lines
23 KiB
Markdown
# Jetpack Compose Component Library
|
|
|
|
## Lists and Collections
|
|
|
|
### Basic LazyColumn
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun ItemList(
|
|
items: List<Item>,
|
|
onItemClick: (Item) -> Unit,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
LazyColumn(
|
|
modifier = modifier.fillMaxSize(),
|
|
contentPadding = PaddingValues(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
items(
|
|
items = items,
|
|
key = { it.id }
|
|
) { item ->
|
|
ItemRow(
|
|
item = item,
|
|
onClick = { onItemClick(item) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pull to Refresh
|
|
|
|
```kotlin
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun RefreshableList(
|
|
items: List<Item>,
|
|
isRefreshing: Boolean,
|
|
onRefresh: () -> Unit
|
|
) {
|
|
val pullToRefreshState = rememberPullToRefreshState()
|
|
|
|
PullToRefreshBox(
|
|
state = pullToRefreshState,
|
|
isRefreshing = isRefreshing,
|
|
onRefresh = onRefresh
|
|
) {
|
|
LazyColumn(
|
|
modifier = Modifier.fillMaxSize()
|
|
) {
|
|
items(items) { item ->
|
|
ItemRow(item = item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Swipe to Dismiss
|
|
|
|
```kotlin
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun SwipeableItem(
|
|
item: Item,
|
|
onDelete: () -> Unit
|
|
) {
|
|
val dismissState = rememberSwipeToDismissBoxState(
|
|
confirmValueChange = { value ->
|
|
if (value == SwipeToDismissBoxValue.EndToStart) {
|
|
onDelete()
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
)
|
|
|
|
SwipeToDismissBox(
|
|
state = dismissState,
|
|
backgroundContent = {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(MaterialTheme.colorScheme.errorContainer)
|
|
.padding(horizontal = 20.dp),
|
|
contentAlignment = Alignment.CenterEnd
|
|
) {
|
|
Icon(
|
|
Icons.Default.Delete,
|
|
contentDescription = "Delete",
|
|
tint = MaterialTheme.colorScheme.onErrorContainer
|
|
)
|
|
}
|
|
}
|
|
) {
|
|
ItemRow(item = item)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Sticky Headers
|
|
|
|
```kotlin
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun GroupedList(
|
|
groups: Map<String, List<Item>>
|
|
) {
|
|
LazyColumn {
|
|
groups.forEach { (header, items) ->
|
|
stickyHeader {
|
|
Surface(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
color = MaterialTheme.colorScheme.surfaceVariant
|
|
) {
|
|
Text(
|
|
text = header,
|
|
modifier = Modifier.padding(16.dp),
|
|
style = MaterialTheme.typography.titleSmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
}
|
|
items(items, key = { it.id }) { item ->
|
|
ItemRow(item = item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Forms and Input
|
|
|
|
### Text Fields
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun LoginForm(
|
|
onLogin: (email: String, password: String) -> Unit
|
|
) {
|
|
var email by rememberSaveable { mutableStateOf("") }
|
|
var password by rememberSaveable { mutableStateOf("") }
|
|
var passwordVisible by rememberSaveable { mutableStateOf(false) }
|
|
var emailError by remember { mutableStateOf<String?>(null) }
|
|
|
|
Column(
|
|
modifier = Modifier.padding(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
) {
|
|
OutlinedTextField(
|
|
value = email,
|
|
onValueChange = {
|
|
email = it
|
|
emailError = if (it.isValidEmail()) null else "Invalid email"
|
|
},
|
|
label = { Text("Email") },
|
|
placeholder = { Text("name@example.com") },
|
|
leadingIcon = { Icon(Icons.Default.Email, null) },
|
|
isError = emailError != null,
|
|
supportingText = emailError?.let { { Text(it) } },
|
|
keyboardOptions = KeyboardOptions(
|
|
keyboardType = KeyboardType.Email,
|
|
imeAction = ImeAction.Next
|
|
),
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
|
|
OutlinedTextField(
|
|
value = password,
|
|
onValueChange = { password = it },
|
|
label = { Text("Password") },
|
|
leadingIcon = { Icon(Icons.Default.Lock, null) },
|
|
trailingIcon = {
|
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
|
Icon(
|
|
if (passwordVisible) Icons.Default.VisibilityOff
|
|
else Icons.Default.Visibility,
|
|
contentDescription = "Toggle password visibility"
|
|
)
|
|
}
|
|
},
|
|
visualTransformation = if (passwordVisible)
|
|
VisualTransformation.None
|
|
else
|
|
PasswordVisualTransformation(),
|
|
keyboardOptions = KeyboardOptions(
|
|
keyboardType = KeyboardType.Password,
|
|
imeAction = ImeAction.Done
|
|
),
|
|
singleLine = true,
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
|
|
Button(
|
|
onClick = { onLogin(email, password) },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
enabled = email.isNotEmpty() && password.isNotEmpty() && emailError == null
|
|
) {
|
|
Text("Sign In")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Search Bar
|
|
|
|
```kotlin
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun SearchableScreen(
|
|
items: List<Item>,
|
|
onItemClick: (Item) -> Unit
|
|
) {
|
|
var query by rememberSaveable { mutableStateOf("") }
|
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
|
|
|
val filteredItems = remember(query, items) {
|
|
if (query.isEmpty()) items
|
|
else items.filter { it.name.contains(query, ignoreCase = true) }
|
|
}
|
|
|
|
SearchBar(
|
|
query = query,
|
|
onQueryChange = { query = it },
|
|
onSearch = { expanded = false },
|
|
active = expanded,
|
|
onActiveChange = { expanded = it },
|
|
placeholder = { Text("Search items") },
|
|
leadingIcon = { Icon(Icons.Default.Search, null) },
|
|
trailingIcon = {
|
|
if (query.isNotEmpty()) {
|
|
IconButton(onClick = { query = "" }) {
|
|
Icon(Icons.Default.Clear, "Clear search")
|
|
}
|
|
}
|
|
},
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = if (expanded) 0.dp else 16.dp)
|
|
) {
|
|
LazyColumn(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
contentPadding = PaddingValues(16.dp)
|
|
) {
|
|
items(filteredItems) { item ->
|
|
ListItem(
|
|
headlineContent = { Text(item.name) },
|
|
supportingContent = { Text(item.description) },
|
|
modifier = Modifier.clickable {
|
|
onItemClick(item)
|
|
expanded = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Selection Controls
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun SettingsScreen() {
|
|
var notificationsEnabled by rememberSaveable { mutableStateOf(true) }
|
|
var selectedOption by rememberSaveable { mutableStateOf(0) }
|
|
var expandedDropdown by remember { mutableStateOf(false) }
|
|
var selectedLanguage by rememberSaveable { mutableStateOf("English") }
|
|
val languages = listOf("English", "Spanish", "French", "German")
|
|
|
|
Column {
|
|
// Switch
|
|
ListItem(
|
|
headlineContent = { Text("Enable Notifications") },
|
|
supportingContent = { Text("Receive push notifications") },
|
|
trailingContent = {
|
|
Switch(
|
|
checked = notificationsEnabled,
|
|
onCheckedChange = { notificationsEnabled = it }
|
|
)
|
|
}
|
|
)
|
|
|
|
HorizontalDivider()
|
|
|
|
// Radio buttons
|
|
Column {
|
|
Text(
|
|
"Theme",
|
|
modifier = Modifier.padding(16.dp),
|
|
style = MaterialTheme.typography.titleSmall
|
|
)
|
|
listOf("System", "Light", "Dark").forEachIndexed { index, option ->
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.selectable(
|
|
selected = selectedOption == index,
|
|
onClick = { selectedOption = index },
|
|
role = Role.RadioButton
|
|
)
|
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
RadioButton(
|
|
selected = selectedOption == index,
|
|
onClick = null
|
|
)
|
|
Spacer(Modifier.width(16.dp))
|
|
Text(option)
|
|
}
|
|
}
|
|
}
|
|
|
|
HorizontalDivider()
|
|
|
|
// Dropdown
|
|
ExposedDropdownMenuBox(
|
|
expanded = expandedDropdown,
|
|
onExpandedChange = { expandedDropdown = it },
|
|
modifier = Modifier.padding(16.dp)
|
|
) {
|
|
OutlinedTextField(
|
|
value = selectedLanguage,
|
|
onValueChange = {},
|
|
readOnly = true,
|
|
label = { Text("Language") },
|
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropdown) },
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.menuAnchor()
|
|
)
|
|
ExposedDropdownMenu(
|
|
expanded = expandedDropdown,
|
|
onDismissRequest = { expandedDropdown = false }
|
|
) {
|
|
languages.forEach { language ->
|
|
DropdownMenuItem(
|
|
text = { Text(language) },
|
|
onClick = {
|
|
selectedLanguage = language
|
|
expandedDropdown = false
|
|
},
|
|
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Dialogs and Bottom Sheets
|
|
|
|
### Alert Dialog
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun DeleteConfirmationDialog(
|
|
itemName: String,
|
|
onConfirm: () -> Unit,
|
|
onDismiss: () -> Unit
|
|
) {
|
|
AlertDialog(
|
|
onDismissRequest = onDismiss,
|
|
icon = {
|
|
Icon(
|
|
Icons.Default.Warning,
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.error
|
|
)
|
|
},
|
|
title = {
|
|
Text("Delete Item?")
|
|
},
|
|
text = {
|
|
Text("Are you sure you want to delete \"$itemName\"? This action cannot be undone.")
|
|
},
|
|
confirmButton = {
|
|
TextButton(
|
|
onClick = onConfirm,
|
|
colors = ButtonDefaults.textButtonColors(
|
|
contentColor = MaterialTheme.colorScheme.error
|
|
)
|
|
) {
|
|
Text("Delete")
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = onDismiss) {
|
|
Text("Cancel")
|
|
}
|
|
}
|
|
)
|
|
}
|
|
```
|
|
|
|
### Modal Bottom Sheet
|
|
|
|
```kotlin
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun OptionsBottomSheet(
|
|
onDismiss: () -> Unit,
|
|
onOptionSelected: (String) -> Unit
|
|
) {
|
|
val sheetState = rememberModalBottomSheetState()
|
|
|
|
ModalBottomSheet(
|
|
onDismissRequest = onDismiss,
|
|
sheetState = sheetState,
|
|
dragHandle = { BottomSheetDefaults.DragHandle() }
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(bottom = 32.dp)
|
|
) {
|
|
Text(
|
|
"Options",
|
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
style = MaterialTheme.typography.titleMedium
|
|
)
|
|
|
|
listOf(
|
|
Triple(Icons.Default.Share, "Share", "share"),
|
|
Triple(Icons.Default.Edit, "Edit", "edit"),
|
|
Triple(Icons.Default.FileCopy, "Duplicate", "duplicate"),
|
|
Triple(Icons.Default.Delete, "Delete", "delete")
|
|
).forEach { (icon, label, action) ->
|
|
ListItem(
|
|
headlineContent = { Text(label) },
|
|
leadingContent = {
|
|
Icon(
|
|
icon,
|
|
contentDescription = null,
|
|
tint = if (action == "delete")
|
|
MaterialTheme.colorScheme.error
|
|
else
|
|
MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
},
|
|
modifier = Modifier.clickable { onOptionSelected(action) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Date and Time Pickers
|
|
|
|
```kotlin
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun DateTimePickerExample() {
|
|
var showDatePicker by remember { mutableStateOf(false) }
|
|
var showTimePicker by remember { mutableStateOf(false) }
|
|
val datePickerState = rememberDatePickerState()
|
|
val timePickerState = rememberTimePickerState()
|
|
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
OutlinedButton(onClick = { showDatePicker = true }) {
|
|
Icon(Icons.Default.CalendarToday, null)
|
|
Spacer(Modifier.width(8.dp))
|
|
Text(
|
|
datePickerState.selectedDateMillis?.let {
|
|
SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())
|
|
.format(Date(it))
|
|
} ?: "Select Date"
|
|
)
|
|
}
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
OutlinedButton(onClick = { showTimePicker = true }) {
|
|
Icon(Icons.Default.Schedule, null)
|
|
Spacer(Modifier.width(8.dp))
|
|
Text(
|
|
String.format("%02d:%02d", timePickerState.hour, timePickerState.minute)
|
|
)
|
|
}
|
|
}
|
|
|
|
if (showDatePicker) {
|
|
DatePickerDialog(
|
|
onDismissRequest = { showDatePicker = false },
|
|
confirmButton = {
|
|
TextButton(onClick = { showDatePicker = false }) {
|
|
Text("OK")
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showDatePicker = false }) {
|
|
Text("Cancel")
|
|
}
|
|
}
|
|
) {
|
|
DatePicker(state = datePickerState)
|
|
}
|
|
}
|
|
|
|
if (showTimePicker) {
|
|
AlertDialog(
|
|
onDismissRequest = { showTimePicker = false },
|
|
confirmButton = {
|
|
TextButton(onClick = { showTimePicker = false }) {
|
|
Text("OK")
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showTimePicker = false }) {
|
|
Text("Cancel")
|
|
}
|
|
},
|
|
text = {
|
|
TimePicker(state = timePickerState)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Loading States
|
|
|
|
### Progress Indicators
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun LoadingStates() {
|
|
Column(
|
|
modifier = Modifier.padding(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(24.dp)
|
|
) {
|
|
// Indeterminate circular
|
|
CircularProgressIndicator()
|
|
|
|
// Determinate circular
|
|
CircularProgressIndicator(
|
|
progress = { 0.7f },
|
|
strokeWidth = 4.dp
|
|
)
|
|
|
|
// Indeterminate linear
|
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
|
|
|
// Determinate linear
|
|
LinearProgressIndicator(
|
|
progress = { 0.7f },
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Skeleton Loading
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun SkeletonLoader(
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
val infiniteTransition = rememberInfiniteTransition(label = "skeleton")
|
|
val alpha by infiniteTransition.animateFloat(
|
|
initialValue = 0.3f,
|
|
targetValue = 0.7f,
|
|
animationSpec = infiniteRepeatable(
|
|
animation = tween(1000, easing = LinearEasing),
|
|
repeatMode = RepeatMode.Reverse
|
|
),
|
|
label = "alpha"
|
|
)
|
|
|
|
Column(
|
|
modifier = modifier.padding(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
repeat(5) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
Box(
|
|
modifier = Modifier
|
|
.size(48.dp)
|
|
.clip(CircleShape)
|
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha))
|
|
)
|
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
Box(
|
|
modifier = Modifier
|
|
.height(16.dp)
|
|
.fillMaxWidth(0.7f)
|
|
.clip(RoundedCornerShape(4.dp))
|
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha))
|
|
)
|
|
Box(
|
|
modifier = Modifier
|
|
.height(12.dp)
|
|
.fillMaxWidth(0.5f)
|
|
.clip(RoundedCornerShape(4.dp))
|
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Content Loading Pattern
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun <T> AsyncContent(
|
|
state: AsyncState<T>,
|
|
onRetry: () -> Unit,
|
|
content: @Composable (T) -> Unit
|
|
) {
|
|
when (state) {
|
|
is AsyncState.Loading -> {
|
|
Box(
|
|
modifier = Modifier.fillMaxSize(),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
CircularProgressIndicator()
|
|
}
|
|
}
|
|
is AsyncState.Success -> {
|
|
content(state.data)
|
|
}
|
|
is AsyncState.Error -> {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(32.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.Center
|
|
) {
|
|
Icon(
|
|
Icons.Default.Error,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(64.dp),
|
|
tint = MaterialTheme.colorScheme.error
|
|
)
|
|
Spacer(Modifier.height(16.dp))
|
|
Text(
|
|
"Something went wrong",
|
|
style = MaterialTheme.typography.titleMedium
|
|
)
|
|
Spacer(Modifier.height(8.dp))
|
|
Text(
|
|
state.message,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
textAlign = TextAlign.Center
|
|
)
|
|
Spacer(Modifier.height(24.dp))
|
|
Button(onClick = onRetry) {
|
|
Text("Try Again")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sealed class AsyncState<out T> {
|
|
object Loading : AsyncState<Nothing>()
|
|
data class Success<T>(val data: T) : AsyncState<T>()
|
|
data class Error(val message: String) : AsyncState<Nothing>()
|
|
}
|
|
```
|
|
|
|
## Animations
|
|
|
|
### Animated Visibility
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun ExpandableCard(
|
|
title: String,
|
|
content: String
|
|
) {
|
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
|
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.clickable { expanded = !expanded }
|
|
.padding(16.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(title, style = MaterialTheme.typography.titleMedium)
|
|
Icon(
|
|
if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
|
contentDescription = if (expanded) "Collapse" else "Expand"
|
|
)
|
|
}
|
|
|
|
AnimatedVisibility(
|
|
visible = expanded,
|
|
enter = expandVertically() + fadeIn(),
|
|
exit = shrinkVertically() + fadeOut()
|
|
) {
|
|
Text(
|
|
text = content,
|
|
modifier = Modifier.padding(top = 12.dp),
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Animated Content
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun AnimatedCounter(count: Int) {
|
|
AnimatedContent(
|
|
targetState = count,
|
|
transitionSpec = {
|
|
if (targetState > initialState) {
|
|
slideInVertically { -it } + fadeIn() togetherWith
|
|
slideOutVertically { it } + fadeOut()
|
|
} else {
|
|
slideInVertically { it } + fadeIn() togetherWith
|
|
slideOutVertically { -it } + fadeOut()
|
|
}.using(SizeTransform(clip = false))
|
|
},
|
|
label = "counter"
|
|
) { targetCount ->
|
|
Text(
|
|
text = "$targetCount",
|
|
style = MaterialTheme.typography.displayMedium
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Gesture-Based Animation
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun SwipeableCard(
|
|
onSwipeLeft: () -> Unit,
|
|
onSwipeRight: () -> Unit,
|
|
content: @Composable () -> Unit
|
|
) {
|
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
|
val animatedOffset by animateFloatAsState(
|
|
targetValue = offsetX,
|
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
|
|
label = "offset"
|
|
)
|
|
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.offset { IntOffset(animatedOffset.roundToInt(), 0) }
|
|
.pointerInput(Unit) {
|
|
detectHorizontalDragGestures(
|
|
onDragEnd = {
|
|
when {
|
|
offsetX > 200f -> {
|
|
onSwipeRight()
|
|
offsetX = 0f
|
|
}
|
|
offsetX < -200f -> {
|
|
onSwipeLeft()
|
|
offsetX = 0f
|
|
}
|
|
else -> offsetX = 0f
|
|
}
|
|
},
|
|
onHorizontalDrag = { _, dragAmount ->
|
|
offsetX += dragAmount
|
|
}
|
|
)
|
|
}
|
|
) {
|
|
content()
|
|
}
|
|
}
|
|
```
|