• 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.gallery.R;
20 
21 import com.android.camera.gallery.IImage;
22 import com.android.camera.gallery.IImageList;
23 
24 import android.app.WallpaperManager;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.graphics.Bitmap;
29 import android.graphics.Canvas;
30 import android.graphics.Matrix;
31 import android.graphics.Path;
32 import android.graphics.PointF;
33 import android.graphics.PorterDuff;
34 import android.graphics.Rect;
35 import android.graphics.RectF;
36 import android.graphics.Region;
37 import android.media.FaceDetector;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.Handler;
41 import android.provider.MediaStore;
42 import android.util.AttributeSet;
43 import android.util.Log;
44 import android.view.MotionEvent;
45 import android.view.View;
46 import android.view.Window;
47 import android.view.WindowManager;
48 import android.widget.Toast;
49 
50 import java.io.File;
51 import java.io.IOException;
52 import java.io.OutputStream;
53 import java.util.ArrayList;
54 import java.util.concurrent.CountDownLatch;
55 
56 /**
57  * The activity can crop specific region of interest from an image.
58  */
59 public 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
onCreate(Bundle icicle)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 
startFaceDetection()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 
onSaveClicked()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 
saveOutput(Bitmap croppedImage)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
onPause()404     protected void onPause() {
405         super.onPause();
406     }
407 
408     @Override
onDestroy()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 
558 class CropImageView extends ImageViewTouchBase {
559     ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>();
560     HighlightView mMotionHighlightView = null;
561     float mLastX, mLastY;
562     int mMotionEdge;
563 
564     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)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 
CropImageView(Context context, AttributeSet attrs)579     public CropImageView(Context context, AttributeSet attrs) {
580         super(context, attrs);
581     }
582 
583     @Override
zoomTo(float scale, float centerX, float centerY)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
zoomIn()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
zoomOut()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
postTranslate(float deltaX, float deltaY)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.
recomputeFocus(MotionEvent event)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
onTouchEvent(MotionEvent event)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.
ensureVisible(HighlightView hv)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.
centerBasedOnHighlightView(HighlightView hv)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
onDraw(Canvas canvas)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 
add(HighlightView hv)790     public void add(HighlightView hv) {
791         mHighlightViews.add(hv);
792         invalidate();
793     }
794 }
795