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