/* * 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 androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.MaterialShapeUtils; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.math.MathUtils; import androidx.core.util.ObjectsCompat; import androidx.customview.view.AbsSavedState; import androidx.core.view.NestedScrollingChild; import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat.NestedScrollType; import androidx.core.view.WindowInsetsCompat; import androidx.appcompat.content.res.AppCompatResources; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ScrollView; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.google.android.material.animation.AnimationUtils; import com.google.android.material.internal.ThemeEnforcement; 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 its 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. * *
* <androidx.coordinatorlayout.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"> * * <androidx.core.widget.NestedScrollView * android:layout_width="match_parent" * android:layout_height="match_parent" * app:layout_behavior="@string/appbar_scrolling_view_behavior"> * * <!-- Your scrolling content --> * * </androidx.core.widget.NestedScrollView> * * <com.google.android.material.appbar.AppBarLayout * android:layout_height="wrap_content" * android:layout_width="match_parent"> * * <androidx.appcompat.widget.Toolbar * ... * app:layout_scrollFlags="scroll|enterAlways"/> * * <com.google.android.material.tabs.TabLayout * ... * app:layout_scrollFlags="scroll|enterAlways"/> * * </com.google.android.material.appbar.AppBarLayout> * * </androidx.coordinatorlayout.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 = 1 << 1; static final int PENDING_ACTION_ANIMATE_ENABLED = 1 << 2; static final int PENDING_ACTION_FORCE = 1 << 3; /** * 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
This scrim is only shown when we have been given a top system inset. * * @param drawable the drawable to display * @attr ref R.styleable#AppBarLayout_statusBarForeground * @see #getStatusBarForeground() */ public void setStatusBarForeground(@Nullable Drawable drawable) { if (statusBarForeground != drawable) { if (statusBarForeground != null) { statusBarForeground.setCallback(null); } statusBarForeground = drawable != null ? drawable.mutate() : null; if (statusBarForeground != null) { if (statusBarForeground.isStateful()) { statusBarForeground.setState(getDrawableState()); } DrawableCompat.setLayoutDirection(statusBarForeground, ViewCompat.getLayoutDirection(this)); statusBarForeground.setVisible(getVisibility() == VISIBLE, false); statusBarForeground.setCallback(this); } ViewCompat.postInvalidateOnAnimation(this); } } /** * Set the color to use for the status bar foreground. * *
This scrim is only shown when we have been given a top system inset. * * @param color the color to display * @attr ref R.styleable#AppBarLayout_statusBarForeground * @see #getStatusBarForeground() */ public void setStatusBarForegroundColor(@ColorInt int color) { setStatusBarForeground(new ColorDrawable(color)); } /** * Set the drawable to use for the status bar foreground from resources. * *
This scrim is only shown when we have been given a top system inset. * * @param resId drawable resource id * @attr ref R.styleable#AppBarLayout_statusBarForeground * @see #getStatusBarForeground() */ public void setStatusBarForegroundResource(@DrawableRes int resId) { setStatusBarForeground(AppCompatResources.getDrawable(getContext(), resId)); } /** * Returns the drawable which is used for the status bar foreground. * * @see #setStatusBarForeground(Drawable) * @attr ref R.styleable#AppBarLayout_statusBarForeground */ @Nullable public Drawable getStatusBarForeground() { return statusBarForeground; } @Override public void draw(Canvas canvas) { super.draw(canvas); // Draw the status bar foreground drawable if we have a top inset if (statusBarForeground != null && getTopInset() > 0) { int saveCount = canvas.save(); canvas.translate(0f, -currentOffset); statusBarForeground.draw(canvas); canvas.restoreToCount(saveCount); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); final int[] state = getDrawableState(); Drawable d = statusBarForeground; if (d != null && d.isStateful() && d.setState(state)) { invalidateDrawable(d); } } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || who == statusBarForeground; } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); final boolean visible = visibility == VISIBLE; if (statusBarForeground != null) { statusBarForeground.setVisible(visible, false); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // If we're set to handle system windows but our first child is not, we need to add some // height to ourselves to pad the first child down below the status bar final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { int newHeight = getMeasuredHeight(); switch (heightMode) { case MeasureSpec.AT_MOST: // For AT_MOST, we need to clamp our desired height with the max height newHeight = MathUtils.clamp( getMeasuredHeight() + getTopInset(), 0, MeasureSpec.getSize(heightMeasureSpec)); break; case MeasureSpec.UNSPECIFIED: // For UNSPECIFIED we can use any height so just add the top inset newHeight += getTopInset(); break; default: // fall out } setMeasuredDimension(getMeasuredWidth(), newHeight); } invalidateScrollRanges(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { // If we need to offset the first child, we need to offset all of them to make space final int topInset = getTopInset(); for (int z = getChildCount() - 1; z >= 0; z--) { ViewCompat.offsetTopAndBottom(getChildAt(z), topInset); } } invalidateScrollRanges(); haveChildWithInterpolator = 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) { haveChildWithInterpolator = true; break; } } if (statusBarForeground != null) { statusBarForeground.setBounds(0, 0, getWidth(), getTopInset()); } // If the user has set liftable manually, don't set liftable state automatically. if (!liftableOverride) { setLiftableState(liftOnScroll || hasCollapsibleChild()); } } private boolean hasCollapsibleChild() { for (int i = 0, z = getChildCount(); i < z; i++) { if (((LayoutParams) getChildAt(i).getLayoutParams()).isCollapsible()) { return true; } } return false; } private void invalidateScrollRanges() { // Invalidate the scroll ranges totalScrollRange = INVALID_SCROLL_RANGE; downPreScrollRange = INVALID_SCROLL_RANGE; downScrollRange = INVALID_SCROLL_RANGE; } @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 void onAttachedToWindow() { super.onAttachedToWindow(); MaterialShapeUtils.setParentAbsoluteElevation(this); } @RequiresApi(VERSION_CODES.LOLLIPOP) @Override public void setElevation(float elevation) { super.setElevation(elevation); MaterialShapeUtils.setElevation(this, elevation); } /** * Sets whether this {@link AppBarLayout} is expanded or not, animating if it has already been * laid out. * *
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); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); clearLiftOnScrollTargetView(); } 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 (i == 0 && ViewCompat.getFitsSystemWindows(child)) { // If this is the first child and it wants to handle system windows, we need to make // sure we don't scroll it past the inset range -= getTopInset(); } 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); } 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 int childRange = 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 childRange += ViewCompat.getMinimumHeight(child); } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { // Only enter by the amount of the collapsed height childRange += childHeight - ViewCompat.getMinimumHeight(child); } else { // Else use the full height childRange += childHeight; } if (i == 0 && ViewCompat.getFitsSystemWindows(child)) { // If this is the first child and it wants to handle system windows, we need to make // sure we don't scroll past the inset childRange = Math.min(childRange, childHeight - getTopInset()); } range += childRange; } 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); 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 onOffsetChanged(int offset) { currentOffset = offset; if (!willNotDraw()) { ViewCompat.postInvalidateOnAnimation(this); } // 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[4]; } final int[] extraStates = tmpStatesArray; final int[] states = super.onCreateDrawableState(extraSpace + extraStates.length); extraStates[0] = liftable ? R.attr.state_liftable : -R.attr.state_liftable; extraStates[1] = liftable && lifted ? R.attr.state_lifted : -R.attr.state_lifted; // Note that state_collapsible and state_collapsed are deprecated. This is to keep compatibility // with existing state list animators that depend on these states. extraStates[2] = liftable ? R.attr.state_collapsible : -R.attr.state_collapsible; extraStates[3] = liftable && lifted ? R.attr.state_collapsed : -R.attr.state_collapsed; return mergeDrawableStates(states, extraStates); } /** * Sets whether the {@link AppBarLayout} is liftable or not. * * @return true if the liftable state changed */ public boolean setLiftable(boolean liftable) { this.liftableOverride = true; return setLiftableState(liftable); } // Internal helper method that updates liftable state without enabling the override. private boolean setLiftableState(boolean liftable) { if (this.liftable != liftable) { this.liftable = liftable; refreshDrawableState(); return true; } return false; } /** * Sets whether the {@link AppBarLayout} is in a lifted state or not. * * @return true if the lifted state changed */ public boolean setLifted(boolean lifted) { return setLiftedState(lifted); } // Internal helper method that updates lifted state. boolean setLiftedState(boolean lifted) { if (this.lifted != lifted) { this.lifted = lifted; refreshDrawableState(); if (liftOnScroll && getBackground() instanceof MaterialShapeDrawable) { startLiftOnScrollElevationOverlayAnimation((MaterialShapeDrawable) getBackground(), lifted); } return true; } return false; } private void startLiftOnScrollElevationOverlayAnimation( final MaterialShapeDrawable background, boolean lifted) { float appBarElevation = getResources().getDimension(R.dimen.design_appbar_elevation); float fromElevation = lifted ? 0 : appBarElevation; float toElevation = lifted ? appBarElevation : 0; if (elevationOverlayAnimator != null) { elevationOverlayAnimator.cancel(); } elevationOverlayAnimator = ValueAnimator.ofFloat(fromElevation, toElevation); elevationOverlayAnimator.setDuration( getResources().getInteger(R.integer.app_bar_elevation_anim_duration)); elevationOverlayAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); elevationOverlayAnimator.addUpdateListener( valueAnimator -> background.setElevation((float) valueAnimator.getAnimatedValue())); elevationOverlayAnimator.start(); } /** * Sets whether the {@link AppBarLayout} lifts on scroll or not. * *
If set to true, the {@link AppBarLayout} will animate to the lifted, or elevated, state when
* content is scrolled beneath it. Requires
* `app:layout_behavior="@string/appbar_scrolling_view_behavior` to be set on the scrolling
* sibling (e.g., `NestedScrollView`, `RecyclerView`, etc.).
*/
public void setLiftOnScroll(boolean liftOnScroll) {
this.liftOnScroll = liftOnScroll;
}
/** Returns whether the {@link AppBarLayout} lifts on scroll or not. */
public boolean isLiftOnScroll() {
return liftOnScroll;
}
/**
* Sets the id of the view that the {@link AppBarLayout} should use to determine whether it should
* be lifted.
*/
public void setLiftOnScrollTargetViewId(@IdRes int liftOnScrollTargetViewId) {
this.liftOnScrollTargetViewId = liftOnScrollTargetViewId;
// Invalidate cached target view so it will be looked up on next scroll.
clearLiftOnScrollTargetView();
}
/**
* Returns the id of the view that the {@link AppBarLayout} should use to determine whether it
* should be lifted.
*/
@IdRes
public int getLiftOnScrollTargetViewId() {
return liftOnScrollTargetViewId;
}
boolean shouldLift(@Nullable View defaultScrollingView) {
View scrollingView = findLiftOnScrollTargetView(defaultScrollingView);
if (scrollingView == null) {
scrollingView = defaultScrollingView;
}
return scrollingView != null
&& (scrollingView.canScrollVertically(-1) || scrollingView.getScrollY() > 0);
}
@Nullable
private View findLiftOnScrollTargetView(@Nullable View defaultScrollingView) {
if (liftOnScrollTargetView == null && liftOnScrollTargetViewId != View.NO_ID) {
View targetView = null;
if (defaultScrollingView != null) {
targetView = defaultScrollingView.findViewById(liftOnScrollTargetViewId);
}
if (targetView == null && getParent() instanceof ViewGroup) {
// Assumes the scrolling view is a child of the AppBarLayout's parent,
// which should be true due to the CoordinatorLayout pattern.
targetView = ((ViewGroup) getParent()).findViewById(liftOnScrollTargetViewId);
}
if (targetView != null) {
liftOnScrollTargetView = new WeakReference<>(targetView);
}
}
return liftOnScrollTargetView != null ? liftOnScrollTargetView.get() : null;
}
private void clearLiftOnScrollTargetView() {
if (liftOnScrollTargetView != null) {
liftOnScrollTargetView.clear();
}
liftOnScrollTargetView = null;
}
/**
* @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;
}
/**
* Whether the first child needs to be offset because it does not want to handle the top window
* inset
*/
private boolean shouldOffsetFirstChild() {
if (getChildCount() > 0) {
final View firstChild = getChildAt(0);
return firstChild.getVisibility() != GONE && !ViewCompat.getFitsSystemWindows(firstChild);
}
return false;
}
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 trigger a layout...
if (!ObjectsCompat.equals(lastInsets, newInsets)) {
lastInsets = newInsets;
requestLayout();
}
return insets;
}
/** A {@link ViewGroup.LayoutParams} implementation for {@link AppBarLayout}. */
public static class LayoutParams extends LinearLayout.LayoutParams {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef(
flag = true,
value = {
SCROLL_FLAG_NO_SCROLL,
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 {}
/**
* Disable scrolling on the view. This flag should not be combined with any of the other scroll
* flags.
*/
public static final int SCROLL_FLAG_NO_SCROLL = 0x0;
/**
* 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 = 1 << 1;
/**
* 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 = 1 << 2;
/**
* 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 = 1 << 3;
/**
* Upon a scroll ending, if the view is only partially visible then it will be snapped and
* scrolled to its closest edge. For example, if the view only has its bottom 25% displayed, it
* will be scrolled off screen completely. Conversely, if its bottom 75% is visible then it will
* be scrolled fully into view.
*/
public static final int SCROLL_FLAG_SNAP = 1 << 4;
/**
* 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 = 1 << 5;
/** 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}. Otherwise, use {@link #SCROLL_FLAG_NO_SCROLL} to disable
* scrolling.
* @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