How to Create a Pet Adoption App Welcome Screen in Jetpack Compose | Jetpack Compose Intro Screen Tutorial

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, and R.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)

Drawable: ic_paw.png, intro_dog.png


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 

1 Comments

Previous Post Next Post