How to Use Jetpack Compose and Flutter to Make a Custom Loading Animation for Mobile App

Loading indicators are an important part of making a mobile experience look and feel smooth and professional. A lot of developers use built-in progress bars or spinners, but custom animations can make your app look different, help with branding, and make it feel like it’s flowing.

This guide will show you how to use Jetpack Compose (for Android) and Flutter to make a more complex custom loading animation for mobile apps. The goal is to make a fun-to-watch multi-ring spinner animation that spins in different directions, at different speeds, and with arcs of different shapes. Your UI will look fresh and new with this.

⭐ Why It’s Important to Have a Custom Loading Animation

A good loading animation will give you

  • More interaction with users
  • Different looks for each brand
  • There are no issues when you switch screens.
  • It feels like a premium app.

People believe they don’t have to wait as long.

You can make complicated animated parts with very little code using Jetpack Compose and Flutter, two modern UI frameworks.

Jetpack Compose for Android lets you make your own loading animation.

Developers can make UI in a declarative way with Jetpack Compose and use powerful APIs to add animations. We want to make a spinner with a lot of rings that are stacked on top of each other and spin by themselves.

1. Model for Setting Up a Ring

First, we discuss the features of each animated ring:

  • Radius

  • Color

  • Stroke width

  • Opacity

  • Arc length

  • Direction of rotation

  • Rotation speed

These properties allow full customization for each ring in the spinner.

RingConfig Model

 
data class RingConfig(
    val id: String,
    val radius: Float,
    val color: Color,
    val strokeWidth: Float,
    val opacity: Float,
    val arcLength: Float,
    val direction: Int,     // +1 or -1
    val speed: Float        // seconds per rotation
)
 
2. Creating the Custom Loading Animation

For smooth animations that never end, Compose has rememberInfiniteTransition.

Each ring has its own float animation that shows how it turns:

  • Independent rotation for each ring

  • Control over speed and direction

  • Smooth, continuous animation

3. Drawing on a Canvas

There are dashed arcs and a rotating effect on the rings drawn on Canvas.

This setup allows:

  • Changeable radius
  • Stroke width that can be changed
  • Dividing arcs into parts
  • Each layer can move by itself.

These pieces work together to make a loading animation that looks great and works well with Android apps that are up to date.

Spinner Composable Function



@Composable
fun Loading(
    rings: List,
    size: Dp = 400.dp
) {
    //  All animations must be created here, NOT inside Canvas
    val transition = rememberInfiniteTransition()

    // Pre-calc animations for each ring
    val rotations = rings.map { ring ->
        transition.animateFloat(
            initialValue = 0f,
            targetValue = ring.direction * 360f,
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = (ring.speed * 1000).toInt(),
                    easing = LinearEasing
                ),
                repeatMode = RepeatMode.Restart
            )
        )
    }

    Box(
        modifier = Modifier.size(size),
    ) {
        Canvas(modifier = Modifier.fillMaxSize()) {

            val center = Offset(this.size.width / 2, this.size.height / 2)

            rings.forEachIndexed { index, ring ->
                val rotation = rotations[index].value

                val circumference = 2f * PI.toFloat() * ring.radius
                val dashArray = circumference * ring.arcLength
                val dashGap = circumference - dashArray

                rotate(rotation, pivot = center) {
                    drawCircle(
                        color = ring.color.copy(alpha = ring.opacity),
                        radius = ring.radius,
                        center = center,
                        style = Stroke(
                            width = ring.strokeWidth,
                            cap = StrokeCap.Round,
                            pathEffect = PathEffect.dashPathEffect(
                                floatArrayOf(dashArray, dashGap),
                                phase = 0f
                            )
                        )
                    )
                }
            }
        }
    }
}

 

How to use this Loading Composable Function

MainActivity.kt

     
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            LoaderTheme {
                CustomSpinLoading()
            }
        }
    }
}

@Composable
fun CustomSpinLoading() {
    val rings = listOf(
        RingConfig(
            id = "a",
            radius = 50f,
            color = Color.Black,
            strokeWidth = 14f,
            opacity = 1f,
            arcLength = 0.6f,
            direction = -1,
            speed = 6f
        ),
        RingConfig(
            id = "b",
            radius = 80f,
            color = Color.Red,
            strokeWidth = 14f,
            opacity = 1f,
            arcLength = 0.8f,
            direction = 1,
            speed = 8f
        ),
        RingConfig(
            id = "c",
            radius = 120f,
            color = Color.Blue,
            strokeWidth = 14f,
            opacity = 0.9f,
            arcLength = 0.6f,
            direction = -1,
            speed = 9f
        ),
        RingConfig(
            id = "d",
            radius = 160f,
            color = Color.Green,
            strokeWidth = 14f,
            opacity = 0.8f,
            arcLength = 0.5f,
            direction = 1,
            speed = 9f
        ),

        RingConfig(
            id = "e",
            radius = 200f,
            color = Color.Cyan,
            strokeWidth = 14f,
            opacity = 0.8f,
            arcLength = 0.4f,
            direction = -1,
            speed = 8f
        )
    )

    Loading(rings = rings, size = 400.dp)
}
  

 

🟩 Flutter Loading Animation That You Can Change

Flutter has its own animation system that is based on controllers and custom painters, but it works in a similar way to declarative programming.

Check out the code below:

RingConfig.dart Model Class

 
class RingConfig {
final String id;
final double radius;
final Color color;
final double strokeWidth;
final double opacity;
final double arcLength;
final int direction; // +1 or -1
final double speed; // seconds per rotation

RingConfig({
required this.id,
required this.radius,
required this.color,
required this.strokeWidth,
required this.opacity,
required this.arcLength,
required this.direction,
required this.speed,
});
}

 
Drawing with CustomPainter

 
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'RingConfig.dart';

class LoadingPainter extends CustomPainter {
final List<RingConfig> rings;
final List<Animation<double>> rotations;

LoadingPainter(this.rings, this.rotations);

@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);

for (int i = 0; i < rings.length; i++) {
final ring = rings[i];
final rotation = rotations[i].value;

final paint = Paint()
..color = ring.color.withValues()
..style = PaintingStyle.stroke
..strokeWidth = ring.strokeWidth
..strokeCap = StrokeCap.round;

final circumference = 2 * pi * ring.radius;
final dashLength = circumference * ring.arcLength;
final dashGap = circumference - dashLength;

final circlePath = Path()
..addOval(Rect.fromCircle(center: center, radius: ring.radius));

final dashedPath = _dashPath(circlePath, dashLength, dashGap);

canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(rotation * pi / 180);
canvas.translate(-center.dx, -center.dy);

canvas.drawPath(dashedPath, paint);

canvas.restore();
}
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

Path _dashPath(Path source, double dashLength, double dashGap) {
final Path dest = Path();
double distance = 0.0;

for (final metric in source.computeMetrics()) {
while (distance < metric.length) {
final next = distance + dashLength;
dest.addPath(metric.extractPath(distance, next), Offset.zero);
distance = next + dashGap;
}
distance = 0.0;
}
return dest;
}

 
Custom Loading Widget


import 'package:flutter/cupertino.dart';
import 'RingConfig.dart';
import 'LoadingPainter.dart';
import 'package:flutter/material.dart';


class Loading extends StatefulWidget {
  final List<RingConfig> rings;
  final double size;

  const Loading({super.key, required this.rings, this.size = 400});

  @override
  State<Loading> createState() => _LoadingState();
}

class _LoadingState extends State<Loading> with TickerProviderStateMixin {
  late List<AnimationController> controllers;
  late List<Animation<double>> rotations;

  @override
  void initState() {
    super.initState();

    controllers = widget.rings.map((ring) {
      return AnimationController(
        duration: Duration(milliseconds: (ring.speed * 1000).toInt()),
        vsync: this,
      )..repeat();
    }).toList();

    rotations = List.generate(
      widget.rings.length,
      (i) => Tween<double>(
        begin: 0,
        end: widget.rings[i].direction * 360,
      ).animate(controllers[i]),
    );
  }

  @override
  void dispose() {
    for (var controller in controllers) {
      controller.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.size,
      height: widget.size,
      child: AnimatedBuilder(
        animation: Listenable.merge(controllers),
        builder: (_, __) {
          return CustomPaint(painter: LoadingPainter(widget.rings, rotations));
        },
      ),
    );
  }
}

 

How to use this Loading widget in Flutter?

main.dart


import 'package:flutter/material.dart';

import 'RingConfig.dart';
import 'Loading.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Loading',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Loading Animation'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    final rings = [
      RingConfig(
        id: "a",
        radius: 25,
        color: Colors.black,
        strokeWidth: 8,
        opacity: 1,
        arcLength: 0.6,
        direction: -1,
        speed: 6,
      ),
      RingConfig(
        id: "b",
        radius: 40,
        color: Colors.red,
        strokeWidth: 8,
        opacity: 1,
        arcLength: 0.8,
        direction: 1,
        speed: 8,
      ),
      RingConfig(
        id: "c",
        radius: 60,
        color: Colors.blue,
        strokeWidth: 8,
        opacity: 0.9,
        arcLength: 0.6,
        direction: -1,
        speed: 9,
      ),
      RingConfig(
        id: "d",
        radius: 80,
        color: Colors.green,
        strokeWidth: 8,
        opacity: 0.8,
        arcLength: 0.5,
        direction: 1,
        speed: 9,
      ),
      RingConfig(
        id: "e",
        radius: 100,
        color: Colors.cyan,
        strokeWidth: 8,
        opacity: 0.8,
        arcLength: 0.4,
        direction: -1,
        speed: 8,
      ),
    ];

    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(child: Loading(rings: rings, size: 400)),
    );
  }
}



Download Source Code

Final Thoughts

You can make a loading animation just for mobile apps (Jetpack Compose and Flutter), and it can be very rewarding. Both frameworks give developers everything they need to make motion graphics that are expressive without having to use animated assets or libraries from other companies.

This multi-ring spinner is a great example of:

  • Design of animations that can be changed
  • Framework-level APIs for drawing
  • Animation loops that go on forever and work well
  • Consistency between platforms
    A well-designed loader can make your user interface a lot better, whether you’re making apps just for Android or for more than one platform.

How to Build a Real-Time Blood Pressure App Android UI Using Jetpack Compose

Modern health applications—whether you’re building a blood pressure app Android, an ECG app Android, or a heart rate monitor Android—require interfaces that are clean, smooth, and capable of updating in real time. With Jetpack Compose, Android developers can design advanced animated dashboards using only a few composable functions.

In this guide, we’ll walk through a complete real-time vital monitoring UI built entirely with Canvas, animations, and Jetpack Compose.
The final setup includes:

  • A pressure gauge (mmHg) for blood pressure visualization
  • A real-time ECG waveform renderer
  • A vital simulator that generates dynamic heartbeat, SpO₂, and pressure values

If you’re creating a health monitoring interface, these components provide a great foundation.

1. The Blood Pressure Gauge’s (PressureGauge) user interface

The PressureGauge composable makes a round gauge that is fully animated and looks like the dials on professional sphygmomanometers used in clinics.
Important parts of the gauge

  •  The needle moves smoothly.
  •  It ticks every 10 mmHg.
  •  Major ticks every 20 mmHg.
  •  A soft radial gradient background.
  • A 270° sweep (–135° to +135°), just like a medical pressure meter.

