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