AppBarLayout improvements

- Tidy up listener implementation
- Nested fling support
- Animate FAB pre-v11
- Added internal ValueAnimatorCompat

Change-Id: I3ee6630177015f2bccbf29e5316ef8afe557c5a8
diff --git a/design/Android.mk b/design/Android.mk
index 08ac299..6bbbef0 100644
--- a/design/Android.mk
+++ b/design/Android.mk
@@ -63,12 +63,23 @@
     android-support-v7-appcompat
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
+# A helper sub-library that makes direct use of Honeycomb MR1 APIs
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-design-honeycomb-mr1
+LOCAL_SDK_VERSION := 12
+LOCAL_SRC_FILES := $(call all-java-files-under, honeycomb-mr1)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-design-honeycomb
+LOCAL_JAVA_LIBRARIES := android-support-design-res \
+    android-support-v4 \
+    android-support-v7-appcompat
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
 # A helper sub-library that makes direct use of Lollipop APIs
 include $(CLEAR_VARS)
 LOCAL_MODULE := android-support-design-lollipop
 LOCAL_SDK_VERSION := 21
 LOCAL_SRC_FILES := $(call all-java-files-under, lollipop)
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-design-honeycomb
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-design-honeycomb-mr1
 LOCAL_JAVA_LIBRARIES := android-support-design-res \
     android-support-v4 \
     android-support-v7-appcompat
diff --git a/design/api/current.txt b/design/api/current.txt
index 8dd4fd8..94c5188 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -3,18 +3,17 @@
   public class AppBarLayout extends android.widget.LinearLayout {
     ctor public AppBarLayout(android.content.Context);
     ctor public AppBarLayout(android.content.Context, android.util.AttributeSet);
-  }
-
-  public static abstract interface AppBarLayout.AppBarLayoutChild {
-    method public abstract int onOffsetUpdate(int);
-    field public static final int STATE_ELEVATED_ABOVE = 1; // 0x1
-    field public static final int STATE_ELEVATED_INLINE = 0; // 0x0
+    method public void addOnOffsetChangedListener(android.support.design.widget.AppBarLayout.OnOffsetChangedListener);
+    method public float getTargetElevation();
+    method public void removeOnOffsetChangedListener(android.support.design.widget.AppBarLayout.OnOffsetChangedListener);
+    method public void setTargetElevation(float);
   }
 
   public static class AppBarLayout.Behavior extends android.support.design.widget.ViewOffsetBehavior {
     ctor public AppBarLayout.Behavior();
     ctor public AppBarLayout.Behavior(android.content.Context, android.util.AttributeSet);
     method public boolean onLayoutChild(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, int);
+    method public boolean onNestedFling(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, float, float, boolean);
     method public void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, int, int, int[]);
     method public void onNestedScroll(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, int, int, int, int);
     method public boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, android.view.View, int);
@@ -39,6 +38,10 @@
     field public static final int SCROLL_FLAG_SCROLL = 1; // 0x1
   }
 
+  public static abstract interface AppBarLayout.OnOffsetChangedListener {
+    method public abstract void onOffsetChanged(android.support.design.widget.AppBarLayout, int);
+  }
+
   public static class AppBarLayout.ScrollingViewBehavior extends android.support.design.widget.ViewOffsetBehavior {
     ctor public AppBarLayout.ScrollingViewBehavior();
     ctor public AppBarLayout.ScrollingViewBehavior(android.content.Context, android.util.AttributeSet);
@@ -49,12 +52,11 @@
     method public void setOverlayTop(int);
   }
 