The needle moves smoothly toward the new pressure value when you use animateFloatAsState(). This makes it feel like a real machine, which is great for the UI of an Android blood pressure app.

PressureGauge Code

  
@Composable
fun PressureGauge(
    pressure: Float,
    maxPressure: Float = 300f,
    modifier: Modifier = Modifier
) {
    val animatedPressure by animateFloatAsState(
        targetValue = pressure,
        animationSpec = tween(800, easing = LinearOutSlowInEasing)
    )

    val size = 260.dp
    val center = size / 2

    Box(
        modifier = modifier
            .size(size)
            .padding(12.dp),
        contentAlignment = Alignment.Center
    ) {
        Canvas(modifier = Modifier.fillMaxSize()) {

            val w = this.size.width
            val h = this.size.height
            val cx = w / 2
            val cy = h / 2

            val rotation = -135f + (animatedPressure.coerceAtMost(maxPressure) / maxPressure) * 270f

            // BACKGROUND GRADIENT
            drawCircle(
                brush = Brush.radialGradient(
                    colors = listOf(Color.White, Color(0xFFE2E8F0))
                ),
                radius = w * 0.45f,
                center = Offset(cx, cy)
            )

            // TICKS
            for (i in 0..maxPressure.toInt() step 10) {
                val angle = -135f + (i / maxPressure) * 270f
                val rad = Math.toRadians((angle - 90).toDouble())

                val major = i % 20 == 0
                val length = if (major) 18f else 10f
                val width = if (major) 2f else 1f

                val rOuter = w * 0.4f
                val rInner = rOuter - length

                val x1 = cx + rOuter * cos(rad).toFloat()
                val y1 = cy + rOuter * sin(rad).toFloat()
                val x2 = cx + rInner * cos(rad).toFloat()
                val y2 = cy + rInner * sin(rad).toFloat()

                drawLine(
                    color = if (major) Color(0xFF334155) else Color(0xFF94A3B8),
                    start = Offset(x1, y1), end = Offset(x2, y2),
                    strokeWidth = width
                )
            }

            // NEEDLE
            rotate(rotation, pivot = Offset(cx, cy)) {
                drawLine(
                    start = Offset(cx, cy),
                    end = Offset(cx, cy - w * 0.35f),
                    color = Color(0xFFEF4444),
                    strokeWidth = 6f,
                    cap = StrokeCap.Round
                )
                drawCircle(
                    color = Color(0xFFEF4444),
                    center = Offset(cx, cy),
                    radius = 10f
                )
            }
        }
    }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(
            text = animatedPressure.toInt().toString(),
            fontSize = 34.sp,
            fontWeight = FontWeight.Bold,
            color = Color(0xFF1E293B)
        )
        Text(
            text = "mmHg",
            color = Color(0xFF64748B),
            fontSize = 12.sp
        )
    }
}
 
2. PulseECG – A real-time renderer for ECG waveforms

The waveform is the most important visual part of an ECG app for Android. The PulseECG composable makes it look like an ECG trace is moving by constantly moving a list of points.
How it works
Keeps a list of 120 points that keeps changing
Changes values based on two conditions:
active: ECG is working
beating: heartbeats
Makes a dark grid that looks like a medical one
Draws a glowing red ECG path to make it look real
It looks a lot like real ECG monitor output and can easily take real medical data.

PulseECG Code

  
@Composable
fun PulseECG(
    active: Boolean,
    beating: Boolean,
    modifier: Modifier = Modifier
) {
    var points by remember { mutableStateOf(List(120) { 50f }) }
    var tick by remember { mutableStateOf(0) }

    LaunchedEffect(active, beating) {
        while (true) {
            delay(40)

            val new = points.toMutableList()
            new.removeAt(0)

            val updated = when {
                !active -> 50f
                !beating -> 50f + (Math.random().toFloat() - 0.5f) * 8f
                else -> when (tick % 12) {
                    0 -> 50f
                    1 -> 30f
                    2 -> 95f
                    3 -> 10f
                    4 -> 60f
                    else -> 50f
                }
            }
            new.add(updated)
            points = new

            if (beating) tick++
        }
    }

    Canvas(
        modifier = modifier
            .fillMaxWidth()
            .height(110.dp)
            .background(Color(0xFF0F172A))
            .padding(4.dp)
    ) {
        val w = size.width
        val h = size.height

        // GRID
        drawRect(Color(0xFF1E293B))
        for (x in 0 until w.toInt() step 20)
            drawLine(Color(0xFF334155), Offset(x.toFloat(), 0f), Offset(x.toFloat(), h))
        for (y in 0 until h.toInt() step 20)
            drawLine(Color(0xFF334155), Offset(0f, y.toFloat()), Offset(w, y.toFloat()))

        // ECG line
        val path = Path()
        points.forEachIndexed { i, v ->
            val x = (i / points.size.toFloat()) * w
            val y = h - (v / 100f) * h
            if (i == 0) path.moveTo(x, y) else path.lineTo(x, y)
        }

        // Glow
        drawPath(
            path = path,
            color = Color(0xFFEF4444).copy(alpha = 0.4f),
            style = Stroke(width = 8f)
        )

        drawPath(
            path = path,
            color = Color(0xFFEF4444),
            style = Stroke(width = 3f)
        )

    }
}
 
3. VitalSimulator – Generating Real-Time Readings

For demo purposes—or previewing UI—you can use VitalSimulator, a small composable that generates random:

  • Blood pressure
  • Heart rate (BPM)
  • SpO₂ oxygen level

This is especially useful while building a heart rate monitor Android interface.

VitalSimulator Code

 
@Composable
fun VitalSimulator(): Triple<Float, Float, Float> {
    var pressure by remember { mutableStateOf(120f) }
    var bpm by remember { mutableStateOf(75f) }
    var spo2 by remember { mutableStateOf(97f) }

    LaunchedEffect(Unit) {
        while (true) {
            delay(1500)
            pressure = (110..135).random().toFloat()
            bpm = (72..90).random().toFloat()
            spo2 = (96..99).random().toFloat()
        }
    }

    return Triple(pressure, bpm, spo2)
}
Putting It All Together

By combining these composables, you can build a polished dashboard showing:

  • Animated blood pressure
  • Real-time ECG waveform
  • BPM and SpO₂ levels

This is an excellent base for medical-style dashboards or consumer wellness applications.




Download Source Code

Final Thoughts

Jetpack Compose makes it possible to build real-time health monitoring dashboards with only a few clean, efficient composables. Whether you’re working on a blood pressure app Android, an ECG app Android, or a heart rate monitor Android, these components give you a professional, animated, and scalable UI foundation.

Flip Coin Source Code Android Java

If you are searching for a Flip Coin Source Code Android example, this guide will walk you through building a realistic coin flip animation in Android using Java. This example includes sound effects, both sides of the coin visible during flipping, and a clean user interface built with ConstraintLayout.

Why Use This Flip Coin Source Code Android Example?

This project is perfect for beginners and intermediate developers who want to learn about animations, MediaPlayer usage, and responsive layouts. Whether you are creating a decision-making app, a fun game, or just experimenting with Android animations, this Flip Coin Source Code Android will save you time and effort.

Flip Coin Source Code Android – MainActivity.java


public class MainActivity extends AppCompatActivity {

    ImageView coinImage;
    Button btnShowSides, btnFlip;
    int side = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        // Set status bar color to black
        getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.black));

        coinImage = findViewById(R.id.coinImage);
        btnShowSides = findViewById(R.id.btnShowSides);
        btnFlip = findViewById(R.id.btnFlip);
        coinImage.setImageResource(R.drawable.head); //  Set initial image
        btnFlip.setEnabled(true);

        btnShowSides.setOnClickListener(view -> {
            showBothSidesInstant();

        });

        btnFlip.setOnClickListener(view -> flipCoinRealistically());
    }


    private void showBothSidesInstant() {
        if (side == 0) {
            coinImage.setImageResource(R.drawable.tail);
            side = 1;
        } else {
            coinImage.setImageResource(R.drawable.head);
            side = 0;
        }
    }

    private void flipCoinRealistically() {
        boolean isHeads = new Random().nextBoolean();

        // Optional: play coin flip sound
        MediaPlayer mp = MediaPlayer.create(this, R.raw.coin_sound);
        mp.start();
        mp.setOnCompletionListener(MediaPlayer::release);

        // Track which side is showing
        final boolean[] showingHeads = {true};

        // Listener to change image mid-flip
        ValueAnimator flipAnimator = ValueAnimator.ofFloat(0f, 1f);
        flipAnimator.setDuration(5000); // total time
        flipAnimator.setInterpolator(new LinearInterpolator());
        flipAnimator.addUpdateListener(animation -> {
            float progress = (float) animation.getAnimatedValue();
            float totalRotation = progress * 4320f; // 12 full flips (10 fast + 2 slow)
            coinImage.setRotationX(totalRotation);

            // At every 180°, swap image
            if ((int) (totalRotation / 180) % 2 == 0 && !showingHeads[0]) {
                coinImage.setImageResource(R.drawable.head);
                showingHeads[0] = true;
            } else if ((int) (totalRotation / 180) % 2 != 0 && showingHeads[0]) {
                coinImage.setImageResource(R.drawable.tail);
                showingHeads[0] = false;
            }
        });

        // When done, show final result
        flipAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                coinImage.setRotationX(0f);
                coinImage.setImageResource(isHeads ? R.drawable.head : R.drawable.tail);
            }
        });

        flipAnimator.start();
    }
    
}

Flip Coin Source Code Android – XML Layout (activity_main.xml)


<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#121212"
    android:padding="24dp">

    <ImageView
        android:id="@+id/coinImage"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="80dp"
        android:contentDescription="Coin Image"
        android:scaleType="centerInside"
        android:src="@drawable/head"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnShowSides"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:backgroundTint="#2196F3"
        android:text="Show Both Sides"
        android:textColor="#FFFFFF"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/coinImage" />

    <Button
        android:id="@+id/btnFlip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:backgroundTint="#4CAF50"
        android:enabled="false"
        android:text="Flip Coin"
        android:textColor="#FFFFFF"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btnShowSides" />

</androidx.constraintlayout.widget.ConstraintLayout>


How to Implement Flip Coin Source Code Android

  1. Copy the Java code into your MainActivity.java file.
  2. Replace the XML layout with the one provided above.
  3. Add head.png and tail.png images in your drawable folder.
  4. Add a coin_sound.mp3 in your res/raw folder.
  5. Run the app to enjoy realistic coin flipping.

Learn More About Android Development

For more details on animations and UI components, check out the Official Android Developer Documentation. You can also explore my Blog for more examples.

This Flip Coin Source Code Android project is easy to customize and can be integrated into decision-making apps, games, or entertainment tools.

Spin Wheel App Source Code – Java, Jetpack Compose and Flutter

Looking to build a fun and engaging spin-the-wheel app for giveaways, lucky draws, games, or random selections? This post covers a complete mini wheel spinner app built with:

  • ✅ Android Java using Custom Views
  • ✅ Android Jetpack Compose
  • ✅ Cross-platform Flutter (Android, iOS, and Web)

It’s lightweight, beautifully animated, and perfect for learners and developers. Whether you’re creating a decision-maker app or adding gamification to your product — this is your go-to solution.




Download Source Code

