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
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// Invalidate the scroll ranges
mTotalScrollRange = INVALID_SCROLL_RANGE;
mDownPreScrollRange = INVALID_SCROLL_RANGE;
mDownPreScrollRange = INVALID_SCROLL_RANGE;
mHaveChildWithInterpolator = false;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams childLp = (LayoutParams) child.getLayoutParams();
final Interpolator interpolator = childLp.getScrollInterpolator();
if (interpolator != null) {
mHaveChildWithInterpolator = true;
break;
}
}
}
@Override
public void setOrientation(int orientation) {
if (orientation != VERTICAL) {
throw new IllegalArgumentException("AppBarLayout is always vertical and does"
+ " not support horizontal orientation");
}
super.setOrientation(orientation);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (p instanceof LinearLayout.LayoutParams) {
return new LayoutParams((LinearLayout.LayoutParams) p);
} else if (p instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) p);
}
return new LayoutParams(p);
}
final boolean hasChildWithInterpolator() {
return mHaveChildWithInterpolator;
}
/**
* Returns the scroll range of all children.
*
* @return the scroll range in px
*/
public final int getTotalScrollRange() {
if (mTotalScrollRange != INVALID_SCROLL_RANGE) {
return mTotalScrollRange;
}
int range = 0;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int childHeight = ViewCompat.isLaidOut(child)
? child.getHeight()
: child.getMeasuredHeight();
final int flags = lp.mScrollFlags;
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
// We're set to scroll so add the child's height
range += childHeight;
if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// For a collapsing scroll, we to take the collapsed height into account.
// We also break straight away since later views can't scroll beneath
// us
range -= ViewCompat.getMinimumHeight(child);
break;
}
} else {
// As soon as a view doesn't have the scroll flag, we end the range calculation.
// This is because views below can not scroll under a fixed view.
break;
}
}
final int top = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
return mTotalScrollRange = (range - top);
}
final boolean hasScrollableChildren() {
return getTotalScrollRange() != 0;
}
/**
* Return the scroll range when scrolling up from a nested pre-scroll.
*/
final int getUpNestedPreScrollRange() {
return getTotalScrollRange();
}
/**
* Return the scroll range when scrolling down from a nested pre-scroll.
*/
final int getDownNestedPreScrollRange() {
if (mDownPreScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return mDownPreScrollRange;
}
int range = 0;
for (int i = getChildCount() - 1; i >= 0; i--) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int childHeight = ViewCompat.isLaidOut(child)
? child.getHeight()
: child.getMeasuredHeight();
final int flags = lp.mScrollFlags;
if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
// The view has the quick return flag combination...
if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
// If they're set to enter collapsed, use the minimum height
range += ViewCompat.getMinimumHeight(child);
} else {
// Else use the full height
range += childHeight;
}
} else if (range > 0) {
// If we've hit an non-quick return scrollable view, and we've already hit a
// quick return view, return now
break;
}
}
return mDownPreScrollRange = range;
}
/**
* Return the scroll range when scrolling down from a nested scroll.
*/
final int getDownNestedScrollRange() {
if (mDownScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return mDownScrollRange;
}
int range = 0;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int childHeight = ViewCompat.isLaidOut(child)
? child.getHeight()
: child.getMeasuredHeight();
final int flags = lp.mScrollFlags;
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
// We're set to scroll so add the child's height
range += childHeight;
if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// For a collapsing exit scroll, we to take the collapsed height into account.
// We also return the range straight away since later views can't scroll
// beneath us
return range - ViewCompat.getMinimumHeight(child);
}
} else {
// As soon as a view doesn't have the scroll flag, we end the range calculation.
// This is because views below can not scroll under a fixed view.
break;
}
}
return mDownScrollRange = range;
}
final int getMinimumHeightForVisibleOverlappingContent() {
final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
final int minHeight = ViewCompat.getMinimumHeight(this);
if (minHeight != 0) {
// If this layout has a min height, use it (doubled)
return (minHeight * 2) + topInset;
}
// Otherwise, we'll use twice the min height of our last child
final int childCount = getChildCount();
return childCount >= 1
? (ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) * 2) + topInset
: 0;
}
/**
* 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)
*/
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;
}
private void setWindowInsets(WindowInsetsCompat insets) {
// Invalidate the total scroll range...
mTotalScrollRange = INVALID_SCROLL_RANGE;
mLastInsets = insets;
// Now dispatch them to our children
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
insets = ViewCompat.dispatchApplyWindowInsets(child, insets);
if (insets.isConsumed()) {
break;
}
}
}
public static class LayoutParams extends LinearLayout.LayoutParams {
/** @hide */
@IntDef(flag=true, value={
SCROLL_FLAG_SCROLL,
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,
SCROLL_FLAG_ENTER_ALWAYS,
SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED
})
@Retention(RetentionPolicy.SOURCE)
public @interface ScrollFlags {}
/**
* The view will be scroll in direct relation to scroll events. This flag needs to be
* set for any of the other flags to take effect. If any sibling views
* before this one do not have this flag, then this value has no effect.
*/
public static final int SCROLL_FLAG_SCROLL = 0x1;
/**
* When exiting (scrolling off screen) the view will be scrolled until it is
* 'collapsed'. The collapsed height is defined by the view's minimum height.
*
* @see ViewCompat#getMinimumHeight(View)
* @see View#setMinimumHeight(int)
*/
public static final int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED = 0x2;
/**
* When entering (scrolling on screen) the view will scroll on any downwards
* scroll event, regardless of whether the scrolling view is also scrolling. This
* is commonly referred to as the 'quick return' pattern.
*/
public static final int SCROLL_FLAG_ENTER_ALWAYS = 0x4;
/**
* An additional flag for 'enterAlways' which modifies the returning view to
* only initially scroll back to it's collapsed height. Once the scrolling view has
* reached the end of it's scroll range, the remainder of this view will be scrolled
* into view. The collapsed height is defined by the view's minimum height.
*
* @see ViewCompat#getMinimumHeight(View)
* @see View#setMinimumHeight(int)
*/
public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 0x8;
/**
* Internal flag which allows quick checking of 'quick return'
*/
static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS;
int mScrollFlags = SCROLL_FLAG_SCROLL;
Interpolator mScrollInterpolator;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AppBarLayout_LayoutParams);
mScrollFlags = a.getInt(R.styleable.AppBarLayout_LayoutParams_layout_scrollFlags, 0);
if (a.hasValue(R.styleable.AppBarLayout_LayoutParams_layout_scrollInterpolator)) {
int resId = a.getResourceId(
R.styleable.AppBarLayout_LayoutParams_layout_scrollInterpolator, 0);
mScrollInterpolator = android.view.animation.AnimationUtils.loadInterpolator(
c, resId);
}
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(int width, int height, float weight) {
super(width, height, weight);
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(LinearLayout.LayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
super(source);
mScrollFlags = source.mScrollFlags;
mScrollInterpolator = source.mScrollInterpolator;
}
/**
* Set the scrolling flags.
*
* @param flags bitwise int of {@link #SCROLL_FLAG_SCROLL},
* {@link #SCROLL_FLAG_EXIT_UNTIL_COLLAPSED}, {@link #SCROLL_FLAG_ENTER_ALWAYS}
* and {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED}.
*
* @see #getScrollFlags()
*
* @attr ref android.support.design.R.styleable#AppBarLayout_LayoutParams_layout_scrollFlags
*/
public void setScrollFlags(@ScrollFlags int flags) {
mScrollFlags = flags;
}
/**
* Returns the scrolling flags.
*
* @see #setScrollFlags(int)
*
* @attr ref android.support.design.R.styleable#AppBarLayout_LayoutParams_layout_scrollFlags
*/
@ScrollFlags
public int getScrollFlags() {
return mScrollFlags;
}
/**
* Set the interpolator to when scrolling the view associated with this
* {@link LayoutParams}.
*
* @param interpolator the interpolator to use, or null to use normal 1-to-1 scrolling.
*
* @attr ref android.support.design.R.styleable#AppBarLayout_LayoutParams_layout_scrollInterpolator
* @see #getScrollInterpolator()
*/
public void setScrollInterpolator(Interpolator interpolator) {
mScrollInterpolator = interpolator;
}
/**
* Returns the {@link Interpolator} being used for scrolling the view associated with this
* {@link LayoutParams}. Null indicates 'normal' 1-to-1 scrolling.
*
* @attr ref android.support.design.R.styleable#AppBarLayout_LayoutParams_layout_scrollInterpolator
* @see #setScrollInterpolator(Interpolator)
*/
public Interpolator getScrollInterpolator() {
return mScrollInterpolator;
}
}
/**
* The default {@link Behavior} for {@link AppBarLayout}. Implements the necessary nested
* scroll handling with offsetting.
*/
public static class Behavior extends ViewOffsetBehavior {
private static final int INVALID_POSITION = -1;
private int mOffsetDelta;
private boolean mSkipNestedPreScroll;
private Runnable mFlingRunnable;
private ScrollerCompat mScroller;
private ValueAnimatorCompat mAnimator;
private int mOffsetToChildIndexOnLayout = INVALID_POSITION;
private boolean mOffsetToChildIndexOnLayoutIsMinHeight;
private float mOffsetToChildIndexOnLayoutPerc;
public Behavior() {}
public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
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
// and the scrolling view is big enough to scroll
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& coordinatorLayout.getHeight() - target.getHeight() <= child.getHeight();
if (started && mAnimator != null) {
// Cancel any offset animation
mAnimator.cancel();
}
return started;
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed) {
if (dy != 0 && !mSkipNestedPreScroll) {
int min, max;
if (dy < 0) {
// We're scrolling down
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// We're scrolling up
min = -child.getUpNestedPreScrollRange();
max = 0;
}
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
}
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed < 0) {
// If the scrolling view is scrolling down but not consuming, it's probably be at
// the top of it's content
scroll(coordinatorLayout, child, dyUnconsumed,
-child.getDownNestedScrollRange(), 0);
// Set the expanding flag so that onNestedPreScroll doesn't handle any events
mSkipNestedPreScroll = true;
} else {
// As we're no longer handling nested scrolls, reset the skip flag
mSkipNestedPreScroll = false;
}
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target) {
// Reset the skip flag
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 (getTopBottomOffsetForScrollingSibling() != 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, getTopBottomOffsetForScrollingSibling(), // 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 appBarLayout,
int layoutDirection) {
boolean handled = super.onLayoutChild(parent, appBarLayout, layoutDirection);
if (mOffsetToChildIndexOnLayout >= 0) {
View child = appBarLayout.getChildAt(mOffsetToChildIndexOnLayout);
int offset = -child.getBottom();
if (mOffsetToChildIndexOnLayoutIsMinHeight) {
offset += ViewCompat.getMinimumHeight(child);
} else {
offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
}
setTopAndBottomOffset(offset);
mOffsetToChildIndexOnLayout = INVALID_POSITION;
}
// Make sure we update the elevation
dispatchOffsetUpdates(appBarLayout);
return handled;
}
private int scroll(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout,
int dy, int minOffset, int maxOffset) {
return setAppBarTopBottomOffset(coordinatorLayout, appBarLayout,
getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}
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 = getTopBottomOffsetForScrollingSibling();
int consumed = 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) {
final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
? interpolateOffset(appBarLayout, newOffset)
: newOffset;
boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
// Update the stored sibling offset
mOffsetDelta = newOffset - interpolatedOffset;
if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
// If the offset hasn't changed and we're using an interpolated scroll
// then we need to keep any dependent views updated. CoL will do this for
// us when we move, but we need to do it manually when we don't (as an
// interpolated scroll may finish early).
coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
}
// Dispatch the updates to any listeners
dispatchOffsetUpdates(appBarLayout);
}
}
return consumed;
}
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 interpolateOffset(AppBarLayout layout, final int offset) {
final int absOffset = Math.abs(offset);
for (int i = 0, z = layout.getChildCount(); i < z; i++) {
final View child = layout.getChildAt(i);
final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
final Interpolator interpolator = childLp.getScrollInterpolator();
if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
if (interpolator != null) {
int childScrollableHeight = 0;
final int flags = childLp.getScrollFlags();
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
// We're set to scroll so add the child's height
childScrollableHeight += child.getHeight();
if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// For a collapsing scroll, we to take the collapsed height into account.
childScrollableHeight -= ViewCompat.getMinimumHeight(child);
}
}
if (childScrollableHeight > 0) {
final int offsetForView = absOffset - child.getTop();
final int interpolatedDiff = Math.round(childScrollableHeight *
interpolator.getInterpolation(
offsetForView / (float) childScrollableHeight));
return Integer.signum(offset) * (child.getTop() + interpolatedDiff);
}
}
// If we get to here then the view on the offset isn't suitable for interpolated
// scrolling. So break out of the loop
break;
}
}
return offset;
}
final int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset() + mOffsetDelta;
}
@Override
public Parcelable onSaveInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout) {
final Parcelable superState = super.onSaveInstanceState(parent, appBarLayout);
final int offset = getTopAndBottomOffset();
// Try and find the first visible child...
for (int i = 0, count = appBarLayout.getChildCount(); i < count; i++) {
View child = appBarLayout.getChildAt(i);
final int visBottom = child.getBottom() + offset;
if (child.getTop() + offset <= 0 && visBottom >= 0) {
final SavedState ss = new SavedState(superState);
ss.firstVisibleChildIndex = i;
ss.firstVisibileChildAtMinimumHeight =
visBottom == ViewCompat.getMinimumHeight(child);
ss.firstVisibileChildPercentageShown = visBottom / (float) child.getHeight();
return ss;
}
}
// Else we'll just return the super state
return superState;
}
@Override
public void onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout,
Parcelable state) {
if (state instanceof SavedState) {
final SavedState ss = (SavedState) state;
super.onRestoreInstanceState(parent, appBarLayout, ss.getSuperState());
mOffsetToChildIndexOnLayout = ss.firstVisibleChildIndex;
mOffsetToChildIndexOnLayoutPerc = ss.firstVisibileChildPercentageShown;
mOffsetToChildIndexOnLayoutIsMinHeight = ss.firstVisibileChildAtMinimumHeight;
} else {
super.onRestoreInstanceState(parent, appBarLayout, state);
mOffsetToChildIndexOnLayout = INVALID_POSITION;
}
}
protected static class SavedState extends View.BaseSavedState {
int firstVisibleChildIndex;
float firstVisibileChildPercentageShown;
boolean firstVisibileChildAtMinimumHeight;
public SavedState(Parcel source) {
super(source);
firstVisibleChildIndex = source.readInt();
firstVisibileChildPercentageShown = source.readFloat();
firstVisibileChildAtMinimumHeight = source.readByte() != 0;
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(firstVisibleChildIndex);
dest.writeFloat(firstVisibileChildPercentageShown);
dest.writeByte((byte) (firstVisibileChildAtMinimumHeight ? 1 : 0));
}
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
/**
* Behavior which should be used by {@link View}s which can scroll vertically and support
* nested scrolling to automatically scroll any {@link AppBarLayout} siblings.
*/
public static class ScrollingViewBehavior extends ViewOffsetBehavior {
private int mOverlayTop;
public ScrollingViewBehavior() {}
public ScrollingViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ScrollingViewBehavior_Params);
mOverlayTop = a.getDimensionPixelSize(
R.styleable.ScrollingViewBehavior_Params_behavior_overlapTop, 0);
a.recycle();
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
if (child.getLayoutParams().height == LayoutParams.MATCH_PARENT) {
// If the child's height is set to match_parent then measure it with it's
// maximum visible visible height
final List dependencies = parent.getDependencies(child);
if (dependencies.isEmpty()) {
// If we don't have any dependencies, return false
return false;
}
final AppBarLayout appBar = findFirstAppBarLayout(dependencies);
if (appBar != null && ViewCompat.isLaidOut(appBar)) {
if (ViewCompat.getFitsSystemWindows(appBar)) {
// If the AppBarLayout is fitting system windows then we need to also,
// otherwise we'll get CoL's compatible layout functionality
ViewCompat.setFitsSystemWindows(child, true);
}
final int height = parent.getHeight() - appBar.getMeasuredHeight()
+ appBar.getTotalScrollRange();
final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.AT_MOST);
// Now measure the scrolling child with the correct height
parent.onMeasureChild(child, parentWidthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
return false;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof Behavior) {
// Offset the child so that it is below the app-bar (with any overlap)
final int appBarOffset = ((Behavior) behavior)
.getTopBottomOffsetForScrollingSibling();
final int expandedMax = dependency.getHeight() - mOverlayTop;
final int collapsedMin = parent.getHeight() - child.getHeight();
if (mOverlayTop != 0 && dependency instanceof AppBarLayout) {
// If we have an overlap top, and the dependency is an AppBarLayout, we control
// the offset ourselves based on the appbar's scroll progress. This is so that
// the scroll happens sequentially rather than linearly
final int scrollRange = ((AppBarLayout) dependency).getTotalScrollRange();
setTopAndBottomOffset(AnimationUtils.lerp(expandedMax, collapsedMin,
Math.abs(appBarOffset) / (float) scrollRange));
} else {
setTopAndBottomOffset(dependency.getHeight() - mOverlayTop + appBarOffset);
}
}
return false;
}
/**
* Set the distance that this view should overlap any {@link AppBarLayout}.
*
* @param overlayTop the distance in px
*/
public void setOverlayTop(int overlayTop) {
mOverlayTop = overlayTop;
}
/**
* Returns the distance that this view should overlap any {@link AppBarLayout}.
*/
public int getOverlayTop() {
return mOverlayTop;
}
private static AppBarLayout findFirstAppBarLayout(List views) {
for (int i = 0, z = views.size(); i < z; i++) {
View view = views.get(i);
if (view instanceof AppBarLayout) {
return (AppBarLayout) view;
}
}
return null;
}
}
}