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