[go: nahoru, domu]

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