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