Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2013 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.ui;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorSet;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.ValueAnimator;
     23 import android.content.Context;
     24 import android.graphics.Bitmap;
     25 import android.graphics.Canvas;
     26 import android.graphics.Paint;
     27 import android.graphics.Path;
     28 import android.graphics.PorterDuff;
     29 import android.graphics.PorterDuffXfermode;
     30 import android.graphics.Rect;
     31 import android.graphics.drawable.ColorDrawable;
     32 import android.graphics.drawable.Drawable;
     33 import android.util.AttributeSet;
     34 import android.view.GestureDetector;
     35 import android.view.MotionEvent;
     36 import android.view.View;
     37 
     38 import com.android.camera.app.CameraAppUI;
     39 import com.android.camera.debug.Log;
     40 import com.android.camera.util.Gusterpolator;
     41 import com.android.camera2.R;
     42 
     43 /**
     44  * This view is designed to handle all the animations during camera mode transition.
     45  * It should only be visible during mode switch.
     46  */
     47 public class ModeTransitionView extends View {
     48     private static final Log.Tag TAG = new Log.Tag("ModeTransView");
     49 
     50     private static final int PEEP_HOLE_ANIMATION_DURATION_MS = 300;
     51     private static final int ICON_FADE_OUT_DURATION_MS = 850;
     52     private static final int FADE_OUT_DURATION_MS = 250;
     53 
     54     private static final int IDLE = 0;
     55     private static final int PULL_UP_SHADE = 1;
     56     private static final int PULL_DOWN_SHADE = 2;
     57     private static final int PEEP_HOLE_ANIMATION = 3;
     58     private static final int FADE_OUT = 4;
     59     private static final int SHOW_STATIC_IMAGE = 5;
     60 
     61     private static final float SCROLL_DISTANCE_MULTIPLY_FACTOR = 2f;
     62     private static final int ALPHA_FULLY_TRANSPARENT = 0;
     63     private static final int ALPHA_FULLY_OPAQUE = 255;
     64     private static final int ALPHA_HALF_TRANSPARENT = 127;
     65 
     66     private final GestureDetector mGestureDetector;
     67     private final Paint mMaskPaint = new Paint();
     68     private final Rect mIconRect = new Rect();
     69     /** An empty drawable to fall back to when mIconDrawable set to null. */
     70     private final Drawable mDefaultDrawable = new ColorDrawable();
     71 
     72     private Drawable mIconDrawable;
     73     private int mBackgroundColor;
     74     private int mWidth = 0;
     75     private int mHeight = 0;
     76     private int mPeepHoleCenterX = 0;
     77     private int mPeepHoleCenterY = 0;
     78     private float mRadius = 0f;
     79     private int mIconSize;
     80     private AnimatorSet mPeepHoleAnimator;
     81     private int mAnimationType = PEEP_HOLE_ANIMATION;
     82     private float mScrollDistance = 0;
     83     private final Path mShadePath = new Path();
     84     private final Paint mShadePaint = new Paint();
     85     private CameraAppUI.AnimationFinishedListener mAnimationFinishedListener;
     86     private float mScrollTrend;
     87     private Bitmap mBackgroundBitmap;
     88 
     89     public ModeTransitionView(Context context, AttributeSet attrs) {
     90         super(context, attrs);
     91         mMaskPaint.setAlpha(0);
     92         mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
     93         mBackgroundColor = getResources().getColor(R.color.video_mode_color);
     94         mGestureDetector = new GestureDetector(getContext(),
     95                 new GestureDetector.SimpleOnGestureListener() {
     96                     @Override
     97                     public boolean onDown(MotionEvent ev) {
     98                         setScrollDistance(0f);
     99                         mScrollTrend = 0f;
    100                         return true;
    101                     }
    102 
    103                     @Override
    104                     public boolean onScroll(MotionEvent e1, MotionEvent e2,
    105                                             float distanceX, float distanceY) {
    106                         setScrollDistance(getScrollDistance()
    107                                 + SCROLL_DISTANCE_MULTIPLY_FACTOR * distanceY);
    108                         mScrollTrend = 0.3f * mScrollTrend + 0.7f * distanceY;
    109                         return false;
    110                     }
    111                 });
    112         mIconSize = getResources().getDimensionPixelSize(R.dimen.mode_transition_view_icon_size);
    113         setIconDrawable(mDefaultDrawable);
    114     }
    115 
    116     /**
    117      * Updates the size and shape of the shade
    118      */
    119     private void updateShade() {
    120         if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) {
    121             mShadePath.reset();
    122             float shadeHeight;
    123             if (mAnimationType == PULL_UP_SHADE) {
    124                 // Scroll distance > 0.
    125                 mShadePath.addRect(0, mHeight - getScrollDistance(), mWidth, mHeight,
    126                         Path.Direction.CW);
    127                 shadeHeight = getScrollDistance();
    128             } else {
    129                 // Scroll distance < 0.
    130                 mShadePath.addRect(0, 0, mWidth, - getScrollDistance(), Path.Direction.CW);
    131                 shadeHeight = getScrollDistance() * (-1);
    132             }
    133 
    134             if (mIconDrawable != null) {
    135                 if (shadeHeight < mHeight / 2 || mHeight == 0) {
    136                     mIconDrawable.setAlpha(ALPHA_FULLY_TRANSPARENT);
    137                 } else {
    138                     int alpha  = ((int) shadeHeight - mHeight / 2)  * ALPHA_FULLY_OPAQUE
    139                             / (mHeight / 2);
    140                     mIconDrawable.setAlpha(alpha);
    141                 }
    142             }
    143             invalidate();
    144         }
    145     }
    146 
    147     /**
    148      * Sets the scroll distance. Note this function gets called in every
    149      * frame during animation. It should be very light weight.
    150      *
    151      * @param scrollDistance the scaled distance that user has scrolled
    152      */
    153     public void setScrollDistance(float scrollDistance) {
    154         // First make sure scroll distance is clamped to the valid range.
    155         if (mAnimationType == PULL_UP_SHADE) {
    156             scrollDistance = Math.min(scrollDistance, mHeight);
    157             scrollDistance = Math.max(scrollDistance, 0);
    158         } else if (mAnimationType == PULL_DOWN_SHADE) {
    159             scrollDistance = Math.min(scrollDistance, 0);
    160             scrollDistance = Math.max(scrollDistance, -mHeight);
    161         }
    162         mScrollDistance = scrollDistance;
    163         updateShade();
    164     }
    165 
    166     public float getScrollDistance() {
    167         return mScrollDistance;
    168     }
    169 
    170     @Override
    171     public void onDraw(Canvas canvas) {
    172         if (mAnimationType == PEEP_HOLE_ANIMATION) {
    173             canvas.drawColor(mBackgroundColor);
    174             if (mPeepHoleAnimator != null) {
    175                 // Draw a transparent circle using clear mode
    176                 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
    177             }
    178         } else if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) {
    179             canvas.drawPath(mShadePath, mShadePaint);
    180         } else if (mAnimationType == IDLE || mAnimationType == FADE_OUT) {
    181             canvas.drawColor(mBackgroundColor);
    182         } else if (mAnimationType == SHOW_STATIC_IMAGE) {
    183             // TODO: These different animation types need to be refactored into
    184             // different animation effects.
    185             canvas.drawBitmap(mBackgroundBitmap, 0, 0, null);
    186             super.onDraw(canvas);
    187             return;
    188         }
    189         super.onDraw(canvas);
    190         mIconDrawable.draw(canvas);
    191     }
    192 
    193     @Override
    194     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    195         mWidth = right - left;
    196         mHeight = bottom - top;
    197         // Center the icon in the view.
    198         mIconRect.set(mWidth / 2 - mIconSize / 2, mHeight / 2 - mIconSize / 2,
    199                 mWidth / 2 + mIconSize / 2, mHeight / 2 + mIconSize / 2);
    200         mIconDrawable.setBounds(mIconRect);
    201     }
    202 
    203     /**
    204      * This is an overloaded function. When no position is provided for the animation,
    205      * the peep hole will start at the default position (i.e. center of the view).
    206      */
    207     public void startPeepHoleAnimation() {
    208         float x = mWidth / 2;
    209         float y = mHeight / 2;
    210         startPeepHoleAnimation(x, y);
    211     }
    212 
    213     /**
    214      * Starts the peep hole animation where the circle is centered at position (x, y).
    215      */
    216     private void startPeepHoleAnimation(float x, float y) {
    217         if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
    218             return;
    219         }
    220         mAnimationType = PEEP_HOLE_ANIMATION;
    221         mPeepHoleCenterX = (int) x;
    222         mPeepHoleCenterY = (int) y;
    223 
    224         int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
    225         int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
    226         int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
    227                 + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
    228 
    229         final ValueAnimator radiusAnimator = ValueAnimator.ofFloat(0, endRadius);
    230         radiusAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
    231 
    232         final ValueAnimator  iconScaleAnimator = ValueAnimator.ofFloat(1f, 0.5f);
    233         iconScaleAnimator.setDuration(ICON_FADE_OUT_DURATION_MS);
    234 
    235         final ValueAnimator  iconAlphaAnimator = ValueAnimator.ofInt(ALPHA_HALF_TRANSPARENT,
    236                 ALPHA_FULLY_TRANSPARENT);
    237         iconAlphaAnimator.setDuration(ICON_FADE_OUT_DURATION_MS);
    238 
    239         mPeepHoleAnimator = new AnimatorSet();
    240         mPeepHoleAnimator.playTogether(radiusAnimator, iconAlphaAnimator, iconScaleAnimator);
    241         mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
    242 
    243         iconAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    244             @Override
    245             public void onAnimationUpdate(ValueAnimator animation) {
    246                 // Modify mask by enlarging the hole
    247                 mRadius = (Float) radiusAnimator.getAnimatedValue();
    248 
    249                 mIconDrawable.setAlpha((Integer) iconAlphaAnimator.getAnimatedValue());
    250                 float scale = (Float) iconScaleAnimator.getAnimatedValue();
    251                 int size = (int) (scale * (float) mIconSize);
    252 
    253                 mIconDrawable.setBounds(mPeepHoleCenterX - size / 2,
    254                         mPeepHoleCenterY - size / 2,
    255                         mPeepHoleCenterX + size / 2,
    256                         mPeepHoleCenterY + size / 2);
    257 
    258                 invalidate();
    259             }
    260         });
    261 
    262         mPeepHoleAnimator.addListener(new Animator.AnimatorListener() {
    263             @Override
    264             public void onAnimationStart(Animator animation) {
    265                 // Sets a HW layer on the view for the animation.
    266                 setLayerType(LAYER_TYPE_HARDWARE, null);
    267             }
    268 
    269             @Override
    270             public void onAnimationEnd(Animator animation) {
    271                 // Sets the layer type back to NONE as a workaround for b/12594617.
    272                 setLayerType(LAYER_TYPE_NONE, null);
    273                 mPeepHoleAnimator = null;
    274                 mRadius = 0;
    275                 mIconDrawable.setAlpha(ALPHA_FULLY_OPAQUE);
    276                 mIconDrawable.setBounds(mIconRect);
    277                 setVisibility(GONE);
    278                 mAnimationType = IDLE;
    279                 if (mAnimationFinishedListener != null) {
    280                     mAnimationFinishedListener.onAnimationFinished(true);
    281                     mAnimationFinishedListener = null;
    282                 }
    283             }
    284 
    285             @Override
    286             public void onAnimationCancel(Animator animation) {
    287 
    288             }
    289 
    290             @Override
    291             public void onAnimationRepeat(Animator animation) {
    292 
    293             }
    294         });
    295         mPeepHoleAnimator.start();
    296 
    297     }
    298 
    299     @Override
    300     public boolean onTouchEvent(MotionEvent ev) {
    301         boolean touchHandled = mGestureDetector.onTouchEvent(ev);
    302         if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
    303             // TODO: Take into account fling
    304             snap();
    305         }
    306         return touchHandled;
    307     }
    308 
    309     /**
    310      * Snaps the shade to position at the end of a gesture.
    311      */
    312     private void snap() {
    313         if (mScrollTrend >= 0 && mAnimationType == PULL_UP_SHADE) {
    314             // Snap to full screen.
    315             snapShadeTo(mHeight, ALPHA_FULLY_OPAQUE);
    316         } else if (mScrollTrend <= 0 && mAnimationType == PULL_DOWN_SHADE) {
    317             // Snap to full screen.
    318             snapShadeTo(-mHeight, ALPHA_FULLY_OPAQUE);
    319         } else if (mScrollTrend < 0 && mAnimationType == PULL_UP_SHADE) {
    320             // Snap back.
    321             snapShadeTo(0, ALPHA_FULLY_TRANSPARENT, false);
    322         } else if (mScrollTrend > 0 && mAnimationType == PULL_DOWN_SHADE) {
    323             // Snap back.
    324             snapShadeTo(0, ALPHA_FULLY_TRANSPARENT, false);
    325         }
    326     }
    327 
    328     private void snapShadeTo(int scrollDistance, int alpha) {
    329         snapShadeTo(scrollDistance, alpha, true);
    330     }
    331 
    332     /**
    333      * Snaps the shade to a given scroll distance and sets the icon alpha. If the shade
    334      * is to snap back out, then hide the view after the animation.
    335      *
    336      * @param scrollDistance scaled user scroll distance
    337      * @param alpha ending alpha of the icon drawable
    338      * @param snapToFullScreen whether this snap animation snaps the shade to full screen
    339      */
    340     private void snapShadeTo(final int scrollDistance, final int alpha,
    341                              final boolean snapToFullScreen) {
    342         if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) {
    343             ObjectAnimator scrollAnimator = ObjectAnimator.ofFloat(this, "scrollDistance",
    344                     scrollDistance);
    345             scrollAnimator.addListener(new Animator.AnimatorListener() {
    346                 @Override
    347                 public void onAnimationStart(Animator animation) {
    348 
    349                 }
    350 
    351                 @Override
    352                 public void onAnimationEnd(Animator animation) {
    353                     setScrollDistance(scrollDistance);
    354                     mIconDrawable.setAlpha(alpha);
    355                     mAnimationType = IDLE;
    356                     if (!snapToFullScreen) {
    357                         setVisibility(GONE);
    358                     }
    359                     if (mAnimationFinishedListener != null) {
    360                         mAnimationFinishedListener.onAnimationFinished(snapToFullScreen);
    361                         mAnimationFinishedListener = null;
    362                     }
    363                 }
    364 
    365                 @Override
    366                 public void onAnimationCancel(Animator animation) {
    367 
    368                 }
    369 
    370                 @Override
    371                 public void onAnimationRepeat(Animator animation) {
    372 
    373                 }
    374             });
    375             scrollAnimator.setInterpolator(Gusterpolator.INSTANCE);
    376             scrollAnimator.start();
    377         }
    378     }
    379 
    380 
    381     /**
    382      * Set the states for the animation that pulls up a shade with given shade color.
    383      *
    384      * @param shadeColorId color id of the shade that will be pulled up
    385      * @param iconId id of the icon that will appear on top the shade
    386      * @param listener a listener that will get notified when the animation
    387      *        is finished. Could be <code>null</code>.
    388      */
    389     public void prepareToPullUpShade(int shadeColorId, int iconId,
    390                                      CameraAppUI.AnimationFinishedListener listener) {
    391         prepareShadeAnimation(PULL_UP_SHADE, shadeColorId, iconId, listener);
    392     }
    393 
    394     /**
    395      * Set the states for the animation that pulls down a shade with given shade color.
    396      *
    397      * @param shadeColorId color id of the shade that will be pulled down
    398      * @param modeIconResourceId id of the icon that will appear on top the shade
    399      * @param listener a listener that will get notified when the animation
    400      *        is finished. Could be <code>null</code>.
    401      */
    402     public void prepareToPullDownShade(int shadeColorId, int modeIconResourceId,
    403                                        CameraAppUI.AnimationFinishedListener listener) {;
    404         prepareShadeAnimation(PULL_DOWN_SHADE, shadeColorId, modeIconResourceId, listener);
    405     }
    406 
    407     /**
    408      * Set the states for the animation that involves a shade.
    409      *
    410      * @param animationType type of animation that will happen to the shade
    411      * @param shadeColorId color id of the shade that will be animated
    412      * @param iconResId id of the icon that will appear on top the shade
    413      * @param listener a listener that will get notified when the animation
    414      *        is finished. Could be <code>null</code>.
    415      */
    416     private void prepareShadeAnimation(int animationType, int shadeColorId, int iconResId,
    417                                        CameraAppUI.AnimationFinishedListener listener) {
    418         mAnimationFinishedListener = listener;
    419         if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
    420             mPeepHoleAnimator.end();
    421         }
    422         mAnimationType = animationType;
    423         resetShade(shadeColorId, iconResId);
    424     }
    425 
    426     /**
    427      * Reset the shade with the given shade color and icon drawable.
    428      *
    429      * @param shadeColorId id of the shade color
    430      * @param modeIconResourceId resource id of the icon drawable
    431      */
    432     private void resetShade(int shadeColorId, int modeIconResourceId) {
    433         // Sets color for the shade.
    434         int shadeColor = getResources().getColor(shadeColorId);
    435         mBackgroundColor = shadeColor;
    436         mShadePaint.setColor(shadeColor);
    437         // Reset scroll distance.
    438         setScrollDistance(0f);
    439         // Sets new drawable.
    440         updateIconDrawableByResourceId(modeIconResourceId);
    441         mIconDrawable.setAlpha(0);
    442         setVisibility(VISIBLE);
    443     }
    444 
    445     /**
    446      * By default, all drawables instances loaded from the same resource share a
    447      * common state; if you modify the state of one instance, all the other
    448      * instances will receive the same modification. So here we need to make sure
    449      * we mutate the drawable loaded from resource.
    450      *
    451      * @param modeIconResourceId resource id of the icon drawable
    452      */
    453     private void updateIconDrawableByResourceId(int modeIconResourceId) {
    454         Drawable iconDrawable = getResources().getDrawable(modeIconResourceId);
    455         if (iconDrawable == null) {
    456             // Resource id not found
    457             Log.e(TAG, "Invalid resource id for icon drawable. Setting icon drawable to null.");
    458             setIconDrawable(null);
    459             return;
    460         }
    461         // Mutate the drawable loaded from resource so modifying its states does
    462         // not affect other drawable instances loaded from the same resource.
    463         setIconDrawable(iconDrawable.mutate());
    464     }
    465 
    466     /**
    467      * In order to make sure icon drawable is never set to null. Fall back to an
    468      * empty drawable when icon needs to get reset.
    469      *
    470      * @param iconDrawable new drawable for icon. A value of <code>null</code> sets
    471      *        the icon drawable to the default drawable.
    472      */
    473     private void setIconDrawable(Drawable iconDrawable) {
    474         if (iconDrawable == null) {
    475             mIconDrawable = mDefaultDrawable;
    476         } else {
    477             mIconDrawable = iconDrawable;
    478         }
    479     }
    480 
    481     /**
    482      * Initialize the mode cover with a mode theme color and a mode icon.
    483      *
    484      * @param colorId resource id of the mode theme color
    485      * @param modeIconResourceId resource id of the icon drawable
    486      */
    487     public void setupModeCover(int colorId, int modeIconResourceId) {
    488         mBackgroundBitmap = null;
    489         // Stop ongoing animation.
    490         if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
    491             mPeepHoleAnimator.cancel();
    492         }
    493         mAnimationType = IDLE;
    494         mBackgroundColor = getResources().getColor(colorId);
    495         // Sets new drawable.
    496         updateIconDrawableByResourceId(modeIconResourceId);
    497         mIconDrawable.setAlpha(ALPHA_FULLY_OPAQUE);
    498         setVisibility(VISIBLE);
    499     }
    500 
    501     /**
    502      * Hides the cover view and notifies the
    503      * {@link com.android.camera.app.CameraAppUI.AnimationFinishedListener} of whether
    504      * the hide animation is successfully finished.
    505      *
    506      * @param animationFinishedListener a listener that will get notified when the
    507      *        animation is finished. Could be <code>null</code>.
    508      */
    509     public void hideModeCover(
    510             final CameraAppUI.AnimationFinishedListener animationFinishedListener) {
    511         if (mAnimationType != IDLE) {
    512             // Nothing to hide.
    513             if (animationFinishedListener != null) {
    514                 // Animation not successful.
    515                 animationFinishedListener.onAnimationFinished(false);
    516             }
    517         } else {
    518             // Start fade out animation.
    519             mAnimationType = FADE_OUT;
    520             ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f);
    521             alphaAnimator.setDuration(FADE_OUT_DURATION_MS);
    522             // Linear interpolation.
    523             alphaAnimator.setInterpolator(null);
    524             alphaAnimator.addListener(new Animator.AnimatorListener() {
    525                 @Override
    526                 public void onAnimationStart(Animator animation) {
    527 
    528                 }
    529 
    530                 @Override
    531                 public void onAnimationEnd(Animator animation) {
    532                     setVisibility(GONE);
    533                     setAlpha(1f);
    534                     if (animationFinishedListener != null) {
    535                         animationFinishedListener.onAnimationFinished(true);
    536                         mAnimationType = IDLE;
    537                     }
    538                 }
    539 
    540                 @Override
    541                 public void onAnimationCancel(Animator animation) {
    542 
    543                 }
    544 
    545                 @Override
    546                 public void onAnimationRepeat(Animator animation) {
    547 
    548                 }
    549             });
    550             alphaAnimator.start();
    551         }
    552     }
    553 
    554     @Override
    555     public void setAlpha(float alpha) {
    556         super.setAlpha(alpha);
    557         int alphaScaled = (int) (255f * getAlpha());
    558         mBackgroundColor = (mBackgroundColor & 0xFFFFFF) | (alphaScaled << 24);
    559         mIconDrawable.setAlpha(alphaScaled);
    560     }
    561 
    562     /**
    563      * Setup the mode cover with a screenshot.
    564      */
    565     public void setupModeCover(Bitmap screenShot) {
    566         mBackgroundBitmap = screenShot;
    567         setVisibility(VISIBLE);
    568         mAnimationType = SHOW_STATIC_IMAGE;
    569     }
    570 
    571     /**
    572      * Hide the mode cover without animation.
    573      */
    574     // TODO: Refactor this and define how cover should be hidden during cover setup
    575     public void hideImageCover() {
    576         mBackgroundBitmap = null;
    577         setVisibility(GONE);
    578         mAnimationType = IDLE;
    579     }
    580 }
    581 
    582