Wednesday, January 4, 2012

Select and Crop Image on Android


Sometimes when creating an Android app that includes user profile picture or avatar, we need to include a feature that enables users to select and crop image to update their profile picture. OnAndroid we can accomplish that by using intent to open image cropper app. To select an image from files, we can pass an intent to image gallery or file manager app then pass the selected image path to camera app to crop the image. It is also the same if we want to take a picture from camera, by passing an intent to camera app to open the camera, take a picture than save it to specified Uri then crop it.
I’ve created a sample project to show how to select and crop image from files or from camera. The source files can be downloaded from my github repository (see the bottom if this post).
MainActivity.java
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
final String [] items        = new String [] {"Take from camera", "Select from Gallery"};
 ArrayAdapter<String> adapter = new ArrayAdapter<String> (this, android.R.layout.select_dialog_item,items);
 AlertDialog.Builder builder  = new AlertDialog.Builder(this);

 builder.setTitle("Select Image");
 builder.setAdapter( adapter, new DialogInterface.OnClickListener() {
    public void onClick( DialogInterface dialog, int item ) { //pick from camer
    if (item == 0) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

        mImageCaptureUri = Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
                 "tmp_avatar_" + String.valueOf(System.currentTimeMillis()) + ".jpg"));

        intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, mImageCaptureUri);

        try {
        intent.putExtra("return-data", true);

        startActivityForResult(intent, PICK_FROM_CAMERA);
        } catch (ActivityNotFoundException e) {
        e.printStackTrace();
        }
       } else { //pick from file
       Intent intent = new Intent();

       intent.setType("image/*");
       intent.setAction(Intent.ACTION_GET_CONTENT);

       startActivityForResult(Intent.createChooser(intent, "Complete action using"), PICK_FROM_FILE);
       }
    }
 } );

final AlertDialog dialog = builder.create();

Button button   = (Button) findViewById(R.id.btn_crop);
mImageView  = (ImageView) findViewById(R.id.iv_photo);

button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    dialog.show();
   }
});
Line 1: In this example, i use a selector dialog to display two image source options, from camera ‘Take from camera’ and from existing files ‘Select from gallery’

 

Line 9: To take a photo from camera, pass intent action ‘MediaStore.ACTION_IMAGE_CAPTURE‘ to open the camera app.
Line 11: Also specify the Uri to save the image on specified path and file name. Note that this Uri variable also used by gallery app to hold the selected image path.
Line 24-29: To select an image from existing files, use Intent.createChooser to open image chooser. Android will automatically display a list of supported applications, such as image gallery or file manager.
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
   if (resultCode != RESULT_OK) return;
       switch (requestCode) {
          case PICK_FROM_CAMERA:
         doCrop();
             break;

          case PICK_FROM_FILE:
         mImageCaptureUri = data.getData();

             doCrop();

             break;
Line 10: After taking a picture, do the crop
Line 6: After selecting image from files, save the selected path
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private void doCrop() {
        final ArrayList<CropOption> cropOptions = new ArrayList<CropOption>();

        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setType("image/*");

        List<ResolveInfo> list = getPackageManager().queryIntentActivities( intent, 0 );

        int size = list.size();

        if (size == 0) {
            Toast.makeText(this, "Can not find image crop app", Toast.LENGTH_SHORT).show();

            return;
        } else {
            intent.setData(mImageCaptureUri);

            intent.putExtra("outputX", 200);
            intent.putExtra("outputY", 200);
            intent.putExtra("aspectX", 1);
            intent.putExtra("aspectY", 1);
            intent.putExtra("scale", true);
            intent.putExtra("return-data", true);

            if (size == 1) {
                Intent i        = new Intent(intent);
                ResolveInfo res = list.get(0);

                i.setComponent( new ComponentName(res.activityInfo.packageName, res.activityInfo.name));

                startActivityForResult(i, CROP_FROM_CAMERA);
            } else {
                for (ResolveInfo res : list) {
                    final CropOption co = new CropOption();

                    co.title    = getPackageManager().getApplicationLabel(res.activityInfo.applicationInfo);
                    co.icon     = getPackageManager().getApplicationIcon(res.activityInfo.applicationInfo);
                    co.appIntent= new Intent(intent);

                    co.appIntent.setComponent( new ComponentName(res.activityInfo.packageName, res.activityInfo.name));

                    cropOptions.add(co);
                }

                CropOptionAdapter adapter = new CropOptionAdapter(getApplicationContext(), cropOptions);

                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setTitle("Choose Crop App");
                builder.setAdapter( adapter, new DialogInterface.OnClickListener() {
                    public void onClick( DialogInterface dialog, int item ) {
                        startActivityForResult( cropOptions.get(item).appIntent, CROP_FROM_CAMERA);
                    }
                });

                builder.setOnCancelListener( new DialogInterface.OnCancelListener() {
                    @Override
                    public void onCancel( DialogInterface dialog ) {

                        if (mImageCaptureUri != null ) {
                            getContentResolver().delete(mImageCaptureUri, null, null );
                            mImageCaptureUri = null;
                        }
                    }
                } );

                AlertDialog alert = builder.create();

                alert.show();
            }
        }
    }
Line 4: Open image crop app by starting an intent ‘com.android.camera.action.CROP‘.
Line 7: Check if there is image cropper app installed.
Line 11: If there is no image cropper app, display warning message
Line 16-23: Specify the image path, crop dimension and scale
Line 25: There is posibility when more than one image cropper app exist, so we have to check for it first. If there is only one app, open then app.
Line 33-68. If there are several app exist, create a custom chooser to let user selects the app.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode != RESULT_OK) return;

        switch (requestCode) {

            case CROP_FROM_CAMERA:
                Bundle extras = data.getExtras();

                if (extras != null) {
                    Bitmap photo = extras.getParcelable("data");

                    mImageView.setImageBitmap(photo);
                }

                File f = new File(mImageCaptureUri.getPath());

                if (f.exists()) f.delete();

                break;
Line 11: After cropping the image, get the bitmap of the cropped image and display it on imageview.
Line 16: Delete the temporary image


3 comments:

  1. "com.android.camera.action.CROP" isn't part of the public API - where do you get it from? Or is this only good for pre-2.1 Android?

    ReplyDelete
  2. I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
    We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
    maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
    harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
    efficient.
    I have a couple suggestions that might make it work better:
    1. Increase the height of the window the cover image is being displayed.
    2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
    3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
    shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
    4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
    in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
    element already)

    ReplyDelete
  3. I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
    We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
    maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
    harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
    efficient.
    I have a couple suggestions that might make it work better:
    1. Increase the height of the window the cover image is being displayed.
    2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
    3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
    shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
    4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
    in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
    element already)

    ReplyDelete