Add compat padding to FloatingActionButton ala CardView

Defaults to disabled, but when enabled, FloatingActionButton
will have the same metrics on both Lollipop+, and older
platforms where the compat shadow is used.

BUG: 25274672

Change-Id: Ide28651124ab31472c588e7d65a32999ec674445
diff --git a/design/api/current.txt b/design/api/current.txt
index b808dba..2161005 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -224,9 +224,13 @@
     ctor public FloatingActionButton(android.content.Context, android.util.AttributeSet, int);
     method public android.graphics.drawable.Drawable getContentBackground();
     method public boolean getContentRect(android.graphics.Rect);
+    method public float getFloatingActionButtonElevation();
+    method public boolean getUseCompatPadding();
     method public void hide();
     method public void hide(android.support.design.widget.FloatingActionButton.OnVisibilityChangedListener);
+    method public void setFloatingActionButtonElevation(float);
     method public void setRippleColor(int);
+    method public void setUseCompatPadding(boolean);
     method public void show();
     method public void show(android.support.design.widget.FloatingActionButton.OnVisibilityChangedListener);
   }
diff --git a/design/base/android/support/design/widget/FloatingActionButtonImpl.java b/design/base/android/support/design/widget/FloatingActionButtonImpl.java
index 169eaa6..189060a 100644
--- a/design/base/android/support/design/widget/FloatingActionButtonImpl.java
+++ b/design/base/android/support/design/widget/FloatingActionButtonImpl.java
@@ -20,6 +20,7 @@
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.PorterDuff;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
 import android.support.annotation.Nullable;
@@ -52,6 +53,7 @@
     final VisibilityAwareImageButton mView;
     final ShadowViewDelegate mShadowViewDelegate;
 
+    private final Rect mTmpRect = new Rect();
     private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
 
     FloatingActionButtonImpl(VisibilityAwareImageButton view,
@@ -76,6 +78,8 @@
         }
     }
 
+    abstract float getElevation();
+
     final void setPressedTranslationZ(float translationZ) {
         if (mPressedTranslationZ != translationZ) {
             mPressedTranslationZ = translationZ;
@@ -99,6 +103,19 @@
         return mContentBackground;
     }
 
+    abstract void onCompatShadowChanged();
+
+    final void updatePadding() {
+        Rect rect = mTmpRect;
+        getPadding(rect);
+        onPaddingUpdated(rect);
+        mShadowViewDelegate.setShadowPadding(rect.left, rect.top, rect.right, rect.bottom);
+    }
+
+    abstract void getPadding(Rect rect);
+
+    void onPaddingUpdated(Rect padding) {}
+
     void onAttachedToWindow() {
         if (requirePreDrawListener()) {
             ensurePreDrawListener();
diff --git a/design/base/android/support/design/widget/ShadowViewDelegate.java b/design/base/android/support/design/widget/ShadowViewDelegate.java
index 9a395e6..83a3a7a 100644
--- a/design/base/android/support/design/widget/ShadowViewDelegate.java
+++ b/design/base/android/support/design/widget/ShadowViewDelegate.java
@@ -22,4 +22,5 @@
     float getRadius();
     void setShadowPadding(int left, int top, int right, int bottom);
     void setBackgroundDrawable(Drawable background);
+    boolean isCompatPaddingEnabled();
 }
diff --git a/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java b/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java
index 0415d33..92f9603 100644
--- a/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java
+++ b/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java
@@ -96,10 +96,7 @@
                 mElevation,
                 mElevation + mPressedTranslationZ);
         mShadowDrawable.setAddPaddingForCorners(false);
-
         mShadowViewDelegate.setBackgroundDrawable(mShadowDrawable);
-
-        updatePadding();
     }
 
     @Override
@@ -121,6 +118,11 @@
     }
 
     @Override
+    float getElevation() {
+        return mElevation;
+    }
+
+    @Override
     void onElevationChanged(float elevation) {
         if (mShadowDrawable != null) {
             mShadowDrawable.setShadowSize(elevation, elevation + mPressedTranslationZ);
@@ -205,10 +207,13 @@
         }
     }
 
