Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 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 android.support.design.widget;
     18 
     19 import android.content.res.ColorStateList;
     20 import android.graphics.Color;
     21 import android.graphics.PorterDuff;
     22 import android.graphics.Rect;
     23 import android.graphics.drawable.Drawable;
     24 import android.graphics.drawable.GradientDrawable;
     25 import android.graphics.drawable.LayerDrawable;
     26 import android.support.design.R;
     27 import android.support.v4.graphics.drawable.DrawableCompat;
     28 import android.view.View;
     29 import android.view.animation.Animation;
     30 import android.view.animation.Transformation;
     31 
     32 class FloatingActionButtonEclairMr1 extends FloatingActionButtonImpl {
     33 
     34     private Drawable mShapeDrawable;
     35     private Drawable mRippleDrawable;
     36     private Drawable mBorderDrawable;
     37 
     38     private float mElevation;
     39     private float mPressedTranslationZ;
     40     private int mAnimationDuration;
     41 
     42     private StateListAnimator mStateListAnimator;
     43 
     44     ShadowDrawableWrapper mShadowDrawable;
     45 
     46     private boolean mIsHiding;
     47 
     48     FloatingActionButtonEclairMr1(View view, ShadowViewDelegate shadowViewDelegate) {
     49         super(view, shadowViewDelegate);
     50 
     51         mAnimationDuration = view.getResources().getInteger(android.R.integer.config_shortAnimTime);
     52 
     53         mStateListAnimator = new StateListAnimator();
     54         mStateListAnimator.setTarget(view);
     55 
     56         // Elevate with translationZ when pressed or focused
     57         mStateListAnimator.addState(PRESSED_ENABLED_STATE_SET,
     58                 setupAnimation(new ElevateToTranslationZAnimation()));
     59         mStateListAnimator.addState(FOCUSED_ENABLED_STATE_SET,
     60                 setupAnimation(new ElevateToTranslationZAnimation()));
     61         // Reset back to elevation by default
     62         mStateListAnimator.addState(EMPTY_STATE_SET,
     63                 setupAnimation(new ResetElevationAnimation()));
     64     }
     65 
     66     @Override
     67     void setBackgroundDrawable(Drawable originalBackground, ColorStateList backgroundTint,
     68             PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) {
     69         // Now we need to tint the original background with the tint, using
     70         // an InsetDrawable if we have a border width
     71         mShapeDrawable = DrawableCompat.wrap(originalBackground.mutate());
     72         DrawableCompat.setTintList(mShapeDrawable, backgroundTint);
     73         if (backgroundTintMode != null) {
     74             DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode);
     75         }
     76 
     77         // Now we created a mask Drawable which will be used for touch feedback.
     78         // As we don't know the actual outline of mShapeDrawable, we'll just guess that it's a
     79         // circle
     80         GradientDrawable touchFeedbackShape = new GradientDrawable();
     81         touchFeedbackShape.setShape(GradientDrawable.OVAL);
     82         touchFeedbackShape.setColor(Color.WHITE);
     83         touchFeedbackShape.setCornerRadius(mShadowViewDelegate.getRadius());
     84 
     85         // We'll now wrap that touch feedback mask drawable with a ColorStateList. We do not need
     86         // to inset for any border here as LayerDrawable will nest the padding for us
     87         mRippleDrawable = DrawableCompat.wrap(touchFeedbackShape);
     88         DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor));
     89         DrawableCompat.setTintMode(mRippleDrawable, PorterDuff.Mode.MULTIPLY);
     90 
     91         final Drawable[] layers;
     92         if (borderWidth > 0) {
     93             mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint);
     94             layers = new Drawable[] {mBorderDrawable, mShapeDrawable, mRippleDrawable};
     95         } else {
     96             mBorderDrawable = null;
     97             layers = new Drawable[] {mShapeDrawable, mRippleDrawable};
     98         }
     99 
    100         mShadowDrawable = new ShadowDrawableWrapper(
    101                 mView.getResources(),
    102                 new LayerDrawable(layers),
    103                 mShadowViewDelegate.getRadius(),
    104                 mElevation,
    105                 mElevation + mPressedTranslationZ);
    106         mShadowDrawable.setAddPaddingForCorners(false);
    107 
    108         mShadowViewDelegate.setBackgroundDrawable(mShadowDrawable);
    109 
    110         updatePadding();
    111     }
    112 
    113     @Override
    114     void setBackgroundTintList(ColorStateList tint) {
    115         DrawableCompat.setTintList(mShapeDrawable, tint);
    116         if (mBorderDrawable != null) {
    117             DrawableCompat.setTintList(mBorderDrawable, tint);
    118         }
    119     }
    120 
    121     @Override
    122     void setBackgroundTintMode(PorterDuff.Mode tintMode) {
    123         DrawableCompat.setTintMode(mShapeDrawable, tintMode);
    124     }
    125 
    126     @Override
    127     void setRippleColor(int rippleColor) {
    128         DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor));
    129     }
    130 
    131     @Override
    132     void setElevation(float elevation) {
    133         if (mElevation != elevation && mShadowDrawable != null) {
    134             mShadowDrawable.setShadowSize(elevation, elevation + mPressedTranslationZ);
    135             mElevation = elevation;
    136             updatePadding();
    137         }
    138     }
    139 
    140     @Override
    141     void setPressedTranslationZ(float translationZ) {
    142         if (mPressedTranslationZ != translationZ && mShadowDrawable != null) {
    143             mPressedTranslationZ = translationZ;
    144             mShadowDrawable.setMaxShadowSize(mElevation + translationZ);
    145             updatePadding();
    146         }
    147     }
    148 
    149     @Override
    150     void onDrawableStateChanged(int[] state) {
    151         mStateListAnimator.setState(state);
    152     }
    153 
    154     @Override
    155     void jumpDrawableToCurrentState() {
    156         mStateListAnimator.jumpToCurrentState();
    157     }
    158 
    159     @Override
    160     void hide() {
    161         if (mIsHiding || mView.getVisibility() != View.VISIBLE) {
    162             // A hide animation is in progress, or we're already hidden. Skip the call
    163             return;
    164         }
    165 
    166         Animation anim = android.view.animation.AnimationUtils.loadAnimation(
    167                 mView.getContext(), R.anim.design_fab_out);
    168         anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
    169         anim.setDuration(SHOW_HIDE_ANIM_DURATION);
    170         anim.setAnimationListener(new AnimationUtils.AnimationListenerAdapter() {
    171             @Override
    172             public void onAnimationStart(Animation animation) {
    173                 mIsHiding = true;
    174             }
    175 
    176             @Override
    177             public void onAnimationEnd(Animation animation) {
    178                 mIsHiding = false;
    179                 mView.setVisibility(View.GONE);
    180             }
    181         });
    182         mView.startAnimation(anim);
    183     }
    184 
    185     @Override
    186     void show() {
    187         if (mView.getVisibility() != View.VISIBLE || mIsHiding) {
    188             // If the view is not visible, or is visible and currently being hidden, run
    189             // the show animation
    190             mView.clearAnimation();
    191             mView.setVisibility(View.VISIBLE);
    192             Animation anim = android.view.animation.AnimationUtils.loadAnimation(
    193                     mView.getContext(), R.anim.design_fab_in);
    194             anim.setDuration(SHOW_HIDE_ANIM_DURATION);
    195             anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
    196             mView.startAnimation(anim);
    197         }
    198     }
    199 
    200     private void updatePadding() {
    201         Rect rect = new Rect();
    202         mShadowDrawable.getPadding(rect);
    203         mShadowViewDelegate.setShadowPadding(rect.left, rect.top, rect.right, rect.bottom);
    204     }
    205 
    206     private Animation setupAnimation(Animation animation) {
    207         animation.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
    208         animation.setDuration(mAnimationDuration);
    209         return animation;
    210     }
    211 
    212     private abstract class BaseShadowAnimation extends Animation {
    213         private float mShadowSizeStart;
    214         private float mShadowSizeDiff;
    215 
    216         @Override
    217         public void reset() {
    218             super.reset();
    219 
    220             mShadowSizeStart = mShadowDrawable.getShadowSize();
    221             mShadowSizeDiff = getTargetShadowSize() - mShadowSizeStart;
    222         }
    223 
    224         @Override
    225         protected void applyTransformation(float interpolatedTime, Transformation t) {
    226             mShadowDrawable.setShadowSize(mShadowSizeStart + (mShadowSizeDiff * interpolatedTime));
    227         }
    228 
    229         /**
    230          * @return the shadow size we want to animate to.
    231          */
    232         protected abstract float getTargetShadowSize();
    233     }
    234 
    235     private class ResetElevationAnimation extends BaseShadowAnimation {
    236         @Override
    237         protected float getTargetShadowSize() {
    238             return mElevation;
    239         }
    240     }
    241 
    242     private class ElevateToTranslationZAnimation extends BaseShadowAnimation {
    243         @Override
    244         protected float getTargetShadowSize() {
    245             return mElevation + mPressedTranslationZ;
    246         }
    247     }
    248 
    249     private static ColorStateList createColorStateList(int selectedColor) {
    250         final int[][] states = new int[3][];
    251         final int[] colors = new int[3];
    252         int i = 0;
    253 
    254         states[i] = FOCUSED_ENABLED_STATE_SET;
    255         colors[i] = selectedColor;
    256         i++;
    257 
    258         states[i] = PRESSED_ENABLED_STATE_SET;
    259         colors[i] = selectedColor;
    260         i++;
    261 
    262         // Default enabled state
    263         states[i] = new int[0];
    264         colors[i] = Color.TRANSPARENT;
    265         i++;
    266 
    267         return new ColorStateList(states, colors);
    268     }
    269 }