OSDN Git Service

Adjust capture indicator position while open/close mode options.
[android-x86/packages-apps-Camera2.git] / src / com / android / camera / widget / RoundedThumbnailView.java
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.camera.widget;
18
19 import java.util.LinkedList;
20
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;
39
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;
45
46 import com.google.common.base.Optional;
47
48 /**
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.
52  *
53  * Thumbnail cropping:
54  *   (1) 100% width and vertically centered for portrait.
55  *   (2) 100% height and horizontally centered for landscape.
56  *
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.
61  *   (4) User close app.
62  *
63  * Visual spec:
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.
69  *
70  * Animation spec:
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.
78  */
79 public class RoundedThumbnailView extends View {
80     private static final Log.Tag TAG = new Log.Tag("RoundedThumbnailView");
81
82     /**
83      * Configurations for the thumbnail pop-out effect.
84      */
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;
89     /**
90      * Configurations for the ripple effect.
91      */
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;
95     /**
96      * Configurations for the hit-state effect.
97      */
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;
102
103     /**
104      * Defines call events.
105      */
106     public interface Callback {
107         public void onHitStateFinished();
108     }
109
110     /** The registered callback. */
111     private Optional<Callback> mCallback;
112
113     /**
114      * Fields for view layout.
115      */
116     private float mThumbnailPadding;
117
118     /**
119      * Fields for the thumbnail pop-out effect.
120      */
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;
139
140     /**
141      * Fields for the ripple effect.
142      */
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
158     // onDraw().
159     private float mCurrentRippleRingDiameter;
160     // The current ripple ring thickness which is updated by the ripple animator and used by
161     // onDraw().
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;
165
166     /**
167      * Fields for the hit state effect.
168      */
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;
174
175     // The waiting queue for all pending reveal requests. The latest request should be in the end of
176     // the queue.
177     private LinkedList<RevealRequest> mRevealRequestWaitQueue = new LinkedList<>();
178
179     // The currently running reveal request.
180     private Optional<RevealRequest> mActiveRevealRequest;
181
182     // The latest finished reveal request. Its thumbnail will be shown until a newer one replace it.
183     private Optional<RevealRequest> mFinishedRevealRequest;
184
185     private View.OnClickListener mOnClickListener = new View.OnClickListener() {
186         @Override
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() {
195                 @Override
196                 public void onAnimationUpdate(ValueAnimator valueAnimator) {
197                     mCurrentHitStateCircleOpacity = (Float) valueAnimator.getAnimatedValue();
198                     invalidate();
199                 }
200             });
201             hitStateAnimator.addListener(new AnimatorListenerAdapter() {
202                 @Override
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();
208                     }
209                 }
210             });
211             hitStateAnimator.start();
212         }
213     };
214
215     /**
216      * Constructs a RoundedThumbnailView.
217      */
218     public RoundedThumbnailView(Context context, AttributeSet attrs) {
219         super(context, attrs);
220
221         mCallback = Optional.absent();
222
223         // Make the view clickable.
224         setClickable(true);
225         setOnClickListener(mOnClickListener);
226
227         mThumbnailPadding = getResources().getDimension(R.dimen.rounded_thumbnail_padding);
228
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);
251
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);
258
259         mActiveRevealRequest = Optional.absent();
260         mFinishedRevealRequest = Optional.absent();
261     }
262
263     @Override
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);
268     }
269
270     @Override
271     protected void onDraw(Canvas canvas) {
272         super.onDraw(canvas);
273
274         final float centerX = canvas.getWidth() / 2;
275         final float centerY = canvas.getHeight() / 2;
276
277         final float viewDiameter = mRippleRingDiameterEnd;
278         final float finalDiameter = mThumbnailShrinkDiameterEnd;
279         final RectF viewBound =
280                 new RectF(0, 0, viewDiameter, viewDiameter);
281
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;
288
289                 canvas.save();
290                 canvas.scale(scaleRatio, scaleRatio, centerX, centerY);
291                 canvas.drawRoundRect(
292                         viewBound,
293                         centerX,
294                         centerY,
295                         thumbnailPaint);
296                 canvas.restore();
297             }
298         }
299
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);
311
312                 canvas.save();
313                 canvas.drawCircle(centerX, centerY, mCurrentRippleRingDiameter / 2, ripplePaint);
314                 canvas.restore();
315             }
316
317             // Achieve the animation effect by scaling the transformation matrix.
318             float scaleRatio = mCurrentThumbnailDiameter / mRippleRingDiameterEnd;
319
320             canvas.save();
321             canvas.scale(scaleRatio, scaleRatio, centerX, centerY);
322
323             // Draw the new popping up thumbnail.
324             Paint thumbnailPaint = mActiveRevealRequest.get().getThumbnailPaint();
325             if (thumbnailPaint != null) {
326                 canvas.drawRoundRect(
327                         viewBound,
328                         centerX,
329                         centerY,
330                         thumbnailPaint);
331             }
332
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);
341
342             canvas.restore();
343         }
344
345         // Draw hit state circle if necessary.
346         if (mCurrentHitStateCircleOpacity != HIT_STATE_CIRCLE_OPACITY_HIDDEN) {
347             canvas.save();
348             final float scaleRatio = finalDiameter / viewDiameter;
349             canvas.scale(scaleRatio, scaleRatio, centerX, centerY);
350
351             // Draw the hit state while circle.
352             mHitStateCirclePaint.setAlpha((int) (mCurrentHitStateCircleOpacity * 255));
353             canvas.drawCircle(centerX, centerY,
354                     mRippleRingDiameterEnd / 2, mHitStateCirclePaint);
355             canvas.restore();
356         }
357     }
358
359     /**
360      * Sets the callback.
361      *
362      * @param callback The callback to be set.
363      */
364     public void setCallback(Callback callback) {
365         mCallback = Optional.of(callback);
366     }
367
368     /**
369      * Gets the padding size with mode options and preview edges.
370      *
371      * @return The padding size with mode options and preview edges.
372      */
373     public float getThumbnailPadding() {
374         return mThumbnailPadding;
375     }
376
377     /**
378      * Gets the diameter of the thumbnail image after the revealing animation.
379      *
380      * @return The diameter of the thumbnail image after the revealing animation.
381      */
382     public float getThumbnailFinalDiameter() {
383         return mThumbnailShrinkDiameterEnd;
384     }
385
386     /**
387      * Starts the thumbnail revealing animation.
388      *
389      * @param accessibilityString An accessibility String to be announced during the revealing
390      *                            animation.
391      */
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();
400     }
401
402     /**
403      * Updates the thumbnail image.
404      *
405      * @param thumbnailBitmap The thumbnail image to be shown.
406      */
407     public void setThumbnail(final Bitmap thumbnailBitmap) {
408         MainThread.checkMainThread();
409         if (mRevealRequestWaitQueue.isEmpty()) {
410             if (mActiveRevealRequest.isPresent()) {
411                 mActiveRevealRequest.get().setThumbnailBitmap(thumbnailBitmap);
412             }
413         } else {
414             // Update the thumbnail in the latest reveal request.
415             RevealRequest latestRevealRequest = mRevealRequestWaitQueue.peekLast();
416             latestRevealRequest.setThumbnailBitmap(thumbnailBitmap);
417         }
418     }
419
420     /**
421      * Hide the thumbnail.
422      */
423     public void hideThumbnail() {
424         MainThread.checkMainThread();
425         // Make this view invisible.
426         setVisibility(GONE);
427
428         // Stop currently running animators.
429         if (mThumbnailAnimatorSet != null && mThumbnailAnimatorSet.isRunning()) {
430             mThumbnailAnimatorSet.removeAllListeners();
431             mThumbnailAnimatorSet.cancel();
432         }
433         if (mRippleAnimator != null && mRippleAnimator.isRunning()) {
434             mRippleAnimator.removeAllListeners();
435             mRippleAnimator.cancel();
436         }
437         // Remove all pending reveal requests.
438         mRevealRequestWaitQueue.clear();
439         mActiveRevealRequest = Optional.absent();
440         mFinishedRevealRequest = Optional.absent();
441     }
442
443     /**
444      * Pick the next request in the reveal request queue and start a reveal animation for the
445      * request.
446      */
447     private void processNextRevealRequest() {
448         // Do nothing if the queue is empty.
449         if (mRevealRequestWaitQueue.isEmpty()) {
450             return;
451         }
452         // Do nothing if the active request is still running.
453         if (mActiveRevealRequest.isPresent()) {
454             return;
455         }
456
457         // Pick the first request in the queue and make it active.
458         mActiveRevealRequest = Optional.of(mRevealRequestWaitQueue.peekFirst());
459         mRevealRequestWaitQueue.removeFirst();
460
461         // Make this view visible.
462         setVisibility(VISIBLE);
463
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);
471             } else {
472                 stretchInterpolator = new AccelerateDecelerateInterpolator();
473             }
474
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() {
481                 @Override
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;
489                     invalidate();
490                 }
491             });
492
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() {
500                 @Override
501                 public void onAnimationUpdate(ValueAnimator valueAnimator) {
502                     mCurrentThumbnailDiameter = (Float) valueAnimator.getAnimatedValue();
503                     invalidate();
504                 }
505             });
506
507             // The stretch and shrink animators play sequentially.
508             mThumbnailAnimatorSet = new AnimatorSet();
509             mThumbnailAnimatorSet.playSequentially(stretchAnimator, shrinkAnimator);
510             mThumbnailAnimatorSet.addListener(new AnimatorListenerAdapter() {
511                 @Override
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();
523                         }
524                     }
525                 }
526             });
527         }
528         // Start thumbnail animation immediately.
529         mThumbnailAnimatorSet.start();
530
531         // Lazily load the ripple animator.
532         if (mRippleAnimator == null) {
533
534             // Ripple effect uses linear_out_slow_in interpolator.
535             Interpolator rippleInterpolator =
536                     InterpolatorHelper.getLinearOutSlowInInterpolator(getContext());
537
538             // When start shrinking the thumbnail, a ripple effect is triggered at the same time.
539             mRippleAnimator =
540                     ValueAnimator.ofFloat(mRippleRingDiameterBegin, mRippleRingDiameterEnd);
541             mRippleAnimator.setDuration(mRippleDurationMs);
542             mRippleAnimator.setInterpolator(rippleInterpolator);
543             mRippleAnimator.addListener(new AnimatorListenerAdapter() {
544                 @Override
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();
556                         }
557                     }
558                 }
559             });
560             mRippleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
561                 @Override
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);
569                     invalidate();
570                 }
571             });
572         }
573         // Start ripple animation after delay.
574         mRippleAnimator.setStartDelay(mRippleStartDelayMs);
575         mRippleAnimator.start();
576
577         // Announce the accessibility string.
578         announceForAccessibility(mActiveRevealRequest.get().getAccessibilityString());
579     }
580
581     /**
582      * Encapsulates necessary information for a complete thumbnail reveal animation.
583      */
584     private static class RevealRequest {
585         // The size of the thumbnail.
586         private float mViewSize;
587
588         // The accessibility string.
589         private String mAccessibilityString;
590
591         // The original full-size image bitmap.
592         private Bitmap mOriginalBitmap;
593
594         // The cached Paint object to draw the thumbnail.
595         private Paint mThumbnailPaint;
596
597         // The flag to indicate if thumbnail animation of this request is full-filled.
598         private boolean mThumbnailAnimationFinished;
599
600         // The flag to indicate if ripple animation of this request is full-filled.
601         private boolean mRippleAnimationFinished;
602
603         /**
604          * Constructs a reveal request. Use setThumbnailBitmap() to specify a source bitmap for the
605          * thumbnail.
606          *
607          * @param viewSize The size of the capture indicator view.
608          * @param accessibilityString The accessibility string of the request.
609          */
610         public RevealRequest(float viewSize, String accessibilityString) {
611             mAccessibilityString = accessibilityString;
612             mViewSize = viewSize;
613         }
614
615         /**
616          * Returns the accessibility string.
617          *
618          * @return the accessibility string.
619          */
620         public String getAccessibilityString() {
621             return mAccessibilityString;
622         }
623
624         /**
625          * Returns the paint object which can be used to draw the thumbnail on a Canvas.
626          *
627          * @return the paint object which can be used to draw the thumbnail on a Canvas.
628          */
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) {
634                     return null;
635                 }
636                 // The original bitmap should be a square shape.
637                 if (mOriginalBitmap.getWidth() != mOriginalBitmap.getHeight()) {
638                     return null;
639                 }
640
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
646                     // matched.
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);
653                 }
654
655                 // Create the paint for drawing the thumbnail in a circle.
656                 mThumbnailPaint = new Paint();
657                 mThumbnailPaint.setAntiAlias(true);
658                 mThumbnailPaint.setShader(shader);
659             }
660             return mThumbnailPaint;
661         }
662
663         /**
664          * Checks if the request is full-filled.
665          *
666          * @return True if both thumbnail animation and ripple animation are finished
667          */
668         public boolean isFinished() {
669             return mThumbnailAnimationFinished && mRippleAnimationFinished;
670         }
671
672         /**
673          * Marks the thumbnail animation is finished.
674          */
675         public void finishThumbnailAnimation() {
676             mThumbnailAnimationFinished = true;
677         }
678
679         /**
680          * Marks the ripple animation is finished.
681          */
682         public void finishRippleAnimation() {
683             mRippleAnimationFinished = true;
684         }
685
686         /**
687          * Updates the thumbnail image.
688          *
689          * @param thumbnailBitmap The thumbnail image to be shown.
690          */
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);
696             }
697         }
698
699         /**
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.
704          *
705          * @param srcBitmap the bitmap image to be cropped in the center.
706          * @return a result square bitmap.
707          */
708         private Bitmap cropCenterBitmap(Bitmap srcBitmap) {
709             int srcWidth = srcBitmap.getWidth();
710             int srcHeight = srcBitmap.getHeight();
711             Bitmap dstBitmap;
712             if (srcWidth >= srcHeight) {
713                 dstBitmap = Bitmap.createBitmap(
714                         srcBitmap, srcWidth / 2 - srcHeight / 2, 0, srcHeight, srcHeight);
715             } else {
716                 dstBitmap = Bitmap.createBitmap(
717                         srcBitmap, 0, srcHeight / 2 - srcWidth / 2, srcWidth, srcWidth);
718             }
719             return dstBitmap;
720         }
721     }
722 }