FloatingActionButton.java revision b6e7e4c2bcefc6bba852f9e36a02c6c2519f6466
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.Resources;
23import android.content.res.TypedArray;
24import android.graphics.PorterDuff;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.os.Build;
28import android.support.annotation.ColorInt;
29import android.support.annotation.DrawableRes;
30import android.support.annotation.IntDef;
31import android.support.annotation.NonNull;
32import android.support.annotation.Nullable;
33import android.support.annotation.VisibleForTesting;
34import android.support.design.R;
35import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
36import android.support.v4.content.res.ConfigurationHelper;
37import android.support.v4.view.ViewCompat;
38import android.support.v7.widget.AppCompatImageHelper;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.view.Gravity;
42import android.view.MotionEvent;
43import android.view.View;
44import android.widget.ImageView;
45
46import java.lang.annotation.Retention;
47import java.lang.annotation.RetentionPolicy;
48import java.util.List;
49
50/**
51 * Floating action buttons are used for a special type of promoted action. They are distinguished
52 * by a circled icon floating above the UI and have special motion behaviors related to morphing,
53 * launching, and the transferring anchor point.
54 *
55 * <p>Floating action buttons come in two sizes: the default and the mini. The size can be
56 * controlled with the {@code fabSize} attribute.</p>
57 *
58 * <p>As this class descends from {@link ImageView}, you can control the icon which is displayed
59 * via {@link #setImageDrawable(Drawable)}.</p>
60 *
61 * <p>The background color of this view defaults to the your theme's {@code colorAccent}. If you
62 * wish to change this at runtime then you can do so via
63 * {@link #setBackgroundTintList(ColorStateList)}.</p>
64 */
65@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
66public class FloatingActionButton extends VisibilityAwareImageButton {
67
68    private static final String LOG_TAG = "FloatingActionButton";
69
70    /**
71     * Callback to be invoked when the visibility of a FloatingActionButton changes.
72     */
73    public abstract static class OnVisibilityChangedListener {
74        /**
75         * Called when a FloatingActionButton has been
76         * {@link #show(OnVisibilityChangedListener) shown}.
77         *
78         * @param fab the FloatingActionButton that was shown.
79         */
80        public void onShown(FloatingActionButton fab) {}
81
82        /**
83         * Called when a FloatingActionButton has been
84         * {@link #hide(OnVisibilityChangedListener) hidden}.
85         *
86         * @param fab the FloatingActionButton that was hidden.
87         */
88        public void onHidden(FloatingActionButton fab) {}
89    }
90
91    // These values must match those in the attrs declaration
92
93    /**
94     * The mini sized button. Will always been smaller than {@link #SIZE_NORMAL}.
95     *
96     * @see #setSize(int)
97     */
98    public static final int SIZE_MINI = 1;
99
100    /**
101     * The normal sized button. Will always been larger than {@link #SIZE_MINI}.
102     *
103     * @see #setSize(int)
104     */
105    public static final int SIZE_NORMAL = 0;
106
107    /**
108     * Size which will change based on the window size. For small sized windows
109     * (largest screen dimension < 470dp) this will select a small sized button, and for
110     * larger sized windows it will select a larger size.
111     *
112     * @see #setSize(int)
113     */
114    public static final int SIZE_AUTO = -1;
115
116    /**
117     * The switch point for the largest screen edge where SIZE_AUTO switches from mini to normal.
118     */
119    private static final int AUTO_MINI_LARGEST_SCREEN_WIDTH = 470;
120
121    /** @hide */
122    @Retention(RetentionPolicy.SOURCE)
123    @IntDef({SIZE_MINI, SIZE_NORMAL, SIZE_AUTO})
124    public @interface Size {}
125
126    private ColorStateList mBackgroundTint;
127    private PorterDuff.Mode mBackgroundTintMode;
128
129    private int mBorderWidth;
130    private int mRippleColor;
131    private int mSize;
132    private int mImagePadding;
133    private int mMaxImageSize;
134
135    private boolean mCompatPadding;
136    private final Rect mShadowPadding = new Rect();
137    private final Rect mTouchArea = new Rect();
138
139    private AppCompatImageHelper mImageHelper;
140
141    private FloatingActionButtonImpl mImpl;
142
143    public FloatingActionButton(Context context) {
144        this(context, null);
145    }
146
147    public FloatingActionButton(Context context, AttributeSet attrs) {
148        this(context, attrs, 0);
149    }
150
151    public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
152        super(context, attrs, defStyleAttr);
153
154        ThemeUtils.checkAppCompatTheme(context);
155
156        TypedArray a = context.obtainStyledAttributes(attrs,
157                R.styleable.FloatingActionButton, defStyleAttr,
158                R.style.Widget_Design_FloatingActionButton);
159        mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint);
160        mBackgroundTintMode = ViewUtils.parseTintMode(a.getInt(
161                R.styleable.FloatingActionButton_backgroundTintMode, -1), null);
162        mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0);
163        mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_AUTO);
164        mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0);
165        final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
166        final float pressedTranslationZ = a.getDimension(
167                R.styleable.FloatingActionButton_pressedTranslationZ, 0f);
168        mCompatPadding = a.getBoolean(R.styleable.FloatingActionButton_useCompatPadding, false);
169        a.recycle();
170
171        mImageHelper = new AppCompatImageHelper(this);
172        mImageHelper.loadFromAttributes(attrs, defStyleAttr);
173
174        mMaxImageSize = (int) getResources().getDimension(R.dimen.design_fab_image_size);
175
176        getImpl().setBackgroundDrawable(mBackgroundTint, mBackgroundTintMode,
177                mRippleColor, mBorderWidth);
178        getImpl().setElevation(elevation);
179        getImpl().setPressedTranslationZ(pressedTranslationZ);
180    }
181
182    @Override
183    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
184        final int preferredSize = getSizeDimension();
185
186        mImagePadding = (preferredSize - mMaxImageSize) / 2;
187        getImpl().updatePadding();
188
189        final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec);
190        final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec);
191
192        // As we want to stay circular, we set both dimensions to be the
193        // smallest resolved dimension
194        final int d = Math.min(w, h);
195
196        // We add the shadow's padding to the measured dimension
197        setMeasuredDimension(
198                d + mShadowPadding.left + mShadowPadding.right,
199                d + mShadowPadding.top + mShadowPadding.bottom);
200    }
201
202    /**
203     * Returns the ripple color for this button.
204     *
205     * @return the ARGB color used for the ripple
206     * @see #setRippleColor(int)
207     */
208    @ColorInt
209    public int getRippleColor() {
210        return mRippleColor;
211    }
212
213    /**
214     * Sets the ripple color for this button.
215     *
216     * <p>When running on devices with KitKat or below, we draw this color as a filled circle
217     * rather than a ripple.</p>
218     *
219     * @param color ARGB color to use for the ripple
220     * @attr ref android.support.design.R.styleable#FloatingActionButton_rippleColor
221     * @see #getRippleColor()
222     */
223    public void setRippleColor(@ColorInt int color) {
224        if (mRippleColor != color) {
225            mRippleColor = color;
226            getImpl().setRippleColor(color);
227        }
228    }
229
230    /**
231     * Returns the tint applied to the background drawable, if specified.
232     *
233     * @return the tint applied to the background drawable
234     * @see #setBackgroundTintList(ColorStateList)
235     */
236    @Nullable
237    @Override
238    public ColorStateList getBackgroundTintList() {
239        return mBackgroundTint;
240    }
241
242    /**
243     * Applies a tint to the background drawable. Does not modify the current tint
244     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
245     *
246     * @param tint the tint to apply, may be {@code null} to clear tint
247     */
248    public void setBackgroundTintList(@Nullable ColorStateList tint) {
249        if (mBackgroundTint != tint) {
250            mBackgroundTint = tint;
251            getImpl().setBackgroundTintList(tint);
252        }
253    }
254
255    /**
256     * Returns the blending mode used to apply the tint to the background
257     * drawable, if specified.
258     *
259     * @return the blending mode used to apply the tint to the background
260     *         drawable
261     * @see #setBackgroundTintMode(PorterDuff.Mode)
262     */
263    @Nullable
264    @Override
265    public PorterDuff.Mode getBackgroundTintMode() {
266        return mBackgroundTintMode;
267    }
268
269    /**
270     * Specifies the blending mode used to apply the tint specified by
271     * {@link #setBackgroundTintList(ColorStateList)}} to the background
272     * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
273     *
274     * @param tintMode the blending mode used to apply the tint, may be
275     *                 {@code null} to clear tint
276     */
277    public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
278        if (mBackgroundTintMode != tintMode) {
279            mBackgroundTintMode = tintMode;
280            getImpl().setBackgroundTintMode(tintMode);
281        }
282    }
283
284    @Override
285    public void setBackgroundDrawable(Drawable background) {
286        Log.i(LOG_TAG, "Setting a custom background is not supported.");
287    }
288
289    @Override
290    public void setBackgroundResource(int resid) {
291        Log.i(LOG_TAG, "Setting a custom background is not supported.");
292    }
293
294    @Override
295    public void setBackgroundColor(int color) {
296        Log.i(LOG_TAG, "Setting a custom background is not supported.");
297    }
298
299    @Override
300    public void setImageResource(@DrawableRes int resId) {
301        // Intercept this call and instead retrieve the Drawable via the image helper
302        mImageHelper.setImageResource(resId);
303    }
304
305    /**
306     * Shows the button.
307     * <p>This method will animate the button show if the view has already been laid out.</p>
308     */
309    public void show() {
310        show(null);
311    }
312
313    /**
314     * Shows the button.
315     * <p>This method will animate the button show if the view has already been laid out.</p>
316     *
317     * @param listener the listener to notify when this view is shown
318     */
319    public void show(@Nullable final OnVisibilityChangedListener listener) {
320        show(listener, true);
321    }
322
323    private void show(OnVisibilityChangedListener listener, boolean fromUser) {
324        getImpl().show(wrapOnVisibilityChangedListener(listener), fromUser);
325    }
326
327    /**
328     * Hides the button.
329     * <p>This method will animate the button hide if the view has already been laid out.</p>
330     */
331    public void hide() {
332        hide(null);
333    }
334
335    /**
336     * Hides the button.
337     * <p>This method will animate the button hide if the view has already been laid out.</p>
338     *
339     * @param listener the listener to notify when this view is hidden
340     */
341    public void hide(@Nullable OnVisibilityChangedListener listener) {
342        hide(listener, true);
343    }
344
345    private void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
346        getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);
347    }
348
349    /**
350     * Set whether FloatingActionButton should add inner padding on platforms Lollipop and after,
351     * to ensure consistent dimensions on all platforms.
352     *
353     * @param useCompatPadding true if FloatingActionButton is adding inner padding on platforms
354     *                         Lollipop and after, to ensure consistent dimensions on all platforms.
355     *
356     * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
357     * @see #getUseCompatPadding()
358     */
359    public void setUseCompatPadding(boolean useCompatPadding) {
360        if (mCompatPadding != useCompatPadding) {
361            mCompatPadding = useCompatPadding;
362            getImpl().onCompatShadowChanged();
363        }
364    }
365
366    /**
367     * Returns whether FloatingActionButton will add inner padding on platforms Lollipop and after.
368     *
369     * @return true if FloatingActionButton is adding inner padding on platforms Lollipop and after,
370     * to ensure consistent dimensions on all platforms.
371     *
372     * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
373     * @see #setUseCompatPadding(boolean)
374     */
375    public boolean getUseCompatPadding() {
376        return mCompatPadding;
377    }
378
379    /**
380     * Sets the size of the button.
381     *
382     * <p>The options relate to the options available on the material design specification.
383     * {@link #SIZE_NORMAL} is larger than {@link #SIZE_MINI}. {@link #SIZE_AUTO} will choose
384     * an appropriate size based on the screen size.</p>
385     *
386     * @param size one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
387     *
388     * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize
389     */
390    public void setSize(@Size int size) {
391        if (size != mSize) {
392            mSize = size;
393            requestLayout();
394        }
395    }
396
397    /**
398     * Returns the chosen size for this button.
399     *
400     * @return one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
401     * @see #setSize(int)
402     */
403    @Size
404    public int getSize() {
405        return mSize;
406    }
407
408    @Nullable
409    private InternalVisibilityChangedListener wrapOnVisibilityChangedListener(
410            @Nullable final OnVisibilityChangedListener listener) {
411        if (listener == null) {
412            return null;
413        }
414
415        return new InternalVisibilityChangedListener() {
416            @Override
417            public void onShown() {
418                listener.onShown(FloatingActionButton.this);
419            }
420
421            @Override
422            public void onHidden() {
423                listener.onHidden(FloatingActionButton.this);
424            }
425        };
426    }
427
428    private int getSizeDimension() {
429        return getSizeDimension(mSize);
430    }
431
432    private int getSizeDimension(@Size final int size) {
433        final Resources res = getResources();
434        switch (size) {
435            case SIZE_AUTO:
436                // If we're set to auto, grab the size from resources and refresh
437                final int width = ConfigurationHelper.getScreenWidthDp(res);
438                final int height = ConfigurationHelper.getScreenHeightDp(res);
439                return Math.max(width, height) < AUTO_MINI_LARGEST_SCREEN_WIDTH
440                        ? getSizeDimension(SIZE_MINI)
441                        : getSizeDimension(SIZE_NORMAL);
442            case SIZE_MINI:
443                return res.getDimensionPixelSize(R.dimen.design_fab_size_mini);
444            case SIZE_NORMAL:
445            default:
446                return res.getDimensionPixelSize(R.dimen.design_fab_size_normal);
447        }
448    }
449
450    @Override
451    protected void onAttachedToWindow() {
452        super.onAttachedToWindow();
453        getImpl().onAttachedToWindow();
454    }
455
456    @Override
457    protected void onDetachedFromWindow() {
458        super.onDetachedFromWindow();
459        getImpl().onDetachedFromWindow();
460    }
461
462    @Override
463    protected void drawableStateChanged() {
464        super.drawableStateChanged();
465        getImpl().onDrawableStateChanged(getDrawableState());
466    }
467
468    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
469    @Override
470    public void jumpDrawablesToCurrentState() {
471        super.jumpDrawablesToCurrentState();
472        getImpl().jumpDrawableToCurrentState();
473    }
474
475    /**
476     * Return in {@code rect} the bounds of the actual floating action button content in view-local
477     * coordinates. This is defined as anything within any visible shadow.
478     *
479     * @return true if this view actually has been laid out and has a content rect, else false.
480     */
481    public boolean getContentRect(@NonNull Rect rect) {
482        if (ViewCompat.isLaidOut(this)) {
483            rect.set(0, 0, getWidth(), getHeight());
484            rect.left += mShadowPadding.left;
485            rect.top += mShadowPadding.top;
486            rect.right -= mShadowPadding.right;
487            rect.bottom -= mShadowPadding.bottom;
488            return true;
489        } else {
490            return false;
491        }
492    }
493
494    /**
495     * Returns the FloatingActionButton's background, minus any compatible shadow implementation.
496     */
497    @NonNull
498    public Drawable getContentBackground() {
499        return getImpl().getContentBackground();
500    }
501
502    private static int resolveAdjustedSize(int desiredSize, int measureSpec) {
503        int result = desiredSize;
504        int specMode = MeasureSpec.getMode(measureSpec);
505        int specSize = MeasureSpec.getSize(measureSpec);
506        switch (specMode) {
507            case MeasureSpec.UNSPECIFIED:
508                // Parent says we can be as big as we want. Just don't be larger
509                // than max size imposed on ourselves.
510                result = desiredSize;
511                break;
512            case MeasureSpec.AT_MOST:
513                // Parent says we can be as big as we want, up to specSize.
514                // Don't be larger than specSize, and don't be larger than
515                // the max size imposed on ourselves.
516                result = Math.min(desiredSize, specSize);
517                break;
518            case MeasureSpec.EXACTLY:
519                // No choice. Do what we are told.
520                result = specSize;
521                break;
522        }
523        return result;
524    }
525
526    @Override
527    public boolean onTouchEvent(MotionEvent ev) {
528        if(getContentRect(mTouchArea) && !mTouchArea.contains((int) ev.getX(), (int) ev.getY())) {
529            return false;
530        }
531
532        return super.onTouchEvent(ev);
533    }
534
535    /**
536     * Behavior designed for use with {@link FloatingActionButton} instances. Its main function
537     * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do
538     * not cover them.
539     */
540    public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
541        private static final boolean AUTO_HIDE_DEFAULT = true;
542
543        private Rect mTmpRect;
544        private OnVisibilityChangedListener mInternalAutoHideListener;
545        private boolean mAutoHideEnabled;
546
547        public Behavior() {
548            super();
549            mAutoHideEnabled = AUTO_HIDE_DEFAULT;
550        }
551
552        public Behavior(Context context, AttributeSet attrs) {
553            super(context, attrs);
554            TypedArray a = context.obtainStyledAttributes(attrs,
555                    R.styleable.FloatingActionButton_Behavior_Layout);
556            mAutoHideEnabled = a.getBoolean(
557                    R.styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide,
558                    AUTO_HIDE_DEFAULT);
559            a.recycle();
560        }
561
562        /**
563         * Sets whether the associated FloatingActionButton automatically hides when there is
564         * not enough space to be displayed. This works with {@link AppBarLayout}
565         * and {@link BottomSheetBehavior}.
566         *
567         * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
568         * @param autoHide true to enable automatic hiding
569         */
570        public void setAutoHideEnabled(boolean autoHide) {
571            mAutoHideEnabled = autoHide;
572        }
573
574        /**
575         * Returns whether the associated FloatingActionButton automatically hides when there is
576         * not enough space to be displayed.
577         *
578         * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
579         * @return true if enabled
580         */
581        public boolean isAutoHideEnabled() {
582            return mAutoHideEnabled;
583        }
584
585        @Override
586        public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams lp) {
587            if (lp.dodgeInsetEdges == Gravity.NO_GRAVITY) {
588                // If the developer hasn't set dodgeInsetEdges, lets set it to BOTTOM so that
589                // we dodge any Snackbars
590                lp.dodgeInsetEdges = Gravity.BOTTOM;
591            }
592        }
593
594        @Override
595        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
596                View dependency) {
597            if (dependency instanceof AppBarLayout) {
598                // If we're depending on an AppBarLayout we will show/hide it automatically
599                // if the FAB is anchored to the AppBarLayout
600                updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child);
601            } else if (isBottomSheet(dependency)) {
602                updateFabVisibilityForBottomSheet(dependency, child);
603            }
604            return false;
605        }
606
607        private static boolean isBottomSheet(View view) {
608            CoordinatorLayout.LayoutParams lp =
609                    (CoordinatorLayout.LayoutParams) view.getLayoutParams();
610            return lp != null && lp.getBehavior() instanceof BottomSheetBehavior;
611        }
612
613        @VisibleForTesting
614        void setInternalAutoHideListener(OnVisibilityChangedListener listener) {
615            mInternalAutoHideListener = listener;
616        }
617
618        private boolean shouldUpdateVisibility(View dependency, FloatingActionButton child) {
619            final CoordinatorLayout.LayoutParams lp =
620                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
621            if (!mAutoHideEnabled) {
622                return false;
623            }
624
625            if (lp.getAnchorId() != dependency.getId()) {
626                // The anchor ID doesn't match the dependency, so we won't automatically
627                // show/hide the FAB
628                return false;
629            }
630
631            //noinspection RedundantIfStatement
632            if (child.getUserSetVisibility() != VISIBLE) {
633                // The view isn't set to be visible so skip changing its visibility
634                return false;
635            }
636
637            return true;
638        }
639
640        private boolean updateFabVisibilityForAppBarLayout(CoordinatorLayout parent,
641                AppBarLayout appBarLayout, FloatingActionButton child) {
642            if (!shouldUpdateVisibility(appBarLayout, child)) {
643                return false;
644            }
645
646            if (mTmpRect == null) {
647                mTmpRect = new Rect();
648            }
649
650            // First, let's get the visible rect of the dependency
651            final Rect rect = mTmpRect;
652            ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);
653
654            if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
655                // If the anchor's bottom is below the seam, we'll animate our FAB out
656                child.hide(mInternalAutoHideListener, false);
657            } else {
658                // Else, we'll animate our FAB back in
659                child.show(mInternalAutoHideListener, false);
660            }
661            return true;
662        }
663
664        private boolean updateFabVisibilityForBottomSheet(View bottomSheet,
665                FloatingActionButton child) {
666            if (!shouldUpdateVisibility(bottomSheet, child)) {
667                return false;
668            }
669            CoordinatorLayout.LayoutParams lp =
670                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
671            if (bottomSheet.getTop() < child.getHeight() / 2 + lp.topMargin) {
672                child.hide(mInternalAutoHideListener, false);
673            } else {
674                child.show(mInternalAutoHideListener, false);
675            }
676            return true;
677        }
678
679        @Override
680        public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
681                int layoutDirection) {
682            // First, let's make sure that the visibility of the FAB is consistent
683            final List<View> dependencies = parent.getDependencies(child);
684            for (int i = 0, count = dependencies.size(); i < count; i++) {
685                final View dependency = dependencies.get(i);
686                if (dependency instanceof AppBarLayout) {
687                    if (updateFabVisibilityForAppBarLayout(
688                            parent, (AppBarLayout) dependency, child)) {
689                        break;
690                    }
691                } else if (isBottomSheet(dependency)) {
692                    if (updateFabVisibilityForBottomSheet(dependency, child)) {
693                        break;
694                    }
695                }
696            }
697            // Now let the CoordinatorLayout lay out the FAB
698            parent.onLayoutChild(child, layoutDirection);
699            // Now offset it if needed
700            offsetIfNeeded(parent, child);
701            return true;
702        }
703
704        @Override
705        public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent,
706                @NonNull FloatingActionButton child, @NonNull Rect rect) {
707            // Since we offset so that any internal shadow padding isn't shown, we need to make
708            // sure that the shadow isn't used for any dodge inset calculations
709            final Rect shadowPadding = child.mShadowPadding;
710            rect.set(child.getLeft() + shadowPadding.left,
711                    child.getTop() + shadowPadding.top,
712                    child.getRight() - shadowPadding.right,
713                    child.getBottom() - shadowPadding.bottom);
714            return true;
715        }
716
717        /**
718         * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method
719         * offsets our layout position so that we're positioned correctly if we're on one of
720         * our parent's edges.
721         */
722        private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) {
723            final Rect padding = fab.mShadowPadding;
724
725            if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) {
726                final CoordinatorLayout.LayoutParams lp =
727                        (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
728
729                int offsetTB = 0, offsetLR = 0;
730
731                if (fab.getRight() >= parent.getWidth() - lp.rightMargin) {
732                    // If we're on the right edge, shift it the right
733                    offsetLR = padding.right;
734                } else if (fab.getLeft() <= lp.leftMargin) {
735                    // If we're on the left edge, shift it the left
736                    offsetLR = -padding.left;
737                }
738                if (fab.getBottom() >= parent.getHeight() - lp.bottomMargin) {
739                    // If we're on the bottom edge, shift it down
740                    offsetTB = padding.bottom;
741                } else if (fab.getTop() <= lp.topMargin) {
742                    // If we're on the top edge, shift it up
743                    offsetTB = -padding.top;
744                }
745
746                if (offsetTB != 0) {
747                    ViewCompat.offsetTopAndBottom(fab, offsetTB);
748                }
749                if (offsetLR != 0) {
750                    ViewCompat.offsetLeftAndRight(fab, offsetLR);
751                }
752            }
753        }
754    }
755
756    /**
757     * Returns the backward compatible elevation of the FloatingActionButton.
758     *
759     * @return the backward compatible elevation in pixels.
760     * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
761     * @see #setCompatElevation(float)
762     */
763    public float getCompatElevation() {
764        return getImpl().getElevation();
765    }
766
767    /**
768     * Updates the backward compatible elevation of the FloatingActionButton.
769     *
770     * @param elevation The backward compatible elevation in pixels.
771     * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
772     * @see #getCompatElevation()
773     * @see #setUseCompatPadding(boolean)
774     */
775    public void setCompatElevation(float elevation) {
776        getImpl().setElevation(elevation);
777    }
778
779    private FloatingActionButtonImpl getImpl() {
780        if (mImpl == null) {
781            mImpl = createImpl();
782        }
783        return mImpl;
784    }
785
786    private FloatingActionButtonImpl createImpl() {
787        final int sdk = Build.VERSION.SDK_INT;
788        if (sdk >= 21) {
789            return new FloatingActionButtonLollipop(this, new ShadowDelegateImpl(),
790                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
791        } else if (sdk >= 14) {
792            return new FloatingActionButtonIcs(this, new ShadowDelegateImpl(),
793                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
794        } else {
795            return new FloatingActionButtonGingerbread(this, new ShadowDelegateImpl(),
796                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
797        }
798    }
799
800    private class ShadowDelegateImpl implements ShadowViewDelegate {
801        @Override
802        public float getRadius() {
803            return getSizeDimension() / 2f;
804        }
805
806        @Override
807        public void setShadowPadding(int left, int top, int right, int bottom) {
808            mShadowPadding.set(left, top, right, bottom);
809            setPadding(left + mImagePadding, top + mImagePadding,
810                    right + mImagePadding, bottom + mImagePadding);
811        }
812
813        @Override
814        public void setBackgroundDrawable(Drawable background) {
815            FloatingActionButton.super.setBackgroundDrawable(background);
816        }
817
818        @Override
819        public boolean isCompatPaddingEnabled() {
820            return mCompatPadding;
821        }
822    }
823}
824