🚀 Key Features

  • 🎯 Smooth spinning animation with auto-colored segments
  • 🧠 Callback on spin result (Java/Compose)
  • 📦 3 complete projects: Android Java, Jetpack Compose & Flutter
  • 🔁 Reusable spinner logic for games, quizzes, and lucky draw
  • 📱 Supports Android & Web (via Flutter)

📁 What’s Inside the ZIP

This repository includes a ZIP file with three fully working source code projects:

  • Android Java: Classic implementation using custom `View`
  • Jetpack Compose: Modern declarative UI with Compose
  • Flutter: Cross-platform app using Dart

⚙️ How to Use

  1. Download the ZIP using the button above
  2. Unzip and open the desired project in Android Studio or VS Code
  3. Click Run and enjoy your animated spinner!

🔗 Get more awesome source codes at: Source Codes


Tags: android java spin wheel, flutter lucky draw app, wheel spinner compose, source code android spinner, alsaeeddev, gamification app, random picker app

File Manager Pro – Android App Source Code

Looking for a production-ready Android file manager app source code written in Java? Look no further! File Manager Pro is a clean, fully functional, open-source file explorer built using modern development practices. It’s perfect for developers who want to learn, customize, or extend a real-world Android file manager project.


🚀 Key Features

  • ✅ Browse files and folders with a responsive UI
  • ✅ Toggle show/hide hidden files support
  • ✅ Multi-select with bottom menu actions: Copy, Move, Delete
  • ✅ List View (Recycler View)
  • ✅ File type icons and image previews
  • ✅ Works on Android 7+

🎯 Built Using

  • ✔️ Java (Android Studio)
  • ✔️ MVVM Architecture
  • ✔️ RecyclerView, ViewModel, LiveData
  • ✔️ XML-based custom layouts


Download Source Code

🌐 About the Developer

Al Saeed – Android App developer and blogger at Al Saeed. Passionate about open-source and clean architecture development.

🔗 Explore more free Android app source codes: View Source Codes

🔗 Learn Jetpack Compose UI development at: Official Android Documentation


🔖 Tags:

Android File Manager App, File Explorer Android Java, Open Source Android App, Java Android Source Code, Android File Browser GitHub, File Manager MVVM, Android Studio Project Free Download


Stunning Custom Analog Clock in Android – Java & Kotlin

Want to design a custom analog clock in Android? In this post, you’ll learn how to create one using both Java and Kotlin. Whether you’re building a utility app, dashboard UI, or simply improving your UI skills, this is the perfect project to implement a working analog clock in Android.

🎯 What’s Inside This Project?

You’ll get the complete source code for both languages, canvas-based drawing, second-by-second ticking hands, and a beautiful rounded design. Let’s dive into development!

✅ What You’ll Learn

  • How to draw a custom analog clock using Canvas
  • Java and Kotlin implementations side by side
  • Center dot, circular frame, and dynamic ticking hands
  • Modern, responsive UI layout for all screen sizes

This article provides two versions: AnalogClockView.java and AnalogClockViewKotlin.kt. You can use either based on your language preference.

📄 XML Layout

To use the custom clock in your activity, add this to your activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <www.alsaeeddev.clock.AnalogClockView
        android:id="@+id/analogClock"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true" />

</RelativeLayout>

📦 Java Code: AnalogClockView.java

 
package www.alsaeeddev.clock;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

import java.util.Calendar;

public class AnalogClockView extends View {

    private Paint paint;
    private int width, height, radius;
    private int padding = 0;
    private int numeralSpacing = 30;
    private int handTruncation, hourHandTruncation;
    private final int[] numbers = {1,2,3,4,5,6,7,8,9,10,11,12};
    private Rect textBounds = new Rect();

    public AnalogClockView(Context context) {
        super(context);
        init();
    }

    public AnalogClockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        width = getWidth();
        height = getHeight();
        int min = Math.min(width, height);
        radius = min / 2 - 40;

        canvas.drawColor(Color.parseColor("#928dab")); // Background

        // Draw clock background
        paint.reset();
        paint.setColor(Color.parseColor("#b3f6f8"));
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle((float) width /2, (float) height /2, radius + 30, paint);

        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.parseColor("#9e7419"));
        paint.setStrokeWidth(8f);
        canvas.drawCircle((float) width /2, (float) height /2, radius + 30, paint);

        // Draw center dot
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.BLACK);
        canvas.drawCircle((float) width /2, (float) height /2, 12, paint);

        // Draw clock numbers
        paint.setTextSize(40);
        paint.setColor(Color.BLACK);
        for (int number : numbers) {
            String tmp = String.valueOf(number);
            paint.getTextBounds(tmp, 0, tmp.length(), textBounds);
            double angle = Math.PI / 6 * (number - 3);
            int x = (int)((double) width /2 + Math.cos(angle) * (radius - numeralSpacing) - (double) textBounds.width() /2);
            int y = (int)((double) height /2 + Math.sin(angle) * (radius - numeralSpacing) + (double) textBounds.height() /2);
            canvas.drawText(tmp, x, y, paint);
        }

        // Get time
        Calendar calendar = Calendar.getInstance();
        int hour = calendar.get(Calendar.HOUR);
        int minute = calendar.get(Calendar.MINUTE);
        int second = calendar.get(Calendar.SECOND);

        drawHand(canvas, (hour + minute / 60.0) * 5f, true, false); // Hour hand
        drawHand(canvas, minute, false, false);                     // Minute hand
        drawHand(canvas, second, false, true);                      // Second hand

        postInvalidateDelayed(1000);
        invalidate();
    }



// draw the clock hands
    private void drawHand(Canvas canvas, double loc, boolean isHour, boolean isSecond) {
        double angle = Math.PI * loc / 30 - Math.PI / 2;
        int handRadius = isHour ? radius - 120 : radius - 60;
        if (isSecond) handRadius = radius - 40;

        paint.setColor(isSecond ? Color.parseColor("#e74c3c") : Color.BLACK);
        paint.setStrokeWidth(isSecond ? 4f : isHour ? 8f : 6f);
        canvas.drawLine((float) width /2, (float) height /2,
                (float)((double) width /2 + Math.cos(angle) * handRadius),
                (float)((double) height /2 + Math.sin(angle) * handRadius),
                paint);
    }
}

🧑‍💻 Kotlin Code: AnalogClockViewKotlin.kt

 
package www.alsaeeddev.clock

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import java.util.Calendar
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin

class AnalogClockViewKotlin @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val paint = Paint()
    private val numbers = (1..12).toList()
    private val textBounds = Rect()
    private var widthCenter = 0
    private var heightCenter = 0
    private var radius = 0

    init {
        paint.isAntiAlias = true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        widthCenter = width / 2
        heightCenter = height / 2
        radius = min(widthCenter, heightCenter) - 60

        drawBackground(canvas)
        drawCenterDot(canvas)
        drawNumbers(canvas)
        drawHands(canvas)

        postInvalidateDelayed(1000)
        invalidate()
    }

    private fun drawBackground(canvas: Canvas) {
        paint.reset()
        paint.isAntiAlias = true

        // Clock circle fill
        paint.color = Color.parseColor("#b3f6f8")
        paint.style = Paint.Style.FILL
        canvas.drawCircle(widthCenter.toFloat(), heightCenter.toFloat(), radius + 30f, paint)

        // Clock border
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 8f
        paint.color = Color.parseColor("#9e7419")
        canvas.drawCircle(widthCenter.toFloat(), heightCenter.toFloat(), radius + 30f, paint)
    }

    private fun drawCenterDot(canvas: Canvas) {
        paint.style = Paint.Style.FILL
        paint.color = Color.BLACK
        canvas.drawCircle(widthCenter.toFloat(), heightCenter.toFloat(), 12f, paint)
    }

    private fun drawNumbers(canvas: Canvas) {
        paint.textSize = 40f
        paint.color = Color.BLACK
        for (number in numbers) {
            val numStr = number.toString()
            paint.getTextBounds(numStr, 0, numStr.length, textBounds)
            val angle = Math.PI / 6 * (number - 3)
            val x = (widthCenter + cos(angle) * (radius - 30) - textBounds.width() / 2).toFloat()
            val y = (heightCenter + sin(angle) * (radius - 30) + textBounds.height() / 2).toFloat()
            canvas.drawText(numStr, x, y, paint)
        }
    }

    private fun drawHands(canvas: Canvas) {
        val calendar = Calendar.getInstance()
        val hour = calendar.get(Calendar.HOUR)
        val minute = calendar.get(Calendar.MINUTE)
        val second = calendar.get(Calendar.SECOND)

        drawHand(canvas, (hour + minute / 60f) * 5, isHour = true, isSecond = false)
        drawHand(canvas, minute.toFloat(), isHour = false, isSecond = false)
        drawHand(canvas, second.toFloat(), isHour = false, isSecond = true)
    }

    private fun drawHand(canvas: Canvas, loc: Float, isHour: Boolean, isSecond: Boolean) {
        val angle = Math.PI * loc / 30 - Math.PI / 2
        val handLength = when {
            isHour -> radius - 120
            isSecond -> radius - 30
            else -> radius - 60
        }

        paint.color = if (isSecond) Color.parseColor("#e74c3c") else Color.BLACK
        paint.strokeWidth = when {
            isHour -> 8f
            isSecond -> 4f
            else -> 6f
        }
        paint.style = Paint.Style.STROKE

        canvas.drawLine(
            widthCenter.toFloat(), heightCenter.toFloat(),
            (widthCenter + cos(angle) * handLength).toFloat(),
            (heightCenter + sin(angle) * handLength).toFloat(),
            paint
        )
    }
}



✅ Conclusion

Creating a custom analog clock in Android is a great way to explore Canvas drawing, custom views, and elegant UI development. Whether you choose Java or Kotlin, the outcome is a clean and dynamic clock component ready for your next project.

We used the Paint class extensively to draw shapes and text on the canvas. To learn more about its capabilities, see the official documentation here:
🔗 Android Developers

Don’t forget to share and follow Al Saeed for more hands-on Android tutorials.

Media Player in Android Studio Using Kotlin | Media Player Source Code

If you’re looking to create a simple media player in Android Studio using Kotlin, this article will guide you through the core of your project: the MainActivity. This is where permissions are handled, songs are loaded from device storage, and user interactions are managed.

Overview of Media Player MainActivity

The MainActivity is responsible for:

  • Checking and requesting runtime permissions
  • Fetching music files from the device storage
  • Binding to the MusicService for background playback
  • Handling user interactions like play, pause, next, and previous

Kotlin Code Breakdown for Media Player

The class implements AppCompatActivity and ServiceConnection to establish communication with the background music service. It uses ActivityMainBinding for UI binding and coroutines to prevent blocking the main thread while querying songs from MediaStore.

Permission Handling for Media Player

For Android 13+ (Tiramisu), it requests READ_MEDIA_AUDIO. For earlier versions, READ_EXTERNAL_STORAGE is used.

Loading Songs

The app queries MediaStore.Audio.Media using a coroutine and populates the songsList if the files exist.

Binding Media Player to MusicService

On successful service connection, the list is passed to the service and the UI is updated with the currently playing song title.

Media Player Controls and Playback

The play/pause, next, and previous buttons update the state using MusicService. The UI is automatically refreshed using updateControls().

Other Media Player App Classes

