Easily Build Android Photo Cropping App

Mayank Chaudhari
7 min readJul 26, 2021

--

Learn to build android photo cropping app in a few simple steps.

https://play.google.com/store/apps/details?id=com.mayank.crop

Prerequisites

Android Studio: To follow with me please install android studio from https://developer.android.com/studio

I assume that you have basic understanding of programing.

Add Library Dependency

Make sure that the scope is set to Android then expand Gradle Scripts and double click on Module level build.gradle file to open it.
Scroll down to dependencies section and add this line — implementation ‘com.mayank:simplecropview:1.0.0’

SimpleCropView is an open source library available on GitHub. The advantage of this library is that it is tiny (just adds about 40kb to your apk while providing easy to use image cropping capabilities).

To add library to your dependencies include following line in the dependencies section in the module level build.gradle file as shown in image.

> implementation ‘com.mayank:simplecropview:1.0.0’

Update Main Activity Layout

Open activity_main.xml under the layout folder under resources. Replace the content of the file with following code.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.mayank.simplecropview.CropImageView
android:id="@+id/cropImageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/spacing_xlarge"
custom:scv_background_color="@color/windowBackground"
custom:scv_crop_mode="fit_image"
custom:scv_frame_color="@color/colorAccent"
custom:scv_frame_stroke_weight="1dp"
custom:scv_guide_color="@color/colorAccent"
custom:scv_guide_show_mode="show_always"
custom:scv_guide_stroke_weight="1dp"
custom:scv_handle_color="@color/colorAccent"
custom:scv_handle_show_mode="show_always"
custom:scv_handle_size="14dp"
custom:scv_min_frame_size="50dp"
custom:scv_overlay_color="@color/overlay"
custom:scv_touch_padding="8dp"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"
android:background="@color/divider"
/>
<HorizontalScrollView
android:id="@+id/tab_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/windowBackground"
android:scrollbars="none"
>
<LinearLayout
android:id="@+id/tab_layout"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/spacing_xsmall"
android:paddingRight="@dimen/spacing_xsmall"
>
<Button
android:id="@+id/buttonFitImage"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="FIT IMAGE" />
<Button
android:id="@+id/button1_1"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="SQUARE" />
<Button
android:id="@+id/button3_4"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="3:4" />
<Button
android:id="@+id/button4_3"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="4:3" />
<Button
android:id="@+id/button9_16"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="9:16" />
<Button
android:id="@+id/button16_9"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="16:9" />
<Button
android:id="@+id/buttonCustom"
android:visibility="gone"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="7:5" />
<Button
android:id="@+id/buttonFree"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="FREE" />
<Button
android:id="@+id/buttonCircle"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:padding="@dimen/spacing_xsmall"
android:text="CIRCLE" />
<Button
android:id="@+id/buttonShowCircleButCropAsSquare"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/spacing_xsmall"
android:onClick="onClick"
android:visibility="gone"
android:padding="@dimen/spacing_xsmall"
android:text="CIRCLE_SQUARE" />
</LinearLayout>
</HorizontalScrollView>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"
android:background="@color/divider"
/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/cropImageView"
android:layout_centerHorizontal="true"
android:background="@color/windowBackground"
android:orientation="horizontal"
>
<ImageButton
android:id="@+id/buttonPickImage"
style="@style/AppTheme.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginRight="@dimen/spacing"
android:onClick="onClick"
android:padding="@dimen/spacing"
android:src="@drawable/ic_photo_library_black_24dp"
android:layout_alignParentStart="true"
android:layout_marginEnd="@dimen/spacing" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="horizontal"
>
<ImageButton
style="@style/AppTheme.Button.Borderless"
android:id="@+id/buttonRotateLeft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
android:padding="@dimen/spacing"
android:src="@drawable/ic_rotate_left_black_24dp" />
<ImageButton
style="@style/AppTheme.Button.Borderless"
android:id="@+id/buttonRotateRight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
android:padding="@dimen/spacing"
android:src="@drawable/ic_rotate_right_black_24dp" />
</LinearLayout> <ImageButton
style="@style/AppTheme.Button.Borderless"
android:id="@+id/buttonDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:onClick="onClick"
android:padding="16dp"
android:src="@drawable/ic_done_black_24dp"
android:layout_alignParentEnd="true" />
</RelativeLayout>
</LinearLayout>

Now lets quickly go through the elements in this layout.

First element in the layout is com.mayank.simplecropview.CropImageView. This is where we perform all image editing work. All this work is handled for you by the library that we added com.mayank.simplecropview

Next we have line separator followed by a HorizontalSrollView containing buttons that we will use for user interaction.

Your layout now should look like following

Layout View

Update MainActivity.java

First let us create some constants that we will need

