cft

How to make an image editor in android

This article is about how to make simple image editor in android with features like rotate, crop.


user

Uday Garg

3 years ago | 19 min read

So in this post i will tell you about how you can make an image editor with rotate, crop, undo and save functionality.
I must tell you that making this was no easy feat i had to research for 2-3 whole days to implement the functionality when clicking pictures from camera.




There are a few things to take care of:

  1. Camera Permission, Read External Storage Permission
  2. api 'com.theartofdev.edmodo:android-image-cropper:2.8.+' Library for cropping image

So to begin, the requirement given to me was:
"There will be 2 screens Home and Edit, in the Home screen there will be an ImageView for the final image to show and 2 buttons, one for camera and other to choose image from gallery. This post will be only for the image from camera.

So first design the Home page with 2 buttons and an ImageView in your layout file.


<?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"
tools:context=".MainActivity">

<ImageView
android:id="@+id/imageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="126dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="106dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView">

<Button
android:id="@+id/selfieButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginLeft="30dp"
android:layout_marginEnd="30dp"
android:layout_marginRight="30dp"
android:layout_weight="1"
android:onClick="clickSelfie"
android:text="Selfie" />

<Button
android:id="@+id/galleryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginLeft="30dp"
android:layout_marginEnd="30dp"
android:layout_marginRight="30dp"
android:layout_weight="1"
android:onClick="clickGallery"
android:text="Gallery" />
</LinearLayout>

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="Select an Image"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />

</androidx.constraintlayout.widget.ConstraintLayout>

It will look something like this:


After that, you will need to set the camera permission in AndroidManifest.xml and your MainActivity from where you are calling the camera Intent.


@RequiresApi(api = Build.VERSION_CODES.Q)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getPhoto();
}
}
}

Then when all the permissions are setup you can start the camera Intent,

So to get a high quality image, you have to store the Uri of the image in the device folder and retrieve it in the onActivityResult and store it into a Bitmap.


private void getPhoto() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
// Create the File where the photo should go
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {
// Error occurred while creating the File
System.out.println(ex.getMessage());
}
// Continue only if the File was successfully created
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(this,
"com.example.imageeditor.fileprovider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, 1);
}
}

}

So this will create a filename in .jpg format and save it in the externalFilesDirectory which will not be accessible to media sharing apps and it will be only used for your app.


 private File createImageFile() throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);

// Save a file: path for use with ACTION_VIEW intents
currentPhotoPath = image.getAbsolutePath();
System.out.println(currentPhotoPath);
return image;
}

Above code will set the unique image name for our image using a Date Time format.

To save it in the files directory, you need to assign a content provider. So give it in your manifest.xml under tag.


<application>
\\
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.imageeditor.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
\\
</application>

You also need to create a path.xml file


<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="my_images" path="Pictures" />
</paths>

This will set the path where the image will be downloaded under your package in your device.

Then you need to save the bitmap image in your gallery and for that you need to make use of ContentValues because the writing of image in the device is deprecated for API 21 and higher.


ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, imageFileName + ".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);

ContentResolver resolver = getContentResolver();
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
OutputStream imageOutStream = null;

try {
if (uri == null) {
throw new IOException("Failed to insert MediaStore row");
}

imageOutStream = resolver.openOutputStream(uri);

if (!bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream)) {
throw new IOException("Failed to compress bitmap");
}


Toast.makeText(this, "Imave Saved", Toast.LENGTH_SHORT).show();

} finally {
if (imageOutStream != null) {
imageOutStream.close();


}
Intent intent = new Intent(this, EditImageActivity.class);

startActivity(intent);

So with this, your image is now saved in the gallery with full quality and it will be set on the image view in the next screen.

Edit Screen

So now comes the edit screen.

Most of your logic will be written here, for the rotate, crop save and undo.

Make a layout file it will look something like this



First lets do the rotate button.
So now you have the image in a bitmap and its set on the iamgeview on the edit screen. Question is how to make it rotate and then save it to a new bitmap to be saved.

For rotation i have used RotateAnimation class which takes fromRotation angle, toRotation angle, image height and width.

then create a Matrix object and set its rotation to the toRotaion value. Initialize 3 variables, mCurrentRotation, fromRotation, toRotation.

Now when you first press rotate button the image will rotate 90 degree from current angle which is 0.
So add 90 each time you press the button and set it to currentRotation and toRotation. And set mCurrentRotation to be %360 so that after 1 complete cycle it resets to 0.
Then after this, you need to create a new bitmap with updated angles.

Like below, then start the animation and the imageview will rotate with a smooth animation for easy look and feel.


public void rotate(View view) {
isRotate = true;
mCurrRotation %= 360;
Matrix matrix = new Matrix();


System.out.println(imageView.getRotation());
fromRotation = mCurrRotation;
toRotation = mCurrRotation += 90;

final RotateAnimation rotateAnimation = new RotateAnimation(
fromRotation, toRotation, imageView.getWidth() / 2, imageView.getHeight() / 2);

rotateAnimation.setDuration(1000);
rotateAnimation.setFillAfter(true);


matrix.setRotate(toRotation);
System.out.println(toRotation + "TO ROTATION");
System.out.println(fromRotation + "FROM ROTATION");
if (croppedBitmap != null) {
cropThenRotateBitmap = Bitmap.createBitmap(croppedBitmap, 0, 0, croppedBitmap.getWidth(), croppedBitmap.getHeight(), matrix, true);
} else {
rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}


imageView.startAnimation(rotateAnimation);


}

Now for crop image

For this add the library in the grade
api 'com.theartofdev.edmodo:android-image-cropper:2.8.+'

thats it you're good to use the crop functionality.


public void crop(View view) {

if(rotateBitmap!=null){
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
rotateBitmap.compress(Bitmap.CompressFormat.JPEG, 100, bytes);
String path = MediaStore.Images.Media.insertImage(getContentResolver(), rotateBitmap, imageFileName+".jpg", null);
// System.out.println(Uri.parse(path));
uri=Uri.parse(path);
}
CropImage.activity(uri)
.start(this);
}

using CropImage.activity it will open a crop view where you can set the crop size and then handle it into the onActivityResult method


public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
CropImage.ActivityResult result = CropImage.getActivityResult(data);
if (resultCode == RESULT_OK) {
resultUri = result.getUri();
imageView.setImageURI(resultUri);
// Matrix matrix = new Matrix();
BitmapDrawable bitmapDrawable = (BitmapDrawable) imageView.getDrawable();
System.out.println(imageView.getRotation());
croppedBitmap = bitmapDrawable.getBitmap();
if (isRotate) {
rotateThenCropBitmap = croppedBitmap;
}

} else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
Exception error = result.getError();
}
}
}