-    private void updatePadding() {
-        Rect rect = new Rect();
+    @Override
+    void onCompatShadowChanged() {
+        // Ignore pre-v21
+    }
+
+    void getPadding(Rect rect) {
         mShadowDrawable.getPadding(rect);
-        mShadowViewDelegate.setShadowPadding(rect.left, rect.top, rect.right, rect.bottom);
     }
 
     private Animation setupAnimation(Animation animation) {
diff --git a/design/lollipop/android/support/design/widget/FloatingActionButtonLollipop.java b/design/lollipop/android/support/design/widget/FloatingActionButtonLollipop.java
index cc3aca9..2b85845 100644
--- a/design/lollipop/android/support/design/widget/FloatingActionButtonLollipop.java
+++ b/design/lollipop/android/support/design/widget/FloatingActionButtonLollipop.java
@@ -22,7 +22,9 @@
 import android.annotation.TargetApi;
 import android.content.res.ColorStateList;
 import android.graphics.PorterDuff;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
 import android.graphics.drawable.LayerDrawable;
 import android.graphics.drawable.RippleDrawable;
 import android.os.Build;
@@ -35,6 +37,7 @@
 class FloatingActionButtonLollipop extends FloatingActionButtonIcs {
 
     private final Interpolator mInterpolator;
+    private InsetDrawable mInsetDrawable;
 
     FloatingActionButtonLollipop(VisibilityAwareImageButton view,
             ShadowViewDelegate shadowViewDelegate) {
@@ -70,7 +73,6 @@
         mContentBackground = mRippleDrawable;
 
         mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable);
-        mShadowViewDelegate.setShadowPadding(0, 0, 0, 0);
     }
 
     @Override
@@ -84,13 +86,15 @@
 
     @Override
     public void onElevationChanged(float elevation) {
-        ViewCompat.setElevation(mView, elevation);
+        mView.setElevation(elevation);
+        if (mShadowViewDelegate.isCompatPaddingEnabled()) {
+            updatePadding();
+        }
     }
 
     @Override
     void onTranslationZChanged(float translationZ) {
         StateListAnimator stateListAnimator = new StateListAnimator();
-
         // Animate translationZ to our value when pressed or focused
         stateListAnimator.addState(PRESSED_ENABLED_STATE_SET,
                 setupAnimator(ObjectAnimator.ofFloat(mView, "translationZ", translationZ)));
@@ -99,8 +103,32 @@
         // Animate translationZ to 0 otherwise
         stateListAnimator.addState(EMPTY_STATE_SET,
                 setupAnimator(ObjectAnimator.ofFloat(mView, "translationZ", 0f)));
-
         mView.setStateListAnimator(stateListAnimator);
+
+        if (mShadowViewDelegate.isCompatPaddingEnabled()) {
+            updatePadding();
+        }
+    }
+
+    @Override
+    public float getElevation() {
+        return mView.getElevation();
+    }
+
+    @Override
+    void onCompatShadowChanged() {
+        updatePadding();
+    }
+
+    @Override
+    void onPaddingUpdated(Rect padding) {
+        if (mShadowViewDelegate.isCompatPaddingEnabled()) {
+            mInsetDrawable = new InsetDrawable(mRippleDrawable,
+                    padding.left, padding.top, padding.right, padding.bottom);
+            mShadowViewDelegate.setBackgroundDrawable(mInsetDrawable);
+        } else {
+            mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable);
+        }
     }
 
     @Override
@@ -127,4 +155,18 @@
     CircularBorderDrawable newCircularDrawable() {
         return new CircularBorderDrawableLollipop();
     }
+
+    void getPadding(Rect rect) {
+        if (mShadowViewDelegate.isCompatPaddingEnabled()) {
+            final float radius = mShadowViewDelegate.getRadius();
+            final float maxShadowSize = getElevation() + mPressedTranslationZ;
+            final int hPadding = (int) Math.ceil(
+                    ShadowDrawableWrapper.calculateHorizontalPadding(maxShadowSize, radius, false));
+            final int vPadding = (int) Math.ceil(
+                    ShadowDrawableWrapper.calculateVerticalPadding(maxShadowSize, radius, false));
+            rect.set(hPadding, vPadding, hPadding, vPadding);
+        } else {
+            rect.set(0, 0, 0, 0);
+        }
+    }
 }
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index 3079180..29ca0af 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -34,6 +34,8 @@
         <attr name="pressedTranslationZ" format="dimension"/>
         <!-- The width of the border around the FAB. -->
         <attr name="borderWidth" format="dimension"/>
+        <!-- Enable compat padding. -->
+        <attr name="useCompatPadding" format="boolean"/>
     </declare-styleable>
 
     <declare-styleable name="ScrimInsetsFrameLayout">