Add following lines just below the line that says public class MainActivity extends AppCompatActivity {

private static final int REQUEST_PICK_IMAGE = 10011;
private static final int REQUEST_SAVE_IMAGE = 10012;
private static final String PROGRESS_DIALOG = "ProgressDialog";
private static final String KEY_FRAME_RECT = "FrameRect";
private static final String KEY_SOURCE_URI = "SourceUri";

We will use these constants later to save and load instanceState and for requesting permissions. Below these constant add some variables that we will use to hold our View objects.

// Views 
private CropImageView mCropView;
private Bitmap.CompressFormat mCompressFormat = Bitmap.CompressFormat.PNG;
private RectF mFrameRect = null;
private Uri mSourceUri = null;

Add following lines in the onCreate() method of your activity below setContentView(R.layout.activity_main)

mCropView = findViewById(R.id.cropImageView);
if (savedInstanceState != null) {
// restore data
mFrameRect = savedInstanceState.getParcelable(KEY_FRAME_RECT);
mSourceUri = savedInstanceState.getParcelable(KEY_SOURCE_URI);
}
if (mSourceUri == null) {
// default data
mSourceUri = getUriFromDrawableResId(this, R.drawable.sample4);
}
// load image
mCropView.load(mSourceUri)
.initialFrameRect(mFrameRect)
.useThumbnail(true)
.execute(mLoadCallback);
Note: Make sure you add an image named sample4.png in the drawable folder

Lets also add getUriFromDrawableResId helper function below the onCreate method.

public static Uri getUriFromDrawableResId(Context context, int drawableResId) {
StringBuilder builder = new StringBuilder().append(ContentResolver.SCHEME_ANDROID_RESOURCE)
.append("://")
.append(context.getResources().getResourcePackageName(drawableResId))
.append("/")
.append(context.getResources().getResourceTypeName(drawableResId))
.append("/")
.append(context.getResources().getResourceEntryName(drawableResId));
return Uri.parse(builder.toString());
}

If you got any other errors. Place cursor where you see red underline and press Alt+Enter then select import class.

We have initialized the app. However, lets add mechanism to load image from gallery and change aspect ratio. Before doing that lets save current state of app when app goes in background so that we can load it when user returns to the app.

We will do this by overriding onSaveInstanceState method of your Activity or AppCompatActivity class. Add following code below the ending braces of your onCreate method

@Override public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// save data
outState.putParcelable(KEY_FRAME_RECT, mCropView.getActualCropRect());
outState.putParcelable(KEY_SOURCE_URI, mCropView.getSourceUri());
}

If you noticed carefully in many views in activity_main.xml file we have set attribute onClick=”onClick”

Now lets define this click event handler in out MainActivity.java file. Add following method (function) in your MainActivity class.

public void onClick(View v) {
switch (v.getId()) {
case R.id.buttonDone:
cropImage();
break;
case R.id.buttonFitImage:
mCropView.setCropMode(CropImageView.CropMode.FIT_IMAGE);
break;
case R.id.button1_1:
mCropView.setCropMode(CropImageView.CropMode.SQUARE);
break;
case R.id.button3_4:
mCropView.setCropMode(CropImageView.CropMode.RATIO_3_4);
break;
case R.id.button4_3:
mCropView.setCropMode(CropImageView.CropMode.RATIO_4_3);
break;
case R.id.button9_16:
mCropView.setCropMode(CropImageView.CropMode.RATIO_9_16);
break;
case R.id.button16_9:
mCropView.setCropMode(CropImageView.CropMode.RATIO_16_9);
break;
case R.id.buttonCustom:
mCropView.setCustomRatio(7, 5);
break;
case R.id.buttonFree:
mCropView.setCropMode(CropImageView.CropMode.FREE);
break;
case R.id.buttonCircle:
mCropView.setCropMode(CropImageView.CropMode.CIRCLE);
break;
case R.id.buttonShowCircleButCropAsSquare:
mCropView.setCropMode(CropImageView.CropMode.CIRCLE_SQUARE);
break;
case R.id.buttonRotateLeft:
mCropView.rotateImage(CropImageView.RotateDegrees.ROTATE_M90D);
break;
case R.id.buttonRotateRight:
mCropView.rotateImage(CropImageView.RotateDegrees.ROTATE_90D);
break;
case R.id.buttonPickImage:
pickImage();
break;
}
}

You may be getting errors as pickImage and cropImage functions are not yet defined. Lets define them

public void pickImage() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"),
REQUEST_PICK_IMAGE);
} else if(checkPermissions(REQUEST_PICK_IMAGE)){
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_SAVE_PICK_IMAGE);
}
}
public void cropImage() {
if(checkPermissions(REQUEST_PICK_IMAGE))
mCropView.crop(mSourceUri).execute(mCropCallback);
}

