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