am a196f6c5: Merge "AppBarLayout improvements" into lmp-mr1-ub-dev

* commit 'a196f6c585e08dd962cb14b659790931a6e92e40':
  AppBarLayout improvements

GitOrigin-RevId=0afaaf631e3c992dc4af9d628444f1eee06758a8

PiperOrigin-RevId: 140545874
This commit is contained in:
Chris Banes 2016-11-30 00:53:41 +00:00 committed by Travis Collins
parent b227636e05
commit d45d59df81
12 changed files with 949 additions and 165 deletions

View File

@ -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

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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'

View File

@ -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();
}
};
}

View File

@ -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();
}
}

30
res/anim/fab_in.xml Normal file
View File

@ -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>

30
res/anim/fab_out.xml Normal file
View File

@ -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>

View File

@ -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.
* <p>
* 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).
*
* <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 @@ public class AppBarLayout extends LinearLayout {
private float mTargetElevation;
private final List<WeakReference<OnOffsetChangedListener>> 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<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 @@ 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.
* <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 @@ public class AppBarLayout extends LinearLayout {
* 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 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<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 @@ public class AppBarLayout extends LinearLayout {
}
final int getTopBottomOffsetForScrollingSibling() {
return mSiblingOffsetTop;
return mLogicalOffsetTop;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}