-  public class CollapsingToolbarLayout extends android.widget.FrameLayout implements android.support.design.widget.AppBarLayout.AppBarLayoutChild {
+  public class CollapsingToolbarLayout extends android.widget.FrameLayout {
     ctor public CollapsingToolbarLayout(android.content.Context);
     ctor public CollapsingToolbarLayout(android.content.Context, android.util.AttributeSet);
     ctor public CollapsingToolbarLayout(android.content.Context, android.util.AttributeSet, int);
     method public int getForegroundScrimColor();
-    method public int onOffsetUpdate(int);
     method public void setCollapsedTitleTextAppearance(int);
     method public void setCollapsedTitleTextColor(int);
     method public void setExpandedTitleColor(int);
diff --git a/design/base/android/support/design/widget/ValueAnimatorCompat.java b/design/base/android/support/design/widget/ValueAnimatorCompat.java
new file mode 100644
index 0000000..1e33f66
--- /dev/null
+++ b/design/base/android/support/design/widget/ValueAnimatorCompat.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.view.animation.Interpolator;
+
+/**
+ * This class offers a very small subset of {@code ValueAnimator}'s API, but works pre-v11 too.
+ * <p>
+ * You shouldn't not instantiate this directly. Instead use {@code ViewUtils.createAnimator()}.
+ */
+class ValueAnimatorCompat {
+
+    interface AnimatorUpdateListener {
+        /**
+         * <p>Notifies the occurrence of another frame of the animation.</p>
+         *
+         * @param animator The animation which was repeated.
+         */
+        void onAnimationUpdate(ValueAnimatorCompat animator);
+    }
+
+    /**
+     * An animation listener receives notifications from an animation.
+     * Notifications indicate animation related events, such as the end or the
+     * repetition of the animation.
+     */
+    interface AnimatorListener {
+        /**
+         * <p>Notifies the start of the animation.</p>
+         *
+         * @param animator The started animation.
+         */
+        void onAnimationStart(ValueAnimatorCompat animator);
+        /**
+         * <p>Notifies the end of the animation. This callback is not invoked
+         * for animations with repeat count set to INFINITE.</p>
+         *
+         * @param animator The animation which reached its end.
+         */
+        void onAnimationEnd(ValueAnimatorCompat animator);
+        /**
+         * <p>Notifies the cancellation of the animation. This callback is not invoked
+         * for animations with repeat count set to INFINITE.</p>
+         *
+         * @param animator The animation which was canceled.
+         */
+        void onAnimationCancel(ValueAnimatorCompat animator);
+    }
+
+    static class AnimatorListenerAdapter implements AnimatorListener {
+        @Override
+        public void onAnimationStart(ValueAnimatorCompat animator) {
+        }
+
+        @Override
+        public void onAnimationEnd(ValueAnimatorCompat animator) {
+        }
+
+        @Override
+        public void onAnimationCancel(ValueAnimatorCompat animator) {
+        }
+    }
+
+    interface Creator {
+        ValueAnimatorCompat createAnimator();
+    }
+
+    static abstract class Impl {
+        interface AnimatorUpdateListenerProxy {
+            void onAnimationUpdate();
+        }
+
+        interface AnimatorListenerProxy {
+            void onAnimationStart();
+            void onAnimationEnd();
+            void onAnimationCancel();
+        }
+
+        abstract void start();
+        abstract boolean isRunning();
+        abstract void setInterpolator(Interpolator interpolator);
+        abstract void setListener(AnimatorListenerProxy listener);
+        abstract void setUpdateListener(AnimatorUpdateListenerProxy updateListener);
+        abstract void setIntValues(int from, int to);
+        abstract int getAnimatedIntValue();
+        abstract void setFloatValues(float from, float to);
+        abstract float getAnimatedFloatValue();
+        abstract void setDuration(int duration);
+        abstract void cancel();
+        abstract float getAnimatedFraction();
+        abstract void end();
+    }
+
+    private final Impl mImpl;
+
+    ValueAnimatorCompat(Impl impl) {
+        mImpl = impl;
+    }
+
+    public void start() {
+        mImpl.start();
+    }
+
+    public boolean isRunning() {
+        return mImpl.isRunning();
+    }
+
+    public void setInterpolator(Interpolator interpolator) {
+        mImpl.setInterpolator(interpolator);
+    }
+
+    public void setUpdateListener(final AnimatorUpdateListener updateListener) {
+        if (updateListener != null) {
+            mImpl.setUpdateListener(new Impl.AnimatorUpdateListenerProxy() {
+                @Override
+                public void onAnimationUpdate() {
+                    updateListener.onAnimationUpdate(ValueAnimatorCompat.this);
+                }
+            });
+        } else {
+            mImpl.setUpdateListener(null);
+        }
+    }
+
+    public void setListener(final AnimatorListener listener) {
+        if (listener != null) {
+            mImpl.setListener(new Impl.AnimatorListenerProxy() {
+                @Override
+                public void onAnimationStart() {
+                    listener.onAnimationStart(ValueAnimatorCompat.this);
+                }
+
+                @Override
+                public void onAnimationEnd() {
+                    listener.onAnimationEnd(ValueAnimatorCompat.this);
+                }
+
+                @Override
+                public void onAnimationCancel() {
+                    listener.onAnimationCancel(ValueAnimatorCompat.this);
+                }
+            });
+        } else {
+            mImpl.setListener(null);
+        }
+    }
+
+    public void setIntValues(int from, int to) {
+        mImpl.setIntValues(from, to);
+    }
+
+    public int getAnimatedIntValue() {
+        return mImpl.getAnimatedIntValue();
+    }
+
+    public void setFloatValues(float from, float to) {
+        mImpl.setFloatValues(from, to);
+    }
+
+    public float getAnimatedFloatValue() {
+        return mImpl.getAnimatedFloatValue();
+    }
+
+    public void setDuration(int duration) {
+        mImpl.setDuration(duration);
+    }
+
+    public void cancel() {
+        mImpl.cancel();
+    }
+
+    public float getAnimatedFraction() {
+        return mImpl.getAnimatedFraction();
+    }
+
+    public void end() {
+        mImpl.end();
+    }
+}
diff --git a/design/build.gradle b/design/build.gradle
index 1740fce..ca8018c 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -12,7 +12,7 @@
 
     sourceSets {
         main.manifest.srcFile 'AndroidManifest.xml'
-        main.java.srcDirs = ['base', 'eclair-mr1', 'honeycomb', 'lollipop', 'src']
+        main.java.srcDirs = ['base', 'eclair-mr1', 'honeycomb', 'honeycomb-mr1', 'lollipop', 'src']
         main.res.srcDir 'res'
         main.assets.srcDir 'assets'
         main.resources.srcDir 'src'
diff --git a/design/eclair-mr1/android/support/design/widget/ValueAnimatorCompatImplEclairMr1.java b/design/eclair-mr1/android/support/design/widget/ValueAnimatorCompatImplEclairMr1.java
new file mode 100644
index 0000000..42cf086
--- /dev/null
+++ b/design/eclair-mr1/android/support/design/widget/ValueAnimatorCompatImplEclairMr1.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * A 'fake' ValueAnimator implementation which uses a Runnable.
+ */
+class ValueAnimatorCompatImplEclairMr1 extends ValueAnimatorCompat.Impl {
+
+    private static final int HANDLER_DELAY = 10;
+    private static final int DEFAULT_DURATION = 200;
+
+    private static final Handler sHandler = new Handler(Looper.getMainLooper());
+
+    private long mStartTime;
+    private boolean mIsRunning;
+
+    private final int[] mIntValues = new int[2];
+    private final float[] mFloatValues = new float[2];
+
+    private int mDuration = DEFAULT_DURATION;
+    private Interpolator mInterpolator;
+    private AnimatorListenerProxy mListener;
+    private AnimatorUpdateListenerProxy mUpdateListener;
+
+    private float mAnimatedFraction;
+
+    @Override
+    public void start() {
+        if (mIsRunning) {
+            // If we're already running, ignore
+            return;
+        }
+
+        if (mInterpolator == null) {
+            mInterpolator = new AccelerateDecelerateInterpolator();
+        }
+
+        mStartTime = SystemClock.uptimeMillis();
+        mIsRunning = true;
+
+        if (mListener != null) {
+            mListener.onAnimationStart();
+        }
+
+        sHandler.postDelayed(mRunnable, HANDLER_DELAY);
+    }
+
+    @Override
+    public boolean isRunning() {
+        return mIsRunning;
+    }
+
+    @Override
+    public void setInterpolator(Interpolator interpolator) {
+        mInterpolator = interpolator;
+    }
+
+    @Override
+    public void setListener(AnimatorListenerProxy listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void setUpdateListener(AnimatorUpdateListenerProxy updateListener) {
+        mUpdateListener = updateListener;
+    }
+
+    @Override
+    public void setIntValues(int from, int to) {
+        mIntValues[0] = from;
+        mIntValues[1] = to;
+    }
+
+    @Override
+    public int getAnimatedIntValue() {
+        return AnimationUtils.lerp(mIntValues[0], mIntValues[1], getAnimatedFraction());
+    }
+
+    @Override
+    public void setFloatValues(float from, float to) {
+        mFloatValues[0] = from;
+        mFloatValues[1] = to;
+    }
+
+    @Override
+    public float getAnimatedFloatValue() {
+        return AnimationUtils.lerp(mFloatValues[0], mFloatValues[1], getAnimatedFraction());
+    }
+
+    @Override
+    public void setDuration(int duration) {
+        mDuration = duration;
+    }
+
+    @Override
+    public void cancel() {
+        mIsRunning = false;
+        sHandler.removeCallbacks(mRunnable);
+
+        if (mListener != null) {
+            mListener.onAnimationCancel();
+        }
+    }
+
+    @Override
+    public float getAnimatedFraction() {
+        return mAnimatedFraction;
+    }
+
+    @Override
+    public void end() {
+        if (mIsRunning) {
+            mIsRunning = false;
+            sHandler.removeCallbacks(mRunnable);
+
+            // Set our animated fraction to 1
+            mAnimatedFraction = 1f;
+
+            if (mUpdateListener != null) {
+                mUpdateListener.onAnimationUpdate();
+            }
+
+            if (mListener != null) {
+                mListener.onAnimationEnd();
+            }
+        }
+    }
+
+    private void update() {
+        if (mIsRunning) {
+            // Update the animated fraction
+            final long elapsed = SystemClock.uptimeMillis() - mStartTime;
+            final float linearFraction = elapsed / (float) mDuration;
+            mAnimatedFraction = mInterpolator != null
+                    ? mInterpolator.getInterpolation(linearFraction)
+                    : linearFraction;
+
+            // If we're running, dispatch tp the listener
+            if (mUpdateListener != null) {
+                mUpdateListener.onAnimationUpdate();
+            }
+
+            // Check to see if we've passed the animation duration
+            if (SystemClock.uptimeMillis() >= (mStartTime + mDuration)) {
+                mIsRunning = false;
+
+                if (mListener != null) {
+                    mListener.onAnimationEnd();
+                }
+            }
+        }
+
+        if (mIsRunning) {
+            // If we're still running, post another delayed runnable
+            sHandler.postDelayed(mRunnable, HANDLER_DELAY);
+        }
+    }
+
+    private final Runnable mRunnable = new Runnable() {
+        public void run() {
+            update();
+        }
+    };
+}
diff --git a/design/honeycomb-mr1/android/support/design/widget/ValueAnimatorCompatImplHoneycombMr1.java b/design/honeycomb-mr1/android/support/design/widget/ValueAnimatorCompatImplHoneycombMr1.java
new file mode 100644
index 0000000..4f9ea39
--- /dev/null
+++ b/design/honeycomb-mr1/android/support/design/widget/ValueAnimatorCompatImplHoneycombMr1.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.view.animation.Interpolator;
+
+class ValueAnimatorCompatImplHoneycombMr1 extends ValueAnimatorCompat.Impl {
+
+    final ValueAnimator mValueAnimator;
+
+    ValueAnimatorCompatImplHoneycombMr1() {
+        mValueAnimator = new ValueAnimator();
+    }
+
+    @Override
+    public void start() {
+        mValueAnimator.start();
+    }
+
+    @Override
+    public boolean isRunning() {
+        return mValueAnimator.isRunning();
+    }
+
+    @Override
+    public void setInterpolator(Interpolator interpolator) {
+        mValueAnimator.setInterpolator(interpolator);
+    }
+
+    @Override
+    public void setUpdateListener(final AnimatorUpdateListenerProxy updateListener) {
+        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                updateListener.onAnimationUpdate();
+            }
+        });
+    }
+
+    @Override
+    public void setListener(final AnimatorListenerProxy listener) {
+        mValueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+                listener.onAnimationStart();
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                listener.onAnimationEnd();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animator) {
+                listener.onAnimationCancel();
+            }
+        });
+    }
+
+    @Override
+    public void setIntValues(int from, int to) {
+        mValueAnimator.setIntValues(from, to);
+    }
+
+    @Override
+    public int getAnimatedIntValue() {
+        return (int) mValueAnimator.getAnimatedValue();
+    }
+
+    @Override
+    public void setFloatValues(float from, float to) {
+        mValueAnimator.setFloatValues(from, to);
+    }
+
+    @Override
+    public float getAnimatedFloatValue() {
+        return (float) mValueAnimator.getAnimatedValue();
+    }
+
+    @Override
+    public void setDuration(int duration) {
+        mValueAnimator.setDuration(duration);
+    }
+
+    @Override
+    public void cancel() {
+        mValueAnimator.cancel();
+    }
+
+    @Override
+    public float getAnimatedFraction() {
+        return mValueAnimator.getAnimatedFraction();
+    }
+
+    @Override
+    public void end() {
+        mValueAnimator.end();
+    }
+}
diff --git a/design/res/anim/fab_in.xml b/design/res/anim/fab_in.xml
new file mode 100644
index 0000000..294050f
--- /dev/null
+++ b/design/res/anim/fab_in.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <alpha android:fromAlpha="0.0"
+           android:toAlpha="1.0"/>
+
+    <scale android:fromXScale="0.0"
+           android:fromYScale="0.0"
+           android:toXScale="1.0"
+           android:toYScale="1.0"
+           android:pivotX="50%"
+           android:pivotY="50%"/>
+
+</set>
diff --git a/design/res/anim/fab_out.xml b/design/res/anim/fab_out.xml
new file mode 100644
index 0000000..0f80a9a
--- /dev/null
+++ b/design/res/anim/fab_out.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <alpha android:fromAlpha="1.0"
+           android:toAlpha="0.0"/>
+
+    <scale android:fromXScale="1.0"
+           android:fromYScale="1.0"
+           android:toXScale="0.0"
+           android:toYScale="0.0"
+           android:pivotX="50%"
+           android:pivotY="50%"/>
+
+</set>
diff --git a/design/src/android/support/design/widget/AppBarLayout.java b/design/src/android/support/design/widget/AppBarLayout.java
index 29216b2..a9d170f 100644
--- a/design/src/android/support/design/widget/AppBarLayout.java
+++ b/design/src/android/support/design/widget/AppBarLayout.java
@@ -21,6 +21,7 @@
 import android.support.annotation.IntDef;
 import android.support.design.R;
 import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ScrollerCompat;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
@@ -29,11 +30,14 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 
 /**
  * AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of
- * Material Design's App bar concept, namely scrolling gestures.
+ * material design's app bar concept, namely scrolling gestures.
  * <p>
  * Children should provide their desired scrolling behavior through
  * {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute:
@@ -89,48 +93,19 @@
 public class AppBarLayout extends LinearLayout {
 
     /**
-     * Interface which allows an implementing child {@link View} of this {@link AppBarLayout} to
-     * receive offset updates, and provide extra information.
+     * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
+     * offset changes.
      */
-    public interface AppBarLayoutChild {
-
-        /** @hide */
-        @IntDef({
-                STATE_ELEVATED_ABOVE,
-                STATE_ELEVATED_INLINE
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @interface ElevatedState {}
-
-        /**
-         * The {@link AppBarLayout} should be elevated above any scrolling content, and this cast
-         * a shadow.
-         *
-         * @see #onOffsetUpdate(int)
-         */
-        int STATE_ELEVATED_ABOVE = 1;
-
-        /**
-         * The {@link AppBarLayout} should not be elevated above any scrolling content.
-         *
-         * @see #onOffsetUpdate(int)
-         */
-        int STATE_ELEVATED_INLINE = 0;
-
+    public interface OnOffsetChangedListener {
         /**
          * Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
          * child views to implement custom behavior based on the offset (for instance pinning a
          * view at a certain y value).
          *
-         * <p>You can influence the elevation of the {@link AppBarLayout} by returning one of
-         * {@link #STATE_ELEVATED_INLINE} or {@link #STATE_ELEVATED_ABOVE}.
-         *
+         * @param appBarLayout the {@link AppBarLayout} which offset has changed
          * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
-         *
-         * @return one of {@link #STATE_ELEVATED_INLINE} or {@link #STATE_ELEVATED_ABOVE}.
          */
-        @ElevatedState
-        int onOffsetUpdate(int verticalOffset);
+        void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
     }
 
     private static final int INVALID_SCROLL_RANGE = -1;
@@ -143,6 +118,8 @@
 
     private float mTargetElevation;
 
+    private final List<WeakReference<OnOffsetChangedListener>> mListeners;
+
     public AppBarLayout(Context context) {
         this(context, null);
     }
@@ -159,6 +136,45 @@
 
         // Use the bounds view outline provider so that we cast a shadow, even without a background
         ViewUtils.setBoundsViewOutlineProvider(this);
+
+        mListeners = new ArrayList<>();
+
+        ViewCompat.setElevation(this, mTargetElevation);
+    }
+
+    /**
+     * Add a listener that will be called when the offset of this {@link AppBarLayout} changes.
+     *
+     * @param listener The listener that will be called when the offset changes.]
+     *
+     * @see #removeOnOffsetChangedListener(OnOffsetChangedListener)
+     */
+    public void addOnOffsetChangedListener(OnOffsetChangedListener listener) {
+        for (int i = 0, z = mListeners.size(); i < z; i++) {
+            final WeakReference<OnOffsetChangedListener> ref = mListeners.get(i);
+            if (ref != null && ref.get() == listener) {
+                // Listener already added
+                return;
+            }
+        }
+        mListeners.add(new WeakReference<>(listener));
+    }
+
+    /**
+     * Remove the previously added {@link OnOffsetChangedListener}.
+     *
+     * @param listener the listener to remove.
+     */
+    public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) {
+        final Iterator<WeakReference<OnOffsetChangedListener>> i = mListeners.iterator();
+        while (i.hasNext()) {
+            final WeakReference<OnOffsetChangedListener> ref = i.next();
+            final OnOffsetChangedListener item = ref.get();
+            if (item == listener || item == null) {
+                // If the item is null, or is our given listener, remove
+                i.remove();
+            }
+        }
     }
 
     @Override
@@ -359,9 +375,26 @@
     }
 
     /**
-     * The elevation value to use when {@link AppBarLayout} is elevated above content.
+     * Set the elevation value to use when this {@link AppBarLayout} should be elevated
+     * above content.
+     * <p>
+     * This method does not do anything itself. A typical use for this method is called from within
+     * an {@link OnOffsetChangedListener} when the offset has changed in such a way to require an
+     * elevation change.
+     *
+     * @param elevation the elevation value to use.
+     *
+     * @see ViewCompat#setElevation(View, float)
      */
-    final float getTargetElevation() {
+    public void setTargetElevation(float elevation) {
+        mTargetElevation = elevation;
+    }
+
+    /**
+     * Returns the elevation value to use when this {@link AppBarLayout} should be elevated
+     * above content.
+     */
+    public float getTargetElevation() {
         return mTargetElevation;
     }
 
@@ -515,9 +548,13 @@
      * scroll handling with offsetting.
      */
     public static class Behavior extends ViewOffsetBehavior<AppBarLayout> {
-        private int mSiblingOffsetTop;
+        private int mLogicalOffsetTop;
 
         private boolean mSkipNestedPreScroll;
+        private Runnable mFlingRunnable;
+        private ScrollerCompat mScroller;
+
+        private ValueAnimatorCompat mAnimator;
 
         public Behavior() {}
 
@@ -529,8 +566,15 @@
         public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                 View directTargetChild, View target, int nestedScrollAxes) {
             // Return true if we're nested scrolling vertically and we have scrollable children
-            return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
+            final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
                     && child.hasScrollableChildren();
+
+            if (started && mAnimator != null) {
+                // Cancel any offset animation
+                mAnimator.cancel();
+            }
+
+            return started;
         }
 
         @Override
@@ -576,13 +620,119 @@
         }
 
         @Override
+        public boolean onNestedFling(final CoordinatorLayout coordinatorLayout,
+                final AppBarLayout child, View target, float velocityX, float velocityY,
+                boolean consumed) {
+            if (!consumed) {
+                // It has been consumed so let's fling ourselves
+                return fling(coordinatorLayout, child, -child.getTotalScrollRange(), 0, -velocityY);
+            } else {
+                // If we're scrolling up and the child also consumed the fling. We'll fake scroll
+                // upto our 'collapsed' offset
+                int targetScroll;
+                if (velocityY < 0) {
+                    // We're scrolling down
+                    targetScroll = -child.getTotalScrollRange()
+                            + child.getDownNestedPreScrollRange();
+
+                    if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
+                        // If we're currently expanded more than the target scroll, we'll return false
+                        // now. This is so that we don't 'scroll' the wrong way.
+                        return false;
+                    }
+                } else {
+                    // We're scrolling up
+                    targetScroll = -child.getUpNestedPreScrollRange();
+
+                    if (getTopBottomOffsetForScrollingSibling() < targetScroll) {
+                        // If we're currently expanded less than the target scroll, we'll return
+                        // false now. This is so that we don't 'scroll' the wrong way.
+                        return false;
+                    }
+                }
+
+                if (mLogicalOffsetTop != targetScroll) {
+                    animateOffsetTo(coordinatorLayout, child, targetScroll);
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
+                final AppBarLayout child, int offset) {
+            if (mAnimator == null) {
+                mAnimator = ViewUtils.createAnimator();
+                mAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
+                mAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimatorCompat animator) {
+                        setAppBarTopBottomOffset(coordinatorLayout, child,
+                                animator.getAnimatedIntValue());
+                    }
+                });
+            } else {
+                mAnimator.cancel();
+            }
+
+            mAnimator.setIntValues(getTopBottomOffsetForScrollingSibling(), offset);
+            mAnimator.start();
+        }
+
+        private boolean fling(CoordinatorLayout coordinatorLayout, AppBarLayout layout, int minOffset,
+                int maxOffset, float velocityY) {
+            if (mFlingRunnable != null) {
+                layout.removeCallbacks(mFlingRunnable);
+            }
+
+            if (mScroller == null) {
+                mScroller = ScrollerCompat.create(layout.getContext());
+            }
+
+            mScroller.fling(
+                    0, mLogicalOffsetTop, // curr
+                    0, Math.round(velocityY), // velocity.
+                    0, 0, // x
+                    minOffset, maxOffset); // y
+
+            if (mScroller.computeScrollOffset()) {
+                mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
+                ViewCompat.postOnAnimation(layout, mFlingRunnable);
+                return true;
+            } else {
+                mFlingRunnable = null;
+                return false;
+            }
+        }
+
+        private class FlingRunnable implements Runnable {
+            private final CoordinatorLayout mParent;
+            private final AppBarLayout mLayout;
+
+            FlingRunnable(CoordinatorLayout parent, AppBarLayout layout) {
+                mParent = parent;
+                mLayout = layout;
+            }
+
+            @Override
+            public void run() {
+                if (mLayout != null && mScroller != null && mScroller.computeScrollOffset()) {
+                    setAppBarTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
+
+                    // Post ourselves so that we run on the next animation
+                    ViewCompat.postOnAnimation(mLayout, this);
+                }
+            }
+        }
+
+        @Override
         public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout child,
                 int layoutDirection) {
             boolean handled = super.onLayoutChild(parent, child, layoutDirection);
 
             // Make sure we update the elevation
-            final int elevationState = dispatchOffsetUpdates(child);
-            checkElevation(child, getTopAndBottomOffset(), elevationState);
+            dispatchOffsetUpdates(child);
 
             return handled;
         }
@@ -590,15 +740,23 @@
         private int scroll(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout,
                 int dy, int minOffset, int maxOffset) {
             return setAppBarTopBottomOffset(coordinatorLayout, appBarLayout,
-                    mSiblingOffsetTop - dy, minOffset, maxOffset);
+                    mLogicalOffsetTop - dy, minOffset, maxOffset);
         }
 
-        private int setAppBarTopBottomOffset(CoordinatorLayout coordinatorLayout,
+        final int setAppBarTopBottomOffset(CoordinatorLayout coordinatorLayout,
+                AppBarLayout appBarLayout, int newOffset) {
+            return setAppBarTopBottomOffset(coordinatorLayout, appBarLayout, newOffset,
+                    Integer.MIN_VALUE, Integer.MAX_VALUE);
+        }
+
+        final int setAppBarTopBottomOffset(CoordinatorLayout coordinatorLayout,
                 AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
-            final int curOffset = mSiblingOffsetTop;
+            final int curOffset = mLogicalOffsetTop;
             int consumed = 0;
 
-            if (minOffset != 0) {
+            if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
+                // If we have some scrolling range, and we're currently within the min and max
+                // offsets, calculate a new offset
                 newOffset = MathUtils.constrain(newOffset, minOffset, maxOffset);
 
                 if (curOffset != newOffset) {
@@ -606,10 +764,11 @@
                             appBarLayout.hasChildWithInterpolator()
                                     ? interpolateOffset(appBarLayout, newOffset)
                                     : newOffset);
+
                     // Update how much dy we have consumed
                     consumed = curOffset - newOffset;
                     // Update the stored sibling offset
-                    mSiblingOffsetTop = newOffset;
+                    mLogicalOffsetTop = newOffset;
 
                     if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
                         // If the offset hasn't changed and we're using an interpolated scroll
@@ -619,44 +778,29 @@
                         coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
                     }
 
-                    // Dispatch the updates to any AppBarLayoutChild children
-                    final int childState = dispatchOffsetUpdates(appBarLayout);
-                    checkElevation(appBarLayout, newOffset, childState);
+                    // Dispatch the updates to any listeners
+                    dispatchOffsetUpdates(appBarLayout);
                 }
             }
 
             return consumed;
         }
 
-        private void checkElevation(AppBarLayout appBarLayout, int offset, int childState) {
-            if (appBarLayout.getHeight() + offset == 0) {
-                // If we're not visible, clear out the elevation
-                ViewCompat.setElevation(appBarLayout, 0f);
-            } else {
-                if (childState == AppBarLayoutChild.STATE_ELEVATED_ABOVE) {
-                    ViewCompat.setElevation(appBarLayout, appBarLayout.getTargetElevation());
-                } else {
-                    ViewCompat.setElevation(appBarLayout, 0f);
+        private void dispatchOffsetUpdates(AppBarLayout layout) {
+            final List<WeakReference<OnOffsetChangedListener>> listeners = layout.mListeners;
+
+            // Iterate backwards through the list so that most recently added listeners
+            // get the first chance to decide
+            for (int i = 0, z = listeners.size(); i < z; i++) {
+                final WeakReference<OnOffsetChangedListener> ref = listeners.get(i);
+                final OnOffsetChangedListener listener = ref != null ? ref.get() : null;
+
+                if (listener != null) {
+                    listener.onOffsetChanged(layout, getTopAndBottomOffset());
                 }
             }
         }
 
-        private int dispatchOffsetUpdates(AppBarLayout layout) {
-            for (int i = 0, z = layout.getChildCount(); i < z; i++) {
-                View child = layout.getChildAt(i);
-                if (child instanceof AppBarLayoutChild) {
-                    final int childState = ((AppBarLayoutChild) child)
-                            .onOffsetUpdate(getTopAndBottomOffset());
-
-                    if (childState == AppBarLayoutChild.STATE_ELEVATED_INLINE) {
-                        return childState;
-                    }
-                }
-            }
-
-            return AppBarLayoutChild.STATE_ELEVATED_ABOVE;
-        }
-
         private int interpolateOffset(AppBarLayout layout, final int offset) {
             final int absOffset = Math.abs(offset);
 
@@ -698,7 +842,7 @@
         }
 
         final int getTopBottomOffsetForScrollingSibling() {
-            return mSiblingOffsetTop;
+            return mLogicalOffsetTop;
         }
     }
 
diff --git a/design/src/android/support/design/widget/CollapsingToolbarLayout.java b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
index ab36ed5..24eb439 100644
--- a/design/src/android/support/design/widget/CollapsingToolbarLayout.java
+++ b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
@@ -31,6 +31,7 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.view.animation.Animation;
 import android.view.animation.Transformation;
 import android.widget.FrameLayout;
@@ -71,7 +72,7 @@
  * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
  * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
  */
-public class CollapsingToolbarLayout extends FrameLayout implements AppBarLayout.AppBarLayoutChild {
+public class CollapsingToolbarLayout extends FrameLayout {
 
     private static final int SCRIM_ANIMATION_DURATION = 600;
 
@@ -89,6 +90,8 @@
     private int mCurrentForegroundColor;
     private boolean mScrimIsShown;
 
+    private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener;
+
     public CollapsingToolbarLayout(Context context) {
         this(context, null);
     }
@@ -155,6 +158,31 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Add an OnOffsetChangedListener if possible
+        final ViewParent parent = getParent();
+        if (parent instanceof AppBarLayout) {
+            if (mOnOffsetChangedListener == null) {
+                mOnOffsetChangedListener = new OffsetUpdateListener();
+            }
+            ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        // Remove our OnOffsetChangedListener if possible and it exists
+        final ViewParent parent = getParent();
+        if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) {
+            ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener);
+        }
+
+        super.onDetachedFromWindow();
+    }
+
+    @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         super.addView(child, index, params);
 
@@ -234,55 +262,6 @@
         mCollapsingTextHelper.setText(title);
     }
 
-    /**
-     * @hide
-     */
-    @Override
-    public int onOffsetUpdate(int verticalOffset) {
-        int pinnedHeight = 0;
-
-        for (int i = 0, z = getChildCount(); i < z; i++) {
-            final View child = getChildAt(i);
-            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-            final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
-
-            switch (lp.mCollapseMode) {
-                case LayoutParams.COLLAPSE_MODE_PIN:
-                    if (getHeight() + verticalOffset >= child.getHeight()) {
-                        offsetHelper.setTopAndBottomOffset(-verticalOffset);
-                    }
-                    pinnedHeight += child.getHeight();
-                    break;
-                case LayoutParams.COLLAPSE_MODE_PARALLAX:
-                    offsetHelper.setTopAndBottomOffset(
-                            Math.round(-verticalOffset * lp.mParallaxMult));
-                    break;
-            }
-        }
-
-        // Show or hide the scrim if needed
-        if (Color.alpha(mForegroundScrimColor) > 0) {
-            if (getHeight() + verticalOffset < getScrimTriggerOffset()) {
-                showScrim();
-            } else {
-                hideScrim();
-            }
-        }
-
-        // Update the collapsing text's fraction
-        mCollapsingTextHelper.setExpansionFraction(Math.abs(verticalOffset) /
-                (float) (getHeight() - ViewCompat.getMinimumHeight(this)));
-
-        if (pinnedHeight > 0 && (getHeight() + verticalOffset) == pinnedHeight) {
-            // If we have some pinned children, and we're offset to only show those views,
-            // we want to be elevate
-            return STATE_ELEVATED_ABOVE;
-        } else {
-            // Otherwise, we're inline with the content
-            return STATE_ELEVATED_INLINE;
-        }
-    }
-
     private void showScrim() {
         if (mScrimIsShown) return;
 
@@ -542,4 +521,54 @@
             return mParallaxMult;
         }
     }
+
+    private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
+        @Override
+        public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
+            int pinnedHeight = 0;
+
+            for (int i = 0, z = getChildCount(); i < z; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
+
+                switch (lp.mCollapseMode) {
+                    case LayoutParams.COLLAPSE_MODE_PIN:
+                        if (getHeight() + verticalOffset >= child.getHeight()) {
+                            offsetHelper.setTopAndBottomOffset(-verticalOffset);
+                        }
+                        pinnedHeight += child.getHeight();
+                        break;
+                    case LayoutParams.COLLAPSE_MODE_PARALLAX:
+                        offsetHelper.setTopAndBottomOffset(
+                                Math.round(-verticalOffset * lp.mParallaxMult));
+                        break;
+                }
+            }
+
+            // Show or hide the scrim if needed
+            if (Color.alpha(mForegroundScrimColor) > 0) {
+                if (getHeight() + verticalOffset < getScrimTriggerOffset()) {
+                    showScrim();
+                } else {
+                    hideScrim();
+                }
+            }
+
+            // Update the collapsing text's fraction
+            final int expandRange = getHeight() - ViewCompat.getMinimumHeight(
+                    CollapsingToolbarLayout.this);
+            mCollapsingTextHelper.setExpansionFraction(
+                    Math.abs(verticalOffset) / (float) expandRange);
+
+            if (pinnedHeight > 0 && (getHeight() + verticalOffset) == pinnedHeight) {
+                // If we have some pinned children, and we're offset to only show those views,
+                // we want to be elevate
+                ViewCompat.setElevation(layout, layout.getTargetElevation());
+            } else {
+                // Otherwise, we're inline with the content
+                ViewCompat.setElevation(layout, 0f);
+            }
+        }
+    }
 }
diff --git a/design/src/android/support/design/widget/FloatingActionButton.java b/design/src/android/support/design/widget/FloatingActionButton.java
index d405de0..c2db0cf 100644
--- a/design/src/android/support/design/widget/FloatingActionButton.java
+++ b/design/src/android/support/design/widget/FloatingActionButton.java
@@ -30,6 +30,7 @@
 import android.support.v4.view.ViewPropertyAnimatorListener;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.animation.Animation;
 import android.widget.ImageView;
 
 import java.util.List;
@@ -369,40 +370,68 @@
         private void animateIn(FloatingActionButton button) {
             button.setVisibility(View.VISIBLE);
 
-            ViewCompat.animate(button)
-                    .scaleX(1f)
-                    .scaleY(1f)
-                    .alpha(1f)
-                    .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
-                    .withLayer()
-                    .setListener(null)
-                    .start();
+            if (Build.VERSION.SDK_INT >= 14) {
+                ViewCompat.animate(button)
+                        .scaleX(1f)
+                        .scaleY(1f)
+                        .alpha(1f)
+                        .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
+                        .withLayer()
+                        .setListener(null)
+                        .start();
+            } else {
+                Animation anim = android.view.animation.AnimationUtils.loadAnimation(
+                        button.getContext(), R.anim.fab_in);
+                anim.setDuration(200);
+                anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
+                button.startAnimation(anim);
+            }
         }
 
-        private void animateOut(FloatingActionButton button) {
-            ViewCompat.animate(button)
-                    .scaleX(0f)
-                    .scaleY(0f)
-                    .alpha(0f)
-                    .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
-                    .withLayer()
-                    .setListener(new ViewPropertyAnimatorListener() {
-                        @Override
-                        public void onAnimationStart(View view) {
-                            mIsAnimatingOut = true;
-                        }
+        private void animateOut(final FloatingActionButton button) {
+            if (Build.VERSION.SDK_INT >= 14) {
+                ViewCompat.animate(button)
+                        .scaleX(0f)
+                        .scaleY(0f)
+                        .alpha(0f)
+                        .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
+                        .withLayer()
+                        .setListener(new ViewPropertyAnimatorListener() {
+                            @Override
+                            public void onAnimationStart(View view) {
+                                mIsAnimatingOut = true;
+                            }
 
-                        @Override
-                        public void onAnimationCancel(View view) {
-                            mIsAnimatingOut = false;
-                        }
+                            @Override
+                            public void onAnimationCancel(View view) {
+                                mIsAnimatingOut = false;
+                            }
 
-                        @Override
-                        public void onAnimationEnd(View view) {
-                            mIsAnimatingOut = false;
-                            view.setVisibility(View.GONE);
-                        }
-                    }).start();
+                            @Override
+                            public void onAnimationEnd(View view) {
+                                mIsAnimatingOut = false;
+                                view.setVisibility(View.GONE);
+                            }
+                        }).start();
+            } else {
+                Animation anim = android.view.animation.AnimationUtils.loadAnimation(
+                        button.getContext(), R.anim.fab_out);
+                anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
+                anim.setDuration(200);
+                anim.setAnimationListener(new AnimationUtils.AnimationListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animation animation) {
+                        mIsAnimatingOut = true;
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animation animation) {
+                        mIsAnimatingOut = false;
+                        button.setVisibility(View.GONE);
+                    }
+                });
+                button.startAnimation(anim);
+            }
         }
     }
 }
diff --git a/design/src/android/support/design/widget/ViewUtils.java b/design/src/android/support/design/widget/ViewUtils.java
index 34fdc68..29a4522 100644
--- a/design/src/android/support/design/widget/ViewUtils.java
+++ b/design/src/android/support/design/widget/ViewUtils.java
@@ -21,6 +21,16 @@
 
 class ViewUtils {
 
+    static final ValueAnimatorCompat.Creator DEFAULT_ANIMATOR_CREATOR
+            = new ValueAnimatorCompat.Creator() {
+        @Override
+        public ValueAnimatorCompat createAnimator() {
+            return new ValueAnimatorCompat(Build.VERSION.SDK_INT >= 12
+                    ? new ValueAnimatorCompatImplHoneycombMr1()
+                    : new ValueAnimatorCompatImplEclairMr1());
+        }
+    };
+
     private interface ViewUtilsImpl {
         void setBoundsViewOutlineProvider(View view);
     }
@@ -54,4 +64,8 @@
         IMPL.setBoundsViewOutlineProvider(view);
     }
 
+    static ValueAnimatorCompat createAnimator() {
+        return DEFAULT_ANIMATOR_CREATOR.createAnimator();
+    }
+
 }