diff --git a/design/src/android/support/design/widget/FloatingActionButton.java b/design/src/android/support/design/widget/FloatingActionButton.java
index 894f5e3..fbb4b5f 100644
--- a/design/src/android/support/design/widget/FloatingActionButton.java
+++ b/design/src/android/support/design/widget/FloatingActionButton.java
@@ -92,6 +92,7 @@
     private int mSize;
     private int mImagePadding;
 
+    private boolean mCompatPadding;
     private final Rect mShadowPadding;
 
     private final FloatingActionButtonImpl mImpl;
@@ -123,6 +124,7 @@
         final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
         final float pressedTranslationZ = a.getDimension(
                 R.styleable.FloatingActionButton_pressedTranslationZ, 0f);
+        mCompatPadding = a.getBoolean(R.styleable.FloatingActionButton_useCompatPadding, false);
         a.recycle();
 
         final ShadowViewDelegate delegate = new ShadowViewDelegate() {
@@ -134,7 +136,6 @@
             @Override
             public void setShadowPadding(int left, int top, int right, int bottom) {
                 mShadowPadding.set(left, top, right, bottom);
-
                 setPadding(left + mImagePadding, top + mImagePadding,
                         right + mImagePadding, bottom + mImagePadding);
             }
@@ -143,6 +144,11 @@
             public void setBackgroundDrawable(Drawable background) {
                 FloatingActionButton.super.setBackgroundDrawable(background);
             }
+
+            @Override
+            public boolean isCompatPaddingEnabled() {
+                return mCompatPadding;
+            }
         };
 
         final int sdk = Build.VERSION.SDK_INT;
@@ -161,6 +167,7 @@
                 mRippleColor, mBorderWidth);
         mImpl.setElevation(elevation);
         mImpl.setPressedTranslationZ(pressedTranslationZ);
+        mImpl.updatePadding();
     }
 
     @Override
@@ -186,6 +193,8 @@
      * When running on devices with KitKat or below, we draw a fill rather than a ripple.
      *
      * @param color ARGB color to use for the ripple.
+     *
+     * @attr ref android.support.design.R.styleable#FloatingActionButton_rippleColor
      */
     public void setRippleColor(@ColorInt int color) {
         if (mRippleColor != color) {
@@ -219,7 +228,6 @@
         }
     }
 
-
     /**
      * Return the blending mode used to apply the tint to the background
      * drawable, if specified.
@@ -308,6 +316,36 @@
         mImpl.hide(wrapOnVisibilityChangedListener(listener), fromUser);
     }
 
+    /**
+     * Set whether FloatingActionButton should add inner padding on platforms Lollipop and after,
+     * to ensure consistent dimensions on all platforms.
+     *
+     * @param useCompatPadding true if FloatingActionButton is adding inner padding on platforms
+     *                         Lollipop and after, to ensure consistent dimensions on all platforms.
+     *
+     * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
+     * @see #getUseCompatPadding()
+     */
+    public void setUseCompatPadding(boolean useCompatPadding) {
+        if (mCompatPadding != useCompatPadding) {
+            mCompatPadding = useCompatPadding;
+            mImpl.onCompatShadowChanged();
+        }
+    }
+
+    /**
+     * Returns whether FloatingActionButton will add inner padding on platforms Lollipop and after.
+     *
+     * @return true if FloatingActionButton is adding inner padding on platforms Lollipop and after,
+     * to ensure consistent dimensions on all platforms.
+     *
+     * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
+     * @see #setUseCompatPadding(boolean)
+     */
+    public boolean getUseCompatPadding() {
+        return mCompatPadding;
+    }
+
     @Nullable
     private InternalVisibilityChangedListener wrapOnVisibilityChangedListener(
             @Nullable final OnVisibilityChangedListener listener) {
@@ -611,4 +649,27 @@
             }
         }
     }
+
+    /**
+     * Returns the backward compatible elevation of the FloatingActionButton.
+     *
+     * @returns the backward compatible elevation in pixels.
+     * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
+     * @see #setFloatingActionButtonElevation(float)
+     */
+    public float getFloatingActionButtonElevation() {
+        return mImpl.getElevation();
+    }
+
+    /**
+     * Updates the backward compatible elevation of the FloatingActionButton.
+     *
+     * @param elevation The backward compatible elevation in pixels.
+     * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
+     * @see #getFloatingActionButtonElevation()
+     * @see #setUseCompatPadding(boolean)
+     */
+    public void setFloatingActionButtonElevation(float elevation) {
+        mImpl.setElevation(elevation);
+    }
 }