To complete the media player, create the following Kotlin classes and paste their code accordingly:

  • MusicService.kt – Manages playback in the background
  • AudioModel.kt – Data class representing a song
  • MusicAdapter.kt – Binds song data to RecyclerView
  • MusicPlayerActivity.kt – Full screen music controls

1. MainActivity.kt


  class MainActivity : AppCompatActivity(), ServiceConnection {
    private var musicService: MusicService? = null
    private var isServiceBound = false
    private lateinit var binding: ActivityMainBinding
    private val songsList: MutableList = mutableListOf()
    private lateinit var adapter: MusicAdapter
    private var isPlaying = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        window.statusBarColor = Color.WHITE

        if (!checkPermission()) {
            requestPermission()

        } else {
            initializeApp()
        }

    }


    private fun initializeApp() {
        // Launch coroutine on Main thread
        CoroutineScope(Dispatchers.Main).launch {

            // Show progress bar
            binding.progressBar.visibility = View.VISIBLE

            val songs = withContext(Dispatchers.IO) {
                val songs = ArrayList()
                val projection = arrayOf(
                    MediaStore.Audio.Media.TITLE,
                    MediaStore.Audio.Media.DATA,
                    MediaStore.Audio.Media.DURATION
                )
                val selection = MediaStore.Audio.Media.IS_MUSIC + " !=0"

                contentResolver.query(
                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                    projection,
                    selection,
                    null,
                    null
                )?.use { cursor ->
                    while (cursor.moveToNext()) {
                        val audioModel = AudioModel(
                            cursor.getString(1),
                            cursor.getString(0),
                            cursor.getString(2)
                        )
                        if (File(audioModel.path).exists()) {
                            songs.add(audioModel)
                        }
                    }
                }
                songs
            }

            // Hide progress bar
            binding.progressBar.visibility = View.GONE

            songsList.clear()
            songsList.addAll(songs)

            adapter = MusicAdapter(this@MainActivity, songsList) { position ->
                if (isPlaying) {
                    musicService?.musicPlay(songsList)
                    binding.songTitle.text = songsList[MyMediaPlayer.currentIndex].title
                    adapter.notifyDataSetChanged()
                } else {

                    if (songsList.isNotEmpty()) {
                        val intent = Intent(this@MainActivity, MusicPlayerActivity::class.java)
                        intent.putExtra("List", songsList as Serializable)
                        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                        startActivity(intent)
                    } else {
                        Toast.makeText(this@MainActivity, "No songs to play", Toast.LENGTH_SHORT)
                            .show()
                    }

                }
            }

            binding.recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)
            binding.recyclerView.adapter = adapter
            binding.noSongFound.visibility = if (songsList.isEmpty()) View.VISIBLE else View.GONE

            setupControls()
        }
    }


    private fun setupControls() {
        val intent = Intent(this, MusicService::class.java)
        bindService(intent, this, Context.BIND_AUTO_CREATE)

        binding.pausePlay.setOnClickListener {
            musicService?.getMusicPlayer()?.let {
                if (it.isPlaying) {
                    it.pause()
                    binding.pausePlay.setImageResource(R.drawable.play_ic)
                } else {
                    it.start()
                    binding.pausePlay.setImageResource(R.drawable.pause_ic)
                }
            }
        }



        binding.next.setOnClickListener { musicNext() }
        binding.previous.setOnClickListener { musicPrevious() }

        binding.controls.setOnClickListener {
            val intent1 = Intent(this@MainActivity, MusicPlayerActivity::class.java)
            intent1.putExtra("IS", true)
            intent1.putExtra("List", songsList as Serializable)
            startActivity(intent1)
        }
    }


    private fun musicNext() {

        musicService?.next()
        binding.songTitle.text = songsList[MyMediaPlayer.currentIndex].title
        adapter.notifyDataSetChanged()

    }


    private fun musicPrevious() {
        musicService?.previous()
        binding.songTitle.text = songsList[MyMediaPlayer.currentIndex].title
        adapter.notifyDataSetChanged()

    }


    private fun checkPermission(): Boolean {

        val result = ContextCompat.checkSelfPermission(
            this@MainActivity,
            Manifest.permission.READ_EXTERNAL_STORAGE
        )

        if (result == PackageManager.PERMISSION_GRANTED) {
            return true

        } else {
            return false
        }
    }


    private fun requestPermission() {
        val permission = Manifest.permission.READ_EXTERNAL_STORAGE

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // For Android 13+ (Tiramisu), use READ_MEDIA_AUDIO instead
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.READ_MEDIA_AUDIO),
                92
            )
        } else {

            ActivityCompat.requestPermissions(
                this,
                arrayOf(permission),
                92
            )

        }
    }


    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (requestCode == 92 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
            initializeApp() // Or load your music files
        } else {
            Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show()
            requestPermission()
        }
    }


    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        val binder = service as MusicService.MyBinder
        musicService = binder.currentService()
        isServiceBound = true
        musicService!!.setSongLIst(songsList)

        // Update the controls based on the current state of the media player
        updateControls()
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        musicService = null
        isServiceBound = false
    }


    private fun updateControls() {
        musicService?.getMusicPlayer()?.let { mediaPlayer ->
            if (mediaPlayer.isPlaying) {
                binding.controls.visibility = View.VISIBLE
                binding.songTitle.text = songsList[MyMediaPlayer.currentIndex].title
                binding.pausePlay.setImageResource(R.drawable.pause_ic)
                isPlaying = true
            } else {
                isPlaying = false
                binding.controls.visibility = View.GONE
            }

            mediaPlayer.setOnCompletionListener {
                musicNext() // Move to the next song when the current song is completed.
            }
        }

    }

    @SuppressLint("NotifyDataSetChanged")
    override fun onResume() {
        super.onResume()
        if (::adapter.isInitialized) {
            adapter.notifyDataSetChanged()
            updateControls()
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        if (isServiceBound) {
            unbindService(this)
            isServiceBound = false
        }
    }
}

2. MusicService.kt


class MusicService : Service() {

    private var myBinder = MyBinder()
    private var mediaPlayer: MediaPlayer? = MyMediaPlayer.getInstance()
    private var currentSongPath: String? = null
    private var isAlreadyPlaying = false
    private var songsList: List? = null


    fun setSongLIst(list: List) {
        songsList = list

    }

    public fun getMusicPlayer(): MediaPlayer? {
        return mediaPlayer
    }


    override fun onBind(intent: Intent?): IBinder? {
        return myBinder

    }


    fun musicPlay(songList: List) {
        currentSongPath = songList[MyMediaPlayer.currentIndex].path
        mediaPlayer!!.reset()
        mediaPlayer!!.setDataSource(currentSongPath)
        mediaPlayer!!.prepare()
        mediaPlayer!!.start()

    }


    fun pause() {
        mediaPlayer?.pause()
        Log.d("MyService", "Pausing music")
    }

    fun stop() {
        mediaPlayer?.stop()
        Log.d("MyService", "Stopping music")
    }


    fun next() {
        if (MyMediaPlayer.currentIndex == songsList!!.size - 1) {
            return
        }

        MyMediaPlayer.currentIndex += 1
        mediaPlayer!!.reset()
        if (songsList != null) {
            musicPlay(songsList!!)
        }
        isAlreadyPlaying = false


    }


    fun previous() {
        if (MyMediaPlayer.currentIndex == 0) {
            return
        }
        MyMediaPlayer.currentIndex -= 1
        mediaPlayer!!.reset()
        if (songsList != null) {
            musicPlay(songsList!!)
        }
        isAlreadyPlaying = false

    }


    fun getPlayingStatus(): Boolean {
        return isAlreadyPlaying
    }


    fun isPlaying(): Boolean {
        return mediaPlayer?.isPlaying ?: false
    }

    fun getCurrentPosition(): Int {
        return mediaPlayer?.currentPosition ?: 0
    }

    fun seekTo(position: Int) {
        mediaPlayer?.seekTo(position)
    }


    override fun onDestroy() {
        super.onDestroy()
    }


    inner class MyBinder : Binder() {
        fun currentService(): MusicService {
            return this@MusicService
        }
    }

}

3. MusicAdapter.kt


class MusicAdapter(
    private val context: Context,
    private val songList: List,
    private val onClick: (Int) -> Unit
) : RecyclerView.Adapter() {


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
        val view = SongItemBinding.inflate(LayoutInflater.from(context), parent, false)
        return ItemHolder(view)
    }

    override fun getItemCount(): Int {
        return songList.size
    }


    public fun getSongLIst(): List {
        return songList
    }

    override fun onBindViewHolder(holder: ItemHolder, position: Int) {
        val audioSong = songList[position]
        holder.bind(audioSong, position)
    }


    inner class ItemHolder(private val binding: SongItemBinding) :
        RecyclerView.ViewHolder(binding.root) {


        fun bind(audioSong: AudioModel, position: Int) {

            if (MyMediaPlayer.currentIndex == position) {
                binding.tvSongTitle.setTextColor(Color.RED)
            } else {
                binding.tvSongTitle.setTextColor(Color.BLACK)
            }
            binding.tvSongTitle.text = audioSong.title
            //   binding.ivMusic.setImageResource(R.drawable.music_ic)

            binding.root.setOnClickListener {
                MyMediaPlayer.getInstance().reset()
                MyMediaPlayer.currentIndex = adapterPosition
                onClick(adapterPosition)


            }

        }

    }
}


Download Source Code

Conclusion

This Android media player in Kotlin is lightweight, efficient, and follows best practices for modern development using ViewBinding, coroutines, and service components. Add more features like notifications, shuffle, or repeat to further enhance your app.

You can also Check Out the official Android Media Player Documentation.
Want to learn more? Visit our tutorials at Blog .

Swipe and Tap Card Animation in Jetpack Compose

Creating an interactive card-based interface in Jetpack Compose is easier than ever. Today, we will explore how to implement a smooth Card Animation in Jetpack using swipe or tap gestures on cards with the CardSwipeOrTap composable. This approach is perfect for modern UI designs where users interact with content via gestures.

Main Activity Setup