so in this get the uri and set it into the imageview.
Then you need to get the bitmap from for the cropped image so extract the bitmap from the imageview using BitmapDrawable

and for undo you just have to null everybitmap and set the image view to the original bitmal and make all the angles 0 so that it goes back to its original rotation.


public void undo(View view) {
Matrix matrix = new Matrix();


toRotation = mCurrRotation += 90;

final RotateAnimation rotateAnimation = new RotateAnimation(
fromRotation, 0, imageView.getWidth() / 2, imageView.getHeight() / 2);

rotateAnimation.setDuration(1000);
rotateAnimation.setFillAfter(true);


matrix.setRotate(toRotation);
System.out.println(toRotation + "TO ROTATION");
System.out.println(fromRotation + "FROM ROTATION");
if (croppedBitmap != null) {
cropThenRotateBitmap = Bitmap.createBitmap(croppedBitmap, 0, 0, croppedBitmap.getWidth(), croppedBitmap.getHeight(), matrix, true);
} else {
rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}

imageView.setImageBitmap(bitmap);
imageView.startAnimation(rotateAnimation);
makeBitmapNull();
}

then lastly save. in here you have to handle all the different scenarios.


@RequiresApi(api = Build.VERSION_CODES.Q)
public void save(View view) throws IOException {


ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, imageFileName + ".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);

ContentResolver resolver = getContentResolver();
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
OutputStream imageOutStream = null;

try {
if (uri == null) {
throw new IOException("Failed to insert MediaStore row");
}

imageOutStream = resolver.openOutputStream(uri);
if (cropThenRotateBitmap != null) {
if (!cropThenRotateBitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream)) {
throw new IOException("Failed to compress bitmap");
}
} else if (rotateThenCropBitmap != null) {
if (!rotateThenCropBitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream)) {
throw new IOException("Failed to compress bitmap");
}
} else if (croppedBitmap != null) {
if (!croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream)) {
throw new IOException("Failed to compress bitmap");
}
} else if (rotateBitmap != null) {
if (!rotateBitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream)) {
throw new IOException("Failed to compress bitmap");
}
} else {
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream)) {
throw new IOException("Failed to compress bitmap");
}
}

Toast.makeText(this, "Imave Saved", Toast.LENGTH_SHORT).show();

} finally {
if (imageOutStream != null) {
imageOutStream.close();
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

finish();
startActivity(intent);
}
}

}

Then you have to take extra care on saving if you have rotated the photo before doing crop or you are cropping then rotating the image. Because all this will effect the bitmap which will finally be saved and displayed in the image view on the main screen.
To solve this was the most challenging part. Sometimes images where not saving, sometimes there comes a previously saved image on the crop button, sometimes a previously saved image would be shown upon saving on the main screen.

To handle all this scenarios i have to make different bitmap for each scenario.

  1. rotate then save
  2. crop then save
  3. rotate then crop then save
  4. crop then rotate then save
  5. rotate undo then save
  6. crop undo then save
  7. save the original image as is
  8. rotate then crop then undo then save
  9. crop then rotate then undo then save

All these scenarios needed to handled to make the app more robust and effective.

Then finally in the onCreate method in the MainActivity set the image view to whatever bitmap is not null


 if (cropThenRotateBitmap != null) {
imageView.setImageBitmap(cropThenRotateBitmap);
textView.setText("Edited Image");
makeBitmapNull();

} else if (rotateThenCropBitmap != null) {
imageView.setImageBitmap(rotateThenCropBitmap);
textView.setText("Edited Image");
makeBitmapNull();

} else if (rotateBitmap != null) {
imageView.setImageBitmap(rotateBitmap);
textView.setText("Edited Image");
makeBitmapNull();
} else if (croppedBitmap != null) {
imageView.setImageBitmap(croppedBitmap);
textView.setText("Edited Image");
makeBitmapNull();
} else if(bitmap!=null) {
imageView.setImageBitmap(bitmap);
textView.setText("Edited Image");
makeBitmapNull();
}

check each condition and according to that set the bitmap.

So with this this tutorial comes to an end.

I learned various things with this assignment, it increased my knowledge about image saving and retriving from the device and use of bitmap and camera. This whole work took me 3 days to finish but the next time i am tasked with a similar requirement it will take me the least amount of time to complete.

Upvote


user
Created by

Uday Garg


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles