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.cooliris.media; 18 19 import android.app.Activity; 20 import android.app.ProgressDialog; 21 import android.content.ContentResolver; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.database.Cursor; 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.ExifInterface; 36 import android.media.FaceDetector; 37 import android.media.MediaScannerConnection; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.provider.MediaStore; 42 import android.provider.MediaStore.Images.ImageColumns; 43 import android.util.AttributeSet; 44 import android.util.Log; 45 import android.view.MotionEvent; 46 import android.view.View; 47 import android.view.Window; 48 import android.view.WindowManager; 49 import android.widget.Toast; 50 51 import java.io.File; 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.io.OutputStream; 55 import java.net.URISyntaxException; 56 import java.util.ArrayList; 57 import java.util.HashMap; 58 import java.util.concurrent.CountDownLatch; 59 60 import com.cooliris.app.App; 61 import com.cooliris.app.Res; 62 63 /** 64 * The activity can crop specific region of interest from an image. 65 */ 66 public class CropImage extends MonitoredActivity { 67 private static final String TAG = "CropImage"; 68 69 public static final int CROP_MSG = 10; 70 public static final int CROP_MSG_INTERNAL = 100; 71 72 private App mApp = null; 73 74 // These are various options can be specified in the intent. 75 private Bitmap.CompressFormat mOutputFormat = Bitmap.CompressFormat.JPEG; // only 76 // used 77 // with 78 // mSaveUri 79 private Uri mSaveUri = null; 80 private int mAspectX, mAspectY; // CR: two definitions per line == sad 81 // panda. 82 private boolean mDoFaceDetection = true; 83 private boolean mCircleCrop = false; 84 private final Handler mHandler = new Handler(); 85 86 // These options specifiy the output image size and whether we should 87 // scale the output to fit it (or just crop it). 88 private int mOutputX, mOutputY; 89 private boolean mScale; 90 private boolean mScaleUp = true; 91 92 boolean mWaitingToPick; // Whether we are wait the user to pick a face. 93 boolean mSaving; // Whether the "save" button is already clicked. 94 95 private CropImageView mImageView; 96 private ContentResolver mContentResolver; 97 98 private Bitmap mBitmap; 99 private MediaItem mItem; 100 private final BitmapManager.ThreadSet mDecodingThreads = new BitmapManager.ThreadSet(); 101 HighlightView mCrop; 102 103 static private final HashMap<Context, MediaScannerConnection> mConnectionMap = new HashMap<Context, MediaScannerConnection>(); 104 105 static public void launchCropperOrFinish(final Context context, final MediaItem item) { 106 final Bundle myExtras = ((Activity) context).getIntent().getExtras(); 107 String cropValue = myExtras != null ? myExtras.getString("crop") : null; 108 final String contentUri = item.mContentUri; 109 if (contentUri == null) 110 return; 111 if (cropValue != null) { 112 Bundle newExtras = new Bundle(); 113 if (cropValue.equals("circle")) { 114 newExtras.putString("circleCrop", "true"); 115 } 116 Intent cropIntent = new Intent(); 117 cropIntent.setData(Uri.parse(contentUri)); 118 cropIntent.setClass(context, CropImage.class); 119 cropIntent.putExtras(newExtras); 120 // Pass through any extras that were passed in. 121 cropIntent.putExtras(myExtras); 122 ((Activity) context).startActivityForResult(cropIntent, CropImage.CROP_MSG); 123 } else { 124 if (contentUri.startsWith("http://")) { 125 // This is a http uri, we must save it locally first and 126 // generate a content uri from it. 127 final ProgressDialog dialog = ProgressDialog.show(context, context.getResources().getString(Res.string.initializing), 128 context.getResources().getString(Res.string.running_face_detection), true, false); 129 if (contentUri != null) { 130 MediaScannerConnection.MediaScannerConnectionClient client = new MediaScannerConnection.MediaScannerConnectionClient() { 131 public void onMediaScannerConnected() { 132 MediaScannerConnection connection = mConnectionMap.get(context); 133 if (connection != null) { 134 try { 135 final String path = UriTexture.writeHttpDataInDirectory(context, contentUri, 136 LocalDataSource.DOWNLOAD_BUCKET_NAME); 137 if (path != null) { 138 connection.scanFile(path, item.mMimeType); 139 } else { 140 shutdown(""); 141 } 142 } catch (Exception e) { 143 shutdown(""); 144 } 145 } 146 } 147 148 public void onScanCompleted(String path, Uri uri) { 149 shutdown(uri.toString()); 150 } 151 152 public void shutdown(String uri) { 153 dialog.dismiss(); 154 performReturn(context, myExtras, uri.toString()); 155 MediaScannerConnection connection = mConnectionMap.get(context); 156 if (connection != null) { 157 connection.disconnect(); 158 mConnectionMap.put(context, null); 159 } 160 } 161 }; 162 MediaScannerConnection connection = new MediaScannerConnection(context, client); 163 mConnectionMap.put(context, connection); 164 connection.connect(); 165 } 166 } else { 167 performReturn(context, myExtras, contentUri); 168 } 169 } 170 } 171 172 static private void performReturn(Context context, Bundle myExtras, String contentUri) { 173 Intent result = new Intent(null, Uri.parse(contentUri)); 174 boolean resultSet = false; 175 if (myExtras != null) { 176 final Uri outputUri = (Uri)myExtras.getParcelable(MediaStore.EXTRA_OUTPUT); 177 if (outputUri != null) { 178 Bundle extras = new Bundle(); 179 OutputStream outputStream = null; 180 try { 181 outputStream = context.getContentResolver().openOutputStream(outputUri); 182 if (outputStream != null) { 183 InputStream inputStream = context.getContentResolver().openInputStream(Uri.parse(contentUri)); 184 Utils.copyStream(inputStream, outputStream); 185 Util.closeSilently(inputStream); 186 } 187 ((Activity) context).setResult(Activity.RESULT_OK, new Intent(outputUri.toString()) 188 .putExtras(extras)); 189 resultSet = true; 190 } catch (Exception ex) { 191 Log.e(TAG, "Cannot save to uri " + outputUri.toString()); 192 } finally { 193 Util.closeSilently(outputStream); 194 } 195 } 196 } 197 if (!resultSet && myExtras != null && myExtras.getBoolean("return-data")) { 198 // The size of a transaction should be below 100K. 199 Bitmap bitmap = null; 200 try { 201 bitmap = UriTexture.createFromUri(context, contentUri, 1024, 1024, 0, null); 202 } catch (IOException e) { 203 ; 204 } catch (URISyntaxException e) { 205 ; 206 } 207 if (bitmap != null) { 208 result.putExtra("data", bitmap); 209 } 210 } 211 if (!resultSet) 212 ((Activity) context).setResult(Activity.RESULT_OK, result); 213 ((Activity) context).finish(); 214 } 215 216 @Override 217 public void onCreate(Bundle icicle) { 218 super.onCreate(icicle); 219 mApp = new App(CropImage.this); 220 mContentResolver = getContentResolver(); 221 requestWindowFeature(Window.FEATURE_NO_TITLE); 222 setContentView(Res.layout.cropimage); 223 224 mImageView = (CropImageView) findViewById(Res.id.image); 225 226 // CR: remove TODO's. 227 // TODO: we may need to show this indicator for the main gallery 228 // application 229 // MenuHelper.showStorageToast(this); 230 231 Intent intent = getIntent(); 232 Bundle extras = intent.getExtras(); 233 234 if (extras != null) { 235 if (extras.getString("circleCrop") != null) { 236 mCircleCrop = true; 237 mAspectX = 1; 238 mAspectY = 1; 239 } 240 mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT); 241 if (mSaveUri != null) { 242 String outputFormatString = extras.getString("outputFormat"); 243 if (outputFormatString != null) { 244 mOutputFormat = Bitmap.CompressFormat.valueOf(outputFormatString); 245 } 246 } 247 mBitmap = (Bitmap) extras.getParcelable("data"); 248 mAspectX = extras.getInt("aspectX"); 249 mAspectY = extras.getInt("aspectY"); 250 mOutputX = extras.getInt("outputX"); 251 mOutputY = extras.getInt("outputY"); 252 mScale = extras.getBoolean("scale", true); 253 mScaleUp = extras.getBoolean("scaleUpIfNeeded", true); 254 mDoFaceDetection = extras.containsKey("noFaceDetection") ? !extras.getBoolean("noFaceDetection") : true; 255 } 256 257 if (mBitmap == null) { 258 // Create a MediaItem representing the URI. 259 Uri target = intent.getData(); 260 String targetScheme = target.getScheme(); 261 int rotation = 0; 262 263 if (targetScheme.equals("content")) { 264 mItem = LocalDataSource.createMediaItemFromUri(this, target, MediaItem.MEDIA_TYPE_IMAGE); 265 } 266 try { 267 if (mItem != null) { 268 mBitmap = UriTexture.createFromUri(this, mItem.mContentUri, 1024, 1024, 0, null); 269 rotation = (int) mItem.mRotation; 270 } else { 271 mBitmap = UriTexture.createFromUri(this, target.toString(), 1024, 1024, 0, null); 272 if (targetScheme.equals("file")) { 273 ExifInterface exif = new ExifInterface(target.getPath()); 274 rotation = (int) Shared.exifOrientationToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 275 ExifInterface.ORIENTATION_NORMAL)); 276 } 277 } 278 } catch (IOException e) { 279 } catch (URISyntaxException e) { 280 } 281 282 if (mBitmap != null && rotation != 0f) { 283 mBitmap = Util.rotate(mBitmap, rotation); 284 } 285 } 286 287 if (mBitmap == null) { 288 Log.e(TAG, "Cannot load bitmap, exiting."); 289 finish(); 290 return; 291 } 292 293 // Make UI fullscreen. 294 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 295 296 findViewById(Res.id.discard).setOnClickListener(new View.OnClickListener() { 297 public void onClick(View v) { 298 setResult(RESULT_CANCELED); 299 finish(); 300 } 301 }); 302 303 findViewById(Res.id.save).setOnClickListener(new View.OnClickListener() { 304 public void onClick(View v) { 305 onSaveClicked(); 306 } 307 }); 308 309 startFaceDetection(); 310 } 311 312 private void startFaceDetection() { 313 if (isFinishing()) { 314 return; 315 } 316 317 mImageView.setImageBitmapResetBase(mBitmap, true); 318 319 Util.startBackgroundJob(this, null, getResources().getString(Res.string.running_face_detection), new Runnable() { 320 public void run() { 321 final CountDownLatch latch = new CountDownLatch(1); 322 final Bitmap b = mBitmap; 323 mHandler.post(new Runnable() { 324 public void run() { 325 if (b != mBitmap && b != null) { 326 mImageView.setImageBitmapResetBase(b, true); 327 mBitmap.recycle(); 328 mBitmap = b; 329 } 330 if (mImageView.getScale() == 1.0f) { 331 mImageView.center(true, true); 332 } 333 latch.countDown(); 334 } 335 }); 336 try { 337 latch.await(); 338 } catch (InterruptedException e) { 339 throw new RuntimeException(e); 340 } 341 mRunFaceDetection.run(); 342 } 343 }, mHandler); 344 } 345 346 private void onSaveClicked() { 347 // CR: TODO! 348 // TODO this code needs to change to use the decode/crop/encode single 349 // step api so that we don't require that the whole (possibly large) 350 // bitmap doesn't have to be read into memory 351 if (mSaving) 352 return; 353 354 if (mCrop == null) { 355 return; 356 } 357 358 mSaving = true; 359 360 Rect r = mCrop.getCropRect(); 361 362 int width = r.width(); // CR: final == happy panda! 363 int height = r.height(); 364 365 // If we are circle cropping, we want alpha channel, which is the 366 // third param here. 367 Bitmap croppedImage = Bitmap.createBitmap(width, height, mCircleCrop ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); 368 { 369 Canvas canvas = new Canvas(croppedImage); 370 Rect dstRect = new Rect(0, 0, width, height); 371 canvas.drawBitmap(mBitmap, r, dstRect, null); 372 } 373 374 if (mCircleCrop) { 375 // OK, so what's all this about? 376 // Bitmaps are inherently rectangular but we want to return 377 // something that's basically a circle. So we fill in the 378 // area around the circle with alpha. Note the all important 379 // PortDuff.Mode.CLEARes. 380 Canvas c = new Canvas(croppedImage); 381 Path p = new Path(); 382 p.addCircle(width / 2F, height / 2F, width / 2F, Path.Direction.CW); 383 c.clipPath(p, Region.Op.DIFFERENCE); 384 c.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 385 } 386 387 // If the output is required to a specific size then scale or fill. 388 if (mOutputX != 0 && mOutputY != 0) { 389 if (mScale) { 390 // Scale the image to the required dimensions. 391 Bitmap old = croppedImage; 392 croppedImage = Util.transform(new Matrix(), croppedImage, mOutputX, mOutputY, mScaleUp); 393 if (old != croppedImage) { 394 old.recycle(); 395 } 396 } else { 397 398 /* 399 * Don't scale the image crop it to the size requested. Create 400 * an new image with the cropped image in the center and the 401 * extra space filled. 402 */ 403 404 // Don't scale the image but instead fill it so it's the 405 // required dimension 406 Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY, Bitmap.Config.RGB_565); 407 Canvas canvas = new Canvas(b); 408 409 Rect srcRect = mCrop.getCropRect(); 410 Rect dstRect = new Rect(0, 0, mOutputX, mOutputY); 411 412 int dx = (srcRect.width() - dstRect.width()) / 2; 413 int dy = (srcRect.height() - dstRect.height()) / 2; 414 415 // If the srcRect is too big, use the center part of it. 416 srcRect.inset(Math.max(0, dx), Math.max(0, dy)); 417 418 // If the dstRect is too big, use the center part of it. 419 dstRect.inset(Math.max(0, -dx), Math.max(0, -dy)); 420 421 // Draw the cropped bitmap in the center. 422 canvas.drawBitmap(mBitmap, srcRect, dstRect, null); 423 424 // Set the cropped bitmap as the new bitmap. 425 croppedImage.recycle(); 426 croppedImage = b; 427 } 428 } 429 430 // Return the cropped image directly or save it to the specified URI. 431 Bundle myExtras = getIntent().getExtras(); 432 if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean("return-data"))) { 433 Bundle extras = new Bundle(); 434 extras.putParcelable("data", croppedImage); 435 setResult(RESULT_OK, (new Intent()).setAction("inline-data").putExtras(extras)); 436 finish(); 437 } else { 438 final Bitmap b = croppedImage; 439 final Runnable save = new Runnable() { 440 public void run() { 441 saveOutput(b); 442 } 443 }; 444 Util.startBackgroundJob(this, null, getResources().getString(Res.string.saving_image), save, mHandler); 445 } 446 } 447 448 private void saveOutput(Bitmap croppedImage) { 449 if (mSaveUri != null) { 450 OutputStream outputStream = null; 451 try { 452 outputStream = mContentResolver.openOutputStream(mSaveUri); 453 if (outputStream != null) { 454 croppedImage.compress(mOutputFormat, 75, outputStream); 455 } 456 // TODO ExifInterface write 457 } catch (IOException ex) { 458 Log.e(TAG, "Cannot open file: " + mSaveUri, ex); 459 } finally { 460 Util.closeSilently(outputStream); 461 } 462 Bundle extras = new Bundle(); 463 setResult(RESULT_OK, new Intent(mSaveUri.toString()).putExtras(extras)); 464 } else { 465 Bundle extras = new Bundle(); 466 extras.putString("rect", mCrop.getCropRect().toString()); 467 if (mItem == null) { 468 // CR: Comments should be full sentences. 469 // this image doesn't belong to the local data source 470 // we can add it locally if necessary 471 } else { 472 File oldPath = new File(mItem.mFilePath); 473 File directory = new File(oldPath.getParent()); 474 475 int x = 0; 476 String fileName = oldPath.getName(); 477 fileName = fileName.substring(0, fileName.lastIndexOf(".")); 478 479 // Try file-1.jpg, file-2.jpg, ... until we find a filename 480 // which 481 // does not exist yet. 482 while (true) { 483 x += 1; 484 String candidate = directory.toString() + "/" + fileName + "-" + x + ".jpg"; 485 boolean exists = (new File(candidate)).exists(); 486 if (!exists) { // CR: inline the expression for exists 487 // here--it's clear enough. 488 break; 489 } 490 } 491 492 MediaItem item = mItem; 493 String title = fileName + "-" + x; 494 String finalFileName = title + ".jpg"; 495 int[] degree = new int[1]; 496 Double latitude = null; 497 Double longitude = null; 498 if (item.isLatLongValid()) { 499 latitude = new Double(item.mLatitude); 500 longitude = new Double(item.mLongitude); 501 } 502 Uri newUri = ImageManager.addImage(mContentResolver, title, 503 item.mDateAddedInSec, item.mDateTakenInMs, latitude, 504 longitude, directory.toString(), finalFileName, 505 croppedImage, null, degree); 506 if (newUri != null) { 507 setResult(RESULT_OK, new Intent().setAction(newUri.toString()).putExtras(extras)); 508 } else { 509 setResult(RESULT_OK, new Intent().setAction(null)); 510 } 511 } 512 } 513 croppedImage.recycle(); 514 finish(); 515 } 516 517 @Override 518 protected void onResume() { 519 super.onResume(); 520 mApp.onResume(); 521 } 522 523 @Override 524 protected void onPause() { 525 super.onPause(); 526 BitmapManager.instance().cancelThreadDecoding(mDecodingThreads); 527 mApp.onPause(); 528 } 529 530 @Override 531 protected void onDestroy() { 532 mApp.shutdown(); 533 super.onDestroy(); 534 } 535 536 Runnable mRunFaceDetection = new Runnable() { 537 float mScale = 1F; 538 Matrix mImageMatrix; 539 FaceDetector.Face[] mFaces = new FaceDetector.Face[3]; 540 int mNumFaces; 541 542 // For each face, we create a HightlightView for it. 543 private void handleFace(FaceDetector.Face f) { 544 PointF midPoint = new PointF(); 545 546 int r = ((int) (f.eyesDistance() * mScale)) * 2; 547 f.getMidPoint(midPoint); 548 midPoint.x *= mScale; 549 midPoint.y *= mScale; 550 551 int midX = (int) midPoint.x; 552 int midY = (int) midPoint.y; 553 554 HighlightView hv = new HighlightView(mImageView); 555 556 int width = mBitmap.getWidth(); 557 int height = mBitmap.getHeight(); 558 559 Rect imageRect = new Rect(0, 0, width, height); 560 561 RectF faceRect = new RectF(midX, midY, midX, midY); 562 faceRect.inset(-r, -r); 563 if (faceRect.left < 0) { 564 faceRect.inset(-faceRect.left, -faceRect.left); 565 } 566 567 if (faceRect.top < 0) { 568 faceRect.inset(-faceRect.top, -faceRect.top); 569 } 570 571 if (faceRect.right > imageRect.right) { 572 faceRect.inset(faceRect.right - imageRect.right, faceRect.right - imageRect.right); 573 } 574 575 if (faceRect.bottom > imageRect.bottom) { 576 faceRect.inset(faceRect.bottom - imageRect.bottom, faceRect.bottom - imageRect.bottom); 577 } 578 579 hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop, mAspectX != 0 && mAspectY != 0); 580 581 mImageView.add(hv); 582 } 583 584 // Create a default HightlightView if we found no face in the picture. 585 private void makeDefault() { 586 HighlightView hv = new HighlightView(mImageView); 587 588 int width = mBitmap.getWidth(); 589 int height = mBitmap.getHeight(); 590 591 Rect imageRect = new Rect(0, 0, width, height); 592 593 // CR: sentences! 594 // make the default size about 4/5 of the width or height 595 int cropWidth = Math.min(width, height) * 4 / 5; 596 int cropHeight = cropWidth; 597 598 if (mAspectX != 0 && mAspectY != 0) { 599 if (mAspectX > mAspectY) { 600 cropHeight = cropWidth * mAspectY / mAspectX; 601 } else { 602 cropWidth = cropHeight * mAspectX / mAspectY; 603 } 604 } 605 606 int x = (width - cropWidth) / 2; 607 int y = (height - cropHeight) / 2; 608 609 RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight); 610 hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop, mAspectX != 0 && mAspectY != 0); 611 mImageView.add(hv); 612 } 613 614 // Scale the image down for faster face detection. 615 private Bitmap prepareBitmap() { 616 if (mBitmap == null) { 617 return null; 618 } 619 620 // 256 pixels wide is enough. 621 if (mBitmap.getWidth() > 256) { 622 mScale = 256.0F / mBitmap.getWidth(); // CR: F => f (or change 623 // all f to F). 624 } 625 Matrix matrix = new Matrix(); 626 matrix.setScale(mScale, mScale); 627 Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true); 628 return faceBitmap; 629 } 630 631 public void run() { 632 mImageMatrix = mImageView.getImageMatrix(); 633 Bitmap faceBitmap = prepareBitmap(); 634 635 mScale = 1.0F / mScale; 636 if (faceBitmap != null && mDoFaceDetection) { 637 FaceDetector detector = new FaceDetector(faceBitmap.getWidth(), faceBitmap.getHeight(), mFaces.length); 638 mNumFaces = detector.findFaces(faceBitmap, mFaces); 639 } 640 641 if (faceBitmap != null && faceBitmap != mBitmap) { 642 faceBitmap.recycle(); 643 } 644 645 mHandler.post(new Runnable() { 646 public void run() { 647 mWaitingToPick = mNumFaces > 1; 648 if (mNumFaces > 0) { 649 for (int i = 0; i < mNumFaces; i++) { 650 handleFace(mFaces[i]); 651 } 652 } else { 653 makeDefault(); 654 } 655 mImageView.invalidate(); 656 if (mImageView.mHighlightViews.size() == 1) { 657 mCrop = mImageView.mHighlightViews.get(0); 658 mCrop.setFocus(true); 659 } 660 661 if (mNumFaces > 1) { 662 // CR: no need for the variable t. just do 663 // Toast.makeText(...).show(). 664 Toast t = Toast.makeText(CropImage.this, Res.string.multiface_crop_help, Toast.LENGTH_SHORT); 665 t.show(); 666 } 667 } 668 }); 669 } 670 }; 671 } 672 673 class CropImageView extends ImageViewTouchBase { 674 ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>(); 675 HighlightView mMotionHighlightView = null; 676 float mLastX, mLastY; 677 int mMotionEdge; 678 679 @Override 680 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 681 super.onLayout(changed, left, top, right, bottom); 682 if (mBitmapDisplayed.getBitmap() != null) { 683 for (HighlightView hv : mHighlightViews) { 684 hv.mMatrix.set(getImageMatrix()); 685 hv.invalidate(); 686 if (hv.mIsFocused) { 687 centerBasedOnHighlightView(hv); 688 } 689 } 690 } 691 } 692 693 public CropImageView(Context context, AttributeSet attrs) { 694 super(context, attrs); 695 } 696 697 @Override 698 protected void zoomTo(float scale, float centerX, float centerY) { 699 super.zoomTo(scale, centerX, centerY); 700 for (HighlightView hv : mHighlightViews) { 701 hv.mMatrix.set(getImageMatrix()); 702 hv.invalidate(); 703 } 704 } 705 706 @Override 707 protected void zoomIn() { 708 super.zoomIn(); 709 for (HighlightView hv : mHighlightViews) { 710 hv.mMatrix.set(getImageMatrix()); 711 hv.invalidate(); 712 } 713 } 714 715 @Override 716 protected void zoomOut() { 717 super.zoomOut(); 718 for (HighlightView hv : mHighlightViews) { 719 hv.mMatrix.set(getImageMatrix()); 720 hv.invalidate(); 721 } 722 } 723 724 @Override 725 protected void postTranslate(float deltaX, float deltaY) { 726 super.postTranslate(deltaX, deltaY); 727 for (int i = 0; i < mHighlightViews.size(); i++) { 728 HighlightView hv = mHighlightViews.get(i); 729 hv.mMatrix.postTranslate(deltaX, deltaY); 730 hv.invalidate(); 731 } 732 } 733 734 // According to the event's position, change the focus to the first 735 // hitting cropping rectangle. 736 private void recomputeFocus(MotionEvent event) { 737 for (int i = 0; i < mHighlightViews.size(); i++) { 738 HighlightView hv = mHighlightViews.get(i); 739 hv.setFocus(false); 740 hv.invalidate(); 741 } 742 743 for (int i = 0; i < mHighlightViews.size(); i++) { 744 HighlightView hv = mHighlightViews.get(i); 745 int edge = hv.getHit(event.getX(), event.getY()); 746 if (edge != HighlightView.GROW_NONE) { 747 if (!hv.hasFocus()) { 748 hv.setFocus(true); 749 hv.invalidate(); 750 } 751 break; 752 } 753 } 754 invalidate(); 755 } 756 757 @Override 758 public boolean onTouchEvent(MotionEvent event) { 759 CropImage cropImage = (CropImage) getContext(); 760 if (cropImage.mSaving) { 761 return false; 762 } 763 764 switch (event.getAction()) { 765 case MotionEvent.ACTION_DOWN: // CR: inline case blocks. 766 if (cropImage.mWaitingToPick) { 767 recomputeFocus(event); 768 } else { 769 for (int i = 0; i < mHighlightViews.size(); i++) { // CR: 770 // iterator 771 // for; if 772 // not, then 773 // i++ => 774 // ++i. 775 HighlightView hv = mHighlightViews.get(i); 776 int edge = hv.getHit(event.getX(), event.getY()); 777 if (edge != HighlightView.GROW_NONE) { 778 mMotionEdge = edge; 779 mMotionHighlightView = hv; 780 mLastX = event.getX(); 781 mLastY = event.getY(); 782 // CR: get rid of the extraneous parens below. 783 mMotionHighlightView.setMode((edge == HighlightView.MOVE) ? HighlightView.ModifyMode.Move 784 : HighlightView.ModifyMode.Grow); 785 break; 786 } 787 } 788 } 789 break; 790 // CR: vertical space before case blocks. 791 case MotionEvent.ACTION_UP: 792 if (cropImage.mWaitingToPick) { 793 for (int i = 0; i < mHighlightViews.size(); i++) { 794 HighlightView hv = mHighlightViews.get(i); 795 if (hv.hasFocus()) { 796 cropImage.mCrop = hv; 797 for (int j = 0; j < mHighlightViews.size(); j++) { 798 if (j == i) { // CR: if j != i do your shit; no need 799 // for continue. 800 continue; 801 } 802 mHighlightViews.get(j).setHidden(true); 803 } 804 centerBasedOnHighlightView(hv); 805 ((CropImage) getContext()).mWaitingToPick = false; 806 return true; 807 } 808 } 809 } else if (mMotionHighlightView != null) { 810 centerBasedOnHighlightView(mMotionHighlightView); 811 mMotionHighlightView.setMode(HighlightView.ModifyMode.None); 812 } 813 mMotionHighlightView = null; 814 break; 815 case MotionEvent.ACTION_MOVE: 816 if (cropImage.mWaitingToPick) { 817 recomputeFocus(event); 818 } else if (mMotionHighlightView != null) { 819 mMotionHighlightView.handleMotion(mMotionEdge, event.getX() - mLastX, event.getY() - mLastY); 820 mLastX = event.getX(); 821 mLastY = event.getY(); 822 823 if (true) { 824 // This section of code is optional. It has some user 825 // benefit in that moving the crop rectangle against 826 // the edge of the screen causes scrolling but it means 827 // that the crop rectangle is no longer fixed under 828 // the user's finger. 829 ensureVisible(mMotionHighlightView); 830 } 831 } 832 break; 833 } 834 835 switch (event.getAction()) { 836 case MotionEvent.ACTION_UP: 837 center(true, true); 838 break; 839 case MotionEvent.ACTION_MOVE: 840 // if we're not zoomed then there's no point in even allowing 841 // the user to move the image around. This call to center puts 842 // it back to the normalized location (with false meaning don't 843 // animate). 844 if (getScale() == 1F) { 845 center(true, true); 846 } 847 break; 848 } 849 850 return true; 851 } 852 853 // Pan the displayed image to make sure the cropping rectangle is visible. 854 private void ensureVisible(HighlightView hv) { 855 Rect r = hv.mDrawRect; 856 857 int panDeltaX1 = Math.max(0, getLeft() - r.left); 858 int panDeltaX2 = Math.min(0, getRight() - r.right); 859 860 int panDeltaY1 = Math.max(0, getTop() - r.top); 861 int panDeltaY2 = Math.min(0, getBottom() - r.bottom); 862 863 int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2; 864 int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2; 865 866 if (panDeltaX != 0 || panDeltaY != 0) { 867 panBy(panDeltaX, panDeltaY); 868 } 869 } 870 871 // If the cropping rectangle's size changed significantly, change the 872 // view's center and scale according to the cropping rectangle. 873 private void centerBasedOnHighlightView(HighlightView hv) { 874 Rect drawRect = hv.mDrawRect; 875 876 float width = drawRect.width(); 877 float height = drawRect.height(); 878 879 float thisWidth = getWidth(); 880 float thisHeight = getHeight(); 881 882 float z1 = thisWidth / width * .6F; 883 float z2 = thisHeight / height * .6F; 884 885 float zoom = Math.min(z1, z2); 886 zoom = zoom * this.getScale(); 887 zoom = Math.max(1F, zoom); 888 889 if ((Math.abs(zoom - getScale()) / zoom) > .1) { 890 float[] coordinates = new float[] { hv.mCropRect.centerX(), hv.mCropRect.centerY() }; 891 getImageMatrix().mapPoints(coordinates); 892 zoomTo(zoom, coordinates[0], coordinates[1], 300F); // CR: 300.0f. 893 } 894 895 ensureVisible(hv); 896 } 897 898 @Override 899 protected void onDraw(Canvas canvas) { 900 super.onDraw(canvas); 901 for (int i = 0; i < mHighlightViews.size(); i++) { 902 mHighlightViews.get(i).draw(canvas); 903 } 904 } 905 906 public void add(HighlightView hv) { 907 mHighlightViews.add(hv); 908 invalidate(); 909 } 910 } 911