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