/* * 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 com.google.android.material.appbar; import com.google.android.material.R; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import com.google.android.material.animation.AnimationUtils; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.math.MathUtils; import android.support.design.widget.CoordinatorLayout; import android.support.v4.util.ObjectsCompat; import android.support.v4.view.AbsSavedState; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat.NestedScrollType; import android.support.v4.view.WindowInsetsCompat; import android.util.AttributeSet; import android.view.View; 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> * * <com.google.android.material.appbar.AppBarLayout * android:layout_height="wrap_content" * android:layout_width="match_parent"> * * <android.support.v7.widget.Toolbar * ... * app:layout_scrollFlags="scroll|enterAlways"/> * * <com.google.android.material.tabs.TabLayout * ... * app:layout_scrollFlags="scroll|enterAlways"/> * * </com.google.android.material.appbar.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 { static final int PENDING_ACTION_NONE = 0x0; static final int PENDING_ACTION_EXPANDED = 0x1; static final int PENDING_ACTION_COLLAPSED = 0x2; static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4; static final int PENDING_ACTION_FORCE = 0x8; /** * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical * offset changes. */ //TODO: remove this base interface after the widget migration public interface BaseOnOffsetChangedListener
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 com.google.android.material.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 com.google.android.material.R.styleable#AppBarLayout_expanded
*/
public void setExpanded(boolean expanded, boolean animate) {
setExpanded(expanded, animate, true);
}
private void setExpanded(boolean expanded, boolean animate, boolean force) {
pendingAction =
(expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
| (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0)
| (force ? PENDING_ACTION_FORCE : 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 (Build.VERSION.SDK_INT >= 19 && p instanceof LinearLayout.LayoutParams) {
return new LayoutParams((LinearLayout.LayoutParams) p);
} else if (p instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) p);
}
return new LayoutParams(p);
}
boolean hasChildWithInterpolator() {
return haveChildWithInterpolator;
}
/**
* Returns the scroll range of all children.
*
* @return the scroll range in px
*/
public final int getTotalScrollRange() {
if (totalScrollRange != INVALID_SCROLL_RANGE) {
return totalScrollRange;
}
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.scrollFlags;
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 totalScrollRange = Math.max(0, range - getTopInset());
}
boolean hasScrollableChildren() {
return getTotalScrollRange() != 0;
}
/** Return the scroll range when scrolling up from a nested pre-scroll. */
int getUpNestedPreScrollRange() {
return getTotalScrollRange();
}
/** Return the scroll range when scrolling down from a nested pre-scroll. */
int getDownNestedPreScrollRange() {
if (downPreScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return downPreScrollRange;
}
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.scrollFlags;
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 if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
// Only enter by the amount of the collapsed height
range += childHeight - ViewCompat.getMinimumHeight(child);
} else {
// Else use the full height (minus the top inset)
range += childHeight - getTopInset();
}
} 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 downPreScrollRange = Math.max(0, range);
}
/** Return the scroll range when scrolling down from a nested scroll. */
int getDownNestedScrollRange() {
if (downScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return downScrollRange;
}
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.scrollFlags;
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 break the range straight away since later views can't scroll
// beneath us
range -= ViewCompat.getMinimumHeight(child) + getTopInset();
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 downScrollRange = Math.max(0, range);
}
void dispatchOffsetUpdates(int offset) {
// Iterate backwards through the list so that most recently added listeners
// get the first chance to decide
if (listeners != null) {
for (int i = 0, z = listeners.size(); i < z; i++) {
final BaseOnOffsetChangedListener listener = listeners.get(i);
if (listener != null) {
listener.onOffsetChanged(this, offset);
}
}
}
}
public final int getMinimumHeightForVisibleOverlappingContent() {
final int topInset = getTopInset();
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();
final int lastChildMinHeight =
childCount >= 1 ? ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) : 0;
if (lastChildMinHeight != 0) {
return (lastChildMinHeight * 2) + topInset;
}
// If we reach here then we don't have a min height explicitly set. Instead we'll take a
// guess at 1/3 of our height being visible
return getHeight() / 3;
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
if (tmpStatesArray == null) {
// Note that we can't allocate this at the class level (in declaration) since
// some paths in super View constructor are going to call this method before
// that
tmpStatesArray = new int[2];
}
final int[] extraStates = tmpStatesArray;
final int[] states = super.onCreateDrawableState(extraSpace + extraStates.length);
extraStates[0] = collapsible ? R.attr.state_collapsible : -R.attr.state_collapsible;
extraStates[1] = collapsible && collapsed ? R.attr.state_collapsed : -R.attr.state_collapsed;
return mergeDrawableStates(states, extraStates);
}
/**
* Sets whether the AppBarLayout has collapsible children or not.
*
* @return true if the collapsible state changed
*/
private boolean setCollapsibleState(boolean collapsible) {
if (this.collapsible != collapsible) {
this.collapsible = collapsible;
refreshDrawableState();
return true;
}
return false;
}
/**
* Sets whether the AppBarLayout is in a collapsed state or not.
*
* @return true if the collapsed state changed
*/
boolean setCollapsedState(boolean collapsed) {
if (this.collapsed != collapsed) {
this.collapsed = collapsed;
refreshDrawableState();
return true;
}
return false;
}
/**
* @deprecated target elevation is now deprecated. AppBarLayout's elevation is now controlled via
* a {@link android.animation.StateListAnimator}. If a target elevation is set, either by this
* method or the {@code app:elevation} attribute, a new state list animator is created which
* uses the given {@code elevation} value.
* @attr ref com.google.android.material.R.styleable#AppBarLayout_elevation
*/
@Deprecated
public void setTargetElevation(float elevation) {
if (Build.VERSION.SDK_INT >= 21) {
ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(this, elevation);
}
}
/**
* @deprecated target elevation is now deprecated. AppBarLayout's elevation is now controlled via
* a {@link android.animation.StateListAnimator}. This method now always returns 0.
*/
@Deprecated
public float getTargetElevation() {
return 0;
}
int getPendingAction() {
return pendingAction;
}
void resetPendingAction() {
pendingAction = PENDING_ACTION_NONE;
}
@VisibleForTesting
final int getTopInset() {
return lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;
}
WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {
WindowInsetsCompat newInsets = null;
if (ViewCompat.getFitsSystemWindows(this)) {
// If we're set to fit system windows, keep the insets
newInsets = insets;
}
// If our insets have changed, keep them and invalidate the scroll ranges...
if (!ObjectsCompat.equals(lastInsets, newInsets)) {
lastInsets = newInsets;
invalidateScrollRanges();
}
return insets;
}
/** TODO: Add a real comment here after the widget migration */
public static class LayoutParams extends LinearLayout.LayoutParams {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef(
flag = true,
value = {
SCROLL_FLAG_SCROLL,
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,
SCROLL_FLAG_ENTER_ALWAYS,
SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,
SCROLL_FLAG_SNAP,
SCROLL_FLAG_SNAP_MARGINS,
}
)
@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;
/**
* An additional flag to be used with 'snap'. If set, the view will be snapped to its top and
* bottom margins, as opposed to the edges of the view itself.
*/
public static final int SCROLL_FLAG_SNAP_MARGINS = 0x20;
/** 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;
static final int COLLAPSIBLE_FLAGS =
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED | SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED;
int scrollFlags = SCROLL_FLAG_SCROLL;
Interpolator scrollInterpolator;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AppBarLayout_Layout);
scrollFlags = a.getInt(R.styleable.AppBarLayout_Layout_layout_scrollFlags, 0);
if (a.hasValue(R.styleable.AppBarLayout_Layout_layout_scrollInterpolator)) {
int resId = a.getResourceId(R.styleable.AppBarLayout_Layout_layout_scrollInterpolator, 0);
scrollInterpolator = 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);
}
@RequiresApi(19)
public LayoutParams(LinearLayout.LayoutParams source) {
// The copy constructor called here only exists on API 19+.
super(source);
}
@RequiresApi(19)
public LayoutParams(LayoutParams source) {
// The copy constructor called here only exists on API 19+.
super(source);
scrollFlags = source.scrollFlags;
scrollInterpolator = source.scrollInterpolator;
}
/**
* 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}, {@link #SCROLL_FLAG_SNAP}, and {@link
* #SCROLL_FLAG_SNAP_MARGINS}.
* @see #getScrollFlags()
* @attr ref com.google.android.material.R.styleable#AppBarLayout_Layout_layout_scrollFlags
*/
public void setScrollFlags(@ScrollFlags int flags) {
scrollFlags = flags;
}
/**
* Returns the scrolling flags.
*
* @see #setScrollFlags(int)
* @attr ref com.google.android.material.R.styleable#AppBarLayout_Layout_layout_scrollFlags
*/
@ScrollFlags
public int getScrollFlags() {
return scrollFlags;
}
/**
* 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 com.google.android.material.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
* @see #getScrollInterpolator()
*/
public void setScrollInterpolator(Interpolator interpolator) {
scrollInterpolator = 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 com.google.android.material.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
* @see #setScrollInterpolator(Interpolator)
*/
public Interpolator getScrollInterpolator() {
return scrollInterpolator;
}
/** Returns true if the scroll flags are compatible for 'collapsing' */
boolean isCollapsible() {
return (scrollFlags & SCROLL_FLAG_SCROLL) == SCROLL_FLAG_SCROLL
&& (scrollFlags & COLLAPSIBLE_FLAGS) != 0;
}
}
/**
* The default {@link Behavior} for {@link AppBarLayout}. Implements the necessary nested scroll
* handling with offsetting.
*/
//TODO: remove the base class and generic type after the widget migration is done
public static class Behavior extends BaseBehavior Dragging is defined as a direct touch on the AppBarLayout with movement. This call does
* not affect any nested scrolling.
*
* @return true if we are in a position to scroll the AppBarLayout via a drag, false if not.
*/
public abstract boolean canDrag(@NonNull T appBarLayout);
}
private int offsetDelta;
@NestedScrollType private int lastStartedType;
private ValueAnimator offsetAnimator;
private int offsetToChildIndexOnLayout = INVALID_POSITION;
private boolean offsetToChildIndexOnLayoutIsMinHeight;
private float offsetToChildIndexOnLayoutPerc;
private WeakReference