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}