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