We need to define mCropCallback and other helper functions. Add below code to your MainActivity.java class

public void showProgress() {
// implement logic to show progress bar in case you realize that cropping is not done instantly
}
public void dismissProgress() {
// implement the logic to hide progressbar
}
public Uri createSaveUri() {
return createNewUri(BasicActivity.this, mCompressFormat);
}
public static String getDirPath() {
String dirPath = "";
File imageDir = null;
File extStorageDir = Environment.getExternalStorageDirectory();
if (extStorageDir.canWrite()) {
imageDir = new File(extStorageDir.getPath() + "/simplecropview");
}
if (imageDir != null) {
if (!imageDir.exists()) {
imageDir.mkdirs();
}
if (imageDir.canWrite()) {
dirPath = imageDir.getPath();
}
}
return dirPath;
}
public static Uri createNewUri(Context context, Bitmap.CompressFormat format) {
long currentTimeMillis = System.currentTimeMillis();
Date today = new Date(currentTimeMillis);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
String title = dateFormat.format(today);
String dirPath = getDirPath();
String fileName = "scv" + title + "." + getMimeType(format);
String path = dirPath + "/" + fileName;
File file = new File(path);
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.TITLE, title);
values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/" + getMimeType(format));
values.put(MediaStore.Images.Media.DATA, path);
long time = currentTimeMillis / 1000;
values.put(MediaStore.MediaColumns.DATE_ADDED, time);
values.put(MediaStore.MediaColumns.DATE_MODIFIED, time);
if (file.exists()) {
values.put(MediaStore.Images.Media.SIZE, file.length());
}
ContentResolver resolver = context.getContentResolver();
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
return uri;
}
public static String getMimeType(Bitmap.CompressFormat format) {
switch (format) {
case JPEG:
return "jpeg";
case PNG:
return "png";
}
return "png";
}
private final LoadCallback mLoadCallback = new LoadCallback() {
@Override public void onSuccess() {
}
@Override public void onError(Throwable e) {
}
};
private final CropCallback mCropCallback = new CropCallback() {
@Override public void onSuccess(Bitmap cropped) {
mCropView.save(cropped)
.compressFormat(mCompressFormat)
.execute(createSaveUri(), mSaveCallback);
}
@Override public void onError(Throwable e) {
}
};
private final SaveCallback mSaveCallback = new SaveCallback() {
@Override public void onSuccess(Uri outputUri) {
dismissProgress();
startResultActivity(outputUri);
}
@Override public void onError(Throwable e) {
dismissProgress();
}
};

Notice if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) line in pickImage and cropImage functions. We need to request for permissions at run time for Android M and above. We add following function to check and request permissions.

public boolean checkPermissions(int reqCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, reqCode);
return false;
} else return true;
}

Override onRequestPermissionsResult method to complete the task after permission is granted.

@Override public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(grantResults[0]==PackageManager.PERMISSION_GRANTED){
if(requestCode == REQUEST_PICK_IMAGE) pickImage();
else if(requestCode==REQUEST_SAVE_IMAGE) cropImage();
}

We have still missed out one thing. Guess what?

Handling image that we receive by requesting in pickImage function. So override onActivityResult method of the activity class.

@Override public void onActivityResult(int requestCode, int resultCode, Intent result) {
super.onActivityResult(requestCode, resultCode, result);
if (resultCode == Activity.RESULT_OK) {
// reset frame rect
mFrameRect = null;
switch (requestCode) {
case REQUEST_PICK_IMAGE:
mSourceUri = result.getData();
mCropView.load(mSourceUri)
.initialFrameRect(mFrameRect)
.useThumbnail(true)
.execute(mLoadCallback);
break;
case REQUEST_SAVE_PICK_IMAGE:
mSourceUri = Utils.ensureUriPermission(BasicActivity.this, result);
mCropView.load(mSourceUri)
.initialFrameRect(mFrameRect)
.useThumbnail(true)
.execute(mLoadCallback);
break;
}
}

Great Job. Congratulations!!! Your crop activity is almost ready.

I know we are missing startResultActivity method. As of now just comment out that line by placing // at start of the line.

Run your app. It is cropping the image and saving it in the simplecropview folder. Please Go to the Github repository and look for the code of ResultActivity and have fun implementing it.

If you have any questions or want to hire a developer visit my home page.

Get the polished sample app using this approach on playstore — https://play.google.com/store/apps/details?id=com.mayank.crop

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Mayank Chaudhari
Mayank Chaudhari

Written by Mayank Chaudhari

Technical Writer | Developer | Researcher | Freelancer | Open Source Contributor

No responses yet

Write a response