CropImage.java revision 666ea1b28a76aeba74744148b15099254d918671
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import com.android.camera.gallery.IImage;
20import com.android.camera.gallery.IImageList;
21
22import android.app.WallpaperManager;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.Matrix;
29import android.graphics.Path;
30import android.graphics.PointF;
31import android.graphics.PorterDuff;
32import android.graphics.Rect;
33import android.graphics.RectF;
34import android.graphics.Region;
35import android.media.FaceDetector;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.Handler;
39import android.provider.MediaStore;
40import android.util.AttributeSet;
41import android.util.Log;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.Window;
45import android.view.WindowManager;
46import android.widget.Toast;
47
48import java.io.File;
49import java.io.IOException;
50import java.io.OutputStream;
51import java.util.ArrayList;
52import java.util.concurrent.CountDownLatch;
53
54/**
55 * The activity can crop specific region of interest from an image.
56 */
57public class CropImage extends MonitoredActivity {
58    private static final String TAG = "CropImage";
59
60    // These are various options can be specified in the intent.
61    private Bitmap.CompressFormat mOutputFormat =
62            Bitmap.CompressFormat.JPEG; // only used with mSaveUri
63    private Uri mSaveUri = null;
64    private boolean mSetWallpaper = false;
65    private int mAspectX, mAspectY;
66    private boolean mDoFaceDetection = true;
67    private boolean mCircleCrop = false;
68    private final Handler mHandler = new Handler();
69
70    // These options specifiy the output image size and whether we should
71    // scale the output to fit it (or just crop it).
72    private int mOutputX, mOutputY;
73    private boolean mScale;
74    private boolean mScaleUp = true;
75
76    boolean mWaitingToPick; // Whether we are wait the user to pick a face.
77    boolean mSaving;  // Whether the "save" button is already clicked.
78
79    private CropImageView mImageView;
80    private ContentResolver mContentResolver;
81
82    private Bitmap mBitmap;
83    HighlightView mCrop;
84
85    private IImageList mAllImages;
86    private IImage mImage;
87
88    @Override
89    public void onCreate(Bundle icicle) {
90        super.onCreate(icicle);
91        mContentResolver = getContentResolver();
92
93        requestWindowFeature(Window.FEATURE_NO_TITLE);
94        setContentView(R.layout.cropimage);
95
96        mImageView = (CropImageView) findViewById(R.id.image);
97
98        MenuHelper.showStorageToast(this);
99
100        Intent intent = getIntent();
101        Bundle extras = intent.getExtras();
102
103        if (extras != null) {
104            if (extras.getString("circleCrop") != null) {
105                mCircleCrop = true;
106                mAspectX = 1;
107                mAspectY = 1;
108            }
109            mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT);
110            if (mSaveUri != null) {
111                String outputFormatString = extras.getString("outputFormat");
112                if (outputFormatString != null) {
113                    mOutputFormat = Bitmap.CompressFormat.valueOf(
114                            outputFormatString);
115                }
116            } else {
117                mSetWallpaper = extras.getBoolean("setWallpaper");
118            }
119            mBitmap = (Bitmap) extras.getParcelable("data");
120            mAspectX = extras.getInt("aspectX");
121            mAspectY = extras.getInt("aspectY");
122            mOutputX = extras.getInt("outputX");
123            mOutputY = extras.getInt("outputY");
124            mScale = extras.getBoolean("scale", true);
125            mScaleUp = extras.getBoolean("scaleUpIfNeeded", true);
126            mDoFaceDetection = extras.containsKey("noFaceDetection")
127                    ? !extras.getBoolean("noFaceDetection")
128                    : true;
129        }
130
131        if (mBitmap == null) {
132            Uri target = intent.getData();
133            mAllImages = ImageManager.makeImageList(mContentResolver, target,
134                    ImageManager.SORT_ASCENDING);
135            mImage = mAllImages.getImageForUri(target);
136            if (mImage != null) {
137                // Don't read in really large bitmaps. Use the (big) thumbnail
138                // instead.
139                // TODO when saving the resulting bitmap use the
140                // decode/crop/encode api so we don't lose any resolution.
141                mBitmap = mImage.thumbBitmap(IImage.ROTATE_AS_NEEDED);
142            }
143        }
144
145        if (mBitmap == null) {
146            finish();
147            return;
148        }
149
150        // Make UI fullscreen.
151        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
152
153        findViewById(R.id.discard).setOnClickListener(
154                new View.OnClickListener() {
155                    public void onClick(View v) {
156                        setResult(RESULT_CANCELED);
157                        finish();
158                    }
159                });
160
161        findViewById(R.id.save).setOnClickListener(
162                new View.OnClickListener() {
163                    public void onClick(View v) {
164                        onSaveClicked();
165                    }
166                });
167
168        startFaceDetection();
169    }
170
171    private void startFaceDetection() {
172        if (isFinishing()) {
173            return;
174        }
175
176        mImageView.setImageBitmapResetBase(mBitmap, true);
177
178        Util.startBackgroundJob(this, null,
179                getResources().getString(R.string.runningFaceDetection),
180                new Runnable() {
181            public void run() {
182                final CountDownLatch latch = new CountDownLatch(1);
183                final Bitmap b = (mImage != null)
184                        ? mImage.fullSizeBitmap(IImage.UNCONSTRAINED,
185                        1024 * 1024)
186                        : mBitmap;
187                mHandler.post(new Runnable() {
188                    public void run() {
189                        if (b != mBitmap && b != null) {
190                            mImageView.setImageBitmapResetBase(b, true);
191                            mBitmap.recycle();
192                            mBitmap = b;
193                        }
194                        if (mImageView.getScale() == 1F) {
195                            mImageView.center(true, true);
196                        }
197                        latch.countDown();
198                    }
199                });
200                try {
201                    latch.await();
202                } catch (InterruptedException e) {
203                    throw new RuntimeException(e);
204                }
205                mRunFaceDetection.run();
206            }
207        }, mHandler);
208    }
209
210    private void onSaveClicked() {
211        // TODO this code needs to change to use the decode/crop/encode single
212        // step api so that we don't require that the whole (possibly large)
213        // bitmap doesn't have to be read into memory
214        if (mCrop == null) {
215            return;
216        }
217
218        if (mSaving) return;
219        mSaving = true;
220
221        Bitmap croppedImage;
222
223        // If the output is required to a specific size, create an new image
224        // with the cropped image in the center and the extra space filled.
225        if (mOutputX != 0 && mOutputY != 0 && !mScale) {
226            // Don't scale the image but instead fill it so it's the
227            // required dimension
228            croppedImage = Bitmap.createBitmap(mOutputX, mOutputY,
229                    Bitmap.Config.RGB_565);
230            Canvas canvas = new Canvas(croppedImage);
231
232            Rect srcRect = mCrop.getCropRect();
233            Rect dstRect = new Rect(0, 0, mOutputX, mOutputY);
234
235            int dx = (srcRect.width() - dstRect.width()) / 2;
236            int dy = (srcRect.height() - dstRect.height()) / 2;
237
238            // If the srcRect is too big, use the center part of it.
239            srcRect.inset(Math.max(0, dx), Math.max(0, dy));
240
241            // If the dstRect is too big, use the center part of it.
242            dstRect.inset(Math.max(0, -dx), Math.max(0, -dy));
243
244            // Draw the cropped bitmap in the center
245            canvas.drawBitmap(mBitmap, srcRect, dstRect, null);
246
247            // Release bitmap memory as soon as possible
248            mImageView.clear();
249            mBitmap.recycle();
250        } else {
251            Rect r = mCrop.getCropRect();
252
253            int width = r.width();
254            int height = r.height();
255
256            // If we are circle cropping, we want alpha channel, which is the
257            // third param here.
258            croppedImage = Bitmap.createBitmap(width, height,
259                    mCircleCrop
260                    ? Bitmap.Config.ARGB_8888
261                    : Bitmap.Config.RGB_565);
262
263            Canvas canvas = new Canvas(croppedImage);
264            Rect dstRect = new Rect(0, 0, width, height);
265            canvas.drawBitmap(mBitmap, r, dstRect, null);
266
267            // Release bitmap memory as soon as possible
268            mImageView.clear();
269            mBitmap.recycle();
270
271            if (mCircleCrop) {
272                // OK, so what's all this about?
273                // Bitmaps are inherently rectangular but we want to return
274                // something that's basically a circle.  So we fill in the
275                // area around the circle with alpha.  Note the all important
276                // PortDuff.Mode.CLEAR.
277                Canvas c = new Canvas(croppedImage);
278                Path p = new Path();
279                p.addCircle(width / 2F, height / 2F, width / 2F,
280                        Path.Direction.CW);
281                c.clipPath(p, Region.Op.DIFFERENCE);
282                c.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
283            }
284
285            // If the required dimension is specified, scale the image.
286            if (mOutputX != 0 && mOutputY != 0 && mScale) {
287                croppedImage = Util.transform(new Matrix(), croppedImage,
288                        mOutputX, mOutputY, mScaleUp, Util.RECYCLE_INPUT);
289            }
290        }
291
292        mImageView.setImageBitmapResetBase(croppedImage, true);
293        mImageView.center(true, true);
294        mImageView.mHighlightViews.clear();
295
296        // Return the cropped image directly or save it to the specified URI.
297        Bundle myExtras = getIntent().getExtras();
298        if (myExtras != null && (myExtras.getParcelable("data") != null
299                || myExtras.getBoolean("return-data"))) {
300            Bundle extras = new Bundle();
301            extras.putParcelable("data", croppedImage);
302            setResult(RESULT_OK,
303                    (new Intent()).setAction("inline-data").putExtras(extras));
304            finish();
305        } else {
306            final Bitmap b = croppedImage;
307            final int msdId = mSetWallpaper
308                    ? R.string.wallpaper
309                    : R.string.savingImage;
310            Util.startBackgroundJob(this, null,
311                    getResources().getString(msdId),
312                    new Runnable() {
313                public void run() {
314                    saveOutput(b);
315                }
316            }, mHandler);
317        }
318    }
319
320    private void saveOutput(Bitmap croppedImage) {
321        if (mSaveUri != null) {
322            OutputStream outputStream = null;
323            try {
324                outputStream = mContentResolver.openOutputStream(mSaveUri);
325                if (outputStream != null) {
326                    croppedImage.compress(mOutputFormat, 75, outputStream);
327                }
328            } catch (IOException ex) {
329                // TODO: report error to caller
330                Log.e(TAG, "Cannot open file: " + mSaveUri, ex);
331            } finally {
332                Util.closeSilently(outputStream);
333            }
334            Bundle extras = new Bundle();
335            setResult(RESULT_OK, new Intent(mSaveUri.toString())
336                    .putExtras(extras));
337        } else if (mSetWallpaper) {
338            try {
339                WallpaperManager.getInstance(this).setBitmap(croppedImage);
340                setResult(RESULT_OK);
341            } catch (IOException e) {
342                Log.e(TAG, "Failed to set wallpaper.", e);
343                setResult(RESULT_CANCELED);
344            }
345        } else {
346            Bundle extras = new Bundle();
347            extras.putString("rect", mCrop.getCropRect().toString());
348
349            File oldPath = new File(mImage.getDataPath());
350            File directory = new File(oldPath.getParent());
351
352            int x = 0;
353            String fileName = oldPath.getName();
354            fileName = fileName.substring(0, fileName.lastIndexOf("."));
355
356            // Try file-1.jpg, file-2.jpg, ... until we find a filename which
357            // does not exist yet.
358            while (true) {
359                x += 1;
360                String candidate = directory.toString()
361                        + "/" + fileName + "-" + x + ".jpg";
362                boolean exists = (new File(candidate)).exists();
363                if (!exists) {
364                    break;
365                }
366            }
367
368            try {
369                int[] degree = new int[1];
370                Uri newUri = ImageManager.addImage(
371                        mContentResolver,
372                        mImage.getTitle(),
373                        mImage.getDateTaken(),
374                        null,    // TODO this null is going to cause us to lose
375                                 // the location (gps).
376                        directory.toString(), fileName + "-" + x + ".jpg",
377                        croppedImage, null,
378                        degree);
379
380                setResult(RESULT_OK, new Intent()
381                        .setAction(newUri.toString())
382                        .putExtras(extras));
383            } catch (Exception ex) {
384                // basically ignore this or put up
385                // some ui saying we failed
386                Log.e(TAG, "store image fail, continue anyway", ex);
387            }
388        }
389
390        final Bitmap b = croppedImage;
391        mHandler.post(new Runnable() {
392            public void run() {
393                mImageView.clear();
394                b.recycle();
395            }
396        });
397
398        finish();
399    }
400
401    @Override
402    protected void onPause() {
403        super.onPause();
404    }
405
406    @Override
407    protected void onDestroy() {
408        if (mAllImages != null) {
409            mAllImages.close();
410        }
411        super.onDestroy();
412    }
413
414    Runnable mRunFaceDetection = new Runnable() {
415        @SuppressWarnings("hiding")
416        float mScale = 1F;
417        Matrix mImageMatrix;
418        FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
419        int mNumFaces;
420
421        // For each face, we create a HightlightView for it.
422        private void handleFace(FaceDetector.Face f) {
423            PointF midPoint = new PointF();
424
425            int r = ((int) (f.eyesDistance() * mScale)) * 2;
426            f.getMidPoint(midPoint);
427            midPoint.x *= mScale;
428            midPoint.y *= mScale;
429
430            int midX = (int) midPoint.x;
431            int midY = (int) midPoint.y;
432
433            HighlightView hv = new HighlightView(mImageView);
434
435            int width = mBitmap.getWidth();
436            int height = mBitmap.getHeight();
437
438            Rect imageRect = new Rect(0, 0, width, height);
439
440            RectF faceRect = new RectF(midX, midY, midX, midY);
441            faceRect.inset(-r, -r);
442            if (faceRect.left < 0) {
443                faceRect.inset(-faceRect.left, -faceRect.left);
444            }
445
446            if (faceRect.top < 0) {
447                faceRect.inset(-faceRect.top, -faceRect.top);
448            }
449
450            if (faceRect.right > imageRect.right) {
451                faceRect.inset(faceRect.right - imageRect.right,
452                               faceRect.right - imageRect.right);
453            }
454
455            if (faceRect.bottom > imageRect.bottom) {
456                faceRect.inset(faceRect.bottom - imageRect.bottom,
457                               faceRect.bottom - imageRect.bottom);
458            }
459
460            hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop,
461                     mAspectX != 0 && mAspectY != 0);
462
463            mImageView.add(hv);
464        }
465
466        // Create a default HightlightView if we found no face in the picture.
467        private void makeDefault() {
468            HighlightView hv = new HighlightView(mImageView);
469
470            int width = mBitmap.getWidth();
471            int height = mBitmap.getHeight();
472
473            Rect imageRect = new Rect(0, 0, width, height);
474
475            // make the default size about 4/5 of the width or height
476            int cropWidth = Math.min(width, height) * 4 / 5;
477            int cropHeight = cropWidth;
478
479            if (mAspectX != 0 && mAspectY != 0) {
480                if (mAspectX > mAspectY) {
481                    cropHeight = cropWidth * mAspectY / mAspectX;
482                } else {
483                    cropWidth = cropHeight * mAspectX / mAspectY;
484                }
485            }
486
487            int x = (width - cropWidth) / 2;
488            int y = (height - cropHeight) / 2;
489
490            RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
491            hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop,
492                     mAspectX != 0 && mAspectY != 0);
493            mImageView.add(hv);
494        }
495
496        // Scale the image down for faster face detection.
497        private Bitmap prepareBitmap() {
498            if (mBitmap == null) {
499                return null;
500            }
501
502            // 256 pixels wide is enough.
503            if (mBitmap.getWidth() > 256) {
504                mScale = 256.0F / mBitmap.getWidth();
505            }
506            Matrix matrix = new Matrix();
507            matrix.setScale(mScale, mScale);
508            Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap
509                    .getWidth(), mBitmap.getHeight(), matrix, true);
510            return faceBitmap;
511        }
512
513        public void run() {
514            mImageMatrix = mImageView.getImageMatrix();
515            Bitmap faceBitmap = prepareBitmap();
516
517            mScale = 1.0F / mScale;
518            if (faceBitmap != null && mDoFaceDetection) {
519                FaceDetector detector = new FaceDetector(faceBitmap.getWidth(),
520                        faceBitmap.getHeight(), mFaces.length);
521                mNumFaces = detector.findFaces(faceBitmap, mFaces);
522            }
523
524            if (faceBitmap != null && faceBitmap != mBitmap) {
525                faceBitmap.recycle();
526            }
527
528            mHandler.post(new Runnable() {
529                public void run() {
530                    mWaitingToPick = mNumFaces > 1;
531                    if (mNumFaces > 0) {
532                        for (int i = 0; i < mNumFaces; i++) {
533                            handleFace(mFaces[i]);
534                        }
535                    } else {
536                        makeDefault();
537                    }
538                    mImageView.invalidate();
539                    if (mImageView.mHighlightViews.size() == 1) {
540                        mCrop = mImageView.mHighlightViews.get(0);
541                        mCrop.setFocus(true);
542                    }
543
544                    if (mNumFaces > 1) {
545                        Toast t = Toast.makeText(CropImage.this,
546                                R.string.multiface_crop_help,
547                                Toast.LENGTH_SHORT);
548                        t.show();
549                    }
550                }
551            });
552        }
553    };
554}
555
556class CropImageView extends ImageViewTouchBase {
557    ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>();
558    HighlightView mMotionHighlightView = null;
559    float mLastX, mLastY;
560    int mMotionEdge;
561
562    @Override
563    protected void onLayout(boolean changed, int left, int top,
564                            int right, int bottom) {
565        super.onLayout(changed, left, top, right, bottom);
566        if (mBitmapDisplayed.getBitmap() != null) {
567            for (HighlightView hv : mHighlightViews) {
568                hv.mMatrix.set(getImageMatrix());
569                hv.invalidate();
570                if (hv.mIsFocused) {
571                    centerBasedOnHighlightView(hv);
572                }
573            }
574        }
575    }
576
577    public CropImageView(Context context, AttributeSet attrs) {
578        super(context, attrs);
579    }
580
581    @Override
582    protected void zoomTo(float scale, float centerX, float centerY) {
583        super.zoomTo(scale, centerX, centerY);
584        for (HighlightView hv : mHighlightViews) {
585            hv.mMatrix.set(getImageMatrix());
586            hv.invalidate();
587        }
588    }
589
590    @Override
591    protected void zoomIn() {
592        super.zoomIn();
593        for (HighlightView hv : mHighlightViews) {
594            hv.mMatrix.set(getImageMatrix());
595            hv.invalidate();
596        }
597    }
598
599    @Override
600    protected void zoomOut() {
601        super.zoomOut();
602        for (HighlightView hv : mHighlightViews) {
603            hv.mMatrix.set(getImageMatrix());
604            hv.invalidate();
605        }
606    }
607
608    @Override
609    protected void postTranslate(float deltaX, float deltaY) {
610        super.postTranslate(deltaX, deltaY);
611        for (int i = 0; i < mHighlightViews.size(); i++) {
612            HighlightView hv = mHighlightViews.get(i);
613            hv.mMatrix.postTranslate(deltaX, deltaY);
614            hv.invalidate();
615        }
616    }
617
618    // According to the event's position, change the focus to the first
619    // hitting cropping rectangle.
620    private void recomputeFocus(MotionEvent event) {
621        for (int i = 0; i < mHighlightViews.size(); i++) {
622            HighlightView hv = mHighlightViews.get(i);
623            hv.setFocus(false);
624            hv.invalidate();
625        }
626
627        for (int i = 0; i < mHighlightViews.size(); i++) {
628            HighlightView hv = mHighlightViews.get(i);
629            int edge = hv.getHit(event.getX(), event.getY());
630            if (edge != HighlightView.GROW_NONE) {
631                if (!hv.hasFocus()) {
632                    hv.setFocus(true);
633                    hv.invalidate();
634                }
635                break;
636            }
637        }
638        invalidate();
639    }
640
641    @Override
642    public boolean onTouchEvent(MotionEvent event) {
643        CropImage cropImage = (CropImage) mContext;
644        if (cropImage.mSaving) {
645            return false;
646        }
647
648        switch (event.getAction()) {
649            case MotionEvent.ACTION_DOWN:
650                if (cropImage.mWaitingToPick) {
651                    recomputeFocus(event);
652                } else {
653                    for (int i = 0; i < mHighlightViews.size(); i++) {
654                        HighlightView hv = mHighlightViews.get(i);
655                        int edge = hv.getHit(event.getX(), event.getY());
656                        if (edge != HighlightView.GROW_NONE) {
657                            mMotionEdge = edge;
658                            mMotionHighlightView = hv;
659                            mLastX = event.getX();
660                            mLastY = event.getY();
661                            mMotionHighlightView.setMode(
662                                    (edge == HighlightView.MOVE)
663                                    ? HighlightView.ModifyMode.Move
664                                    : HighlightView.ModifyMode.Grow);
665                            break;
666                        }
667                    }
668                }
669                break;
670            case MotionEvent.ACTION_UP:
671                if (cropImage.mWaitingToPick) {
672                    for (int i = 0; i < mHighlightViews.size(); i++) {
673                        HighlightView hv = mHighlightViews.get(i);
674                        if (hv.hasFocus()) {
675                            cropImage.mCrop = hv;
676                            for (int j = 0; j < mHighlightViews.size(); j++) {
677                                if (j == i) {
678                                    continue;
679                                }
680                                mHighlightViews.get(j).setHidden(true);
681                            }
682                            centerBasedOnHighlightView(hv);
683                            ((CropImage) mContext).mWaitingToPick = false;
684                            return true;
685                        }
686                    }
687                } else if (mMotionHighlightView != null) {
688                    centerBasedOnHighlightView(mMotionHighlightView);
689                    mMotionHighlightView.setMode(
690                            HighlightView.ModifyMode.None);
691                }
692                mMotionHighlightView = null;
693                break;
694            case MotionEvent.ACTION_MOVE:
695                if (cropImage.mWaitingToPick) {
696                    recomputeFocus(event);
697                } else if (mMotionHighlightView != null) {
698                    mMotionHighlightView.handleMotion(mMotionEdge,
699                            event.getX() - mLastX,
700                            event.getY() - mLastY);
701                    mLastX = event.getX();
702                    mLastY = event.getY();
703
704                    if (true) {
705                        // This section of code is optional. It has some user
706                        // benefit in that moving the crop rectangle against
707                        // the edge of the screen causes scrolling but it means
708                        // that the crop rectangle is no longer fixed under
709                        // the user's finger.
710                        ensureVisible(mMotionHighlightView);
711                    }
712                }
713                break;
714        }
715
716        switch (event.getAction()) {
717            case MotionEvent.ACTION_UP:
718                center(true, true);
719                break;
720            case MotionEvent.ACTION_MOVE:
721                // if we're not zoomed then there's no point in even allowing
722                // the user to move the image around.  This call to center puts
723                // it back to the normalized location (with false meaning don't
724                // animate).
725                if (getScale() == 1F) {
726                    center(true, true);
727                }
728                break;
729        }
730
731        return true;
732    }
733
734    // Pan the displayed image to make sure the cropping rectangle is visible.
735    private void ensureVisible(HighlightView hv) {
736        Rect r = hv.mDrawRect;
737
738        int panDeltaX1 = Math.max(0, mLeft - r.left);
739        int panDeltaX2 = Math.min(0, mRight - r.right);
740
741        int panDeltaY1 = Math.max(0, mTop - r.top);
742        int panDeltaY2 = Math.min(0, mBottom - r.bottom);
743
744        int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
745        int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
746
747        if (panDeltaX != 0 || panDeltaY != 0) {
748            panBy(panDeltaX, panDeltaY);
749        }
750    }
751
752    // If the cropping rectangle's size changed significantly, change the
753    // view's center and scale according to the cropping rectangle.
754    private void centerBasedOnHighlightView(HighlightView hv) {
755        Rect drawRect = hv.mDrawRect;
756
757        float width = drawRect.width();
758        float height = drawRect.height();
759
760        float thisWidth = getWidth();
761        float thisHeight = getHeight();
762
763        float z1 = thisWidth / width * .6F;
764        float z2 = thisHeight / height * .6F;
765
766        float zoom = Math.min(z1, z2);
767        zoom = zoom * this.getScale();
768        zoom = Math.max(1F, zoom);
769
770        if ((Math.abs(zoom - getScale()) / zoom) > .1) {
771            float [] coordinates = new float[] {hv.mCropRect.centerX(),
772                                                hv.mCropRect.centerY()};
773            getImageMatrix().mapPoints(coordinates);
774            zoomTo(zoom, coordinates[0], coordinates[1], 300F);
775        }
776
777        ensureVisible(hv);
778    }
779
780    @Override
781    protected void onDraw(Canvas canvas) {
782        super.onDraw(canvas);
783        for (int i = 0; i < mHighlightViews.size(); i++) {
784            mHighlightViews.get(i).draw(canvas);
785        }
786    }
787
788    public void add(HighlightView hv) {
789        mHighlightViews.add(hv);
790        invalidate();
791    }
792}
793