Home | History | Annotate | Download | only in widget
      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 {
     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