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 android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.graphics.Bitmap;
25import android.graphics.BitmapShader;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Matrix;
29import android.graphics.Paint;
30import android.graphics.RectF;
31import android.graphics.Shader;
32import android.util.AttributeSet;
33import android.view.View;
34import android.view.animation.AccelerateDecelerateInterpolator;
35import android.view.animation.AnimationUtils;
36import android.view.animation.Interpolator;
37
38import com.android.camera.async.MainThread;
39import com.android.camera.debug.Log;
40import com.android.camera.ui.motion.InterpolatorHelper;
41import com.android.camera.util.ApiHelper;
42import com.android.camera2.R;
43import 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 */
76public 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