• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapShader;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Matrix;
29 import android.graphics.Paint;
30 import android.graphics.RectF;
31 import android.graphics.Shader;
32 import android.util.AttributeSet;
33 import android.view.View;
34 import android.view.animation.AccelerateDecelerateInterpolator;
35 import android.view.animation.AnimationUtils;
36 import android.view.animation.Interpolator;
37 
38 import com.android.camera.async.MainThread;
39 import com.android.camera.debug.Log;
40 import com.android.camera.ui.motion.InterpolatorHelper;
41 import com.android.camera.util.ApiHelper;
42 import com.android.camera2.R;
43 import 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  */
76 public 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 {
onHitStateFinished()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      */
RoundedThumbnailView(Context context, AttributeSet attrs)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onDraw(Canvas canvas)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      */
setCallback(Callback callback)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      */
getThumbnailPadding()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      */
getThumbnailFinalDiameter()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      */
startRevealThumbnailAnimation(String accessibilityString)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      */
setThumbnail(final Bitmap thumbnailBitmap, final int rotation)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      */
hideThumbnail()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      */
clearAnimations()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      */
runPendingRequestAnimation()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 
processRevealRequests()590     private void processRevealRequests() {
591         if(mForegroundRequest != null && mForegroundRequest.isFinished()) {
592             mBackgroundRequest = mForegroundRequest;
593             mForegroundRequest = null;
594         }
595     }
596 
597     @Override
hasOverlappingRendering()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          */
RevealRequest(float viewSize, String accessibilityString)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          */
getAccessibilityString()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          */
getThumbnailPaint()647         public Paint getThumbnailPaint() {
648             return mThumbnailPaint;
649         }
650 
651         /**
652          * Used to precompute the thumbnail paint from the given source bitmap.
653          */
precomputeThumbnailPaint(Bitmap srcBitmap, int rotation)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          */
isFinished()701         public boolean isFinished() {
702             return mThumbnailAnimationFinished && mRippleAnimationFinished;
703         }
704 
705         /**
706          * Marks the thumbnail animation is finished.
707          */
finishThumbnailAnimation()708         public void finishThumbnailAnimation() {
709             mThumbnailAnimationFinished = true;
710         }
711 
712         /**
713          * Marks the ripple animation is finished.
714          */
finishRippleAnimation()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          */
setThumbnailBitmap(Bitmap thumbnailBitmap, int rotation)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          */
cropCenterBitmap(Bitmap srcBitmap)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