FloatingActionButton.java revision de373c43e108bc24b46c947c7a4da26774cdb874
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.annotation.TargetApi;
20import android.content.Context;
21import android.content.res.ColorStateList;
22import android.content.res.TypedArray;
23import android.graphics.PorterDuff;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.os.Build;
27import android.support.annotation.Nullable;
28import android.support.design.R;
29import android.support.v4.view.ViewCompat;
30import android.support.v4.view.ViewPropertyAnimatorListener;
31import android.util.AttributeSet;
32import android.view.View;
33import android.widget.ImageView;
34
35import java.util.List;
36
37/**
38 * Floating action buttons are used for a special type of promoted action. They are distinguished
39 * by a circled icon floating above the UI and have special motion behaviors related to morphing,
40 * launching, and the transferring anchor point.
41 *
42 * Floating action buttons come in two sizes: the default, which should be used in most cases, and
43 * the mini, which should only be used to create visual continuity with other elements on the
44 * screen.
45 */
46@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
47public class FloatingActionButton extends ImageView {
48
49    // These values must match those in the attrs declaration
50    private static final int SIZE_MINI = 1;
51    private static final int SIZE_NORMAL = 0;
52
53    private ColorStateList mBackgroundTint;
54    private PorterDuff.Mode mBackgroundTintMode;
55
56    private int mRippleColor;
57    private int mSize;
58    private int mContentPadding;
59
60    private final Rect mShadowPadding;
61
62    private final FloatingActionButtonImpl mImpl;
63
64    public FloatingActionButton(Context context) {
65        this(context, null);
66    }
67
68    public FloatingActionButton(Context context, AttributeSet attrs) {
69        this(context, attrs, 0);
70    }
71
72    public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
73        super(context, attrs, defStyleAttr);
74
75        mShadowPadding = new Rect();
76
77        TypedArray a = context.obtainStyledAttributes(attrs,
78                R.styleable.FloatingActionButton, defStyleAttr,
79                R.style.Widget_Design_FloatingActionButton);
80        Drawable background = a.getDrawable(R.styleable.FloatingActionButton_android_background);
81        mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint);
82        mBackgroundTintMode = parseTintMode(a.getInt(
83                R.styleable.FloatingActionButton_backgroundTintMode, -1), null);
84        mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0);
85        mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_NORMAL);
86        final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
87        final float pressedTranslationZ = a.getDimension(
88                R.styleable.FloatingActionButton_pressedTranslationZ, 0f);
89        a.recycle();
90
91        final ShadowViewDelegate delegate = new ShadowViewDelegate() {
92            @Override
93            public float getRadius() {
94                return getSizeDimension() / 2f;
95            }
96
97            @Override
98            public void setShadowPadding(int left, int top, int right, int bottom) {
99                mShadowPadding.set(left, top, right, bottom);
100
101                setPadding(left + mContentPadding, top + mContentPadding,
102                        right + mContentPadding, bottom + mContentPadding);
103            }
104
105            @Override
106            public void setBackgroundDrawable(Drawable background) {
107                FloatingActionButton.super.setBackgroundDrawable(background);
108            }
109        };
110
111        if (Build.VERSION.SDK_INT >= 21) {
112            mImpl = new FloatingActionButtonLollipop(this, delegate);
113        } else {
114            mImpl = new FloatingActionButtonEclairMr1(this, delegate);
115        }
116
117        final int maxContentSize = (int) getResources().getDimension(R.dimen.fab_content_size);
118        mContentPadding = (getSizeDimension() - maxContentSize) / 2;
119
120        mImpl.setBackgroundDrawable(background, mBackgroundTint,
121                mBackgroundTintMode, mRippleColor);
122        mImpl.setElevation(elevation);
123        mImpl.setPressedTranslationZ(pressedTranslationZ);
124
125        setClickable(true);
126    }
127
128    @Override
129    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
130        final int preferredSize = getSizeDimension();
131
132        final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec);
133        final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec);
134
135        // As we want to stay circular, we set both dimensions to be the
136        // smallest resolved dimension
137        final int d = Math.min(w, h);
138
139        // We add the shadow's padding to the measured dimension
140        setMeasuredDimension(
141                d + mShadowPadding.left + mShadowPadding.right,
142                d + mShadowPadding.top + mShadowPadding.bottom);
143    }
144
145    /**
146     * Set the ripple color for this {@link FloatingActionButton}.
147     * <p>
148     * When running on devices with KitKat or below, we draw a fill rather than a ripple.
149     *
150     * @param color ARGB color to use for the ripple.
151     */
152    public void setRippleColor(int color) {
153        if (mRippleColor != color) {
154            mRippleColor = color;
155            mImpl.setRippleColor(color);
156        }
157    }
158
159    /**
160     * Return the tint applied to the background drawable, if specified.
161     *
162     * @return the tint applied to the background drawable
163     * @see #setBackgroundTintList(ColorStateList)
164     */
165    @Nullable
166    @Override
167    public ColorStateList getBackgroundTintList() {
168        return mBackgroundTint;
169    }
170
171    /**
172     * Applies a tint to the background drawable. Does not modify the current tint
173     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
174     *
175     * @param tint the tint to apply, may be {@code null} to clear tint
176     */
177    public void setBackgroundTintList(@Nullable ColorStateList tint) {
178        mImpl.setBackgroundTintList(tint);
179    }
180
181
182    /**
183     * Return the blending mode used to apply the tint to the background
184     * drawable, if specified.
185     *
186     * @return the blending mode used to apply the tint to the background
187     *         drawable
188     * @see #setBackgroundTintMode(PorterDuff.Mode)
189     */
190    @Nullable
191    @Override
192    public PorterDuff.Mode getBackgroundTintMode() {
193        return mBackgroundTintMode;
194    }
195
196    /**
197     * Specifies the blending mode used to apply the tint specified by
198     * {@link #setBackgroundTintList(ColorStateList)}} to the background
199     * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
200     *
201     * @param tintMode the blending mode used to apply the tint, may be
202     *                 {@code null} to clear tint
203     */
204    public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
205        mImpl.setBackgroundTintMode(tintMode);
206    }
207
208    @Override
209    public void setBackgroundDrawable(Drawable background) {
210        if (mImpl != null) {
211            mImpl.setBackgroundDrawable(
212                background, mBackgroundTint, mBackgroundTintMode, mRippleColor);
213        }
214    }
215
216    final int getSizeDimension() {
217        switch (mSize) {
218            case SIZE_MINI:
219                return getResources().getDimensionPixelSize(R.dimen.fab_size_mini);
220            case SIZE_NORMAL:
221            default:
222                return getResources().getDimensionPixelSize(R.dimen.fab_size_normal);
223        }
224    }
225
226    @Override
227    protected void drawableStateChanged() {
228        super.drawableStateChanged();
229        mImpl.onDrawableStateChanged(getDrawableState());
230    }
231
232    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
233    @Override
234    public void jumpDrawablesToCurrentState() {
235        super.jumpDrawablesToCurrentState();
236        mImpl.jumpDrawableToCurrentState();
237    }
238
239    private static int resolveAdjustedSize(int desiredSize, int measureSpec) {
240        int result = desiredSize;
241        int specMode = MeasureSpec.getMode(measureSpec);
242        int specSize = MeasureSpec.getSize(measureSpec);
243        switch (specMode) {
244            case MeasureSpec.UNSPECIFIED:
245                // Parent says we can be as big as we want. Just don't be larger
246                // than max size imposed on ourselves.
247                result = desiredSize;
248                break;
249            case MeasureSpec.AT_MOST:
250                // Parent says we can be as big as we want, up to specSize.
251                // Don't be larger than specSize, and don't be larger than
252                // the max size imposed on ourselves.
253                result = Math.min(desiredSize, specSize);
254                break;
255            case MeasureSpec.EXACTLY:
256                // No choice. Do what we are told.
257                result = specSize;
258                break;
259        }
260        return result;
261    }
262
263    static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) {
264        switch (value) {
265            case 3:
266                return PorterDuff.Mode.SRC_OVER;
267            case 5:
268                return PorterDuff.Mode.SRC_IN;
269            case 9:
270                return PorterDuff.Mode.SRC_ATOP;
271            case 14:
272                return PorterDuff.Mode.MULTIPLY;
273            case 15:
274                return PorterDuff.Mode.SCREEN;
275            default:
276                return defaultMode;
277        }
278    }
279
280    /**
281     * Behavior designed for use with {@link FloatingActionButton} instances. It's main function
282     * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do
283     * not cover them.
284     */
285    public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
286        // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is
287        // because we can use view translation properties which greatly simplifies the code.
288        private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;
289
290        private Rect mTmpRect;
291        private boolean mIsAnimatingOut;
292        private float mTranslationY;
293
294        @Override
295        public boolean layoutDependsOn(CoordinatorLayout parent,
296                FloatingActionButton child,
297                View dependency) {
298            // We're dependent on all SnackbarLayouts (if enabled)
299            return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
300        }
301
302        @Override
303        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
304                View dependency) {
305            if (dependency instanceof Snackbar.SnackbarLayout) {
306                updateFabTranslationForSnackbar(parent, child, dependency);
307            } else if (dependency instanceof AppBarLayout) {
308                final AppBarLayout appBarLayout = (AppBarLayout) dependency;
309                if (mTmpRect == null) {
310                    mTmpRect = new Rect();
311                }
312
313                // First, let's get the visible rect of the dependency
314                final Rect rect = mTmpRect;
315                ViewGroupUtils.getDescendantRect(parent, dependency, rect);
316
317                if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
318                    // If the anchor's bottom is below the seam, we'll animate our FAB out
319                    if (!mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
320                        animateOut(child);
321                    }
322                } else {
323                    // Else, we'll animate our FAB back in
324                    if (child.getVisibility() != View.VISIBLE) {
325                        animateIn(child);
326                    }
327                }
328            }
329            return false;
330        }
331
332        private void updateFabTranslationForSnackbar(CoordinatorLayout parent,
333                FloatingActionButton fab, View snackbar) {
334            final float translationY = getFabTranslationYForSnackbar(parent, fab);
335            if (translationY != mTranslationY) {
336                // First, cancel any current animation
337                ViewCompat.animate(fab).cancel();
338
339                if (Math.abs(translationY - mTranslationY) == snackbar.getHeight()) {
340                    // If we're travelling by the height of the Snackbar then we probably need to
341                    // animate to the value
342                    ViewCompat.animate(fab)
343                            .translationY(translationY)
344                            .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
345                            .setListener(null);
346                } else {
347                    // Else we'll set use setTranslationY
348                    ViewCompat.setTranslationY(fab, translationY);
349                }
350                mTranslationY = translationY;
351            }
352        }
353
354        private float getFabTranslationYForSnackbar(CoordinatorLayout parent,
355                FloatingActionButton fab) {
356            float minOffset = 0;
357            final List<View> dependencies = parent.getDependencies(fab);
358            for (int i = 0, z = dependencies.size(); i < z; i++) {
359                final View view = dependencies.get(i);
360                if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
361                    minOffset = Math.min(minOffset,
362                            ViewCompat.getTranslationY(view) - view.getHeight());
363                }
364            }
365
366            return minOffset;
367        }
368
369        private void animateIn(FloatingActionButton button) {
370            button.setVisibility(View.VISIBLE);
371
372            ViewCompat.animate(button)
373                    .scaleX(1f)
374                    .scaleY(1f)
375                    .alpha(1f)
376                    .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
377                    .withLayer()
378                    .setListener(null)
379                    .start();
380        }
381
382        private void animateOut(FloatingActionButton button) {
383            ViewCompat.animate(button)
384                    .scaleX(0f)
385                    .scaleY(0f)
386                    .alpha(0f)
387                    .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
388                    .withLayer()
389                    .setListener(new ViewPropertyAnimatorListener() {
390                        @Override
391                        public void onAnimationStart(View view) {
392                            mIsAnimatingOut = true;
393                        }
394
395                        @Override
396                        public void onAnimationCancel(View view) {
397                            mIsAnimatingOut = false;
398                        }
399
400                        @Override
401                        public void onAnimationEnd(View view) {
402                            mIsAnimatingOut = false;
403                            view.setVisibility(View.GONE);
404                        }
405                    }).start();
406        }
407    }
408}
409