Pet Adoption App UI with Jetpack Compose
Overview
A beautiful UI creates a welcoming first impression for a pet adoption app, encouraging users to connect with animals in need.
In this article, we’ll explore how to build a pet adoption app’s intro screen using Jetpack Compose, featuring an animated gradient background, playful paw prints, a dog image, a curved path shape , text section, and a swipe-to-slide button to captivate users from the moment they open the app.
🐾 Key UI Components
This UI includes an animated gradient background, decorative paw prints, a dog image, a curved bottom section with text, and a swipe-to-slide button to engage users.
- 🐾 Animated gradient background for a vibrant look.
- 🐾 Paw prints to reinforce the pet adoption theme.
- 🐾 Dog image to showcase adoptable pets.
- 🐾 Curved bottom section with welcoming text.
- 🐾 Swipe-to-slide button for interactive navigation.
Key Features Table
Feature | Description |
---|---|
🐾 Animated Gradient | Dynamic orange gradient background for visual appeal. |
🐾 Paw Prints | Decorative paw prints scattered across the background. |
🐾 Pet Image | Rounded dog image to highlight adoptable pets. |
🐾 Curved Section | Curved bottom area with title and subtitle text. |
🐾 Swipe Button | Interactive swipe-to-slide button with a paw icon. |
Implementation Steps
This code creates a pet adoption app intro screen with a dynamic gradient background, paw prints, a dog image, a curved text section, and a swipe-to-slide button. Each step below explains a specific component with a mini code snippet for clarity, building up to the full implementation.
🐾 Step 1: Set Up Main Activity
Initialize the main activity with edge-to-edge display, a custom theme, and a scaffold to host the UI.
package com.android.uix
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import com.android.uix.ui.theme.ComposeUIXTheme
val quicksandFontFamily = FontFamily(
Font(R.font.quicksand_regular, weight = FontWeight.Normal),
Font(R.font.quicksand_bold, weight = FontWeight.Bold)
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeUIXTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
content = { innerPadding ->
PetAdoptionScreen(
innerPadding = innerPadding,
fontFamily = quicksandFontFamily
)
}
)
}
}
}
}
Explanation: The `MainActivity` sets up the app with an edge-to-edge display for an immersive experience, applies the `ComposeUIXTheme`, and uses a `Scaffold` to structure the UI, passing padding to the `PetAdoptionScreen` composable.
🐾 Step 2: Create Pet Adoption Screen
Build the main screen with an animated gradient background, paw prints, dog image, and curved bottom section.
@Composable
fun PetAdoptionScreen(
innerPadding: PaddingValues,
fontFamily: FontFamily = quicksandFontFamily
) {
val infiniteTransition = rememberInfiniteTransition(label = "gradientAnimation")
val gradientProgress by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(10000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "gradientProgress"
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.background(
Brush.linearGradient(
colors = listOf(Color(0xFFfa7d20), Color(0xFFFF5722)),
start = Offset(0f, 0f),
end = Offset(gradientProgress * 1000f, 1000f)
)
)
) {
PawPrintsBackground()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(50.dp))
DogImage()
CurvedBottomSection(fontFamily = fontFamily)
}
}
}
Explanation: The `PetAdoptionScreen` creates a `Box` with an animated orange gradient background using `infiniteTransition`, layers paw prints via `PawPrintsBackground`, and arranges a dog image and curved section in a centered `Column`.
🐾 Step 3: Add Paw Prints Background
Draw decorative paw prints on a canvas to enhance the pet adoption theme.
@Composable
fun PawPrintsBackground() {
Canvas(modifier = Modifier.fillMaxSize()) {
val pawPositions = listOf(
Offset(size.width * 0.15f, size.height * 0.15f),
Offset(size.width * 0.8f, size.height * 0.2f),
Offset(size.width * 0.1f, size.height * 0.35f),
Offset(size.width * 0.75f, size.height * 0.45f),
Offset(size.width * 0.9f, size.height * 0.65f)
)
pawPositions.forEach { position ->
drawPawPrint(position, Color.White.copy(alpha = 0.2f))
}
}
}
fun DrawScope.drawPawPrint(center: Offset, color: Color) {
val mainPadSize = 25.dp.toPx()
val toeSize = 12.dp.toPx()
drawCircle(color = color, radius = mainPadSize / 2, center = center)
val toePositions = listOf(
center + Offset(-20.dp.toPx(), -25.dp.toPx()),
center + Offset(-5.dp.toPx(), -30.dp.toPx()),
center + Offset(10.dp.toPx(), -30.dp.toPx()),
center + Offset(25.dp.toPx(), -25.dp.toPx())
)
toePositions.forEach { toeCenter ->
drawOval(
color = color,
topLeft = toeCenter - Offset(toeSize / 2, toeSize / 3),
size = Size(toeSize, toeSize * 0.8f)
)
}
}
Explanation: The `PawPrintsBackground` uses a `Canvas` to draw paw prints at predefined positions, with `drawPawPrint` rendering a main pad (circle) and four toes (ovals) in a semi-transparent white color.
🐾 Step 4: Display Dog Image
Show a rounded dog image to highlight the pet adoption theme.
@Composable
fun DogImage() {
Box(
modifier = Modifier
.size(350.dp)
.clip(RoundedCornerShape(40.dp))
.background(Color.Transparent)
) {
Image(
painter = painterResource(id = R.drawable.intro_dog),
contentDescription = "Dog Image",
modifier = Modifier
.fillMaxSize()
.align(Alignment.BottomCenter),
contentScale = ContentScale.Fit
)
}
}
Explanation: The `DogImage` composable displays a dog image in a 350dp square `Box` with rounded corners, using `ContentScale.Fit` to ensure the image fits without distortion.
🐾 Step 5: Create Curved Bottom Section
Design a curved section with title and subtitle text, adapting to light/dark themes.
@Composable
fun CurvedBottomSection(
fontFamily: FontFamily = quicksandFontFamily
) {
val isDarkTheme = isSystemInDarkTheme()
val waveColor = if (isDarkTheme) Color.Black else Color.White
val textColor = if (isDarkTheme) Color.White else Color.Black
val subTextColor = if (isDarkTheme) Color(0xFFB0B0B0) else Color.Gray
Box(
modifier = Modifier
.fillMaxWidth()
.height(600.dp)
) {
Canvas(
modifier = Modifier
.fillMaxSize()
.background(Color.Transparent)
) {
val path = Path().apply {
moveTo(0f, size.height * 0.3f)
quadraticBezierTo(
size.width * 0.25f, size.height * 0.2f,
size.width * 0.5f, size.height * 0.3f
)
quadraticBezierTo(
size.width * 0.75f, size.height * 0.4f,
size.width, size.height * 0.3f
)
lineTo(size.width, size.height)
lineTo(0f, size.height)
close()
}
drawPath(path, color = waveColor)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp, vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(120.dp))
Text(
text = "Find Your Buddy",
fontFamily = fontFamily,
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 32.sp
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Your next best friend is waiting inside.",
fontFamily = fontFamily,
fontSize = 16.sp,
color = subTextColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp
)
Spacer(modifier = Modifier.height(40.dp))
SwipeToSlideButton(onSwipeComplete = { /* Handle swipe completion */ })
Spacer(modifier = Modifier.height(40.dp))
}
}
}
Explanation: The `CurvedBottomSection` uses a `Canvas` to draw a curved background with quadratic Bezier curves, and a `Column` to display a bold title and subtitle, adapting colors to light/dark themes using `isSystemInDarkTheme`.
🐾 Step 6: Implement Swipe-to-Slide Button
Create an interactive swipe button with a paw icon and animated fill effect.
@Composable
fun SwipeToSlideButton(onSwipeComplete: () -> Unit) {
val density = LocalDensity.current
var offsetX by remember { mutableStateOf(with(density) { 20.dp.toPx() }) }
val animatedOffset = remember { Animatable(with(density) { 20.dp.toPx() }) }
val scope = rememberCoroutineScope()
val buttonWidth = 300.dp
val buttonHeight = 60.dp
val thumbSize = 60.dp
val maxOffset = with(density) { (buttonWidth - thumbSize).toPx() }
val isCompleted = animatedOffset.value >= maxOffset - 1f
Box(
modifier = Modifier
.width(buttonWidth)
.height(buttonHeight)
.clip(RoundedCornerShape(buttonHeight / 2))
) {
Canvas(modifier = Modifier.fillMaxSize()) {
val fillEnd = animatedOffset.value + with(density) { thumbSize.toPx() } / 2
drawRect(
color = Color(0xFFFF7A00),
topLeft = Offset.Zero,
size = Size(fillEnd.coerceAtMost(with(density) { buttonWidth.toPx() }), size.height)
)
}
Canvas(modifier = Modifier.fillMaxSize()) {
if (!isCompleted) {
val dashWidth = with(density) { 8.dp.toPx() }
val dashGap = with(density) { 6.dp.toPx() }
val strokeWidth = with(density) { 4.dp.toPx() }
val fillEnd = animatedOffset.value + with(density) { thumbSize.toPx() } / 2
val path = Path().apply {
addRoundRect(
androidx.compose.ui.geometry.RoundRect(
rect = Rect(
offset = Offset(fillEnd, 0f),
size = Size(
(size.width - fillEnd).coerceAtLeast(0f),
size.height
)
),
topLeft = CornerRadius(0f),
topRight = CornerRadius(with(density) { buttonHeight.toPx() } / 2),
bottomRight = CornerRadius(with(density) { buttonHeight.toPx() } / 2),
bottomLeft = CornerRadius(0f)
)
)
}
drawPath(
path,
color = Color(0xFFFF7A00),
style = Stroke(
width = strokeWidth,
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(dashWidth, dashGap),
phase = 0f
),
cap = StrokeCap.Square
)
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.align(Alignment.CenterStart),
contentAlignment = Alignment.Center
) {
Text(
text = "Started",
color = Color(0xFFFF7A00).copy(alpha = 0.7f),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
maxLines = 1
)
}
Box(
modifier = Modifier
.offset(x = with(density) { animatedOffset.value.toDp() })
.size(thumbSize)
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
scope.launch {
if (animatedOffset.value > maxOffset * 0.7f) {
animatedOffset.animateTo(
maxOffset,
animationSpec = tween(200)
)
onSwipeComplete()
} else {
animatedOffset.animateTo(
with(density) { 20.dp.toPx() },
animationSpec = tween(200)
)
}
}
}
) { _, dragAmount ->
scope.launch {
val newValue = (animatedOffset.value + dragAmount.x)
.coerceIn(0f, maxOffset)
animatedOffset.snapTo(newValue)
offsetX = newValue
}
}
}
) {
Canvas(modifier = Modifier.size(thumbSize)) {
drawCircle(
color = Color(0xFFFF7A00).copy(alpha = 0.1f),
radius = size.width,
center = center,
blendMode = BlendMode.SrcOver
)
}
Box(
modifier = Modifier
.size(59.dp)
.background(Color(0xFFFF7A00), CircleShape),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.size(40.dp)) {
drawCircle(
color = Color.White,
radius = size.width / 2,
style = Stroke(width = with(density) { 3.dp.toPx() })
)
}
Image(
painter = painterResource(id = R.drawable.ic_paw),
contentDescription = "Paw Icon",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.White)
)
}
}
}
}
🐾 Full Source Code
Below is the complete source code for the pet adoption app’s intro screen, combining all steps into a single implementation.
MainActivity.kt
package com.android.uix
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.android.uix.ui.theme.ComposeUIXTheme
import kotlinx.coroutines.launch
import android.os.Build
import androidx.compose.ui.graphics.toArgb
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
// Define Quicksand font family as a top-level constant
val quicksandFontFamily = FontFamily(
Font(R.font.quicksand_regular, weight = FontWeight.Normal),
Font(R.font.quicksand_bold, weight = FontWeight.Bold)
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Allow drawing under status bar if needed
WindowCompat.setDecorFitsSystemWindows(window, true)
// Set status bar color to orange
val orangeColor = androidx.compose.ui.graphics.Color(0xFFFF5824)
window.statusBarColor = orangeColor.toArgb()
// Set icon color for contrast (false = light icons, true = dark icons)
WindowInsetsControllerCompat(window, window.decorView)
.isAppearanceLightStatusBars = false // light icons on orange background
setContent {
// Apply custom theme
ComposeUIXTheme {
// Set up scaffold for layout structure
Scaffold(
modifier = Modifier.fillMaxSize(),
content = { innerPadding ->
// Pass padding and font family to PetAdoptionScreen
PetAdoptionScreen(
innerPadding = innerPadding,
fontFamily = quicksandFontFamily
)
}
)
}
}
}
}
// Main composable for the pet adoption screen
@Composable
fun PetAdoptionScreen(
innerPadding: PaddingValues,
fontFamily: FontFamily = quicksandFontFamily
) {
// Initialize infinite transition for gradient animation
val infiniteTransition = rememberInfiniteTransition(label = "gradientAnimation")
// Animate gradient progress from 0 to 1
val gradientProgress by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(10000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "gradientProgress"
)
// Create main container with animated gradient background
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.background(
Brush.linearGradient(
colors = listOf(Color(0xFFfa7d20), Color(0xFFFF5722)),
start = Offset(0f, 0f),
end = Offset(gradientProgress * 1000f, 1000f)
)
)
) {
// Render paw prints background
PawPrintsBackground()
// Arrange content vertically in a column
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Add vertical spacing
Spacer(modifier = Modifier.height(50.dp))
// Display dog image
DogImage()
// Render curved bottom section
CurvedBottomSection(fontFamily = fontFamily)
}
}
}
// Bottom section with curved design and text
@Composable
fun CurvedBottomSection(
fontFamily: FontFamily = quicksandFontFamily
) {
// Detect system theme for color selection
val isDarkTheme = isSystemInDarkTheme()
// Set colors based on theme
val waveColor = if (isDarkTheme) Color.Black else Color.White
val textColor = if (isDarkTheme) Color.White else Color.Black
val subTextColor = if (isDarkTheme) Color(0xFFB0B0B0) else Color.Gray
// Create container for curved section
Box(
modifier = Modifier
.fillMaxWidth()
.height(600.dp)
) {
// Draw curved background on canvas
Canvas(
modifier = Modifier
.fillMaxSize()
.background(Color.Transparent)
) {
// Define path for curved shape
val path = Path().apply {
moveTo(0f, size.height * 0.3f)
quadraticBezierTo(
size.width * 0.25f, size.height * 0.2f,
size.width * 0.5f, size.height * 0.3f
)
quadraticBezierTo(
size.width * 0.75f, size.height * 0.4f,
size.width, size.height * 0.3f
)
lineTo(size.width, size.height)
lineTo(0f, size.height)
close()
}
// Draw the curved path
drawPath(path, color = waveColor)
}
// Arrange text content vertically
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp, vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Add spacing before text
Spacer(modifier = Modifier.height(120.dp))
// Display main title text
Text(
text = "Find Your Buddy",
fontFamily = fontFamily,
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 32.sp
)
// Add spacing between texts
Spacer(modifier = Modifier.height(16.dp))
// Display subtitle text
Text(
text = "Your next best friend is waiting inside.",
fontFamily = fontFamily,
fontSize = 16.sp,
color = subTextColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp
)
// Add spacing after text
Spacer(modifier = Modifier.height(40.dp))
// Render swipe-to-slide button
SwipeToSlideButton(onSwipeComplete = { /* Handle swipe completion */ })
// Add final spacing
Spacer(modifier = Modifier.height(40.dp))
}
}
}
// Swipe-to-slide button with drag gesture
@Composable
fun SwipeToSlideButton(onSwipeComplete: () -> Unit) {
// Initialize variables for thumb position and animation
val density = LocalDensity.current
var offsetX by remember { mutableStateOf(with(density) { 20.dp.toPx() }) }
val animatedOffset = remember { Animatable(with(density) { 20.dp.toPx() }) }
val scope = rememberCoroutineScope()
// Define button and thumb dimensions
val buttonWidth = 300.dp
val buttonHeight = 60.dp
val thumbSize = 60.dp
val maxOffset = with(density) { (buttonWidth - thumbSize).toPx() }
val isCompleted = animatedOffset.value >= maxOffset - 1f
// Main container for the button
Box(
modifier = Modifier
.width(buttonWidth)
.height(buttonHeight)
.clip(RoundedCornerShape(buttonHeight / 2))
) {
// Draw filled orange background
Canvas(modifier = Modifier.fillMaxSize()) {
val fillEnd = animatedOffset.value + with(density) { thumbSize.toPx() } / 2
drawRect(
color = Color(0xFFFF7A00),
topLeft = Offset.Zero,
size = Size(fillEnd.coerceAtMost(with(density) { buttonWidth.toPx() }), size.height)
)
}
// Draw dashed border for unfilled area
Canvas(modifier = Modifier.fillMaxSize()) {
if (!isCompleted) {
val dashWidth = with(density) { 8.dp.toPx() }
val dashGap = with(density) { 6.dp.toPx() }
val strokeWidth = with(density) { 4.dp.toPx() }
val fillEnd = animatedOffset.value + with(density) { thumbSize.toPx() } / 2
val path = Path().apply {
addRoundRect(
androidx.compose.ui.geometry.RoundRect(
rect = Rect(
offset = Offset(fillEnd, 0f),
size = Size(
(size.width - fillEnd).coerceAtLeast(0f),
size.height
)
),
topLeft = CornerRadius(0f),
topRight = CornerRadius(with(density) { buttonHeight.toPx() } / 2),
bottomRight = CornerRadius(with(density) { buttonHeight.toPx() } / 2),
bottomLeft = CornerRadius(0f)
)
)
}
drawPath(
path,
color = Color(0xFFFF7A00),
style = Stroke(
width = strokeWidth,
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(dashWidth, dashGap),
phase = 0f
),
cap = StrokeCap.Square
)
)
}
}
// Display centered text
Box(
modifier = Modifier
.fillMaxSize()
.align(Alignment.CenterStart),
contentAlignment = Alignment.Center
) {
Text(
text = "Started",
color = Color(0xFFFF7A00).copy(alpha = 0.7f),
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
maxLines = 1
)
}
// Draggable thumb with icon
Box(
modifier = Modifier
.offset(x = with(density) { animatedOffset.value.toDp() })
.size(thumbSize)
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
scope.launch {
if (animatedOffset.value > maxOffset * 0.7f) {
animatedOffset.animateTo(
maxOffset,
animationSpec = tween(200)
)
onSwipeComplete()
} else {
animatedOffset.animateTo(
with(density) { 20.dp.toPx() },
animationSpec = tween(200)
)
}
}
}
) { _, dragAmount ->
scope.launch {
val newValue = (animatedOffset.value + dragAmount.x)
.coerceIn(0f, maxOffset)
animatedOffset.snapTo(newValue)
offsetX = newValue
}
}
}
) {
// Draw glow effect
Canvas(modifier = Modifier.size(thumbSize)) {
drawCircle(
color = Color(0xFFFF7A00).copy(alpha = 0.1f),
radius = size.width,
center = center,
blendMode = BlendMode.SrcOver
)
}
// Draw thumb with white ring and paw icon
Box(
modifier = Modifier
.size(59.dp)
.background(Color(0xFFFF7A00), CircleShape),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.size(40.dp)) {
drawCircle(
color = Color.White,
radius = size.width / 2,
style = Stroke(width = with(density) { 3.dp.toPx() })
)
}
Image(
painter = painterResource(id = R.drawable.ic_paw),
contentDescription = "Paw Icon",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.White)
)
}
}
}
}
// Display dog image with rounded corners
@Composable
fun DogImage() {
// Create container for dog image
Box(
modifier = Modifier
.size(350.dp)
.clip(RoundedCornerShape(40.dp))
.background(Color.Transparent)
) {
// Render dog image
Image(
painter = painterResource(id = R.drawable.intro_dog),
contentDescription = "Dog Image",
modifier = Modifier
.fillMaxSize()
.align(Alignment.BottomCenter),
contentScale = ContentScale.Fit
)
}
}
// Draw paw prints as background decoration
@Composable
fun PawPrintsBackground() {
// Create canvas for paw prints
Canvas(modifier = Modifier.fillMaxSize()) {
// Define positions for paw prints
val pawPositions = listOf(
Offset(size.width * 0.15f, size.height * 0.15f),
Offset(size.width * 0.8f, size.height * 0.2f),
Offset(size.width * 0.1f, size.height * 0.35f),
Offset(size.width * 0.75f, size.height * 0.45f),
Offset(size.width * 0.9f, size.height * 0.65f)
)
// Draw each paw print
pawPositions.forEach { position ->
drawPawPrint(position, Color.White.copy(alpha = 0.2f))
}
}
}
// Draw a single paw print at specified position
fun DrawScope.drawPawPrint(center: Offset, color: Color) {
// Define sizes for paw pad and toes
val mainPadSize = 25.dp.toPx()
val toeSize = 12.dp.toPx()
// Draw main paw pad
drawCircle(color = color, radius = mainPadSize / 2, center = center)
// Define toe positions
val toePositions = listOf(
center + Offset(-20.dp.toPx(), -25.dp.toPx()),
center + Offset(-5.dp.toPx(), -30.dp.toPx()),
center + Offset(10.dp.toPx(), -30.dp.toPx()),
center + Offset(25.dp.toPx(), -25.dp.toPx())
)
// Draw each toe
toePositions.forEach { toeCenter ->
drawOval(
color = color,
topLeft = toeCenter - Offset(toeSize / 2, toeSize / 3),
size = Size(toeSize, toeSize * 0.8f)
)
}
}
// -------------------- PetAdoptionScreen --------------------
@Preview(name = "PetAdoption Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
@Composable
fun PetAdoptionScreenPreviewLight() {
PetAdoptionScreen(innerPadding = PaddingValues(0.dp))
}
@Preview(name = "PetAdoption Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun PetAdoptionScreenPreviewDark() {
PetAdoptionScreen(innerPadding = PaddingValues(0.dp))
}
// -------------------- CurvedBottomSection --------------------
@Preview(name = "CurvedBottom Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
@Composable
fun CurvedBottomSectionPreviewLight() {
CurvedBottomSection()
}
@Preview(name = "CurvedBottom Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun CurvedBottomSectionPreviewDark() {
CurvedBottomSection()
}
// -------------------- SwipeToSlideButton --------------------
@Preview(name = "SwipeToSlide Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
@Composable
fun SwipeToSlideButtonPreviewLight() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
contentAlignment = Alignment.Center
) {
SwipeToSlideButton(onSwipeComplete = {})
}
}
@Preview(name = "SwipeToSlide Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun SwipeToSlideButtonPreviewDark() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
contentAlignment = Alignment.Center
) {
SwipeToSlideButton(onSwipeComplete = {})
}
}
// -------------------- DogImage --------------------
@Preview(name = "DogImage Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
@Composable
fun DogImagePreviewLight() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF1C2526))
) {
DogImage()
}
}
@Preview(name = "DogImage Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun DogImagePreviewDark() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
DogImage()
}
}
// -------------------- PawPrintsBackground --------------------
@Preview(name = "PawPrints Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
@Composable
fun PawPrintsBackgroundPreviewLight() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF1C2526))
) {
PawPrintsBackground()
}
}
@Preview(name = "PawPrints Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun PawPrintsBackgroundPreviewDark() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
PawPrintsBackground()
}
}
Tips for Improvement
This implementation is designed as a single-page demo for simplicity, but here are tips to enhance it for production:
- 🐾 Separation of Concerns: Split the code into multiple composable files (e.g.,
PawPrintsBackground.kt
,SwipeToSlideButton.kt
) to improve modularity and maintainability. - 🐾Custom Theme Enhancements: Extend
ComposeUIXTheme
with custom typography, shapes, and colors to strengthen the PETA brand identity. - 🐾Navigation Integration: Implement the
onSwipeComplete
lambda to navigate to a new screen (e.g., pet listing) using Jetpack Navigation or a similar library. - 🐾Organize Previews: Move preview composables to a separate file or module to keep the main code clean and focused on production logic.
- 🐾Resource Validation: Ensure resources like
R.font.quicksand_regular
,R.font.quicksand_bold
,R.drawable.intro_dog
, andR.drawable.ic_paw
are properly defined in your project. - 🐾Dynamic Images: Replace the static dog image with a dynamic image loader (e.g., Coil) to showcase different pets from a data source.
Assets:
Font: Quicksand (Google Fonts)
Feedback
Do you have suggestions to improve this pet adoption app UI or the Jetpack Compose code? Share your ideas, optimizations, or alternative approaches in the comments to help the Jetpack compose Dev community. We’ll update this guide with your feedback!
Demo only: In production, split UI into multiple composables and screens for maintainability
UI looks good