We start by setting up the MainActivity that uses a Scaffold layout inside the RecyclerViewTheme. This ensures that our UI fills the entire screen while providing padding to our card interactions.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RecyclerViewTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    CardSwipeOrTap(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

Building the Card Swipe or Tap Composable

The CardSwipeOrTap composable manages the card stack. It uses a Box to overlay cards on top of each other and applies a slight rotation and vertical offset for a realistic stacked effect.

Each card is individually rendered using the CardItem composable. We use a simple CoroutineScope to delay actions and allow for smooth animations between swipe or tap events.

@Composable
fun CardSwipeOrTap(modifier: Modifier = Modifier) {
    val scope = rememberCoroutineScope()
    var cards by remember { mutableStateOf(listOf(0, 1, 2)) }
    var isAnimating by remember { mutableStateOf(false) }

    val cardColors = listOf(
        Color(0xFFFF9800), // Orange
        Color(0xFFF44336), // Red
        Color(0xFF00E676)  // Light Green
    )

    Box(
        modifier = modifier.fillMaxSize().padding(32.dp),
        contentAlignment = Alignment.Center
    ) {
        cards.reversed().forEachIndexed { index, cardIndex ->
            CardItem(
                index = cardIndex,
                color = cardColors[cardIndex % cardColors.size],
                rotation = if (index == 0) 0f else if (index == 1) -10f else 10f,
                offsetY = (index * 20).dp,
                isTop = index == cards.size - 1,
                isAnimating = isAnimating,
                onSwiped = {
                    if (!isAnimating) {
                        isAnimating = true
                        scope.launch {
                            kotlinx.coroutines.delay(300)
                            val updated = cards.toMutableList()
                            val removed = updated.removeAt(0)
                            updated.add(removed)
                            cards = updated
                            isAnimating = false
                        }
                    }
                }
            )
        }
    }
}

Animating Card Movement with CardItem

The CardItem composable handles the animation when a user swipes or taps a card. It uses animateDpAsState and animateFloatAsState to animate the card’s offset and opacity, giving the appearance of a disappearing card when an interaction occurs.

@Composable
fun CardItem(
    index: Int,
    color: Color,
    rotation: Float,
    offsetY: Dp,
    isTop: Boolean,
    isAnimating: Boolean,
    onSwiped: () -> Unit
) {
    val animatedOffsetX by animateDpAsState(
        targetValue = if (isAnimating && isTop) 500.dp else 0.dp,
        animationSpec = if (isAnimating && isTop) tween(durationMillis = 500, easing = FastOutLinearInEasing) else snap(),
        label = "OffsetXAnimation"
    )

    val animatedAlpha by animateFloatAsState(
        targetValue = if (isAnimating && isTop) 0f else 1f,
        animationSpec = if (isAnimating && isTop) tween(durationMillis = 3000) else snap(),
        label = "AlphaAnimation"
    )

    Card(
        modifier = Modifier
            .fillMaxWidth(0.7f)
            .aspectRatio(1f)
            .offset(x = animatedOffsetX, y = offsetY)
            .graphicsLayer {
                rotationZ = rotation
                alpha = animatedAlpha
            }
            .pointerInput(isTop) {
                if (isTop && !isAnimating) {
                    detectDragGestures(
                        onDragEnd = { onSwiped() },
                        onDrag = { change, _ -> change.consume() }
                    )
                }
            }
            .pointerInput(isTop) {
                if (isTop && !isAnimating) {
                    detectTapGestures(
                        onTap = { onSwiped() }
                    )
                }
            },
        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
        shape = RoundedCornerShape(20.dp),
        colors = CardDefaults.cardColors(
            containerColor = color,
            contentColor = Color.White
        )
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "Card #$index",
                style = MaterialTheme.typography.titleLarge,
                color = Color.White
            )
        }
    }
}


Final Thoughts for Card Animation in Jetpack

With just a few lines of Kotlin code, you can create a stunning card swipe and tap animation in Jetpack Compose. This approach is not only visually appealing but also highly interactive, offering a smooth user experience without the complexity of a traditional RecyclerView setup. Try it out in your next Android app project!

You can also read the official Jetpack Compose animation documentation.

News API Integration In Android App Java

In today’s mobile-first world, delivering real-time content quickly and smoothly is crucial for user engagement. In this tutorial on News API Integration In Android App Java, you’ll learn how to fetch and display news articles in an Android app using Retrofit, RecyclerView, and ProgressBar, while implementing an infinite scroll feature — all written in Java.

By the end of this guide, you will have a working news app that fetches the latest articles based on a search query and loads more content as the user scrolls down.


🚀 Key Components We’ll Use

  • RecyclerView — To display news articles in a scrollable list.
  • ProgressBar — To show loading status while fetching data.
  • Retrofit — For efficient and safe API calls.
  • Infinite Scrolling — Load more news automatically when the user reaches the end.
  • Handler — To simulate slight loading delays during pagination.

🔧 Step-by-Step Implementation

1. Setting Up RecyclerView

First, we initialize our RecyclerView and set up a custom NewsAdapter to bind news articles dynamically.


private void setupRecyclerView() {
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    adapter = new NewsAdapter(this, listArticles, article -> {
        NewsDialogFragment dialogFragment = NewsDialogFragment.newInstance(article);
        dialogFragment.show(getSupportFragmentManager(), "NewsDialogFragment");
    });
    recyclerView.setAdapter(adapter);
}

Here, each news item is clickable and opens a DialogFragment to show article details.


2. Fetching News Using Retrofit

We use Retrofit to fetch news data asynchronously from an API. Here’s the main method:

🔔 Note: Make sure to replace API_KEY with your actual News API key to successfully fetch the news data.
You can get your API key from NewsAPI.org.

private void fetchNews(String query, int limit, int offset) {
    NewsApiService apiService = ApiClient.getRetrofitInstance().create(NewsApiService.class);

    Call<ArticlesResponse> call = apiService.getArticles(query, limit, offset, API_KEY);
    call.enqueue(new Callback<ArticlesResponse>() {
        @Override
        public void onResponse(@NonNull Call<ArticlesResponse> call, @NonNull Response<ArticlesResponse> response) {
            isLoading = false;
            adapter.removeLoadingFooter();
            if (response.isSuccessful() && response.body() != null) {
                List<ArticlesResponse.Article> newArticles = response.body().getArticles();
                if (newArticles != null && !newArticles.isEmpty()) {
                    int oldSize = listArticles.size();
                    listArticles.addAll(newArticles);
                    adapter.notifyItemRangeInserted(oldSize, newArticles.size());
                }
            }
            webProgress.setVisibility(View.GONE);
        }

        @Override
        public void onFailure(@NonNull Call<ArticlesResponse> call, @NonNull Throwable t) {
            webProgress.setVisibility(View.GONE);
            Toast.makeText(MainActivity.this, "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
        }
    });
}

3. Adding Infinite Scroll (Load More News)

To enable infinite scrolling, we attach a scroll listener to the RecyclerView. When the user scrolls near the bottom, we load more news automatically.


recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
        int totalItemCount = Objects.requireNonNull(layoutManager).getItemCount();
        int lastVisibleItem = layoutManager.findLastVisibleItemPosition();

        if (!isLoading && lastVisibleItem == totalItemCount - 10) {
            isLoading = true;
            adapter.addLoadingFooter();

            new Handler().postDelayed(() -> {
                currentOffset += PAGE_SIZE;
                fetchNews(QUERY, PAGE_SIZE, currentOffset);
            }, 1000); // simulate network delay
        }
    }
});

✅ This ensures a seamless user experience without manual refresh!


📱 Full MainActivity Java Code

For your reference, here’s the complete MainActivity.java:


public class MainActivity extends AppCompatActivity {
    private ProgressBar webProgress;
    private RecyclerView recyclerView;
    private NewsAdapter adapter;
    private List listArticles = new ArrayList<>();
    private boolean isLoading = false;
    private int currentOffset = 0;
    private final int PAGE_SIZE = 20;
    private final String QUERY = "bitcoin"; // or anything you use

    private static final String API_KEY = "API_KEY";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webProgress = findViewById(R.id.web_progress);
        webProgress.setVisibility(VISIBLE);

        recyclerView = findViewById(R.id.recycler_view);
        setupRecyclerView();

        fetchNews(QUERY, PAGE_SIZE, currentOffset);
    }


    private void fetchNews(String query, int limit, int offset) {
        NewsApiService apiService = ApiClient.getRetrofitInstance().create(NewsApiService.class);

        Call call = apiService.getArticles(query, limit, offset, API_KEY);
        call.enqueue(new Callback() {
            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) {

                isLoading = false;
                adapter.removeLoadingFooter();

                if (response.isSuccessful() && response.body() != null) {
                    List newArticles = response.body().getArticles();

                    if (newArticles != null && !newArticles.isEmpty()) {
                        int oldSize = listArticles.size();
                        listArticles.addAll(newArticles);

                        adapter.notifyItemRangeInserted(oldSize, newArticles.size());
                    }

                    NewsLog.INSTANCE.d("onResponse", "News Loaded");

                }else {

                    NewsLog.INSTANCE.d("onResponse", "Failed to load news");

                }

                webProgress.setVisibility(View.GONE);

            }

            @Override
            public void onFailure(@NonNull Call call, @NonNull Throwable t) {
                webProgress.setVisibility(View.GONE);
                Toast.makeText(MainActivity.this, "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
                NewsLog.INSTANCE.d("onFailure", t.getMessage());
            }
        });
    }


    private void setupRecyclerView(){

        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        adapter = new NewsAdapter(this, listArticles, article -> {
            // listen news click here, to open the news
            NewsDialogFragment dialogFragment = NewsDialogFragment.newInstance(article);
            dialogFragment.show(getSupportFragmentManager(), "NewsDialogFragment");

        });
        recyclerView.setAdapter(adapter);

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                int totalItemCount = Objects.requireNonNull(layoutManager).getItemCount();
                int lastVisibleItem = layoutManager.findLastVisibleItemPosition();

                if (!isLoading && lastVisibleItem == totalItemCount - 10) {
                    isLoading = true;
                    adapter.addLoadingFooter();

                    new Handler().postDelayed(() -> {
                        currentOffset += PAGE_SIZE;
                        fetchNews(QUERY, PAGE_SIZE, currentOffset);
                    }, 1000); // simulate delay

                }
            }
        });

    }

}

📱 Full NewsAdapter Java Code

For your reference, here’s the complete NewsAdapter.java:


public class NewsAdapter extends RecyclerView.Adapter {

    private static final int VIEW_TYPE_ITEM = 0;
    private static final int VIEW_TYPE_LOADING = 1;
    private List articles;
    private Context context;
    private boolean isLoadingAdded = false;
    private onNewsClickListener listener;

    public NewsAdapter(Context context, List articles, onNewsClickListener listener) {
        this.context = context;
        this.articles = articles;
        this.listener = listener;
    }

    @Override
    public int getItemViewType(int position) {
        return (position == articles.size() && isLoadingAdded) ? VIEW_TYPE_LOADING : VIEW_TYPE_ITEM;
    }

    @Override
    public int getItemCount() {
        return articles.size() + (isLoadingAdded ? 1 : 0);
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (viewType == VIEW_TYPE_ITEM) {
            View view = LayoutInflater.from(context).inflate(R.layout.item_article, parent, false);
            return new ArticleViewHolder(view);
        } else {
            View view = LayoutInflater.from(context).inflate(R.layout.item_loading, parent, false);
            return new LoadingViewHolder(view);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (getItemViewType(position) == VIEW_TYPE_ITEM) {
            ArticleViewHolder viewHolder = (ArticleViewHolder) holder;
            ArticlesResponse.Article article = articles.get(position);
            viewHolder.title.setText(article.getTitle());
            viewHolder.description.setText(article.getDescription());
            // Add image loading if needed

            Glide.with(context)
                    .load(article.getFeedImage())  // Image URL or resource
                    .apply(new RequestOptions().placeholder(R.drawable.place_holder)
                          //  .error(R.drawable.error_image)
                    )
                    .into(((ArticleViewHolder) holder).imageView);  // The ImageView where the image will be loaded

            holder.itemView.setOnClickListener(v -> {
                int position1 = holder.getAdapterPosition();
                if(listener != null && position1 != RecyclerView.NO_POSITION){
                    listener.onClick(article);
                }
            });

        }
    }

    public void addLoadingFooter() {
        isLoadingAdded = true;
        notifyItemInserted(articles.size());
    }

    public void removeLoadingFooter() {
        if (isLoadingAdded) {
            isLoadingAdded = false;
            notifyItemRemoved(articles.size());
        }
    }

    static class ArticleViewHolder extends RecyclerView.ViewHolder {
        TextView title, description;
        ImageView imageView;

        public ArticleViewHolder(View itemView) {
            super(itemView);
            title = itemView.findViewById(R.id.article_title);
            description = itemView.findViewById(R.id.article_description);
            imageView = itemView.findViewById(R.id.iv_news);
        }
    }

    static class LoadingViewHolder extends RecyclerView.ViewHolder {
        public LoadingViewHolder(View itemView) {
            super(itemView);
        }
    }
}



Download Source Code

🎯 Final Thoughts

By combining Retrofit, RecyclerView, and ProgressBar, we’ve built a highly efficient, real-time news app that supports infinite scrolling in Android using Java.

This pattern is extremely useful for creating news apps, social media feeds, blogs, and any app that requires real-time content loading.

Pro Tip: Always handle error cases smartly (like no internet, API limit errors) to enhance user experience even further.

Barcode Scanner Invoice Generator App in Android Java

Introduction
In this tutorial, we will explore how to develop a Barcode Scanner Invoice Generator App in Android Java that allows users to scan barcodes, add products to a list, and generate a PDF invoice. This app is ideal for small businesses or retail stores that need a quick and efficient way to manage transactions. By the end of this guide, you’ll be able to build a fully functional barcode scanner invoice generator app using modern Android development tools.


Prerequisites

Before getting started, ensure you have the following:

