MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Most animation tutorials focus on what's possible. This one focuses on what's useful: the animations that make an app feel polished and responsive without becoming distracting or slowing users down.
On this page
Animation is easy to overdo. Apps with constant motion feel exhausting. Apps with zero motion feel dead. The right animations are the ones that help users understand what just changed and why.
Here are the animations worth implementing.
Before adding any animation, ask: does this help the user understand what changed? If yes, add it. If it's purely decorative, reconsider.
Good animation purposes:
Animation that gets in the way:
The most useful animation in day-to-day Compose development:
// Show/hide with smooth transition
var showFab by remember { mutableStateOf(false) }
AnimatedVisibility(
visible = showFab,
enter = fadeIn() + scaleIn(initialScale = 0.8f),
exit = fadeOut() + scaleOut(targetScale = 0.8f)
) {
FloatingActionButton(onClick = { addTask() }) {
Icon(Icons.Default.Add, contentDescription = "Add task")
}
}This FAB slides in when a list is scrolled to the top — a common pattern that's expected on Android.
For list items appearing/disappearing:
LazyColumn {
items(tasks, key = { it.id }) { task ->
AnimatedVisibility(
visible = true,
enter = expandVertically() + fadeIn()
) {
TaskItem(task = task)
}
}
}When content height changes (expandable cards, collapsible sections):
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(
animationSpec = tween(durationMillis = 200, easing = EaseInOut)
)
.clickable { isExpanded = !isExpanded }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = task.title, style = MaterialTheme.typography.bodyLarge)
if (isExpanded) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = task.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}animateContentSize()When individual properties change — color, size, offset:
val taskColor by animateColorAsState(
targetValue = if (task.completed) {
MaterialTheme.colorScheme.surfaceVariant
} else {
MaterialTheme.colorScheme.surface
},
animationSpec = tween(durationMillis = 200),
label = "task_background_color"
)
Surface(
color = taskColor,
modifier = Modifier.fillMaxWidth()
) {
TaskContent(task = task)
}The task card smoothly transitions to a muted color when completed. This is better UX than an instant color change.
For position/offset:
val fabOffset by animateDpAsState(
targetValue = if (isScrolled) 80.dp else 0.dp,
label = "fab_offset"
)
FloatingActionButton(
modifier = Modifier.offset(y = fabOffset)
) { ... }When the entire content of a container changes:
Crossfade(
targetState = uiState,
animationSpec = tween(durationMillis = 200),
label = "content_crossfade"
) { state ->
when (state) {
is TaskUiState.Loading -> LoadingSpinner()
is TaskUiState.Success -> TaskList(tasks = state.tasks)
is TaskUiState.Error -> ErrorView(message = state.message)
is TaskUiState.Empty -> EmptyState()
}
}Without
CrossfadeSwipe-to-delete is a standard Android pattern. Compose provides
SwipeToDismissBox@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeableTaskItem(
task: Task,
onDismiss: (Task) -> Unit
) {
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { dismissValue ->
if (dismissValue == SwipeToDismissBoxValue.EndToStart) {
onDismiss(task)
true
} else false
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.error),
contentAlignment = Alignment.CenterEnd
) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.onError,
modifier = Modifier.padding(end = 16.dp)
)
}
}
) {
TaskItem(task = task)
}
}The shimmer loading effect (animated placeholder) is better UX than a spinner for list content:
@Composable
fun ShimmerTaskItem() {
val shimmerColors = listOf(
MaterialTheme.colorScheme.surface,
MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.colorScheme.surface
)
val transition = rememberInfiniteTransition(label = "shimmer")
val translateAnim by transition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1200, easing = LinearEasing)
),
label = "shimmer_translate"
)
val brush = Brush.linearGradient(
colors = shimmerColors,
start = Offset.Zero,
end = Offset(x = translateAnim, y = translateAnim)
)
Column(modifier = Modifier.padding(16.dp)) {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(16.dp)
.clip(RoundedCornerShape(4.dp))
.background(brush)
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(
modifier = Modifier
.fillMaxWidth(0.6f)
.height(12.dp)
.clip(RoundedCornerShape(4.dp))
.background(brush)
)
}
}Animations that modify properties Compose can handle in the rendering layer (offset, scale, alpha, clip) don't trigger recomposition. They're fast.
Animations that modify layout properties (size, padding) trigger recomposition on every frame. Use them sparingly.
// Fast — GPU handles this, no recomposition
.graphicsLayer {
alpha = animatedAlpha
scaleX = animatedScale
scaleY = animatedScale
}
// Slower — triggers recomposition each frame
.size(animatedSize) // Layout changeAnimatedVisibilityCrossfadeanimateColorAsStateanimateDpAsStateanimateContentSize().graphicsLayer {}Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read