diff --git a/Android.mk b/Android.mk index 08ac299ed..6bbbef0fe 100644 --- a/Android.mk +++ b/Android.mk @@ -63,12 +63,23 @@ LOCAL_JAVA_LIBRARIES := android-support-design-res \ 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/api/current.txt b/api/current.txt index 8dd4fd8e6..94c518868 100644 --- a/api/current.txt +++ b/api/current.txt @@ -3,18 +3,17 @@ package android.support.design.widget { 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 @@ package android.support.design.widget { 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 @@ package android.support.design.widget { 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/base/android/support/design/widget/ValueAnimatorCompat.java b/base/android/support/design/widget/ValueAnimatorCompat.java new file mode 100644 index 000000000..1e33f661a --- /dev/null +++ b/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. + *

+ * You shouldn't not instantiate this directly. Instead use {@code ViewUtils.createAnimator()}. + */ +class ValueAnimatorCompat { + + interface AnimatorUpdateListener { + /** + *

Notifies the occurrence of another frame of the animation.

+ * + * @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 { + /** + *

Notifies the start of the animation.

+ * + * @param animator The started animation. + */ + void onAnimationStart(ValueAnimatorCompat animator); + /** + *

Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animator The animation which reached its end. + */ + void onAnimationEnd(ValueAnimatorCompat animator); + /** + *

Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @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/build.gradle b/build.gradle index 1740fce33..ca8018c7b 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ android { 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/eclair-mr1/android/support/design/widget/ValueAnimatorCompatImplEclairMr1.java b/eclair-mr1/android/support/design/widget/ValueAnimatorCompatImplEclairMr1.java new file mode 100644 index 000000000..42cf086ed --- /dev/null +++ b/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/honeycomb-mr1/android/support/design/widget/ValueAnimatorCompatImplHoneycombMr1.java b/honeycomb-mr1/android/support/design/widget/ValueAnimatorCompatImplHoneycombMr1.java new file mode 100644 index 000000000..4f9ea39e2 --- /dev/null +++ b/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/res/anim/fab_in.xml b/res/anim/fab_in.xml new file mode 100644 index 000000000..294050f54 --- /dev/null +++ b/res/anim/fab_in.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/res/anim/fab_out.xml b/res/anim/fab_out.xml new file mode 100644 index 000000000..0f80a9ab0 --- /dev/null +++ b/res/anim/fab_out.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/src/android/support/design/widget/AppBarLayout.java b/src/android/support/design/widget/AppBarLayout.java index 29216b2a6..a9d170f59 100644 --- a/src/android/support/design/widget/AppBarLayout.java +++ b/src/android/support/design/widget/AppBarLayout.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; 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 android.widget.LinearLayout; 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. *

* Children should provide their desired scrolling behavior through * {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute: @@ -89,48 +93,19 @@ import java.util.List; 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). * - *

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 @@ public class AppBarLayout extends LinearLayout { private float mTargetElevation; + private final List> mListeners; + public AppBarLayout(Context context) { this(context, null); } @@ -159,6 +136,45 @@ public class AppBarLayout extends LinearLayout { // 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 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> i = mListeners.iterator(); + while (i.hasNext()) { + final WeakReference 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 @@ public class AppBarLayout extends LinearLayout { } /** - * 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. + *

+ * 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 @@ public class AppBarLayout extends LinearLayout { * scroll handling with offsetting. */ public static class Behavior extends ViewOffsetBehavior { - 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 class AppBarLayout extends LinearLayout { 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 @@ -575,14 +619,120 @@ public class AppBarLayout extends LinearLayout { mSkipNestedPreScroll = false; } + @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 @@ public class AppBarLayout extends LinearLayout { 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 @@ public class AppBarLayout extends LinearLayout { 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 @@ public class AppBarLayout extends LinearLayout { 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> 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 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 @@ public class AppBarLayout extends LinearLayout { } final int getTopBottomOffsetForScrollingSibling() { - return mSiblingOffsetTop; + return mLogicalOffsetTop; } } diff --git a/src/android/support/design/widget/CollapsingToolbarLayout.java b/src/android/support/design/widget/CollapsingToolbarLayout.java index ab36ed565..24eb439f8 100644 --- a/src/android/support/design/widget/CollapsingToolbarLayout.java +++ b/src/android/support/design/widget/CollapsingToolbarLayout.java @@ -31,6 +31,7 @@ import android.util.AttributeSet; 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 @@ import java.lang.annotation.RetentionPolicy; * @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 @@ public class CollapsingToolbarLayout extends FrameLayout implements AppBarLayout private int mCurrentForegroundColor; private boolean mScrimIsShown; + private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener; + public CollapsingToolbarLayout(Context context) { this(context, null); } @@ -154,6 +157,31 @@ public class CollapsingToolbarLayout extends FrameLayout implements AppBarLayout setWillNotDraw(false); } + @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 @@ public class CollapsingToolbarLayout extends FrameLayout implements AppBarLayout 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 @@ public class CollapsingToolbarLayout extends FrameLayout implements AppBarLayout 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/src/android/support/design/widget/FloatingActionButton.java b/src/android/support/design/widget/FloatingActionButton.java index 742bfa1fe..f8e04ddd1 100644 --- a/src/android/support/design/widget/FloatingActionButton.java +++ b/src/android/support/design/widget/FloatingActionButton.java @@ -31,6 +31,7 @@ import android.support.v4.view.ViewCompat; 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; @@ -370,40 +371,68 @@ public class FloatingActionButton extends ImageView { 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/src/android/support/design/widget/ViewUtils.java b/src/android/support/design/widget/ViewUtils.java index 34fdc6883..29a4522e4 100644 --- a/src/android/support/design/widget/ViewUtils.java +++ b/src/android/support/design/widget/ViewUtils.java @@ -21,6 +21,16 @@ import android.view.View; 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 @@ class ViewUtils { IMPL.setBoundsViewOutlineProvider(view); } + static ValueAnimatorCompat createAnimator() { + return DEFAULT_ANIMATOR_CREATOR.createAnimator(); + } + }