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 
17 package android.support.design.widget;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.PorterDuff;
25 import android.graphics.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.os.Build;
28 import android.support.annotation.ColorInt;
29 import android.support.annotation.DrawableRes;
30 import android.support.annotation.IntDef;
31 import android.support.annotation.NonNull;
32 import android.support.annotation.Nullable;
33 import android.support.design.R;
34 import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
35 import android.support.v4.content.res.ConfigurationHelper;
36 import android.support.v4.view.ViewCompat;
37 import android.support.v7.widget.AppCompatDrawableManager;
38 import android.support.v7.widget.AppCompatImageHelper;
39 import android.util.AttributeSet;
40 import android.util.Log;
41 import android.view.MotionEvent;
42 import android.view.View;
43 import android.widget.ImageView;
44 
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import 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)
65 public 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          */
onShown(FloatingActionButton fab)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          */
onHidden(FloatingActionButton fab)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 
FloatingActionButton(Context context)142     public FloatingActionButton(Context context) {
143         this(context, null);
144     }
145 
FloatingActionButton(Context context, AttributeSet attrs)146     public FloatingActionButton(Context context, AttributeSet attrs) {
147         this(context, attrs, 0);
148     }
149 
FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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      */
setRippleColor(@olorInt int color)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
getBackgroundTintList()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      */
setBackgroundTintList(@ullable ColorStateList tint)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
getBackgroundTintMode()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      */
setBackgroundTintMode(@ullable PorterDuff.Mode tintMode)264     public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
265         if (mBackgroundTintMode != tintMode) {
266             mBackgroundTintMode = tintMode;
267             getImpl().setBackgroundTintMode(tintMode);
268         }
269     }
270 
271     @Override
setBackgroundDrawable(Drawable background)272     public void setBackgroundDrawable(Drawable background) {
273         Log.i(LOG_TAG, "Setting a custom background is not supported.");
274     }
275 
276     @Override
setBackgroundResource(int resid)277     public void setBackgroundResource(int resid) {
278         Log.i(LOG_TAG, "Setting a custom background is not supported.");
279     }
280 
281     @Override
setBackgroundColor(int color)282     public void setBackgroundColor(int color) {
283         Log.i(LOG_TAG, "Setting a custom background is not supported.");
284     }
285 
286     @Override
setImageResource(@rawableRes int resId)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      */
show()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      */
show(@ullable final OnVisibilityChangedListener listener)306     public void show(@Nullable final OnVisibilityChangedListener listener) {
307         show(listener, true);
308     }
309 
show(OnVisibilityChangedListener listener, boolean fromUser)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      */
hide()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      */
hide(@ullable OnVisibilityChangedListener listener)328     public void hide(@Nullable OnVisibilityChangedListener listener) {
329         hide(listener, true);
330     }
331 
hide(@ullable OnVisibilityChangedListener listener, boolean fromUser)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      */
setUseCompatPadding(boolean useCompatPadding)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      */
getUseCompatPadding()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      */
setSize(@ize int size)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
getSize()391     public int getSize() {
392         return mSize;
393     }
394 
395     @Nullable
wrapOnVisibilityChangedListener( @ullable final OnVisibilityChangedListener listener)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