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