/* * 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.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; import android.support.design.R; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.WindowInsetsCompat; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Interpolator; 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.List; /** * AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of * material designs app bar concept, namely scrolling gestures. *
* Children should provide their desired scrolling behavior through * {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute: * {@code app:layout_scrollFlags}. * *
* This view depends heavily on being used as a direct child within a {@link CoordinatorLayout}. * If you use AppBarLayout within a different {@link ViewGroup}, most of it's functionality will * not work. *
* AppBarLayout also requires a separate scrolling sibling in order to know when to scroll. * The binding is done through the {@link ScrollingViewBehavior} behavior class, meaning that you * should set your scrolling view's behavior to be an instance of {@link ScrollingViewBehavior}. * A string resource containing the full class name is available. * *
* <android.support.design.widget.CoordinatorLayout * xmlns:android="http://schemas.android.com/apk/res/android" * xmlns:app="http://schemas.android.com/apk/res-auto" * android:layout_width="match_parent" * android:layout_height="match_parent"> * * <android.support.v4.widget.NestedScrollView * android:layout_width="match_parent" * android:layout_height="match_parent" * app:layout_behavior="@string/appbar_scrolling_view_behavior"> * * <!-- Your scrolling content --> * * </android.support.v4.widget.NestedScrollView> * * <android.support.design.widget.AppBarLayout * android:layout_height="wrap_content" * android:layout_width="match_parent"> * * <android.support.v7.widget.Toolbar * ... * app:layout_scrollFlags="scroll|enterAlways"/> * * <android.support.design.widget.TabLayout * ... * app:layout_scrollFlags="scroll|enterAlways"/> * * </android.support.design.widget.AppBarLayout> * * </android.support.design.widget.CoordinatorLayout> ** * @see * http://www.google.com/design/spec/layout/structure.html#structure-app-bar */ @CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class) public class AppBarLayout extends LinearLayout { private static final int PENDING_ACTION_NONE = 0x0; private static final int PENDING_ACTION_EXPANDED = 0x1; private static final int PENDING_ACTION_COLLAPSED = 0x2; private static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4; /** * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical * offset changes. */ 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). * * @param appBarLayout the {@link AppBarLayout} which offset has changed * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px */ void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset); } private static final int INVALID_SCROLL_RANGE = -1; private int mTotalScrollRange = INVALID_SCROLL_RANGE; private int mDownPreScrollRange = INVALID_SCROLL_RANGE; private int mDownScrollRange = INVALID_SCROLL_RANGE; boolean mHaveChildWithInterpolator; private float mTargetElevation; private int mPendingAction = PENDING_ACTION_NONE; private WindowInsetsCompat mLastInsets; private final List
As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a * direct child of a {@link CoordinatorLayout}.
* * @param expanded true if the layout should be fully expanded, false if it should * be fully collapsed * * @attr ref android.support.design.R.styleable#AppBarLayout_expanded */ public void setExpanded(boolean expanded) { setExpanded(expanded, ViewCompat.isLaidOut(this)); } /** * Sets whether this {@link AppBarLayout} is expanded or not. * *As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a * direct child of a {@link CoordinatorLayout}.
* * @param expanded true if the layout should be fully expanded, false if it should * be fully collapsed * @param animate Whether to animate to the new state * * @attr ref android.support.design.R.styleable#AppBarLayout_expanded */ public void setExpanded(boolean expanded, boolean animate) { mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED) | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0); requestLayout(); } @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); } private 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 = 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 + lp.topMargin + lp.bottomMargin; 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; } } return mTotalScrollRange = (range - getTopInset()); } private boolean hasScrollableChildren() { return getTotalScrollRange() != 0; } /** * Return the scroll range when scrolling up from a nested pre-scroll. */ private int getUpNestedPreScrollRange() { return getTotalScrollRange(); } /** * Return the scroll range when scrolling down from a nested pre-scroll. */ private 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 = child.getMeasuredHeight(); final int flags = lp.mScrollFlags; if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) { // First take the margin into account range += lp.topMargin + lp.bottomMargin; // 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. */ private 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(); int childHeight = child.getMeasuredHeight(); childHeight += lp.topMargin + lp.bottomMargin; 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 mDownScrollRange = (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 int getPendingAction() {
return mPendingAction;
}
private void resetPendingAction() {
mPendingAction = PENDING_ACTION_NONE;
}
private int getTopInset() {
return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
}
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,
SCROLL_FLAG_SNAP
})
@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;
/**
* Upon a scroll ending, if the view is only partially visible then it will be snapped
* and scrolled to it's closest edge. For example, if the view only has it's bottom 25%
* displayed, it will be scrolled off screen completely. Conversely, if it's bottom 75%
* is visible then it will be scrolled fully into view.
*/
public static final int SCROLL_FLAG_SNAP = 0x10;
/**
* Internal flags which allows quick checking features
*/
static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS;
static final int FLAG_SNAP = SCROLL_FLAG_SCROLL | SCROLL_FLAG_SNAP;
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},
* {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED} and {@link #SCROLL_FLAG_SNAP }.
*
* @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 HeaderBehavior