📱 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>

Output


💡 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.

📥
Click here to Download
the source code or ask questions in the comments below!

Leave a Comment

Your email address will not be published. Required fields are marked *