  • Android Studio installed
  • Basic knowledge of Java/Kotlin
  • Dependencies for barcode scanning and PDF generation

Step 1: Adding Dependencies

To enable barcode scanning and PDF generation, add the following dependencies to your build.gradle file:

implementation 'com.google.mlkit:barcode-scanning:17.2.0'
implementation 'com.itextpdf:itext7-core:7.1.15'

Step 2: Implement Barcode Scanner

To scan barcodes, use the Google ML Kit. Integrate the camera preview and process the scanned barcode:

BarcodeScannerOptions options =
    new BarcodeScannerOptions.Builder()
        .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
        .build();

BarcodeScanner scanner = BarcodeScanning.getClient(options);

Once a barcode is scanned, retrieve the product details and add them to a list.


Step 3: Displaying Items in RecyclerView

Create a RecyclerView adapter to show scanned products dynamically:

public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ViewHolder> {
    private List<Product> productList;
    
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Product product = productList.get(position);
        holder.name.setText(product.getName());
        holder.price.setText(String.valueOf(product.getPrice()));
    }
}

Step 4: Generating a PDF Invoice

Use iText7 to create a PDF file containing the product details and save it to device storage:

PdfWriter writer = new PdfWriter(filePath);
PdfDocument pdfDoc = new PdfDocument(writer);
Document document = new Document(pdfDoc);
document.add(new Paragraph("Invoice"));

for (Product product : productList) {
    document.add(new Paragraph(product.getName() + " - " + product.getPrice()));
}
document.close();

Complete Code Here


1. MainActivity.java


public class MainActivity extends AppCompatActivity {

    Button btnCreatePDf;
    Bitmap bitmap, scaledBitmap;
    EditText etCustomerName;
    private final String[] informationArray = new String[]{"Name", "Company Name", "Address", "Phone", "Email"};

    private int srNumber;
    String ItemName, price, Quantity, priceTotal;


    private RecyclerView recyclerView;

    private MyAdapter adapter;
    private final List listItem = new ArrayList<>();
    private final List fullCodeList = new ArrayList<>();


    private static final int CAMERA_PERMISSION_REQUEST_CODE = 200;


    private DecoratedBarcodeView barcodeView;

    private final Map<String, String> map = new HashMap<>();

    int startingIndex = 5;
    String copyDecodeText;
    int yAxisForValue = 280;
    private List totalPricePerItemList = new ArrayList<>();
    private String customerName;

    String pattern = "^[-+]?[\\d&]*\\.?\\d+$";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        etCustomerName = findViewById(R.id.etCustomerName);
        btnCreatePDf = findViewById(R.id.btnNext);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo_2);

        recyclerView = findViewById(R.id.myRecyclerView);
        barcodeView = findViewById(R.id.zxingBarcodeScanner);
        barcodeView.setStatusText("");

        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        adapter = new MyAdapter(this, listItem, (position, quantity) -> {
            listItem.get(position).setItemQuantity(quantity);
        });
        recyclerView.setAdapter(adapter);

        map.put("1244", "Cold Drink");
        map.put("0054", "Burger");
        map.put("0187", "Sandwich");
        map.put("7176", "Pizza");

        startScanning();
        //scaledBitmap = Bitmap.createScaledBitmap(bitmap,100,100,false);
        ActivityCompat.requestPermissions(this, new String[]{
                Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, PackageManager.PERMISSION_GRANTED);
        {

            //  createPdf();
            startScanning();
            createPdf();
        }

    }


    private void startScanning() {
        barcodeView.decodeContinuous(new BarcodeCallback() {
            @Override
            public void barcodeResult(BarcodeResult result) {
                // Handle the decoded result
                String decodedText = result.getText();
                // addItemToRecyclerView(decodedText);

                if (decodedText.length() == 14) {
                    // Check if barcode has been scanned before
                    if (!fullCodeList.contains(decodedText)) {

                        fullCodeList.add(decodedText);

                        copyDecodeText = decodedText;

                        // Extract the last 4 digits using substring
                        double lastDigitPrice = Double.parseDouble(decodedText.substring(decodedText.length() - 4));

                        String productId = copyDecodeText.substring(startingIndex, startingIndex + 4);

                        if (String.valueOf(lastDigitPrice).matches(pattern) && productId.matches(pattern)) {
                            ItemModel itemModel = new ItemModel(lastDigitPrice, map.get(productId), 1);
                            addItemToRecyclerView(itemModel);
                        }
                    }
                }

            }


            @Override
            public void possibleResultPoints(List resultPoints) {
                // You can use this callback method to show visual cues on the viewfinder.
            }


        });
    }


    private void addItemToRecyclerView(ItemModel item) {
        //  listItem.add(item);
        //   adapter.addItem(item);
        listItem.add(item);
        adapter.notifyItemInserted(listItem.size() - 1);
    }


    private void createPdf() {
        btnCreatePDf.setOnClickListener(v -> {

            if(etCustomerName.getText().length() != 0 && adapter.getItemCount() != 0){

                customerName = etCustomerName.getText().toString();

                PdfDocument pdfDocument = new PdfDocument();
                Paint paint = new Paint();
                //    paint.setLetterSpacing(0.01f);

                PdfDocument.PageInfo pageInfo1 = new PdfDocument.PageInfo.Builder(595, 842, 1).create();
                PdfDocument.Page myPage1 = pdfDocument.startPage(pageInfo1);
                Canvas canvas = myPage1.getCanvas();


                // insert the picture
                int endPosition = pageInfo1.getPageWidth() - 100;
                scaledBitmap = Bitmap.createScaledBitmap(bitmap, 100, 100, false);

                canvas.drawBitmap(scaledBitmap, endPosition, 0, paint);

                // draw a text as like invoice
                paint.setTextAlign(Paint.Align.CENTER);
                paint.setTextSize(32.0f);
                paint.setColor(ContextCompat.getColor(this, R.color.green));
                paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
                canvas.drawText("Invoice", (float) pageInfo1.getPageWidth() / 2, 100, paint);


                // customer name headiing
                paint.setTextAlign(Paint.Align.LEFT);
                paint.setTextSize(10.0f);
                paint.setColor(Color.BLACK);
                paint.setTypeface(Typeface.defaultFromStyle(Typeface.ITALIC));
                canvas.drawText("Customer Name: "+customerName, 80, 170, paint);


          /*  int endX = pageInfo1.getPageWidth() - 5;

            float textWidth = paint.measureText(getCurrentDate());
            float x = pageInfo1.getPageWidth() - textWidth;*/

                // date and time heading
                paint.setTextAlign(Paint.Align.RIGHT);
                canvas.drawText("Date: " + getCurrentDate(), 590, 170, paint);
                canvas.drawText("Time: " + getCurrentTime(), 590, 180, paint);

                //draw rectangle stroke etc
                paint.setTextAlign(Paint.Align.RIGHT);
                paint.setStyle(Paint.Style.STROKE);
                paint.setStrokeWidth(1);
                //draw rectangle
                canvas.drawRect(10, 220, pageInfo1.getPageWidth() - 10, 250, paint);
                // draw four line vertical, to make portions in rectangle
                canvas.drawLine(100, 220, 100, 250, paint);
                canvas.drawLine(300, 220, 300, 250, paint);
                canvas.drawLine(430, 220, 430, 250, paint);
                canvas.drawLine(500, 220, 500, 250, paint);


                paint.setStrokeWidth(0);
                paint.setStyle(Paint.Style.FILL);
                paint.setTextSize(12.0f);
                paint.setTextAlign(Paint.Align.LEFT);
                paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));

                // draw texts invoice data headings
                canvas.drawText("Sr No.", 15, 240, paint);
                canvas.drawText("Item Name", 105, 240, paint);
                canvas.drawText("Price", 305, 240, paint);
                canvas.drawText("Qty", 435, 240, paint);
                canvas.drawText("Total", 505, 240, paint);

                srNumber = 0;
                for (int sNo = 0; sNo < listItem.size(); sNo++) { srNumber++; // draw values as like qty price item name etc canvas.drawText(srNumber + "", 17, yAxisForValue, paint); canvas.drawText(listItem.get(sNo).getItemName(), 106, yAxisForValue, paint); canvas.drawText(String.valueOf(listItem.get(sNo).getItemPrice()), 306, yAxisForValue, paint); canvas.drawText(String.valueOf(listItem.get(sNo).getItemQuantity()), 435, yAxisForValue, paint); canvas.drawText(calculateTotalPerItem(listItem.get(sNo).getItemPrice(), listItem.get(sNo).getItemQuantity()), 508, yAxisForValue, paint); yAxisForValue += 20; } yAxisForValue += 20; // draw line below of value and above of total canvas.drawLine(300, yAxisForValue, pageInfo1.getPageWidth() - 10, yAxisForValue, paint); yAxisForValue += 20; // draw texts as like total canvas.drawText("Sub Total", 306, yAxisForValue, paint); canvas.drawText(String.valueOf(calculateSubTotal()), 508, yAxisForValue, paint); canvas.drawText(":", 435, yAxisForValue, paint); yAxisForValue += 20; canvas.drawText("Tax (5%)", 306, yAxisForValue, paint); canvas.drawText(String.valueOf(calculateTax()), 508, yAxisForValue, paint); canvas.drawText(":", 435, yAxisForValue, paint); Paint paint2 = new Paint(); paint2.setStrokeWidth(1); // Set the stroke width to 5 (adjust as needed) paint2.setColor(ContextCompat.getColor(this, R.color.black)); // Set the stroke color to black paint2.setStyle(Paint.Style.STROKE); // Set the style to fill and stroke yAxisForValue += 35; // this value uses for top int bottom = yAxisForValue + 45; paint.setColor(ContextCompat.getColor(this, R.color.green)); canvas.drawRect(300, yAxisForValue, pageInfo1.getPageWidth() - 10, bottom, paint); canvas.drawRect(300, yAxisForValue, pageInfo1.getPageWidth() - 10, bottom, paint2); int yValueForGrandTotal = bottom - yAxisForValue; int calc = yValueForGrandTotal / 3; int finalCalc = calc * 2; yAxisForValue += finalCalc; paint.setColor(Color.WHITE); paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); paint.setTextSize(16.0f); canvas.drawText("Total", 320, yAxisForValue, paint); canvas.drawText(String.valueOf(calculateSubTotal() + calculateTax()), 515, yAxisForValue, paint); paint.setColor(ContextCompat.getColor(this, R.color.black)); paint.setTextAlign(Paint.Align.LEFT); paint.setTextSize(8.0f); canvas.drawText("Invoice Number: " + System.currentTimeMillis(), 17, pageInfo1.getPageHeight() - 20, paint); paint.setTextAlign(Paint.Align.RIGHT); canvas.drawText("Generate by Al Saeed", pageInfo1.getPageWidth() - 17, pageInfo1.getPageHeight() - 20, paint); pdfDocument.finishPage(myPage1); String folderName = "AlsaeedFolder"; // Define your custom folder name File customFolder = new File(Environment.getExternalStorageDirectory(), folderName); if (!customFolder.exists()) { customFolder.mkdirs(); // Create the folder if it doesn't exist } File file = new File(customFolder, "myPDF.pdf"); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        pdfDocument.writeTo(Files.newOutputStream(file.toPath()));
                    } else {
                        pdfDocument.writeTo(new FileOutputStream(file));
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

                Toast.makeText(this, "File save in " + file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
                // pdfDocument.close();


                // Open the PDF file using a PDF viewer app
                Intent intent = new Intent(Intent.ACTION_VIEW);
                Uri pdfUri = FileProvider.getUriForFile(this, "alsaeeddev.com.fileProvider", file);
                intent.setDataAndType(pdfUri, "application/pdf");
                intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Grant read permissions

                try {
                    startActivity(intent); // Launch the PDF viewer activity
                } catch (ActivityNotFoundException e) {
                    // Handle the case where no PDF viewer app is available
                    Toast.makeText(getApplicationContext(), "No PDF viewer app found", Toast.LENGTH_SHORT).show();
                }

                Toast.makeText(this, "File saved in " + file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
                pdfDocument.close();

            }else {
                etCustomerName.setError("Enter Name");
            }




        });
    }


    private String getCurrentDate() {
        // Get the current date
        Calendar calendar = Calendar.getInstance();
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH) + 1; // Month is zero-based, so add 1
        int day = calendar.get(Calendar.DAY_OF_MONTH);

// Construct the date string
        return year + "-" + month + "-" + day;

    }


    private String getCurrentTime() {
        // Get the current time
        Calendar calendar = Calendar.getInstance();
        int hour = calendar.get(Calendar.HOUR_OF_DAY); // 24-hour format
        int minute = calendar.get(Calendar.MINUTE);
        int second = calendar.get(Calendar.SECOND);

// Construct the time string
        return hour + ":" + minute + ":" + second;
    }


    private String calculateTotalPerItem(double price, int quantity) {
        double total = price * quantity;
        totalPricePerItemList.add(total);
        return String.valueOf(total);
    }


    private double calculateSubTotal() {
        double subTotal = 0;
        for (int i = 0; i < totalPricePerItemList.size(); i++) {
            subTotal += totalPricePerItemList.get(i);
        }
        return subTotal;
    }


    private double calculateTax() {

        return calculateSubTotal() * 0.5;
    }

    @Override
    protected void onResume() {
        super.onResume();
        barcodeView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        yAxisForValue = 280;
        totalPricePerItemList.clear();
        barcodeView.pause();

    }

    @Override
    protected void onStop() {
        yAxisForValue = 280;
        totalPricePerItemList.clear();
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        listItem.clear();
        fullCodeList.clear();
        super.onDestroy();
    }
}
</pre

2. MyAdapter.java

 


public class MyAdapter extends RecyclerView.Adapter {
    private final List itemList;

   private final Context context;
  private final QuantityChangeListener listener;

    public MyAdapter(Context context, List data, QuantityChangeListener listener) {
        this.itemList = data;
       this.listener = listener;
       this.context = context;
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.rv_item, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        holder.bindData(position);

      //  holder.itemView.setOnClickListener(v -> Toast.makeText(context, itemList.get(position).getItemQuantity(), Toast.LENGTH_SHORT).show());
    }

    @Override
    public int getItemCount() {
        return itemList.size();
    }

    public void addItem(String item) {
      /*  mData.add(item);
        notifyItemInserted(mData.size() - 1);*/

     /*   mData.add(0, item); // Insert item at index 0
        notifyItemInserted(0);*/
    }

    public  class MyViewHolder extends RecyclerView.ViewHolder {
        TextView itemName, itemPrice;
        EditText editText;


        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            itemName = itemView.findViewById(R.id.tvItemName);
            itemPrice = itemView.findViewById(R.id.tvItemPrice);
            editText = itemView.findViewById(R.id.etQuantity);


        }

        public void bindData(int position) {
            ItemModel item = itemList.get(position);
            itemName.setText(item.getItemName());
           itemPrice.setText(String.valueOf(item.getItemPrice()));
           editText.setText(String.valueOf(item.getItemQuantity()));

           editText.addTextChangedListener(new TextWatcher() {
               @Override
               public void beforeTextChanged(CharSequence s, int start, int count, int after) {

               }

               @Override
               public void onTextChanged(CharSequence s, int start, int before, int count) {
                   if(!TextUtils.isEmpty(s.toString())){
                       int quantity = Integer.parseInt(s.toString());
                       if(quantity != 0) {
                           item.setItemQuantity(quantity);
                           //   if(listener != null && position != RecyclerView.NO_POSITION) {
                           listener.onQuantityChanged(position, quantity);
                       }
                     //  }
                   }
               }

               @Override
               public void afterTextChanged(Editable s) {

               }
           });





        }

   /*     public interface QuantityChangeListener {
            void onQuantityChanged(int position, int quantity);
        }

        public void setListener(QuantityChangeListener listener){
            listener = listener;
        }*/


    }

}
</pre

3. QuantityChangeListener.java

 


public interface QuantityChangeListener {
    void onQuantityChanged(int position, int quantity);
}
</pre

4. ItemModel.java

 


public class ItemModel {
    private final double itemPrice;
    private final String itemName;
    private int itemQuantity;

    public int getItemQuantity() {
        return itemQuantity;
    }

    public void setItemQuantity(int quantity){
        this.itemQuantity = quantity;
    }



    public double getItemPrice() {
        return itemPrice;
    }

    public String getItemName() {
        return itemName;
    }


    public ItemModel(double itemPrice, String itemName, int itemQuantity) {
        this.itemPrice = itemPrice;
        this.itemName = itemName;
        this.itemQuantity = itemQuantity;
    }




}
</pre

5. activity_main.xml

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:elevation="10dp"
    android:id="@+id/main"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/myRecyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="1dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="1dp"
        app:layout_constraintBottom_toTopOf="@id/btnNext"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintTop_toBottomOf="@+id/zxingBarcodeScanner" />

    <com.journeyapps.barcodescanner.DecoratedBarcodeView
        android:id="@+id/zxingBarcodeScanner"
        android:layout_width="0dp"
        android:layout_height="150dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/etCustomerName" />

 

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btnNext"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="Generate Invoice"
        android:textAllCaps="false"
        android:background="@drawable/btn_bg"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintStart_toStartOf="parent" />

    <EditText
        android:id="@+id/etCustomerName"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:background="@drawable/btn_bg"
        android:ems="10"
        android:textSize="14sp"
        android:paddingStart="8dp"
        android:paddingEnd="8dp"
        android:inputType="text"
        android:hint="Customer name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

6. rv_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="5dp"
        android:elevation="8dp"
        android:backgroundTint="#E7EFE6"
        app:cardCornerRadius="8dp"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/tvItemName"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:padding="3dp"
                android:text="Pepsi"
                android:textColor="@color/black"
                android:textStyle="bold"
                app:layout_constraintEnd_toStartOf="@+id/etQuantity"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <TextView
                android:id="@+id/tvItemPrice"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                android:layout_marginBottom="8dp"
                android:padding="3dp"
                android:text="$8"
                android:layout_marginTop="3dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tvItemName" />

            <TextView
                android:id="@+id/textView"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginEnd="5dp"
                android:layout_marginBottom="8dp"
                android:padding="3dp"
                android:text="Price"
                android:layout_marginTop="3dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/tvItemPrice"
                app:layout_constraintTop_toBottomOf="@+id/tvItemName" />

            <EditText
                android:id="@+id/etQuantity"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:text="1"
                android:layout_marginEnd="8dp"
                android:paddingStart="8dp"
                android:paddingEnd="8dp"
                android:ems="4"
                android:gravity="center"
                app:layout_constraintBottom_toBottomOf="@+id/tvItemName"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="@+id/tvItemName" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>


Download Source Code

Conclusion

By following these steps, you can build a functional Android application that scans barcodes, lists products in a RecyclerView, and generates a PDF invoice. This project is highly useful for businesses looking for an easy invoicing system within their Android app.

📱 Android Image Compressor App with Image Picker and Storage

If you’re looking to build an Android Image Compressor App with Image Picker that lets users select, compress, and save images directly to their device, you’re in the right place. In this tutorial, we’ll walk you through how to create an Android image compression tool using Java, ActivityResultLauncher, SeekBar, and a multithreaded approach for performance.

🔧 What This App Does

  • Select images using the modern Image Picker API
  • Display original and compressed images
  • Adjust compression quality via SeekBar
  • Save compressed images to external storage
  • Optimize image processing using ExecutorService

🚀 Step-by-Step Breakdown

1. Image Selection Using ActivityResultLauncher

The app uses ActivityResultContracts.PickVisualMedia() to allow users to select an image from their gallery. Once an image is picked, it’s displayed in the original image view using:

 pickMultipleMedia.launch(new PickVisualMediaRequest.Builder()
    .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE)
    .build());

2. Live Compression Quality Adjustment

A SeekBar allows users to set the compression level in real-time. The compressionQuality variable updates with each user interaction, and the value is shown dynamically using a TextView.

 tvSeekValue.setText("Compression: " + progress + "%");

3. Image Compression in Background Threads

To keep the UI responsive, the app uses ExecutorService to handle compression on a background thread. This ensures the main thread isn’t blocked while processing images.

 Bitmap compressedBitmap = compressImage(imageModel.getOriginalImage(), compressionQuality);

4. Saving Compressed Images

Once an image is compressed, users can tap a button to save it into the Pictures/CompressedImages folder. The file is written using FileOutputStream, and then registered in the device gallery.

 addImageToGallery(file);

5. Displaying the Result

The app shows both the original and compressed image side by side, allowing users to visually compare them. A progress bar is used to indicate when compression is happening in the background.

🧠 Key Concepts Used

  • Bitmap Compression with Bitmap.compress()
  • Multithreading with Executors.newFixedThreadPool()
  • Modern Media Picker with ActivityResultContracts
  • Storage Access with Environment.getExternalStoragePublicDirectory()
  • Gallery Update via MediaStore

🛡️ Final Touch: Cleanup

Always remember to shut down the ExecutorService when the activity is destroyed to prevent memory leaks:

 @Override
protected void onDestroy() {
    super.onDestroy();
    executorService.shutdown();
}

Complete Code Here


1. MainActivity.java


public class MainActivity extends AppCompatActivity {


