1 /* 2 * Copyright (C) 2014 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.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapShader; 26 import android.graphics.Canvas; 27 import android.graphics.Color; 28 import android.graphics.Matrix; 29 import android.graphics.Paint; 30 import android.graphics.RectF; 31 import android.graphics.Shader; 32 import android.util.AttributeSet; 33 import android.view.View; 34 import android.view.animation.AccelerateDecelerateInterpolator; 35 import android.view.animation.AnimationUtils; 36 import android.view.animation.Interpolator; 37 38 import com.android.camera.async.MainThread; 39 import com.android.camera.debug.Log; 40 import com.android.camera.ui.motion.InterpolatorHelper; 41 import com.android.camera.util.ApiHelper; 42 import com.android.camera2.R; 43 import com.google.common.base.Optional; 44 45 /** 46 * A view that shows a pop-out effect for a thumbnail image as the new capture indicator design for 47 * Haleakala. When a photo is taken, this view will appear in the bottom right corner of the view 48 * finder to indicate the capture is done. 49 * 50 * Thumbnail cropping: 51 * (1) 100% width and vertically centered for portrait. 52 * (2) 100% height and horizontally centered for landscape. 53 * 54 * General behavior spec: Hide the capture indicator by fading out using fast_out_linear_in (150ms): 55 * (1) User open filmstrip. 56 * (2) User switch module. 57 * (3) User switch front/back camera. 58 * (4) User close app. 59 * 60 * Visual spec: 61 * (1) A 12dp spacing between mode option overlay and thumbnail. 62 * (2) A circular mask that excludes the corners of the preview image. 63 * (3) A solid white layer that sits on top of the preview and is also masked by 2). 64 * (4) The preview thumbnail image. 65 * (5) A 'ripple' which is just a white circular stroke. 66 * 67 * Animation spec: 68 * - For (2) only the scale animates, from 50%(24dp) to 114%(54dp) in 200ms then falls back to 69 * 100%(48dp) in 200ms. Both steps use the same easing: fast_out_slow_in. 70 * - For (3), change opacity from 50% to 0% over 150ms, easing is exponential. 71 * - For (4), doesn't animate. 72 * - For (5), starts animating after 100ms, when (1) is at its peak radius and all animations take 73 * 200ms, using linear_out_slow in. Opacity goes from 40% to 0%, radius goes from 40dp to 70dp, 74 * stroke width goes from 5dp to 1dp. 75 */ 76 public class RoundedThumbnailView extends View { 77 private static final Log.Tag TAG = new Log.Tag("RoundedThumbnailView"); 78 79 // Configurations for the thumbnail pop-out effect. 80 private static final long THUMBNAIL_STRETCH_DURATION_MS = 200; 81 private static final long THUMBNAIL_SHRINK_DURATION_MS = 200; 82 private static final float THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN = 0.5f; 83 private static final float THUMBNAIL_REVEAL_CIRCLE_OPACITY_END = 0.0f; 84 85 // Configurations for the ripple effect. 86 private static final long RIPPLE_DURATION_MS = 200; 87 private static final float RIPPLE_OPACITY_BEGIN = 0.4f; 88 private static final float RIPPLE_OPACITY_END = 0.0f; 89 90 // Configurations for the hit-state effect. 91 private static final float HIT_STATE_CIRCLE_OPACITY_HIDDEN = -1.0f; 92 private static final float HIT_STATE_CIRCLE_OPACITY_BEGIN = 0.7f; 93 private static final float HIT_STATE_CIRCLE_OPACITY_END = 0.0f; 94 private static final long HIT_STATE_DURATION_MS = 150; 95 96 /** Defines call events. */ 97 public interface Callback { 98 public void onHitStateFinished(); 99 } 100 101 /** The registered callback. */ 102 private Optional<Callback> mCallback; 103 104 // Fields for view layout. 105 private float mThumbnailPadding; 106 private RectF mViewRect; 107 108 // Fields for the thumbnail pop-out effect. 109 /** The animators to move the thumbnail. */ 110 private AnimatorSet mThumbnailAnimatorSet; 111 /** The current diameter for the thumbnail image. */ 112 private float mCurrentThumbnailDiameter; 113 /** The current reveal circle opacity. */ 114 private float mCurrentRevealCircleOpacity; 115 /** The duration of the stretch phase in thumbnail pop-out effect. */ 116 private long mThumbnailStretchDurationMs; 117 /** The duration of the shrink phase in thumbnail pop-out effect. */ 118 private long mThumbnailShrinkDurationMs; 119 /** 120 * The beginning diameter of the thumbnail for the stretch phase in 121 * thumbnail pop-out effect. 122 */ 123 private float mThumbnailStretchDiameterBegin; 124 /** 125 * The ending diameter of the thumbnail for the stretch phase in thumbnail 126 * pop-out effect. 127 */ 128 private float mThumbnailStretchDiameterEnd; 129 /** 130 * The beginning diameter of the thumbnail for the shrink phase in thumbnail 131 * pop-out effect. 132 */ 133 private float mThumbnailShrinkDiameterBegin; 134 /** 135 * The ending diameter of the thumbnail for the shrink phase in thumbnail 136 * pop-out effect. 137 */ 138 private float mThumbnailShrinkDiameterEnd; 139 /** Paint object for the reveal circle. */ 140 private final Paint mRevealCirclePaint; 141 142 // Fields for the ripple effect. 143 /** The start delay of the ripple effect. */ 144 private long mRippleStartDelayMs; 145 /** The duration of the ripple effect. */ 146 private long mRippleDurationMs; 147 /** The beginning diameter of the ripple ring. */ 148 private float mRippleRingDiameterBegin; 149 /** The ending diameter of the ripple ring. */ 150 private float mRippleRingDiameterEnd; 151 /** The beginning thickness of the ripple ring. */ 152 private float mRippleRingThicknessBegin; 153 /** The ending thickness of the ripple ring. */ 154 private float mRippleRingThicknessEnd; 155 /** A lazily loaded animator for the ripple effect. */ 156 private ValueAnimator mRippleAnimator; 157 /** 158 * The current ripple ring diameter which is updated by the ripple animator 159 * and used by onDraw(). 160 */ 161 private float mCurrentRippleRingDiameter; 162 /** 163 * The current ripple ring thickness which is updated by the ripple animator 164 * and used by onDraw(). 165 */ 166 private float mCurrentRippleRingThickness; 167 /** 168 * The current ripple ring opacity which is updated by the ripple animator 169 * and used by onDraw(). 170 */ 171 private float mCurrentRippleRingOpacity; 172 /** The paint used for drawing the ripple effect. */ 173 private final Paint mRipplePaint; 174 175 // Fields for the hit state effect. 176 /** The paint to draw hit state circle. */ 177 private final Paint mHitStateCirclePaint; 178 /** 179 * The current hit state circle opacity (0.0 - 1.0) which is updated by the 180 * hit state animator. If -1, the hit state circle won't be drawn. 181 */ 182 private float mCurrentHitStateCircleOpacity; 183 184 /** 185 * The pending reveal request. This is created when start is called, but is 186 * not drawn until the thumbnail is available. Once the bitmap is available 187 * it is swapped into the foreground request. 188 */ 189 private RevealRequest mPendingRequest; 190 191 /** The currently animating reveal request. */ 192 private RevealRequest mForegroundRequest; 193 194 /** 195 * The latest finished reveal request. Its thumbnail will be shown until 196 * a newer one replace it. 197 */ 198 private RevealRequest mBackgroundRequest; 199 200 private View.OnClickListener mOnClickListener = new View.OnClickListener() { 201 @Override 202 public void onClick(View v) { 203 // Trigger the hit state animation. Fade out the hit state white 204 // circle by changing the alpha. 205 final ValueAnimator hitStateAnimator = ValueAnimator.ofFloat( 206 HIT_STATE_CIRCLE_OPACITY_BEGIN, HIT_STATE_CIRCLE_OPACITY_END); 207 hitStateAnimator.setDuration(HIT_STATE_DURATION_MS); 208 hitStateAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 209 hitStateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 210 @Override 211 public void onAnimationUpdate(ValueAnimator valueAnimator) { 212 mCurrentHitStateCircleOpacity = (Float) valueAnimator.getAnimatedValue(); 213 invalidate(); 214 } 215 }); 216 hitStateAnimator.addListener(new AnimatorListenerAdapter() { 217 @Override 218 public void onAnimationEnd(Animator animation) { 219 super.onAnimationEnd(animation); 220 mCurrentHitStateCircleOpacity = HIT_STATE_CIRCLE_OPACITY_HIDDEN; 221 if (mCallback.isPresent()) { 222 mCallback.get().onHitStateFinished(); 223 } 224 } 225 }); 226 hitStateAnimator.start(); 227 } 228 }; 229 230 /** 231 * Constructs a RoundedThumbnailView. 232 */ 233 public RoundedThumbnailView(Context context, AttributeSet attrs) { 234 super(context, attrs); 235 236 mCallback = Optional.absent(); 237 238 // Make the view clickable. 239 setClickable(true); 240 setOnClickListener(mOnClickListener); 241 242 mThumbnailPadding = getResources().getDimension(R.dimen.rounded_thumbnail_padding); 243 244 // Load thumbnail pop-out effect constants. 245 mThumbnailStretchDurationMs = THUMBNAIL_STRETCH_DURATION_MS; 246 mThumbnailShrinkDurationMs = THUMBNAIL_SHRINK_DURATION_MS; 247 mThumbnailStretchDiameterBegin = 248 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_min); 249 mThumbnailStretchDiameterEnd = 250 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_max); 251 mThumbnailShrinkDiameterBegin = mThumbnailStretchDiameterEnd; 252 mThumbnailShrinkDiameterEnd = 253 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_normal); 254 // Load ripple effect constants. 255 float startDelayRatio = 0.5f; 256 mRippleStartDelayMs = (long) (mThumbnailStretchDurationMs * startDelayRatio); 257 mRippleDurationMs = RIPPLE_DURATION_MS; 258 mRippleRingDiameterEnd = 259 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_diameter_max); 260 261 mViewRect = new RectF(0, 0, mRippleRingDiameterEnd, mRippleRingDiameterEnd); 262 263 mRippleRingDiameterBegin = 264 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_diameter_min); 265 mRippleRingThicknessBegin = 266 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_thick_max); 267 mRippleRingThicknessEnd = 268 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_thick_min); 269 270 mCurrentHitStateCircleOpacity = HIT_STATE_CIRCLE_OPACITY_HIDDEN; 271 // Draw the reveal while circle. 272 mHitStateCirclePaint = new Paint(); 273 mHitStateCirclePaint.setAntiAlias(true); 274 mHitStateCirclePaint.setColor(Color.WHITE); 275 mHitStateCirclePaint.setStyle(Paint.Style.FILL); 276 277 mRipplePaint = new Paint(); 278 mRipplePaint.setAntiAlias(true); 279 mRipplePaint.setColor(Color.WHITE); 280 mRipplePaint.setStyle(Paint.Style.STROKE); 281 282 mRevealCirclePaint = new Paint(); 283 mRevealCirclePaint.setAntiAlias(true); 284 mRevealCirclePaint.setColor(Color.WHITE); 285 mRevealCirclePaint.setStyle(Paint.Style.FILL); 286 } 287 288 @Override 289 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 290 // Ignore the spec since the size should be fixed. 291 int desiredSize = (int) mRippleRingDiameterEnd; 292 setMeasuredDimension(desiredSize, desiredSize); 293 } 294 295 @Override 296 protected void onDraw(Canvas canvas) { 297 super.onDraw(canvas); 298 299 final float centerX = canvas.getWidth() / 2; 300 final float centerY = canvas.getHeight() / 2; 301 302 final float viewDiameter = mRippleRingDiameterEnd; 303 final float finalDiameter = mThumbnailShrinkDiameterEnd; 304 305 canvas.clipRect(mViewRect); 306 307 // Draw the thumbnail of latest finished reveal request. 308 if (mBackgroundRequest != null) { 309 Paint thumbnailPaint = mBackgroundRequest.getThumbnailPaint(); 310 if (thumbnailPaint != null) { 311 // Draw the old thumbnail with the final diameter. 312 float scaleRatio = finalDiameter / viewDiameter; 313 314 canvas.save(); 315 canvas.scale(scaleRatio, scaleRatio, centerX, centerY); 316 canvas.drawRoundRect( 317 mViewRect, 318 centerX, 319 centerY, 320 thumbnailPaint); 321 canvas.restore(); 322 } 323 } 324 325 // Draw animated parts (thumbnail and ripple) if there exists a reveal request. 326 if (mForegroundRequest != null) { 327 // Draw ripple ring first or the ring will cover thumbnail. 328 if (mCurrentRippleRingThickness > 0) { 329 // Draw the ripple ring. 330 mRipplePaint.setAlpha((int) (mCurrentRippleRingOpacity * 255)); 331 mRipplePaint.setStrokeWidth(mCurrentRippleRingThickness); 332 333 canvas.save(); 334 canvas.drawCircle(centerX, centerY, mCurrentRippleRingDiameter / 2, mRipplePaint); 335 canvas.restore(); 336 } 337 338 // Achieve the animation effect by scaling the transformation matrix. 339 float scaleRatio = mCurrentThumbnailDiameter / mRippleRingDiameterEnd; 340 341 canvas.save(); 342 canvas.scale(scaleRatio, scaleRatio, centerX, centerY); 343 344 // Draw the new popping up thumbnail. 345 Paint thumbnailPaint = mForegroundRequest.getThumbnailPaint(); 346 if (thumbnailPaint != null) { 347 canvas.drawRoundRect( 348 mViewRect, 349 centerX, 350 centerY, 351 thumbnailPaint); 352 } 353 354 // Draw the reveal while circle. 355 mRevealCirclePaint.setAlpha((int) (mCurrentRevealCircleOpacity * 255)); 356 canvas.drawCircle(centerX, centerY, 357 mRippleRingDiameterEnd / 2, mRevealCirclePaint); 358 359 canvas.restore(); 360 } 361 362 // Draw hit state circle if necessary. 363 if (mCurrentHitStateCircleOpacity != HIT_STATE_CIRCLE_OPACITY_HIDDEN) { 364 canvas.save(); 365 final float scaleRatio = finalDiameter / viewDiameter; 366 canvas.scale(scaleRatio, scaleRatio, centerX, centerY); 367 368 // Draw the hit state while circle. 369 mHitStateCirclePaint.setAlpha((int) (mCurrentHitStateCircleOpacity * 255)); 370 canvas.drawCircle(centerX, centerY, 371 mRippleRingDiameterEnd / 2, mHitStateCirclePaint); 372 canvas.restore(); 373 } 374 } 375 376 /** 377 * Sets the callback. 378 * 379 * @param callback The callback to be set. 380 */ 381 public void setCallback(Callback callback) { 382 mCallback = Optional.of(callback); 383 } 384 385 /** 386 * Gets the padding size with mode options and preview edges. 387 * 388 * @return The padding size with mode options and preview edges. 389 */ 390 public float getThumbnailPadding() { 391 return mThumbnailPadding; 392 } 393 394 /** 395 * Gets the diameter of the thumbnail image after the revealing animation. 396 * 397 * @return The diameter of the thumbnail image after the revealing animation. 398 */ 399 public float getThumbnailFinalDiameter() { 400 return mThumbnailShrinkDiameterEnd; 401 } 402 403 /** 404 * Starts the thumbnail revealing animation. 405 * 406 * @param accessibilityString An accessibility String to be announced during the revealing 407 * animation. 408 */ 409 public void startRevealThumbnailAnimation(String accessibilityString) { 410 MainThread.checkMainThread(); 411 // Create a new request. 412 mPendingRequest = new RevealRequest(getMeasuredWidth(), accessibilityString); 413 } 414 415 /** 416 * Updates the thumbnail image. 417 * 418 * @param thumbnailBitmap The thumbnail image to be shown. 419 * @param rotation The orientation of the image in degrees. 420 */ 421 public void setThumbnail(final Bitmap thumbnailBitmap, final int rotation) { 422 MainThread.checkMainThread(); 423 424 if(mPendingRequest != null) { 425 mPendingRequest.setThumbnailBitmap(thumbnailBitmap, rotation); 426 427 runPendingRequestAnimation(); 428 } else { 429 Log.e(TAG, "Pending thumb was null!"); 430 } 431 } 432 433 /** 434 * Hide the thumbnail. 435 */ 436 public void hideThumbnail() { 437 MainThread.checkMainThread(); 438 // Make this view invisible. 439 setVisibility(GONE); 440 441 clearAnimations(); 442 443 // Remove all pending reveal requests. 444 mPendingRequest = null; 445 mForegroundRequest = null; 446 mBackgroundRequest = null; 447 } 448 449 /** 450 * Stop currently running animators. 451 */ 452 private void clearAnimations() { 453 // Stop currently running animators. 454 if (mThumbnailAnimatorSet != null && mThumbnailAnimatorSet.isRunning()) { 455 mThumbnailAnimatorSet.removeAllListeners(); 456 mThumbnailAnimatorSet.cancel(); 457 // Release the animator so that a new instance will be created and 458 // its listeners properly reconnected. Fix for b/19034435 459 mThumbnailAnimatorSet = null; 460 } 461 462 if (mRippleAnimator != null && mRippleAnimator.isRunning()) { 463 mRippleAnimator.removeAllListeners(); 464 mRippleAnimator.cancel(); 465 // Release the animator so that a new instance will be created and 466 // its listeners properly reconnected. Fix for b/19034435 467 mRippleAnimator = null; 468 } 469 } 470 471 /** 472 * Set the foreground request to the background, complete it, and run the 473 * animation for the pending thumbnail. 474 */ 475 private void runPendingRequestAnimation() { 476 // Shift foreground to background, and pending to foreground. 477 if (mForegroundRequest != null) { 478 mBackgroundRequest = mForegroundRequest; 479 mBackgroundRequest.finishRippleAnimation(); 480 mBackgroundRequest.finishThumbnailAnimation(); 481 } 482 483 mForegroundRequest = mPendingRequest; 484 mPendingRequest = null; 485 486 // Make this view visible. 487 setVisibility(VISIBLE); 488 489 // Ensure there are no running animations. 490 clearAnimations(); 491 492 Interpolator stretchInterpolator; 493 if (ApiHelper.isLOrHigher()) { 494 // Both phases use fast_out_flow_in interpolator. 495 stretchInterpolator = AnimationUtils.loadInterpolator( 496 getContext(), android.R.interpolator.fast_out_slow_in); 497 } else { 498 stretchInterpolator = new AccelerateDecelerateInterpolator(); 499 } 500 501 // The first phase of thumbnail animation. Stretch the thumbnail to the maximal size. 502 ValueAnimator stretchAnimator = ValueAnimator.ofFloat( 503 mThumbnailStretchDiameterBegin, mThumbnailStretchDiameterEnd); 504 stretchAnimator.setDuration(mThumbnailStretchDurationMs); 505 stretchAnimator.setInterpolator(stretchInterpolator); 506 stretchAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 507 @Override 508 public void onAnimationUpdate(ValueAnimator valueAnimator) { 509 mCurrentThumbnailDiameter = (Float) valueAnimator.getAnimatedValue(); 510 float fraction = valueAnimator.getAnimatedFraction(); 511 float opacityDiff = THUMBNAIL_REVEAL_CIRCLE_OPACITY_END - 512 THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN; 513 mCurrentRevealCircleOpacity = 514 THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN + fraction * opacityDiff; 515 invalidate(); 516 } 517 }); 518 519 // The second phase of thumbnail animation. Shrink the thumbnail to the final size. 520 Interpolator shrinkInterpolator = stretchInterpolator; 521 ValueAnimator shrinkAnimator = ValueAnimator.ofFloat( 522 mThumbnailShrinkDiameterBegin, mThumbnailShrinkDiameterEnd); 523 shrinkAnimator.setDuration(mThumbnailShrinkDurationMs); 524 shrinkAnimator.setInterpolator(shrinkInterpolator); 525 shrinkAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 526 @Override 527 public void onAnimationUpdate(ValueAnimator valueAnimator) { 528 mCurrentThumbnailDiameter = (Float) valueAnimator.getAnimatedValue(); 529 invalidate(); 530 } 531 }); 532 533 // The stretch and shrink animators play sequentially. 534 mThumbnailAnimatorSet = new AnimatorSet(); 535 mThumbnailAnimatorSet.playSequentially(stretchAnimator, shrinkAnimator); 536 mThumbnailAnimatorSet.addListener(new AnimatorListenerAdapter() { 537 @Override 538 public void onAnimationEnd(Animator animation) { 539 if (mForegroundRequest != null) { 540 // Mark the thumbnail animation as finished. 541 mForegroundRequest.finishThumbnailAnimation(); 542 processRevealRequests(); 543 } 544 } 545 }); 546 547 // Start thumbnail animation immediately. 548 mThumbnailAnimatorSet.start(); 549 550 // Lazily load the ripple animator. 551 // Ripple effect uses linear_out_slow_in interpolator. 552 Interpolator rippleInterpolator = 553 InterpolatorHelper.getLinearOutSlowInInterpolator(getContext()); 554 555 // When start shrinking the thumbnail, a ripple effect is triggered at the same time. 556 mRippleAnimator = 557 ValueAnimator.ofFloat(mRippleRingDiameterBegin, mRippleRingDiameterEnd); 558 mRippleAnimator.setDuration(mRippleDurationMs); 559 mRippleAnimator.setInterpolator(rippleInterpolator); 560 mRippleAnimator.addListener(new AnimatorListenerAdapter() { 561 @Override 562 public void onAnimationEnd(Animator animation) { 563 if (mForegroundRequest != null) { 564 mForegroundRequest.finishRippleAnimation(); 565 processRevealRequests(); 566 } 567 } 568 }); 569 mRippleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 570 @Override 571 public void onAnimationUpdate(ValueAnimator valueAnimator) { 572 mCurrentRippleRingDiameter = (Float) valueAnimator.getAnimatedValue(); 573 float fraction = valueAnimator.getAnimatedFraction(); 574 mCurrentRippleRingThickness = mRippleRingThicknessBegin + 575 fraction * (mRippleRingThicknessEnd - mRippleRingThicknessBegin); 576 mCurrentRippleRingOpacity = RIPPLE_OPACITY_BEGIN + 577 fraction * (RIPPLE_OPACITY_END - RIPPLE_OPACITY_BEGIN); 578 invalidate(); 579 } 580 }); 581 582 // Start ripple animation after delay. 583 mRippleAnimator.setStartDelay(mRippleStartDelayMs); 584 mRippleAnimator.start(); 585 586 // Announce the accessibility string. 587 announceForAccessibility(mForegroundRequest.getAccessibilityString()); 588 } 589 590 private void processRevealRequests() { 591 if(mForegroundRequest != null && mForegroundRequest.isFinished()) { 592 mBackgroundRequest = mForegroundRequest; 593 mForegroundRequest = null; 594 } 595 } 596 597 @Override 598 public boolean hasOverlappingRendering() { 599 return true; 600 } 601 602 /** 603 * Encapsulates necessary information for a complete thumbnail reveal animation. 604 */ 605 private static class RevealRequest { 606 // The size of the thumbnail. 607 private float mViewSize; 608 609 // The accessibility string. 610 private String mAccessibilityString; 611 612 // The cached Paint object to draw the thumbnail. 613 private Paint mThumbnailPaint; 614 615 // The flag to indicate if thumbnail animation of this request is full-filled. 616 private boolean mThumbnailAnimationFinished; 617 618 // The flag to indicate if ripple animation of this request is full-filled. 619 private boolean mRippleAnimationFinished; 620 621 /** 622 * Constructs a reveal request. Use setThumbnailBitmap() to specify a source bitmap for the 623 * thumbnail. 624 * 625 * @param viewSize The size of the capture indicator view. 626 * @param accessibilityString The accessibility string of the request. 627 */ 628 public RevealRequest(float viewSize, String accessibilityString) { 629 mAccessibilityString = accessibilityString; 630 mViewSize = viewSize; 631 } 632 633 /** 634 * Returns the accessibility string. 635 * 636 * @return the accessibility string. 637 */ 638 public String getAccessibilityString() { 639 return mAccessibilityString; 640 } 641 642 /** 643 * Returns the paint object which can be used to draw the thumbnail on a Canvas. 644 * 645 * @return the paint object which can be used to draw the thumbnail on a Canvas. 646 */ 647 public Paint getThumbnailPaint() { 648 return mThumbnailPaint; 649 } 650 651 /** 652 * Used to precompute the thumbnail paint from the given source bitmap. 653 */ 654 private void precomputeThumbnailPaint(Bitmap srcBitmap, int rotation) { 655 // Lazy loading the thumbnail paint object. 656 if (mThumbnailPaint == null) { 657 // Can't create a paint object until the thumbnail bitmap is available. 658 if (srcBitmap == null) { 659 return; 660 } 661 // The original bitmap should be a square shape. 662 if (srcBitmap.getWidth() != srcBitmap.getHeight()) { 663 return; 664 } 665 666 // Create a bitmap shader for the paint. 667 BitmapShader shader = new BitmapShader( 668 srcBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 669 if (srcBitmap.getWidth() != mViewSize) { 670 // Create a transformation matrix for the bitmap shader if the size is not 671 // matched. 672 RectF srcRect = new RectF( 673 0.0f, 0.0f, srcBitmap.getWidth(), srcBitmap.getHeight()); 674 RectF dstRect = new RectF(0.0f, 0.0f, mViewSize, mViewSize); 675 676 Matrix shaderMatrix = new Matrix(); 677 678 // Scale the shader to fit the destination view size. 679 shaderMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL); 680 681 // Rotate the image around the given source rect point. 682 shaderMatrix.preRotate(rotation, 683 srcRect.width() / 2, 684 srcRect.height() / 2); 685 686 shader.setLocalMatrix(shaderMatrix); 687 } 688 689 // Create the paint for drawing the thumbnail in a circle. 690 mThumbnailPaint = new Paint(); 691 mThumbnailPaint.setAntiAlias(true); 692 mThumbnailPaint.setShader(shader); 693 } 694 } 695 696 /** 697 * Checks if the request is full-filled. 698 * 699 * @return True if both thumbnail animation and ripple animation are finished 700 */ 701 public boolean isFinished() { 702 return mThumbnailAnimationFinished && mRippleAnimationFinished; 703 } 704 705 /** 706 * Marks the thumbnail animation is finished. 707 */ 708 public void finishThumbnailAnimation() { 709 mThumbnailAnimationFinished = true; 710 } 711 712 /** 713 * Marks the ripple animation is finished. 714 */ 715 public void finishRippleAnimation() { 716 mRippleAnimationFinished = true; 717 } 718 719 /** 720 * Updates the thumbnail image. 721 * 722 * @param thumbnailBitmap The thumbnail image to be shown. 723 * @param rotation The orientation of the image in degrees. 724 */ 725 public void setThumbnailBitmap(Bitmap thumbnailBitmap, int rotation) { 726 Bitmap originalBitmap = thumbnailBitmap; 727 // Crop the image if it is not square. 728 if (originalBitmap.getWidth() != originalBitmap.getHeight()) { 729 originalBitmap = cropCenterBitmap(originalBitmap); 730 } 731 732 precomputeThumbnailPaint(originalBitmap, rotation); 733 } 734 735 /** 736 * Obtains a square bitmap by cropping the center of a bitmap. If the given image is 737 * portrait, the cropped image keeps 100% original width and vertically centered to the 738 * original image. If the given image is landscape, the cropped image keeps 100% original 739 * height and horizontally centered to the original image. 740 * 741 * @param srcBitmap the bitmap image to be cropped in the center. 742 * @return a result square bitmap. 743 */ 744 private Bitmap cropCenterBitmap(Bitmap srcBitmap) { 745 int srcWidth = srcBitmap.getWidth(); 746 int srcHeight = srcBitmap.getHeight(); 747 Bitmap dstBitmap; 748 if (srcWidth >= srcHeight) { 749 dstBitmap = Bitmap.createBitmap( 750 srcBitmap, srcWidth / 2 - srcHeight / 2, 0, srcHeight, srcHeight); 751 } else { 752 dstBitmap = Bitmap.createBitmap( 753 srcBitmap, 0, srcHeight / 2 - srcWidth / 2, srcWidth, srcWidth); 754 } 755 return dstBitmap; 756 } 757 } 758 } 759