Crop image via rectangle or circle shape
In this section I'll show how to crop image via rectangle shape using Android-Image-Cropper and image from camera.
To get picture from Camera and write access to disk I'm going to use EasyPermissions.
Add the below line in your module’s build.gradle file:
dependencies { ... implementation 'pub.devrel:easypermissions:1.2.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.7.0' }
Add permissions and CropImageActivity
into your AndroidManifest.xml
<uses-feature android:name="android.hardware.camera" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application ..> <activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity" android:theme="@style/Base.Theme.AppCompat"/> </application>
Following is layout for MainActivity.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/btn" android:onClick="take" android:text="Take" android:layout_centerHorizontal="true" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:id="@+id/iv" android:layout_below="@id/btn" android:layout_centerHorizontal="true" android:layout_width="400dp" android:layout_height="400dp" /> </RelativeLayout>
Following is MainActivity.
public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks { private ImageView iv; private File file; private Uri uri; private Activity activity = MainActivity.this; private String TAG = MainActivity.class.getSimpleName(); private String PERM_RATIONALE = "This app needs access to your camera."; private static final int RC_SETTINGS = 126; private static final int RC_PERM_CAMERA_STORAGE = 127; private static final int CAMERA_TAKE_REQUEST = 200; private String[] wantedPerms = {Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iv = findViewById(R.id.iv); if (!EasyPermissions.hasPermissions(activity, wantedPerms)) { EasyPermissions.requestPermissions(activity, PERM_RATIONALE, RC_PERM_CAMERA_STORAGE, wantedPerms); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch(requestCode){ case CAMERA_TAKE_REQUEST: CropImage.activity(android.net.Uri.parse(file.toURI().toString())) .setAspectRatio(1,1) .setFixAspectRatio(true) .start(activity); break; case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE: CropImage.ActivityResult result = CropImage.getActivityResult(data); if (resultCode == RESULT_OK) { iv.setImageURI(result.getUri()); } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { Exception error = result.getError(); Log.d(TAG, "onActivityResult: " + error.getMessage()); } break; } } // // Permissions // @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); // Forward results to EasyPermissions EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); } @Override public void onPermissionsGranted(int requestCode, List perms) { Log.d(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size()); } @Override public void onPermissionsDenied(int requestCode, List perms) { Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size()); if (EasyPermissions.somePermissionPermanentlyDenied(activity, perms)) { new AppSettingsDialog.Builder(activity) .setTitle("Permissions Required") .setPositiveButton("Settings") .setNegativeButton("Cancel") .setRequestCode(RC_SETTINGS) .build() .show(); } } @AfterPermissionGranted(RC_PERM_CAMERA_STORAGE) private void afterCameraStoragePermission() { if (EasyPermissions.hasPermissions(this, wantedPerms)) { Log.d(TAG, "Already have permission, do the thing"); } else { Log.d(TAG, "Do not have permission, request them now"); EasyPermissions.requestPermissions(activity, PERM_RATIONALE, RC_PERM_CAMERA_STORAGE, wantedPerms); } } // // Camera // @TargetApi(Build.VERSION_CODES.M) public void take(View v) { if(checkCameraExists()) { if (EasyPermissions.hasPermissions(activity, wantedPerms)) { launchCamera(); } else { EasyPermissions.requestPermissions(activity, PERM_RATIONALE, RC_PERM_CAMERA_STORAGE, wantedPerms); } } else { Toast.makeText(activity, "Camera not available.", Toast.LENGTH_SHORT).show(); } } public boolean checkCameraExists() { return activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); } private void launchCamera() { Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); file = new File(Environment.getExternalStorageDirectory(), String.valueOf(System.currentTimeMillis()) + ".jpg"); uri = FileProvider.getUriForFile(activity, activity.getApplicationContext().getPackageName() + ".provider", file); intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, uri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivityForResult(intent, CAMERA_TAKE_REQUEST); } }
Android 7.0 Nougat introduced some file system permission changes in order to improve security. If you’ve already updated your app to targetSdkVersion 24
(or higher) and you’re passing a file://
URI outside your package domain through an Intent
, then what you’ll get is a FileUriExposedException
.
So, Android may throw FileUriExposedException
in Android 7.0 (API level 24) and above, this exception will come when you will expose a file://
URIs outside your package domain through Intent
.
FileProvider
is a special subclass of ContentProvider which allows us to securely share file through a content://
URI instead of file://
one. Why is this a better approach? Because you’re granting a temporary access to the file, which will be available for the receiver activity or service until they are active/running.
We create our own class inheriting FileProvider
in order to make sure our FileProvider
doesn't conflict with FileProviders
declared in imported dependencies as described here.
Add a class extending FileProvider
public class GenericFileProvider extends FileProvider {}
Next, add the GenericFileProvider
in our AndroidManifest.xml:
<manifest> ... <application> ... <provider android:name=".GenericFileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_provider_paths" /> </provider> ... </application> </manifest>
We’re going to set android:exported
to false
because we don’t need it to be public, android:grantUriPermissions
to true
because it will grant temporary access to files and android:authorities
to a domain you control, so if your domain is me.proft.superapp
then you can use something like me.proft.superapp.provider
. The authority of a provider should be unique and that’s the reason why we are using our application ID plus something like .provider.
Then we need to create the file_provider_path
in the res/xml
folder. That’s the file which defines the folders which contain the files you will be allowed to share safely. In our case we just need access to the external storage folder:
<?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external_files" path="." /> </paths>
The final step is to change the line of code below in
File file = new File(Environment.getExternalStorageDirectory(), String.valueOf(System.currentTimeMillis()) + ".jpg"); Uri photoURI = Uri.fromFile(file);
to
Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", file);
If you're using an Intent
to make the system open your file, you may need to add the following line of code:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Instead of using Uri.fromFile(file)
we create our URI with FileProvider.getUriForFile(context, string, file)
which will generate a new content://
URI with the authority defined pointing to the file we passed in.
Result
Other useful libs for image crop:
Crop image via custom shape from bitmap
Sometimes you want to crop a bitmap, but it’s not an ordinary cropping, it’s a cropping to get rounded corners, cropping to get it as a circle, cropping to get it as a star, or cropping to get it as any shape.
In this post, we will crop a heart from an Android bitmap.
At first get a bitmap to crop a shape from it.
Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.landscape);
Create an empty and mutable bitmap with the same height and width of the source.
Bitmap output = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888);
Create a canvas with the mutable bitmap to draw into.
Canvas canvas = new Canvas(output);
Create a paint with any solid color, this color is for drawing a heart which you want to crop from the bitmap.
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(0XFF000000);
Draw a heart path at the center of the canvas.
canvas.drawPath(getPath(src), paint);
In our case, the heart shape which you have drawn is called the destination image, it’s the shape which you want to crop from the bitmap.
The magic will be in the next line of code, set the transfer mode which defines how source pixels are composited or merged with the destination pixels.
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
Draw the source image on the canvas which has the destination image and use the paint with the SRC_IN
transformation mode.
canvas.drawBitmap(src, 0, 0, paint);
Following is MainActivity
class.
public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks { private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iv = findViewById(R.id.iv); Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.landscape); Bitmap output = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(output); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(0XFF000000); canvas.drawPath(getPath(src), paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(src, 0, 0, paint); iv.setImageBitmap(output); } private Path getPath(Bitmap src) { return resizePath(PathParser.createPathFromPathData(getString(R.string.heart)), src.getWidth(), src.getHeight()); } private Path resizePath(Path path, float width, float height) { RectF bounds = new RectF(0, 0, width, height); Path resizedPath = new Path(path); RectF src = new RectF(); resizedPath.computeBounds(src, true); Matrix resizeMatrix = new Matrix(); resizeMatrix.setRectToRect(src, bounds, Matrix.ScaleToFit.CENTER); resizedPath.transform(resizeMatrix); return resizedPath; } }
Following is strings.xml.
<resources> <string name="heart"> M25.119,2.564c12.434,0.023,18.68,5.892,24.88,17.612 c6.2-11.721,12.446- 17.589,24.877-17.612c13.81-0.025,25.035,10.575,25.061,23.66c0.033,23.708-24.952, 47.46-49.938,71.212 C25.016,73.685,0.03,49.932,0.064,26.224C0.085, 13.14,11.309,2.539,25.119,2.564z </string> </resources>
You can get PathParser
here.
Result