    private Button btnImagesSelect, btnSaveImages, btnCompressImages;
    private TextView tvSeekValue;
    private SeekBar compressionSeekBar;
    private int compressionQuality = 100;  // Default to 100%
    ImageModel imageModel;
    private ImageView originalImageView, compressedImageView;
    private ProgressBar progressBar;

    private final ExecutorService executorService = Executors.newFixedThreadPool(2);

    ActivityResultLauncher pickMultipleMedia =
            registerForActivityResult(new ActivityResultContracts.PickVisualMedia(), uri -> {


                if (uri != null) {
                    // Handle the selected media URI (image or video)
                    Log.d("Selected URI", uri.toString());
                    setImageInImageView(uri);
                } else {
                    Log.d("PickMedia", "No media selected");
                }


            });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        originalImageView = findViewById(R.id.originalImageView);
        compressedImageView = findViewById(R.id.compressedImageView);
        btnImagesSelect = findViewById(R.id.selectImagesButton);
        btnSaveImages = findViewById(R.id.saveImagesButton);
        btnCompressImages = findViewById(R.id.compressImages);
        compressionSeekBar = findViewById(R.id.compressionSeekBar);
        tvSeekValue = findViewById(R.id.tvSeekValue);
        progressBar = findViewById(R.id.progressBar);


        btnImagesSelect.setOnClickListener(view -> openImagePicker());
        btnSaveImages.setOnClickListener(view -> saveCompressedImages());

        btnCompressImages.setOnClickListener(v -> compressImagesInBackground());

        compressionSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                compressionQuality = progress;
                tvSeekValue.setText("Compression: " + progress + "%");
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
            }
        });
    }


    // open image picker to pick the image
    private void openImagePicker() {
        pickMultipleMedia.launch(new PickVisualMediaRequest.Builder()
                .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE)
                .build());
    }



    // set the selected image in the original image view
    private void setImageInImageView(Uri uri) {
        executorService.execute(() -> {
            try {
                InputStream inputStream = getContentResolver().openInputStream(uri);
                Bitmap originalBitmap = BitmapFactory.decodeStream(inputStream);
                imageModel = new ImageModel(originalBitmap, uri);


                runOnUiThread(() -> {
                    originalImageView.setImageBitmap(originalBitmap);
                    compressedImageView.setImageBitmap(null);

                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }



    // compressed image in the background
    private void compressImagesInBackground() {
        progressBar.setVisibility(View.VISIBLE);
        executorService.execute(() -> {

            Bitmap compressedBitmap = compressImage(imageModel.getOriginalImage(), compressionQuality);
            imageModel.setCompressedImage(compressedBitmap);

            runOnUiThread(() -> {
                compressedImageView.setImageBitmap(compressedBitmap);
                progressBar.setVisibility(View.GONE);
            });

        });
    }


    //compress image method, which is calling in the compressImagesInBackground method
    private Bitmap compressImage(Bitmap original, int quality) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        original.compress(Bitmap.CompressFormat.JPEG, quality, stream);
        byte[] byteArray = stream.toByteArray();
        return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);
    }



    // save the compressed image in the phone storage
    private void saveCompressedImages() {
        executorService.execute(() -> {
            File directory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "CompressedImages");
            if (!directory.exists()) {
                directory.mkdirs();
            }

            File file = new File(directory, "compressed_" + System.currentTimeMillis() + ".jpg");
            try (FileOutputStream fos = new FileOutputStream(file)) {
                imageModel.getCompressedImage().compress(Bitmap.CompressFormat.JPEG, 100, fos);
                fos.flush();
                addImageToGallery(file);
            } catch (IOException e) {
                e.printStackTrace();
            }

            // Show toast with full image path
            runOnUiThread(() -> Toast.makeText(
                    this,
                    "Image saved at:\n" + file.getAbsolutePath(),
                    Toast.LENGTH_LONG
            ).show());
        });
    }


    //show the image in the gallery
    private void addImageToGallery(File file) {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        executorService.shutdown();
    }
}

📦 2. ImageModel.java – Custom Model Class for Handling Images

💡 Note: This is a reusable model class used to store and manage both the original and compressed image Bitmaps, along with the image URI. While it’s helpful for clean code and scalability, you can also work without a model class if you prefer a simpler implementation.


public class ImageModel {
    private Bitmap originalImage;
    private Bitmap compressedImage;
    private Uri imageUri;

    public ImageModel(Bitmap originalImage, Uri imageUri) {
        this.originalImage = originalImage;
        this.imageUri = imageUri;
        this.compressedImage = originalImage;
    }

    public Bitmap getOriginalImage() {
        return originalImage;
    }

    public Bitmap getCompressedImage() {
        return compressedImage;
    }

    public void setCompressedImage(Bitmap compressedImage) {
        this.compressedImage = compressedImage;
    }

    public Uri getImageUri() {
        return imageUri;
    }
}

3. activity_main.xml

 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tvOriginal"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="Original Image"
            android:textColor="@color/black"
            android:textSize="16sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@id/cvOriginal"
            app:layout_constraintEnd_toEndOf="@id/cvOriginal"
            app:layout_constraintStart_toStartOf="@id/cvOriginal"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tvCompressed"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:gravity="center"
            android:text="Compressed Image"
            android:textColor="@color/black"
            android:textSize="16sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@id/cvCompressed"
            app:layout_constraintEnd_toEndOf="@id/cvCompressed"
            app:layout_constraintStart_toStartOf="@id/cvCompressed"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.cardview.widget.CardView
            android:id="@+id/cvOriginal"
            android:layout_width="0dp"
            android:layout_height="300dp"
            app:cardCornerRadius="8dp"
            app:layout_constraintBottom_toTopOf="@+id/compressionSeekBar"
            app:layout_constraintEnd_toStartOf="@+id/cvCompressed"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <ImageView
                android:id="@+id/originalImageView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop" />

        </androidx.cardview.widget.CardView>

        <androidx.cardview.widget.CardView
            android:id="@+id/cvCompressed"
            android:layout_width="0dp"
            android:layout_height="300dp"
            android:layout_marginStart="16dp"
            app:cardCornerRadius="8dp"
            app:layout_constraintBottom_toTopOf="@+id/compressionSeekBar"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/cvOriginal"
            app:layout_constraintTop_toTopOf="parent">

            <ImageView
                android:id="@+id/compressedImageView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop" />

        </androidx.cardview.widget.CardView>

        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="@id/cvCompressed"
            app:layout_constraintEnd_toEndOf="@id/cvCompressed"
            app:layout_constraintStart_toStartOf="@id/cvCompressed"
            app:layout_constraintTop_toTopOf="@id/cvCompressed" />

        <SeekBar
            android:id="@+id/compressionSeekBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:max="100"
            android:progress="100"
            app:layout_constraintBottom_toTopOf="@+id/selectImagesButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/cvCompressed" />

        <TextView
            android:id="@+id/tvSeekValue"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            android:text="100%"
            app:layout_constraintBottom_toTopOf="@id/selectImagesButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="@id/compressionSeekBar"
            app:layout_constraintTop_toBottomOf="@id/compressionSeekBar" />

        <Button
            android:id="@+id/selectImagesButton"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="Select Image"
            app:layout_constraintBottom_toTopOf="@+id/compressImages"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <Button
            android:id="@+id/compressImages"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:text="Compress Image"
            app:layout_constraintBottom_toTopOf="@+id/saveImagesButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <Button
            android:id="@+id/saveImagesButton"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:text="Save Compressed Image"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</ScrollView>


Download Source Code

💡 Final Thoughts

This Android image compressor app provides a practical implementation of media handling, compression, and storage. Whether you’re building a photo editing app or simply want to reduce image size before upload, this codebase gives you a solid foundation to build on.

Simple Web Browser in Android Using WebView in Java

Looking to create a Simple Web Browser in Android Using WebView in Java that mimics Google Chrome? In this tutorial, we’ll walk you through building a full-featured, modern web browser in Java using Android Studio. This browser includes essential features like web page loading, navigation buttons, progress bar, and URL input with “GO” button support — all wrapped in a sleek UI similar to Chrome.

🔧 Features of This Android Web Browser

  • URL input bar with IME GO button handling
  • Full WebView support with JavaScript enabled
  • Progress bar to indicate page loading status
  • Back and Forward navigation buttons
  • Proper back press handling using OnBackPressedDispatcher

📱 MainActivity.java – Complete Code Overview

The heart of our browser lies in the MainActivity.java. Here’s what each component does:

public class MainActivity extends AppCompatActivity {

    private WebView webView;
    private EditText urlInput;
    private ProgressBar progressBar;
    private ImageButton backButton, forwardButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getOnBackPressedDispatcher().addCallback(this, backPressedCallback);

        urlInput = findViewById(R.id.urlInput);
        webView = findViewById(R.id.webView);
        progressBar = findViewById(R.id.progressBar);
        backButton = findViewById(R.id.backButton);
        forwardButton = findViewById(R.id.forwardButton);

        // Enable JavaScript
        webView.getSettings().setJavaScriptEnabled(true);

        // Handle page load progress
        webView.setWebChromeClient(new WebChromeClient() {
            public void onProgressChanged(WebView view, int progress) {
                progressBar.setProgress(progress);
                progressBar.setVisibility(progress == 100 ? View.GONE : View.VISIBLE);
            }
        });

        // Load initial page
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("https://www.google.com");

        // Handle "GO" key in soft keyboard
        urlInput.setOnEditorActionListener((v, actionId, event) -> {
            if (actionId == EditorInfo.IME_ACTION_GO || actionId == EditorInfo.IME_ACTION_DONE) {
                loadUrlFromInput();
                return true;
            }
            return false;
        });

        // Navigation buttons
        backButton.setOnClickListener(v -> {
            if (webView.canGoBack()) webView.goBack();
        });

        forwardButton.setOnClickListener(v -> {
            if (webView.canGoForward()) webView.goForward();
        });
    }

    private void loadUrlFromInput() {
        String url = urlInput.getText().toString().trim();
        if (!url.startsWith("http")) {
            url = "https://" + url;
        }
        webView.loadUrl(url);
    }

    // Handle back press using OnBackPressedDispatcher
    private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) {
        @Override
        public void handleOnBackPressed() {
            if (webView.canGoBack()) {
                webView.goBack();
            } else {
                finish();
            }
        }
    };
}

🧱 activity_main.xml – Layout Code

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp">

        <ImageButton
            android:id="@+id/backButton"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:background="@android:color/transparent"
            android:contentDescription="Back"
            android:src="@android:drawable/ic_media_previous" />

        <EditText
            android:id="@+id/urlInput"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:background="@drawable/et_bg"
            android:hint="Enter URL"
            android:imeOptions="actionGo"
            android:inputType="textUri"
            android:lines="1"
            android:layout_marginStart="3dp"
            android:layout_marginEnd="3dp"
            android:paddingStart="8dp"
            android:paddingEnd="8dp" />

        <ImageButton
            android:id="@+id/forwardButton"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:background="@android:color/transparent"
            android:contentDescription="Forward"
            android:src="@android:drawable/ic_media_next" />

    </LinearLayout>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="4dp"
        android:indeterminate="false"
        android:max="100" />

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>



Download Source Code

🎨 UI Design Tips

  • Use a clean layout with a URL input bar on top.
  • Keep navigation buttons intuitive with back/forward arrows.
  • Use a small progress bar under the input to show load progress.