2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.camera.widget;
19 import java.util.LinkedList;
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.AnimatorSet;
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapShader;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Matrix;
31 import android.graphics.Paint;
32 import android.graphics.RectF;
33 import android.graphics.Shader;
34 import android.util.AttributeSet;
35 import android.view.View;
36 import android.view.animation.AccelerateDecelerateInterpolator;
37 import android.view.animation.AnimationUtils;
38 import android.view.animation.Interpolator;
40 import com.android.camera.async.MainThread;
41 import com.android.camera.debug.Log;
42 import com.android.camera.ui.motion.InterpolatorHelper;
43 import com.android.camera.util.ApiHelper;
44 import com.android.camera2.R;
46 import com.google.common.base.Optional;
49 * A view that shows a pop-out effect for a thumbnail image as the new capture indicator design for
50 * Haleakala. When a photo is taken, this view will appear in the bottom right corner of the view
51 * finder to indicate the capture is done.
54 * (1) 100% width and vertically centered for portrait.
55 * (2) 100% height and horizontally centered for landscape.
57 * General behavior spec: Hide the capture indicator by fading out using fast_out_linear_in (150ms):
58 * (1) User open filmstrip.
59 * (2) User switch module.
60 * (3) User switch front/back camera.
64 * (1) A 12dp spacing between mode option overlay and thumbnail.
65 * (2) A circular mask that excludes the corners of the preview image.
66 * (3) A solid white layer that sits on top of the preview and is also masked by 2).
67 * (4) The preview thumbnail image.
68 * (5) A 'ripple' which is just a white circular stroke.
71 * - For (2) only the scale animates, from 50%(24dp) to 114%(54dp) in 200ms then falls back to
72 * 100%(48dp) in 200ms. Both steps use the same easing: fast_out_slow_in.
73 * - For (3), change opacity from 50% to 0% over 150ms, easing is exponential.
74 * - For (4), doesn't animate.
75 * - For (5), starts animating after 100ms, when (1) is at its peak radius and all animations take
76 * 200ms, using linear_out_slow in. Opacity goes from 40% to 0%, radius goes from 40dp to 70dp,
77 * stroke width goes from 5dp to 1dp.
79 public class RoundedThumbnailView extends View {
80 private static final Log.Tag TAG = new Log.Tag("RoundedThumbnailView");
83 * Configurations for the thumbnail pop-out effect.
85 private static final long THUMBNAIL_STRETCH_DURATION_MS = 200;
86 private static final long THUMBNAIL_SHRINK_DURATION_MS = 200;
87 private static final float THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN = 0.5f;
88 private static final float THUMBNAIL_REVEAL_CIRCLE_OPACITY_END = 0.0f;
90 * Configurations for the ripple effect.
92 private static final long RIPPLE_DURATION_MS = 200;
93 private static final float RIPPLE_OPACITY_BEGIN = 0.4f;
94 private static final float RIPPLE_OPACITY_END = 0.0f;
96 * Configurations for the hit-state effect.
98 private static final float HIT_STATE_CIRCLE_OPACITY_HIDDEN = -1.0f;
99 private static final float HIT_STATE_CIRCLE_OPACITY_BEGIN = 0.7f;
100 private static final float HIT_STATE_CIRCLE_OPACITY_END = 0.0f;
101 private static final long HIT_STATE_DURATION_MS = 150;
104 * Defines call events.
106 public interface Callback {
107 public void onHitStateFinished();
110 /** The registered callback. */
111 private Optional<Callback> mCallback;
114 * Fields for view layout.
116 private float mThumbnailPadding;
119 * Fields for the thumbnail pop-out effect.
121 // The animators to move the thumbnail.
122 private AnimatorSet mThumbnailAnimatorSet;
123 // The current diameter for the thumbnail image.
124 private float mCurrentThumbnailDiameter;
125 // The current reveal circle opacity.
126 private float mCurrentRevealCircleOpacity;
127 // The duration of the stretch phase in thumbnail pop-out effect.
128 private long mThumbnailStretchDurationMs;
129 // The duration of the shrink phase in thumbnail pop-out effect.
130 private long mThumbnailShrinkDurationMs;
131 // The beginning diameter of the thumbnail for the stretch phase in thumbnail pop-out effect.
132 private float mThumbnailStretchDiameterBegin;
133 // The ending diameter of the thumbnail for the stretch phase in thumbnail pop-out effect.
134 private float mThumbnailStretchDiameterEnd;
135 // The beginning diameter of the thumbnail for the shrink phase in thumbnail pop-out effect.
136 private float mThumbnailShrinkDiameterBegin;
137 // The ending diameter of the thumbnail for the shrink phase in thumbnail pop-out effect.
138 private float mThumbnailShrinkDiameterEnd;
141 * 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 // The current ripple ring diameter which is updated by the ripple animator and used by
159 private float mCurrentRippleRingDiameter;
160 // The current ripple ring thickness which is updated by the ripple animator and used by
162 private float mCurrentRippleRingThickness;
163 // The current ripple ring opacity which is updated by the ripple animator and used by onDraw().
164 private float mCurrentRippleRingOpacity;
167 * Fields for the hit state effect.
169 // The paint to draw hit state circle.
170 private final Paint mHitStateCirclePaint;
171 // The current hit state circle opacity (0.0 - 1.0) which is updated by the
172 // hit state animator. If -1, the hit state circle won't be drawn.
173 private float mCurrentHitStateCircleOpacity;
175 // The waiting queue for all pending reveal requests. The latest request should be in the end of
177 private LinkedList<RevealRequest> mRevealRequestWaitQueue = new LinkedList<>();
179 // The currently running reveal request.
180 private Optional<RevealRequest> mActiveRevealRequest;
182 // The latest finished reveal request. Its thumbnail will be shown until a newer one replace it.
183 private Optional<RevealRequest> mFinishedRevealRequest;
185 private View.OnClickListener mOnClickListener = new View.OnClickListener() {
187 public void onClick(View v) {
188 // Trigger the hit state animation. Fade out the hit state white
189 // circle by changing the alpha.
190 final ValueAnimator hitStateAnimator = ValueAnimator.ofFloat(
191 HIT_STATE_CIRCLE_OPACITY_BEGIN, HIT_STATE_CIRCLE_OPACITY_END);
192 hitStateAnimator.setDuration(HIT_STATE_DURATION_MS);
193 hitStateAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
194 hitStateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
196 public void onAnimationUpdate(ValueAnimator valueAnimator) {
197 mCurrentHitStateCircleOpacity = (Float) valueAnimator.getAnimatedValue();
201 hitStateAnimator.addListener(new AnimatorListenerAdapter() {
203 public void onAnimationEnd(Animator animation) {
204 super.onAnimationEnd(animation);
205 mCurrentHitStateCircleOpacity = HIT_STATE_CIRCLE_OPACITY_HIDDEN;
206 if (mCallback.isPresent()) {
207 mCallback.get().onHitStateFinished();
211 hitStateAnimator.start();
216 * Constructs a RoundedThumbnailView.
218 public RoundedThumbnailView(Context context, AttributeSet attrs) {
219 super(context, attrs);
221 mCallback = Optional.absent();
223 // Make the view clickable.
225 setOnClickListener(mOnClickListener);
227 mThumbnailPadding = getResources().getDimension(R.dimen.rounded_thumbnail_padding);
229 // Load thumbnail pop-out effect constants.
230 mThumbnailStretchDurationMs = THUMBNAIL_STRETCH_DURATION_MS;
231 mThumbnailShrinkDurationMs = THUMBNAIL_SHRINK_DURATION_MS;
232 mThumbnailStretchDiameterBegin =
233 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_min);
234 mThumbnailStretchDiameterEnd =
235 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_max);
236 mThumbnailShrinkDiameterBegin = mThumbnailStretchDiameterEnd;
237 mThumbnailShrinkDiameterEnd =
238 getResources().getDimension(R.dimen.rounded_thumbnail_diameter_normal);
239 // Load ripple effect constants.
240 float startDelayRatio = 0.5f;
241 mRippleStartDelayMs = (long) (mThumbnailStretchDurationMs * startDelayRatio);
242 mRippleDurationMs = RIPPLE_DURATION_MS;
243 mRippleRingDiameterEnd =
244 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_diameter_max);
245 mRippleRingDiameterBegin =
246 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_diameter_min);
247 mRippleRingThicknessBegin =
248 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_thick_max);
249 mRippleRingThicknessEnd =
250 getResources().getDimension(R.dimen.rounded_thumbnail_ripple_ring_thick_min);
252 mCurrentHitStateCircleOpacity = HIT_STATE_CIRCLE_OPACITY_HIDDEN;
253 // Draw the reveal while circle.
254 mHitStateCirclePaint = new Paint();
255 mHitStateCirclePaint.setAntiAlias(true);
256 mHitStateCirclePaint.setColor(Color.WHITE);
257 mHitStateCirclePaint.setStyle(Paint.Style.FILL);
259 mActiveRevealRequest = Optional.absent();
260 mFinishedRevealRequest = Optional.absent();
264 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
265 // Ignore the spec since the size should be fixed.
266 int desiredSize = (int) mRippleRingDiameterEnd;
267 setMeasuredDimension(desiredSize, desiredSize);
271 protected void onDraw(Canvas canvas) {
272 super.onDraw(canvas);
274 final float centerX = canvas.getWidth() / 2;
275 final float centerY = canvas.getHeight() / 2;
277 final float viewDiameter = mRippleRingDiameterEnd;
278 final float finalDiameter = mThumbnailShrinkDiameterEnd;
279 final RectF viewBound =
280 new RectF(0, 0, viewDiameter, viewDiameter);
282 // Draw the thumbnail of latest finished reveal request.
283 if (mFinishedRevealRequest.isPresent()) {
284 Paint thumbnailPaint = mFinishedRevealRequest.get().getThumbnailPaint();
285 if (thumbnailPaint != null) {
286 // Draw the old thumbnail with the final diameter.
287 float scaleRatio = finalDiameter / viewDiameter;
290 canvas.scale(scaleRatio, scaleRatio, centerX, centerY);
291 canvas.drawRoundRect(
300 // Draw animated parts (thumbnail and ripple) if there exists a reveal request.
301 if (mActiveRevealRequest.isPresent()) {
302 // Draw ripple ring first or the ring will cover thumbnail.
303 if (mCurrentRippleRingThickness > 0) {
304 // Draw the ripple ring.
305 Paint ripplePaint = new Paint();
306 ripplePaint.setAntiAlias(true);
307 ripplePaint.setStrokeWidth(mCurrentRippleRingThickness);
308 ripplePaint.setColor(Color.WHITE);
309 ripplePaint.setAlpha((int) (mCurrentRippleRingOpacity * 255));
310 ripplePaint.setStyle(Paint.Style.STROKE);
313 canvas.drawCircle(centerX, centerY, mCurrentRippleRingDiameter / 2, ripplePaint);
317 // Achieve the animation effect by scaling the transformation matrix.
318 float scaleRatio = mCurrentThumbnailDiameter / mRippleRingDiameterEnd;
321 canvas.scale(scaleRatio, scaleRatio, centerX, centerY);
323 // Draw the new popping up thumbnail.
324 Paint thumbnailPaint = mActiveRevealRequest.get().getThumbnailPaint();
325 if (thumbnailPaint != null) {
326 canvas.drawRoundRect(
333 // Draw the reveal while circle.
334 Paint revealCirclePaint = new Paint();
335 revealCirclePaint.setAntiAlias(true);
336 revealCirclePaint.setColor(Color.WHITE);
337 revealCirclePaint.setAlpha((int) (mCurrentRevealCircleOpacity * 255));
338 revealCirclePaint.setStyle(Paint.Style.FILL);
339 canvas.drawCircle(centerX, centerY,
340 mRippleRingDiameterEnd / 2, revealCirclePaint);
345 // Draw hit state circle if necessary.
346 if (mCurrentHitStateCircleOpacity != HIT_STATE_CIRCLE_OPACITY_HIDDEN) {
348 final float scaleRatio = finalDiameter / viewDiameter;
349 canvas.scale(scaleRatio, scaleRatio, centerX, centerY);
351 // Draw the hit state while circle.
352 mHitStateCirclePaint.setAlpha((int) (mCurrentHitStateCircleOpacity * 255));
353 canvas.drawCircle(centerX, centerY,
354 mRippleRingDiameterEnd / 2, mHitStateCirclePaint);
362 * @param callback The callback to be set.
364 public void setCallback(Callback callback) {
365 mCallback = Optional.of(callback);
369 * Gets the padding size with mode options and preview edges.
371 * @return The padding size with mode options and preview edges.
373 public float getThumbnailPadding() {
374 return mThumbnailPadding;
378 * Gets the diameter of the thumbnail image after the revealing animation.
380 * @return The diameter of the thumbnail image after the revealing animation.
382 public float getThumbnailFinalDiameter() {
383 return mThumbnailShrinkDiameterEnd;
387 * Starts the thumbnail revealing animation.
389 * @param accessibilityString An accessibility String to be announced during the revealing
392 public void startRevealThumbnailAnimation(String accessibilityString) {
393 MainThread.checkMainThread();
394 // Create a new request.
395 RevealRequest latestRevealRequest =
396 new RevealRequest(getMeasuredWidth(), accessibilityString);
397 mRevealRequestWaitQueue.addLast(latestRevealRequest);
398 // Process the next request.
399 processNextRevealRequest();
403 * Updates the thumbnail image.
405 * @param thumbnailBitmap The thumbnail image to be shown.
407 public void setThumbnail(final Bitmap thumbnailBitmap) {
408 MainThread.checkMainThread();
409 if (mRevealRequestWaitQueue.isEmpty()) {
410 if (mActiveRevealRequest.isPresent()) {
411 mActiveRevealRequest.get().setThumbnailBitmap(thumbnailBitmap);
414 // Update the thumbnail in the latest reveal request.
415 RevealRequest latestRevealRequest = mRevealRequestWaitQueue.peekLast();
416 latestRevealRequest.setThumbnailBitmap(thumbnailBitmap);
421 * Hide the thumbnail.
423 public void hideThumbnail() {
424 MainThread.checkMainThread();
425 // Make this view invisible.
428 // Stop currently running animators.
429 if (mThumbnailAnimatorSet != null && mThumbnailAnimatorSet.isRunning()) {
430 mThumbnailAnimatorSet.removeAllListeners();
431 mThumbnailAnimatorSet.cancel();
433 if (mRippleAnimator != null && mRippleAnimator.isRunning()) {
434 mRippleAnimator.removeAllListeners();
435 mRippleAnimator.cancel();
437 // Remove all pending reveal requests.
438 mRevealRequestWaitQueue.clear();
439 mActiveRevealRequest = Optional.absent();
440 mFinishedRevealRequest = Optional.absent();
444 * Pick the next request in the reveal request queue and start a reveal animation for the
447 private void processNextRevealRequest() {
448 // Do nothing if the queue is empty.
449 if (mRevealRequestWaitQueue.isEmpty()) {
452 // Do nothing if the active request is still running.
453 if (mActiveRevealRequest.isPresent()) {
457 // Pick the first request in the queue and make it active.
458 mActiveRevealRequest = Optional.of(mRevealRequestWaitQueue.peekFirst());
459 mRevealRequestWaitQueue.removeFirst();
461 // Make this view visible.
462 setVisibility(VISIBLE);
464 // Lazily load the thumbnail animator.
465 if (mThumbnailAnimatorSet == null) {
466 Interpolator stretchInterpolator;
467 if (ApiHelper.isLOrHigher()) {
468 // Both phases use fast_out_flow_in interpolator.
469 stretchInterpolator = AnimationUtils.loadInterpolator(
470 getContext(), android.R.interpolator.fast_out_slow_in);
472 stretchInterpolator = new AccelerateDecelerateInterpolator();
475 // The first phase of thumbnail animation. Stretch the thumbnail to the maximal size.
476 ValueAnimator stretchAnimator = ValueAnimator.ofFloat(
477 mThumbnailStretchDiameterBegin, mThumbnailStretchDiameterEnd);
478 stretchAnimator.setDuration(mThumbnailStretchDurationMs);
479 stretchAnimator.setInterpolator(stretchInterpolator);
480 stretchAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
482 public void onAnimationUpdate(ValueAnimator valueAnimator) {
483 mCurrentThumbnailDiameter = (Float) valueAnimator.getAnimatedValue();
484 float fraction = valueAnimator.getAnimatedFraction();
485 float opacityDiff = THUMBNAIL_REVEAL_CIRCLE_OPACITY_END -
486 THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN;
487 mCurrentRevealCircleOpacity =
488 THUMBNAIL_REVEAL_CIRCLE_OPACITY_BEGIN + fraction * opacityDiff;
493 // The second phase of thumbnail animation. Shrink the thumbnail to the final size.
494 Interpolator shrinkInterpolator = stretchInterpolator;
495 ValueAnimator shrinkAnimator = ValueAnimator.ofFloat(
496 mThumbnailShrinkDiameterBegin, mThumbnailShrinkDiameterEnd);
497 shrinkAnimator.setDuration(mThumbnailShrinkDurationMs);
498 shrinkAnimator.setInterpolator(shrinkInterpolator);
499 shrinkAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
501 public void onAnimationUpdate(ValueAnimator valueAnimator) {
502 mCurrentThumbnailDiameter = (Float) valueAnimator.getAnimatedValue();
507 // The stretch and shrink animators play sequentially.
508 mThumbnailAnimatorSet = new AnimatorSet();
509 mThumbnailAnimatorSet.playSequentially(stretchAnimator, shrinkAnimator);
510 mThumbnailAnimatorSet.addListener(new AnimatorListenerAdapter() {
512 public void onAnimationEnd(Animator animation) {
513 if (mActiveRevealRequest.isPresent()) {
514 final RevealRequest activeRevealRequest = mActiveRevealRequest.get();
515 // Mark the thumbnail animation as finished.
516 activeRevealRequest.finishThumbnailAnimation();
517 // Process the next reveal request if both thumbnail animation and ripple
518 // animation are both finished.
519 if (activeRevealRequest.isFinished()) {
520 mFinishedRevealRequest = Optional.of(activeRevealRequest);
521 mActiveRevealRequest = Optional.absent();
522 processNextRevealRequest();
528 // Start thumbnail animation immediately.
529 mThumbnailAnimatorSet.start();
531 // Lazily load the ripple animator.
532 if (mRippleAnimator == null) {
534 // Ripple effect uses linear_out_slow_in interpolator.
535 Interpolator rippleInterpolator =
536 InterpolatorHelper.getLinearOutSlowInInterpolator(getContext());
538 // When start shrinking the thumbnail, a ripple effect is triggered at the same time.
540 ValueAnimator.ofFloat(mRippleRingDiameterBegin, mRippleRingDiameterEnd);
541 mRippleAnimator.setDuration(mRippleDurationMs);
542 mRippleAnimator.setInterpolator(rippleInterpolator);
543 mRippleAnimator.addListener(new AnimatorListenerAdapter() {
545 public void onAnimationEnd(Animator animation) {
546 if (mActiveRevealRequest.isPresent()) {
547 final RevealRequest activeRevealRequest = mActiveRevealRequest.get();
548 // Mark the ripple animation as finished.
549 activeRevealRequest.finishRippleAnimation();
550 // Process the next reveal request if both thumbnail animation and ripple
551 // animation are both finished.
552 if (activeRevealRequest.isFinished()) {
553 mFinishedRevealRequest = Optional.of(activeRevealRequest);
554 mActiveRevealRequest = Optional.absent();
555 processNextRevealRequest();
560 mRippleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
562 public void onAnimationUpdate(ValueAnimator valueAnimator) {
563 mCurrentRippleRingDiameter = (Float) valueAnimator.getAnimatedValue();
564 float fraction = valueAnimator.getAnimatedFraction();
565 mCurrentRippleRingThickness = mRippleRingThicknessBegin +
566 fraction * (mRippleRingThicknessEnd - mRippleRingThicknessBegin);
567 mCurrentRippleRingOpacity = RIPPLE_OPACITY_BEGIN +
568 fraction * (RIPPLE_OPACITY_END - RIPPLE_OPACITY_BEGIN);
573 // Start ripple animation after delay.
574 mRippleAnimator.setStartDelay(mRippleStartDelayMs);
575 mRippleAnimator.start();
577 // Announce the accessibility string.
578 announceForAccessibility(mActiveRevealRequest.get().getAccessibilityString());
582 * Encapsulates necessary information for a complete thumbnail reveal animation.
584 private static class RevealRequest {
585 // The size of the thumbnail.
586 private float mViewSize;
588 // The accessibility string.
589 private String mAccessibilityString;
591 // The original full-size image bitmap.
592 private Bitmap mOriginalBitmap;
594 // The cached Paint object to draw the thumbnail.
595 private Paint mThumbnailPaint;
597 // The flag to indicate if thumbnail animation of this request is full-filled.
598 private boolean mThumbnailAnimationFinished;
600 // The flag to indicate if ripple animation of this request is full-filled.
601 private boolean mRippleAnimationFinished;
604 * Constructs a reveal request. Use setThumbnailBitmap() to specify a source bitmap for the
607 * @param viewSize The size of the capture indicator view.
608 * @param accessibilityString The accessibility string of the request.
610 public RevealRequest(float viewSize, String accessibilityString) {
611 mAccessibilityString = accessibilityString;
612 mViewSize = viewSize;
616 * Returns the accessibility string.
618 * @return the accessibility string.
620 public String getAccessibilityString() {
621 return mAccessibilityString;
625 * Returns the paint object which can be used to draw the thumbnail on a Canvas.
627 * @return the paint object which can be used to draw the thumbnail on a Canvas.
629 public Paint getThumbnailPaint() {
630 // Lazy loading the thumbnail paint object.
631 if (mThumbnailPaint == null) {
632 // Can't create a paint object until the thumbnail bitmap is available.
633 if (mOriginalBitmap == null) {
636 // The original bitmap should be a square shape.
637 if (mOriginalBitmap.getWidth() != mOriginalBitmap.getHeight()) {
641 // Create a bitmap shader for the paint.
642 BitmapShader shader = new BitmapShader(
643 mOriginalBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
644 if (mOriginalBitmap.getWidth() != mViewSize) {
645 // Create a transformation matrix for the bitmap shader if the size is not
647 RectF srcRect = new RectF(
648 0.0f, 0.0f, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
649 RectF dstRect = new RectF(0.0f, 0.0f, mViewSize, mViewSize);
650 Matrix shaderMatrix = new Matrix();
651 shaderMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL);
652 shader.setLocalMatrix(shaderMatrix);
655 // Create the paint for drawing the thumbnail in a circle.
656 mThumbnailPaint = new Paint();
657 mThumbnailPaint.setAntiAlias(true);
658 mThumbnailPaint.setShader(shader);
660 return mThumbnailPaint;
664 * Checks if the request is full-filled.
666 * @return True if both thumbnail animation and ripple animation are finished
668 public boolean isFinished() {
669 return mThumbnailAnimationFinished && mRippleAnimationFinished;
673 * Marks the thumbnail animation is finished.
675 public void finishThumbnailAnimation() {
676 mThumbnailAnimationFinished = true;
680 * Marks the ripple animation is finished.
682 public void finishRippleAnimation() {
683 mRippleAnimationFinished = true;
687 * Updates the thumbnail image.
689 * @param thumbnailBitmap The thumbnail image to be shown.
691 public void setThumbnailBitmap(Bitmap thumbnailBitmap) {
692 mOriginalBitmap = thumbnailBitmap;
693 // Crop the image if it is not square.
694 if (mOriginalBitmap.getWidth() != mOriginalBitmap.getHeight()) {
695 mOriginalBitmap = cropCenterBitmap(mOriginalBitmap);
700 * Obtains a square bitmap by cropping the center of a bitmap. If the given image is
701 * portrait, the cropped image keeps 100% original width and vertically centered to the
702 * original image. If the given image is landscape, the cropped image keeps 100% original
703 * height and horizontally centered to the original image.
705 * @param srcBitmap the bitmap image to be cropped in the center.
706 * @return a result square bitmap.
708 private Bitmap cropCenterBitmap(Bitmap srcBitmap) {
709 int srcWidth = srcBitmap.getWidth();
710 int srcHeight = srcBitmap.getHeight();
712 if (srcWidth >= srcHeight) {
713 dstBitmap = Bitmap.createBitmap(
714 srcBitmap, srcWidth / 2 - srcHeight / 2, 0, srcHeight, srcHeight);
716 dstBitmap = Bitmap.createBitmap(
717 srcBitmap, 0, srcHeight / 2 - srcWidth / 2, srcWidth, srcWidth);