RoundedThumbnailView.java revision 8be316c7a8caf962cf3fcf5e49d332fb2718319f
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 17package com.android.camera.widget; 18 19import java.util.LinkedList; 20 21import android.animation.Animator; 22import android.animation.AnimatorListenerAdapter; 23import android.animation.AnimatorSet; 24import android.animation.ValueAnimator; 25import android.content.Context; 26import android.content.res.Configuration; 27import android.graphics.Bitmap; 28import android.graphics.BitmapShader; 29import android.graphics.Canvas; 30import android.graphics.Color; 31import android.graphics.Matrix; 32import android.graphics.Paint; 33import android.graphics.RectF; 34import android.graphics.Shader; 35import android.util.AttributeSet; 36import android.view.View; 37import android.view.animation.AccelerateDecelerateInterpolator; 38import android.view.animation.AnimationUtils; 39import android.view.animation.DecelerateInterpolator; 40import android.view.animation.Interpolator; 41 42import com.android.camera.debug.Log; 43import com.android.camera.util.ApiHelper; 44import com.android.camera.util.CameraUtil; 45import com.android.camera2.R; 46 47/** 48 * A view that shows a pop-out effect for a thumbnail image as the new capture indicator design for 49 * Haleakala. When a photo is taken, this view will appear in the bottom right corner of the view 50 * finder to indicate the capture is done. 51 * 52 * Thumbnail cropping: 53 * (1) 100% width and vertically centered for portrait. 54 * (2) 100% height and horizontally centered for landscape. 55 * 56 * General behavior spec: Hide the capture indicator by fading out using fast_out_linear_in (150ms): 57 * (1) User open filmstrip. 58 * (2) User switch module. 59 * (3) User switch front/back camera. 60 * (4) User close app. 61 * 62 * Visual spec: 63 * (1) A 12dp spacing between mode option overlay and thumbnail. 64 * (2) A circular mask that excludes the corners of the preview image. 65 * (3) A solid white layer that sits on top of the preview and is also masked by 2). 66 * (4) The preview thumbnail image. 67 * (5) A 'ripple' which is just a white circular stroke. 68 * 69 * Animation spec: 70 * - For (2) only the scale animates, from 50%(24dp) to 114%(54dp) in 200ms then falls back to 71 * 100%(48dp) in 200ms. Both steps use the same easing: fast_out_slow_in. 72 * - For (3), change opacity from 50% to 0% over 150ms, easing is exponential. 73 * - For (4), doesn't animate. 74 * - For (5), starts animating after 100ms, when (1) is at its peak radius and all animations take 75 * 200ms, using linear_out_slow in. Opacity goes from 40% to 0%, radius goes from 40dp to 70dp, 76 * stroke width goes from 5dp to 1dp. 77 */ 78public class RoundedThumbnailView extends View { 79 private static final Log.Tag TAG = new Log.Tag("RoundedThumbnailView"); 80 81 /** 82 * Configurations for the thumbnail pop-out effect. 83 */ 84 private static final long THUMBNAIL_STRETCH_DURATION_MS = 200; 85 private static final long THUMBNAIL_SHRINK_DURATION_MS = 200; 86 private static final float THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN = 0.5f; 87 private static final float THUMBNAIL_REVEAL_CIRCLE_OPACITY_END = 0.0f; 88 /** 89 * Configurations for the ripple effect. 90 */ 91 private static final long RIPPLE_DURATION_MS = 200; 92 private static final float RIPPLE_OPACITY_BEGIN = 0.4f; 93 private static final float RIPPLE_OPACITY_END = 0.0f; 94 95 /** 96 * Fields for view layout. 97 */ 98 private float mThumbnailPadding; 99 100 /** 101 * Fields for the thumbnail pop-out effect. 102 */ 103 // The duration of the stretch phase in thumbnail pop-out effect. 104 private long mThumbnailStretchDurationMs; 105 // The duration of the shrink phase in thumbnail pop-out effect. 106 private long mThumbnailShrinkDurationMs; 107 // The beginning diameter of the thumbnail for the stretch phase in thumbnail pop-out effect. 108 private float mThumbnailStretchDiameterBegin; 109 // The ending diameter of the thumbnail for the stretch phase in thumbnail pop-out effect. 110 private float mThumbnailStretchDiameterEnd; 111 // The beginning diameter of the thumbnail for the shrink phase in thumbnail pop-out effect. 112 private float mThumbnailShrinkDiameterBegin; 113 // The ending diameter of the thumbnail for the shrink phase in thumbnail pop-out effect. 114 private float mThumbnailShrinkDiameterEnd; 115 116 private AnimatorSet mThumbnailAnimatorSet; 117 private float mCurrentThumbnailDiameter; 118 private float mCurrentRevealCircleOpacity; 119 120 /** 121 * Fields for the ripple effect. 122 */ 123 // The start delay of the ripple effect. 124 private long mRippleStartDelayMs; 125 // The duration of the ripple effect. 126 private long mRippleDurationMs; 127 // The beginning diameter of the ripple ring. 128 private float mRippleRingDiameterBegin; 129 // The ending diameter of the ripple ring. 130 private float mRippleRingDiameterEnd; 131 // The beginning thickness of the ripple ring. 132 private float mRippleRingThicknessBegin; 133 // The ending thickness of the ripple ring. 134 private float mRippleRingThicknessEnd; 135 // A lazily loaded animator for the ripple effect. 136 private ValueAnimator mRippleAnimator; 137 // The current ripple ring diameter which is updated by the ripple animator and used by 138 // onDraw(). 139 private float mCurrentRippleRingDiameter; 140 // The current ripple ring thickness which is updated by the ripple animator and used by 141 // onDraw(). 142 private float mCurrentRippleRingThickness; 143 // The current ripple ring opacity which is updated by the ripple animator and used byonDraw(). 144 private float mCurrentRippleRingOpacity; 145 146 // The waiting queue for all pending reveal requests. The latest request should be in the end of 147 // the queue. 148 private LinkedList<RevealRequest> mRevealRequestWaitQueue = new LinkedList<>(); 149 150 // The currently running reveal request. 151 private RevealRequest mActiveRevealRequest; 152 153 // The latest finished reveal request. Its thumbnail will be shown until a newer one replace it. 154 private RevealRequest mFinishedRevealRequest; 155 156 /** 157 * Constructs a RoundedThumbnailView. 158 */ 159 public RoundedThumbnailView(Context context, AttributeSet attrs) { 160 super(context, attrs); 161 162 // Make the view clickable. 163 setClickable(true); 164 165 // TODO: Adjust layout when mode option overlay is visible. 166 mThumbnailPadding = getResources().getDimension(R.dimen.rounded_thumbnail_padding); 167 168 // Load thumbnail pop-out effect constants. 169 mThumbnailStretchDurationMs = THUMBNAIL_STRETCH_DURATION_MS; 170 mThumbnailShrinkDurationMs = THUMBNAIL_SHRINK_DURATION_MS; 171 mThumbnailStretchDiameterBegin = 172 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_min); 173 mThumbnailStretchDiameterEnd = 174 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_max); 175 mThumbnailShrinkDiameterBegin = mThumbnailStretchDiameterEnd; 176 mThumbnailShrinkDiameterEnd = 177 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_normal); 178 // Load ripple effect constants. 179 float startDelayRatio = 0.5f; 180 mRippleStartDelayMs = (long) (mThumbnailStretchDurationMs * startDelayRatio); 181 mRippleDurationMs = RIPPLE_DURATION_MS; 182 mRippleRingDiameterEnd = 183 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_diameter_max); 184 mRippleRingDiameterBegin = 185 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_diameter_min); 186 mRippleRingThicknessBegin = 187 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_thick_max); 188 mRippleRingThicknessEnd = 189 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_thick_min); 190 } 191 192 @Override 193 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 194 // Ignore the spec since the size should be fixed. 195 int desiredSize = (int) mRippleRingDiameterEnd; 196 setMeasuredDimension(desiredSize, desiredSize); 197 } 198 199 @Override 200 protected void onDraw(Canvas canvas) { 201 super.onDraw(canvas); 202 203 float centerX = canvas.getWidth() / 2; 204 float centerY = canvas.getHeight() / 2; 205 RectF viewBound = 206 new RectF(0, 0, mRippleRingDiameterEnd, mRippleRingDiameterEnd); 207 208 // Draw the thumbnail of latest finished reveal request. 209 if (mFinishedRevealRequest != null) { 210 Paint thumbnailPaint = mFinishedRevealRequest.getThumbnailPaint(); 211 if (thumbnailPaint != null) { 212 // Draw the old thumbnail with the final diameter. 213 float scaleRatio = mThumbnailShrinkDiameterEnd / mRippleRingDiameterEnd; 214 215 canvas.save(); 216 canvas.scale(scaleRatio, scaleRatio, centerX, centerY); 217 canvas.drawRoundRect( 218 viewBound, 219 centerX, 220 centerY, 221 thumbnailPaint); 222 canvas.restore(); 223 } 224 } 225 226 // Draw animated parts (thumbnail and ripple) if there exists a reveal request. 227 if (mActiveRevealRequest != null) { 228 // Draw ripple ring first or the ring will cover thumbnail. 229 if (mCurrentRippleRingThickness > 0) { 230 // Draw the ripple ring. 231 Paint ripplePaint = new Paint(); 232 ripplePaint.setAntiAlias(true); 233 ripplePaint.setStrokeWidth(mCurrentRippleRingThickness); 234 ripplePaint.setColor(Color.WHITE); 235 ripplePaint.setAlpha((int) (mCurrentRippleRingOpacity * 255)); 236 ripplePaint.setStyle(Paint.Style.STROKE); 237 238 canvas.save(); 239 canvas.drawCircle(centerX, centerY, mCurrentRippleRingDiameter / 2, ripplePaint); 240 canvas.restore(); 241 } 242 243 // Achieve the animation effect by scaling the transformation matrix. 244 float scaleRatio = mCurrentThumbnailDiameter / mRippleRingDiameterEnd; 245 246 canvas.save(); 247 canvas.scale(scaleRatio, scaleRatio, centerX, centerY); 248 249 // Draw the new popping up thumbnail. 250 Paint thumbnailPaint = mActiveRevealRequest.getThumbnailPaint(); 251 if (thumbnailPaint != null) { 252 canvas.drawRoundRect( 253 viewBound, 254 centerX, 255 centerY, 256 thumbnailPaint); 257 258 } 259 260 // Draw the reveal while circle. 261 Paint revealCirclePaint = new Paint(); 262 revealCirclePaint.setAntiAlias(true); 263 revealCirclePaint.setColor(Color.WHITE); 264 revealCirclePaint.setAlpha((int) (mCurrentRevealCircleOpacity * 255)); 265 revealCirclePaint.setStyle(Paint.Style.FILL); 266 canvas.drawCircle(centerX, centerY, 267 mRippleRingDiameterEnd / 2, revealCirclePaint); 268 269 canvas.restore(); 270 } 271 } 272 273 /** 274 * Calculates the desired layout of capture indicator. 275 * 276 * @param parentRect The bound of the view which contains capture indicator. 277 * @param uncoveredPreviewRect The uncovered preview bound which contains mode option 278 * overlay and capture indicator. 279 * @return the desired view bound for capture indicator. 280 */ 281 public RectF getDesiredLayout(RectF parentRect, RectF uncoveredPreviewRect) { 282 float parentViewWidth = parentRect.right - parentRect.left; 283 float x = 0; 284 float y = 0; 285 286 // The view bound is based on the maximal ripple ring diameter. This is the diff of maximal 287 // ripple ring radius and the final thumbnail radius. 288 float radius_diff_max_normal = (mRippleRingDiameterEnd - mThumbnailShrinkDiameterEnd) / 2; 289 float modeSwitchThreeDotsDiameter = mThumbnailShrinkDiameterEnd; 290 float modeSwitchThreeDotsBottomPadding = mThumbnailPadding; 291 292 int orientation = getResources().getConfiguration().orientation; 293 int rotation = CameraUtil.getDisplayRotation(); 294 if (orientation == Configuration.ORIENTATION_PORTRAIT) { 295 // The view finder of 16:9 aspect ratio might have a black padding. 296 float previewRightEdgeGap = 297 parentRect.right - uncoveredPreviewRect.right; 298 x = parentViewWidth - previewRightEdgeGap - mThumbnailPadding - 299 mThumbnailShrinkDiameterEnd - radius_diff_max_normal; 300 y = uncoveredPreviewRect.bottom; 301 y -= modeSwitchThreeDotsBottomPadding + modeSwitchThreeDotsDiameter + 302 mThumbnailPadding + mThumbnailShrinkDiameterEnd + radius_diff_max_normal; 303 } 304 if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 305 float previewTopEdgeGap = uncoveredPreviewRect.top; 306 x = uncoveredPreviewRect.right; 307 x -= modeSwitchThreeDotsBottomPadding + modeSwitchThreeDotsDiameter + 308 mThumbnailPadding + mThumbnailShrinkDiameterEnd + radius_diff_max_normal; 309 y = previewTopEdgeGap + mThumbnailPadding - radius_diff_max_normal; 310 } 311 return new RectF(x, y, x + mRippleRingDiameterEnd, y + mRippleRingDiameterEnd); 312 } 313 314 /** 315 * Starts the thumbnail revealing animation. 316 * 317 * @param accessibilityString An accessibility String to be announced during the revealing 318 * animation. 319 */ 320 public void startRevealThumbnailAnimation(String accessibilityString) { 321 // Create a new request. 322 RevealRequest latestRevealRequest = 323 new RevealRequest(getMeasuredWidth(), accessibilityString); 324 mRevealRequestWaitQueue.addLast(latestRevealRequest); 325 // Process the next request. 326 processNextRevealRequest(); 327 } 328 329 /** 330 * Updates the thumbnail image. 331 * 332 * @param thumbnailBitmap The thumbnail image to be shown. 333 */ 334 public void setThumbnail(final Bitmap thumbnailBitmap) { 335 if (mRevealRequestWaitQueue.isEmpty()) { 336 if (mActiveRevealRequest != null) { 337 mActiveRevealRequest.setThumbnailBitmap(thumbnailBitmap); 338 } 339 } else { 340 // Update the thumbnail in the latest reveal request. 341 RevealRequest latestRevealRequest = mRevealRequestWaitQueue.peekLast(); 342 latestRevealRequest.setThumbnailBitmap(thumbnailBitmap); 343 } 344 } 345 346 /** 347 * Hide the thumbnail. 348 */ 349 public void hideThumbnail() { 350 // Make this view invisible. 351 setVisibility(GONE); 352 353 // Stop currently running animators. 354 if (mThumbnailAnimatorSet != null && mThumbnailAnimatorSet.isRunning()) { 355 mThumbnailAnimatorSet.removeAllListeners(); 356 mThumbnailAnimatorSet.cancel(); 357 } 358 if (mRippleAnimator != null && mRippleAnimator.isRunning()) { 359 mRippleAnimator.removeAllListeners(); 360 mRippleAnimator.cancel(); 361 } 362 // Remove all pending reveal requests. 363 mRevealRequestWaitQueue.clear(); 364 mActiveRevealRequest = null; 365 mFinishedRevealRequest = null; 366 } 367 368 /** 369 * Pick the next request in the reveal request queue and start a reveal animation for the 370 * request. 371 */ 372 private void processNextRevealRequest() { 373 // Do nothing if the queue is empty. 374 if (mRevealRequestWaitQueue.isEmpty()) { 375 return; 376 } 377 // Do nothing if the active request is still running. 378 if (mActiveRevealRequest != null) { 379 return; 380 } 381 382 // Pick the first request in the queue and make it active. 383 mActiveRevealRequest = mRevealRequestWaitQueue.peekFirst(); 384 mRevealRequestWaitQueue.removeFirst(); 385 386 // Make this view visible. 387 setVisibility(VISIBLE); 388 389 // Lazily load the thumbnail animator. 390 if (mThumbnailAnimatorSet == null) { 391 Interpolator stretchInterpolator; 392 if (ApiHelper.isLOrHigher()) { 393 // Both phases use fast_out_flow_in interpolator. 394 stretchInterpolator = AnimationUtils.loadInterpolator( 395 getContext(), android.R.interpolator.fast_out_slow_in); 396 } else { 397 stretchInterpolator = new AccelerateDecelerateInterpolator(); 398 } 399 400 // The first phase of thumbnail animation. Stretch the thumbnail to the maximal size. 401 ValueAnimator stretchAnimator = ValueAnimator.ofFloat( 402 mThumbnailStretchDiameterBegin, mThumbnailStretchDiameterEnd); 403 stretchAnimator.setDuration(mThumbnailStretchDurationMs); 404 stretchAnimator.setInterpolator(stretchInterpolator); 405 stretchAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 406 @Override 407 public void onAnimationUpdate(ValueAnimator valueAnimator) { 408 mCurrentThumbnailDiameter = (Float) valueAnimator.getAnimatedValue(); 409 float fraction = valueAnimator.getAnimatedFraction(); 410 float opacityDiff = THUMBNAIL_REVEAL_CIRCLE_OPACITY_END - 411 THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN; 412 mCurrentRevealCircleOpacity = 413 THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN + fraction * opacityDiff; 414 invalidate(); 415 } 416 }); 417 418 // The second phase of thumbnail animation. Shrink the thumbnail to the final size. 419 Interpolator shrinkInterpolator = stretchInterpolator; 420 ValueAnimator shrinkAnimator = ValueAnimator.ofFloat( 421 mThumbnailShrinkDiameterBegin, mThumbnailShrinkDiameterEnd); 422 shrinkAnimator.setDuration(mThumbnailShrinkDurationMs); 423 shrinkAnimator.setInterpolator(shrinkInterpolator); 424 shrinkAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 425 @Override 426 public void onAnimationUpdate(ValueAnimator valueAnimator) { 427 mCurrentThumbnailDiameter = (Float) valueAnimator.getAnimatedValue(); 428 invalidate(); 429 } 430 }); 431 432 // The stretch and shrink animators play sequentially. 433 mThumbnailAnimatorSet = new AnimatorSet(); 434 mThumbnailAnimatorSet.playSequentially(stretchAnimator, shrinkAnimator); 435 mThumbnailAnimatorSet.addListener(new AnimatorListenerAdapter() { 436 @Override 437 public void onAnimationEnd(Animator animation) { 438 // Mark the thumbnail animation as finished. 439 mActiveRevealRequest.finishThumbnailAnimation(); 440 // Process the next reveal request if both thumbnail animation and ripple 441 // animation are both finished. 442 if (mActiveRevealRequest.isFinished()) { 443 mFinishedRevealRequest = mActiveRevealRequest; 444 mActiveRevealRequest = null; 445 processNextRevealRequest(); 446 } 447 } 448 }); 449 } 450 // Start thumbnail animation immediately. 451 mThumbnailAnimatorSet.start(); 452 453 // Lazily load the ripple animator. 454 if (mRippleAnimator == null) { 455 456 // Ripple effect uses linear_out_slow_in interpolator. 457 Interpolator rippleInterpolator; 458 if (ApiHelper.isLOrHigher()) { 459 // Both phases use fast_out_flow_in interpolator. 460 rippleInterpolator = AnimationUtils.loadInterpolator( 461 getContext(), android.R.interpolator.linear_out_slow_in); 462 } else { 463 rippleInterpolator = new DecelerateInterpolator(); 464 } 465 466 // When start shrinking the thumbnail, a ripple effect is triggered at the same time. 467 mRippleAnimator = 468 ValueAnimator.ofFloat(mRippleRingDiameterBegin, mRippleRingDiameterEnd); 469 mRippleAnimator.setDuration(mRippleDurationMs); 470 mRippleAnimator.setInterpolator(rippleInterpolator); 471 mRippleAnimator.addListener(new AnimatorListenerAdapter() { 472 @Override 473 public void onAnimationEnd(Animator animation) { 474 // Mark the ripple animation as finished. 475 mActiveRevealRequest.finishRippleAnimation(); 476 // Process the next reveal request if both thumbnail animation and ripple 477 // animation are both finished. 478 if (mActiveRevealRequest.isFinished()) { 479 mFinishedRevealRequest = mActiveRevealRequest; 480 mActiveRevealRequest = null; 481 processNextRevealRequest(); 482 } 483 } 484 }); 485 mRippleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 486 @Override 487 public void onAnimationUpdate(ValueAnimator valueAnimator) { 488 mCurrentRippleRingDiameter = (Float) valueAnimator.getAnimatedValue(); 489 float fraction = valueAnimator.getAnimatedFraction(); 490 mCurrentRippleRingThickness = mRippleRingThicknessBegin + 491 fraction * (mRippleRingThicknessEnd - mRippleRingThicknessBegin); 492 mCurrentRippleRingOpacity = RIPPLE_OPACITY_BEGIN + 493 fraction * (RIPPLE_OPACITY_END - RIPPLE_OPACITY_BEGIN); 494 invalidate(); 495 } 496 }); 497 } 498 // Start ripple animation after delay. 499 mRippleAnimator.setStartDelay(mRippleStartDelayMs); 500 mRippleAnimator.start(); 501 502 // Announce the accessibility string. 503 announceForAccessibility(mActiveRevealRequest.getAccessibilityString()); 504 } 505 506 /** 507 * Encapsulates necessary information for a complete thumbnail reveal animation. 508 */ 509 private static class RevealRequest { 510 // The size of the thumbnail. 511 private float mViewSize; 512 513 // The accessibility string. 514 private String mAccessibilityString; 515 516 // The original full-size image bitmap. 517 private Bitmap mOriginalBitmap; 518 519 // The cached Paint object to draw the thumbnail. 520 private Paint mThumbnailPaint; 521 522 // The flag to indicate if thumbnail animation of this request is full-filled. 523 private boolean mThumbnailAnimationFinished; 524 525 // The flag to indicate if ripple animation of this request is full-filled. 526 private boolean mRippleAnimationFinished; 527 528 /** 529 * Constructs a reveal request. Use setThumbnailBitmap() to specify a source bitmap for the 530 * thumbnail. 531 * 532 * @param viewSize The size of the capture indicator view. 533 * @param accessibilityString The accessibility string of the request. 534 */ 535 public RevealRequest(float viewSize, String accessibilityString) { 536 mAccessibilityString = accessibilityString; 537 mViewSize = viewSize; 538 } 539 540 /** 541 * Returns the accessibility string. 542 * 543 * @return the accessibility string. 544 */ 545 public String getAccessibilityString() { 546 return mAccessibilityString; 547 } 548 549 /** 550 * Returns the paint object which can be used to draw the thumbnail on a Canvas. 551 * 552 * @return the paint object which can be used to draw the thumbnail on a Canvas. 553 */ 554 public Paint getThumbnailPaint() { 555 // Lazy loading the thumbnail paint object. 556 if (mThumbnailPaint == null) { 557 // Can't create a paint object until the thumbnail bitmap is available. 558 if (mOriginalBitmap == null) { 559 return null; 560 } 561 // The original bitmap should be a square shape. 562 if (mOriginalBitmap.getWidth() != mOriginalBitmap.getHeight()) { 563 return null; 564 } 565 566 // Create a bitmap shader for the paint. 567 BitmapShader shader = new BitmapShader( 568 mOriginalBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 569 if (mOriginalBitmap.getWidth() != mViewSize) { 570 // Create a transformation matrix for the bitmap shader if the size is not 571 // matched. 572 RectF srcRect = new RectF( 573 0.0f, 0.0f, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight()); 574 RectF dstRect = new RectF(0.0f, 0.0f, mViewSize, mViewSize); 575 Matrix shaderMatrix = new Matrix(); 576 shaderMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL); 577 shader.setLocalMatrix(shaderMatrix); 578 } 579 580 // Create the paint for drawing the thumbnail in a circle. 581 mThumbnailPaint = new Paint(); 582 mThumbnailPaint.setAntiAlias(true); 583 mThumbnailPaint.setShader(shader); 584 } 585 return mThumbnailPaint; 586 } 587 588 /** 589 * Checks if the request is full-filled. 590 * 591 * @return True if both thumbnail animation and ripple animation are finished 592 */ 593 public boolean isFinished() { 594 return mThumbnailAnimationFinished && mRippleAnimationFinished; 595 } 596 597 /** 598 * Marks the thumbnail animation is finished. 599 */ 600 public void finishThumbnailAnimation() { 601 mThumbnailAnimationFinished = true; 602 } 603 604 /** 605 * Marks the ripple animation is finished. 606 */ 607 public void finishRippleAnimation() { 608 mRippleAnimationFinished = true; 609 } 610 611 /** 612 * Updates the thumbnail image. 613 * 614 * @param thumbnailBitmap The thumbnail image to be shown. 615 */ 616 public void setThumbnailBitmap(Bitmap thumbnailBitmap) { 617 mOriginalBitmap = thumbnailBitmap; 618 // Crop the image if it is not square. 619 if (mOriginalBitmap.getWidth() != mOriginalBitmap.getHeight()) { 620 mOriginalBitmap = cropCenterBitmap(mOriginalBitmap); 621 } 622 } 623 624 /** 625 * Obtains a square bitmap by cropping the center of a bitmap. If the given image is 626 * portrait, the cropped image keeps 100% original width and vertically centered to the 627 * original image. If the given image is landscape, the cropped image keeps 100% original 628 * height and horizontally centered to the original image. 629 * 630 * @param srcBitmap the bitmap image to be cropped in the center. 631 * @return a result square bitmap. 632 */ 633 private Bitmap cropCenterBitmap(Bitmap srcBitmap) { 634 int srcWidth = srcBitmap.getWidth(); 635 int srcHeight = srcBitmap.getHeight(); 636 Bitmap dstBitmap; 637 if (srcWidth >= srcHeight) { 638 dstBitmap = Bitmap.createBitmap( 639 srcBitmap, srcWidth / 2 - srcHeight / 2, 0, srcHeight, srcHeight); 640 } else { 641 dstBitmap = Bitmap.createBitmap( 642 srcBitmap, 0, srcHeight / 2 - srcWidth / 2, srcWidth, srcWidth); 643 } 644 return dstBitmap; 645 } 646 } 647}