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