We all have once used the MS-Paint in our childhood, and when the system was shifted from desks to our palms, we started doodling on Instagram Stories, Hike, WhatsApp, and many more such apps. But have you ever thought about how these functionalities were brought to life? So, In this article, we will be discussing the basic approach used by such apps and will create a basic replica of such apps. A sample video is given below to get an idea about what we are going to do in this article. Note that we are going to implement this project using both Java and Kotlin language.
Step by Step Implementation
Step 1: Create a New Project
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. Select Java/Kotlin as the programming language.
Step 2: Add storage permissions in AndroidManifest
Add the following permissions in AndroidManifest.xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Step 3: Adding the dependency in gradle.build.kts (Module :app)
We will wil be adding two libraries, one for the draw canvas and the other for the color palette. Refer to DrawingCanvas and ColorPicker to check out their github repositories.
dependencies {
implementation("com.github.Miihir79:DrawingCanvas:1.1.2")
implementation ("com.github.Dhaval2404:ColorPicker:2.3")
}
Adding Jitpack support in settings.gradle.kts
dependencyResolutionManagement {
..
repositories {
..
maven { url = uri("https://jitpack.io/") }
}
}
Step 3: Add icons in drawable
Add the following drawable files in res > drawable folder.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M223.33,842Q185.67,842 149,827Q112.33,812 86.67,782.67Q118.67,774.67 137.67,752.5Q156.67,730.33 156.67,691.33Q156.67,645.33 188.67,613.33Q220.67,581.33 266.67,581.33Q312.67,581.33 344.67,613.33Q376.67,645.33 376.67,691.33Q376.67,756 332,799Q287.33,842 223.33,842ZM453.33,604L356.67,504L726.67,134Q739.67,121 756.5,120.5Q773.33,120 787.33,134L824.67,171.33Q838.67,185.33 838.33,202.33Q838,219.33 824.67,232.67L453.33,604Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M393.33,760Q297.67,760 228.83,696.33Q160,632.67 160,538.67Q160,444.67 228.83,381Q297.67,317.33 393.33,317.33L673.33,317.33L562.67,206.67L609.33,160L800,350.67L609.33,541.33L562.67,494.67L673.33,384L392.67,384Q325,384 275.83,428.33Q226.67,472.67 226.67,538.67Q226.67,604.67 275.83,649Q325,693.33 392.67,693.33L694,693.33L694,760L393.33,760Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,395.67 112.17,322.67Q144.33,249.67 199.83,195.67Q255.33,141.67 329.67,110.83Q404,80 488.67,80Q568,80 639,106.83Q710,133.67 763.5,181.17Q817,228.67 848.5,293.83Q880,359 880,436Q880,546.33 814.67,608.5Q749.33,670.67 646.67,670.67L572,670.67Q557,670.67 547.17,681.67Q537.33,692.67 537.33,706Q537.33,728 552,748.17Q566.67,768.33 566.67,794.67Q566.67,836.67 543.5,858.33Q520.33,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM251.33,510.67Q273.33,510.67 289,495Q304.67,479.33 304.67,457.33Q304.67,435.33 289,419.67Q273.33,404 251.33,404Q229.33,404 213.67,419.67Q198,435.33 198,457.33Q198,479.33 213.67,495Q229.33,510.67 251.33,510.67ZM375.33,344Q397.33,344 413,328.33Q428.67,312.67 428.67,290.67Q428.67,268.67 413,253Q397.33,237.33 375.33,237.33Q353.33,237.33 337.67,253Q322,268.67 322,290.67Q322,312.67 337.67,328.33Q353.33,344 375.33,344ZM584.67,344Q606.67,344 622.33,328.33Q638,312.67 638,290.67Q638,268.67 622.33,253Q606.67,237.33 584.67,237.33Q562.67,237.33 547,253Q531.33,268.67 531.33,290.67Q531.33,312.67 547,328.33Q562.67,344 584.67,344ZM712,510.67Q734,510.67 749.67,495Q765.33,479.33 765.33,457.33Q765.33,435.33 749.67,419.67Q734,404 712,404Q690,404 674.33,419.67Q658.67,435.33 658.67,457.33Q658.67,479.33 674.33,495Q690,510.67 712,510.67ZM480,813.33Q490.33,813.33 495.17,808.67Q500,804 500,794.67Q500,780.67 485.33,766.33Q470.67,752 470.67,712Q470.67,667.33 500.33,635.67Q530,604 574.67,604L646.67,604Q719.33,604 766.33,561.5Q813.33,519 813.33,436Q813.33,307.67 715.83,227.17Q618.33,146.67 488.67,146.67Q346,146.67 246.33,243.33Q146.67,340 146.67,480Q146.67,618.33 244.17,715.83Q341.67,813.33 480,813.33Z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M266,760L266,693.33L567.33,693.33Q635,693.33 684.17,649Q733.33,604.67 733.33,538.67Q733.33,472.67 684.17,428.33Q635,384 567.33,384L286.67,384L397.33,494.67L350.67,541.33L160,350.67L350.67,160L397.33,206.67L286.67,317.33L566.67,317.33Q662.33,317.33 731.17,381Q800,444.67 800,538.67Q800,632.67 731.17,696.33Q662.33,760 566.67,760L266,760Z"/>
</vector>
Step 4: Working with the activity_main.xml file
Navigate to the app > res > layout > activity_main.xml and add the below code to that file. Below is the code for the activity_main.xml file.
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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="horizontal"
android:weightSum="4">
<ImageButton
android:id="@+id/btn_undo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/primary_color"
android:src="@drawable/undo_icon" />
<ImageButton
android:id="@+id/btn_redo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/primary_color"
android:src="@drawable/btn_redo" />
<ImageButton
android:id="@+id/btn_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/primary_color"
android:src="@drawable/palette_icon" />
<ImageButton
android:id="@+id/btn_stroke"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@color/primary_color"
android:src="@drawable/brush_icon" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.slider.RangeSlider
android:id="@+id/rangebar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:visibility="visible" />
</LinearLayout>
</LinearLayout>
<com.mihir.drawingcanvas.drawingView
android:id="@+id/draw_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linear" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:weightSum="2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/btn_clean"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@color/black"
android:backgroundTint="@color/primary_color"
android:text="Clean"
android:textColor="@color/black" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@color/black"
android:backgroundTint="@color/primary_color"
android:text="Save"
android:textColor="@color/black" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Step 5: Working with the MainActivity file
Go to the MainActivity file and refer to the following code. Below is the code for the MainActivity file. Comments are added inside the code to understand the code in more detail.
package org.geeksforgeeks.paint;
import android.Manifest;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.github.dhaval2404.colorpicker.ColorPickerDialog;
import com.github.dhaval2404.colorpicker.model.ColorShape;
import com.google.android.material.slider.RangeSlider;
import com.mihir.drawingcanvas.drawingView;
import java.io.File;
import java.io.FileOutputStream;
public class MainActivity extends AppCompatActivity {
private drawingView draw;
private ImageButton undo, redo, color, stroke;
private Button clear, save;
private RangeSlider rangeSlider;
private static final int STORAGE_PERMISSION_CODE = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Request storage permissions if not granted
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_CODE);
}
// Initialize views
draw = findViewById(R.id.draw_view);
undo = findViewById(R.id.btn_undo);
redo = findViewById(R.id.btn_redo);
color = findViewById(R.id.btn_color);
clear = findViewById(R.id.btn_clean);
stroke = findViewById(R.id.btn_stroke);
save = findViewById(R.id.btn_save);
rangeSlider = findViewById(R.id.rangebar);
// Initially hide the range slider
rangeSlider.setVisibility(View.GONE);
undo.setOnClickListener(v -> draw.undo());
redo.setOnClickListener(v -> draw.redo());
clear.setOnClickListener(v -> draw.clearDrawingBoard());
// Toggle visibility of the brush size slider
stroke.setOnClickListener(v -> {
if (rangeSlider.getVisibility() == View.VISIBLE) {
rangeSlider.setVisibility(View.GONE);
} else {
rangeSlider.setVisibility(View.VISIBLE);
}
});
// Adjust brush size based on slider value
rangeSlider.addOnChangeListener((slider, value, fromUser) ->
draw.setSizeForBrush((int) (20 * value))
);
// Show color picker
color.setOnClickListener(v ->
ColorPickerDialog
.Builder(this)
.setTitle("Choose color")
.setColorShape(ColorShape.SQAURE)
.setDefaultColor(getResources().getColor(R.color.black))
.setColorListener((selectedColor, colorHex) -> draw.setBrushColor(selectedColor))
.show()
);
// Save drawing to storage
save.setOnClickListener(v -> draw.post(() -> {
Bitmap bitmap = getBitmapFromView(draw);
Uri imageUri = saveBitmapToStorage(bitmap);
if (imageUri != null) {
Log.e("draw", "✅ Image saved at: " + imageUri);
Toast.makeText(this, "Image saved!", Toast.LENGTH_SHORT).show();
shareImage(imageUri);
} else {
Log.e("draw", "❌ Saving failed!");
Toast.makeText(this, "Failed to save image", Toast.LENGTH_SHORT).show();
}
}));
}
private Bitmap getBitmapFromView(View view) {
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (view.getBackground() != null) {
view.getBackground().draw(canvas);
} else {
canvas.drawColor(Color.WHITE);
}
view.draw(canvas);
return bitmap;
}
private Uri saveBitmapToStorage(Bitmap bitmap) {
String filename = "Drawing_" + System.currentTimeMillis() + ".png";
Uri uri = null;
try {
File imagesDir = new File(getExternalFilesDir(null) + "/Pictures/DrawingApp");
if (!imagesDir.exists()) {
imagesDir.mkdirs(); // Create folder if not exists
}
File imageFile = new File(imagesDir, filename);
FileOutputStream outputStream = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.flush();
outputStream.close();
// Update gallery
MediaStore.Images.Media.insertImage(getContentResolver(), imageFile.getAbsolutePath(), filename, null);
uri = Uri.fromFile(imageFile);
} catch (Exception e) {
Log.e("draw", "❌ Error saving image: " + e.getMessage());
e.printStackTrace();
}
return uri;
}
private void shareImage(Uri uri) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
shareIntent.setType("image/png");
startActivity(Intent.createChooser(shareIntent, "Share via"));
}
}
package org.geeksforgeeks.paint
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.github.dhaval2404.colorpicker.ColorPickerDialog
import com.github.dhaval2404.colorpicker.model.ColorShape
import com.google.android.material.slider.RangeSlider
import com.mihir.drawingcanvas.drawingView
import android.Manifest
import android.widget.Toast
class MainActivity : AppCompatActivity() {
private lateinit var draw: drawingView
private lateinit var undo: ImageButton
private lateinit var redo: ImageButton
private lateinit var color: ImageButton
private lateinit var stroke: ImageButton
private lateinit var clear: Button
private lateinit var save: Button
private lateinit var rangeSlider: RangeSlider
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Check & request storage permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 100)
}
draw = findViewById(R.id.draw_view)
undo = findViewById(R.id.btn_undo)
redo = findViewById(R.id.btn_redo)
color = findViewById(R.id.btn_color)
clear = findViewById(R.id.btn_clean)
stroke = findViewById(R.id.btn_stroke)
save = findViewById(R.id.btn_save)
rangeSlider = findViewById(R.id.rangebar)
// Set initial visibility of the rangeSlider to GONE
rangeSlider.visibility = View.GONE
undo.setOnClickListener { draw.undo() }
clear.setOnClickListener { draw.clearDrawingBoard() }
redo.setOnClickListener { draw.redo() }
stroke.setOnClickListener {
// Toggle visibility of the rangeSlider
if (rangeSlider.visibility == View.VISIBLE) {
rangeSlider.visibility = View.GONE
} else {
rangeSlider.visibility = View.VISIBLE
}
}
// Set up the RangeSlider listener to change brush size
rangeSlider.addOnChangeListener(RangeSlider.OnChangeListener { slider: RangeSlider?, value: Float, fromUser: Boolean ->
// Set brush size based on slider value
draw.setSizeForBrush((20 * value).toInt())
})
color.setOnClickListener {
ColorPickerDialog
.Builder(this)
.setTitle("Choose color")
.setColorShape(ColorShape.SQAURE)
.setDefaultColor(R.color.black)
.setColorListener { color, colorHex ->
draw.setBrushColor(color)
}
.show()
}
save.setOnClickListener {
draw.post {
val bitmap = getBitmapFromView(draw)
val imageUri = saveBitmapToStorage(bitmap)
if (imageUri != null) {
println("Image saved at: $imageUri")
} else {
println("Saving failed!")
}
}
}
}
private fun getBitmapFromView(view: View): Bitmap {
val returnedBitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(returnedBitmap)
val bgDrawable = view.background
if (bgDrawable != null) {
bgDrawable.draw(canvas)
} else {
canvas.drawColor(Color.WHITE)
}
view.draw(canvas)
return returnedBitmap
}
private fun saveBitmapToStorage(bitmap: Bitmap): Uri? {
val filename = "Drawing_${System.currentTimeMillis()}.png"
var uri: Uri? = null
try {
val imagesDir = getExternalFilesDir(null)?.absolutePath + "/Pictures/DrawingApp"
val file = java.io.File(imagesDir)
if (!file.exists()) {
file.mkdirs() // Create folder if it doesn't exist
}
val imageFile = java.io.File(file, filename)
val outputStream = java.io.FileOutputStream(imageFile)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.flush()
outputStream.close()
// Tell the gallery to update
MediaStore.Images.Media.insertImage(contentResolver, imageFile.absolutePath, filename, null)
uri = Uri.fromFile(imageFile)
Log.e("draw", "✅ Image saved successfully at: $uri")
Toast.makeText(this, "Image saved in Gallery", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("draw", "❌ Failed to save image: ${e.message}")
Toast.makeText(this, "Failed to save image", Toast.LENGTH_SHORT).show()
e.printStackTrace()
}
return uri
}
private fun shareImage(uri: Uri) {
Toast.makeText(this, "Sharing image URI: $uri", Toast.LENGTH_SHORT).show()
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uri)
type = "image/png"
}
startActivity(Intent.createChooser(shareIntent, "Share via"))
}
}
Output:
Refer to this github repository to get the entire code.
Future Scope
There are plenty of things you can add to this project like:-
- Adding a mask to the painted object, i.e. creating a blur or emboss effect on the stroke.
- Adding animations to the app.
- Adding a color selector for canvas, i.e. changing the color of canvas from the default White color as per the user requirement.
- Adding a sharing button, to directly share the drawing on various apps.
- Adding an eraser functionality that clears the specific path/stroke on which the eraser is dragged.
- Adding a shape picker, by which a user can directly select any particular shape from the list and can drag on the screen to create that shape.
- Enhancing UI, by adding a BottomSheet, vectors, etc.
"Anyone can put paint on a canvas, but only a true master can bring the painting to life.", we finish building our app, now draw some awesome paintings on this canvas and become a "true master".