mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-20 20:12:52 +08:00
This is a first step in reversing the dependency flow around the `theme` package in MDC. In a future commit, I'll make theme contain all the various MaterialComponents theme definitions, and it will thus depend on the various packages containing our components (widget, button, toggle, etc.). The theme package will be empty as of this commit, but since I plan to use it in the next one I've left the build infrastructure in place. PiperOrigin-RevId: 180737806
1533 lines
56 KiB
Java
1533 lines
56 KiB
Java
/*
|
|
* 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 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 android.support.design.animation.AnimationUtils;
|
|
import android.support.design.internal.ThemeEnforcement;
|
|
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.
|
|
*
|
|
* <p>Children should provide their desired scrolling behavior through {@link
|
|
* LayoutParams#setScrollFlags(int)} and the associated layout xml attribute: {@code
|
|
* app:layout_scrollFlags}.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <pre>
|
|
* <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>
|
|
* </pre>
|
|
*
|
|
* @see <a href="http://www.google.com/design/spec/layout/structure.html#structure-app-bar">
|
|
* http://www.google.com/design/spec/layout/structure.html#structure-app-bar</a>
|
|
*/
|
|
@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.
|
|
*/
|
|
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 totalScrollRange = INVALID_SCROLL_RANGE;
|
|
private int downPreScrollRange = INVALID_SCROLL_RANGE;
|
|
private int downScrollRange = INVALID_SCROLL_RANGE;
|
|
|
|
private boolean haveChildWithInterpolator;
|
|
|
|
private int pendingAction = PENDING_ACTION_NONE;
|
|
|
|
private WindowInsetsCompat lastInsets;
|
|
|
|
private List<OnOffsetChangedListener> listeners;
|
|
|
|
private boolean collapsible;
|
|
private boolean collapsed;
|
|
|
|
private int[] tmpStatesArray;
|
|
|
|
public AppBarLayout(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public AppBarLayout(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
setOrientation(VERTICAL);
|
|
|
|
ThemeEnforcement.checkAppCompatTheme(context);
|
|
|
|
if (Build.VERSION.SDK_INT >= 21) {
|
|
// Use the bounds view outline provider so that we cast a shadow, even without a
|
|
// background
|
|
ViewUtilsLollipop.setBoundsViewOutlineProvider(this);
|
|
|
|
// If we're running on API 21+, we should reset any state list animator from our
|
|
// default style
|
|
ViewUtilsLollipop.setStateListAnimatorFromAttrs(
|
|
this, attrs, 0, R.style.Widget_Design_AppBarLayout);
|
|
}
|
|
|
|
final TypedArray a =
|
|
context.obtainStyledAttributes(
|
|
attrs, R.styleable.AppBarLayout, 0, R.style.Widget_Design_AppBarLayout);
|
|
ViewCompat.setBackground(this, a.getDrawable(R.styleable.AppBarLayout_android_background));
|
|
if (a.hasValue(R.styleable.AppBarLayout_expanded)) {
|
|
setExpanded(
|
|
a.getBoolean(R.styleable.AppBarLayout_expanded, false),
|
|
false, /* animate */
|
|
false /* force */);
|
|
}
|
|
if (Build.VERSION.SDK_INT >= 21 && a.hasValue(R.styleable.AppBarLayout_elevation)) {
|
|
ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(
|
|
this, a.getDimensionPixelSize(R.styleable.AppBarLayout_elevation, 0));
|
|
}
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
// In O+, we have these values set in the style. Since there is no defStyleAttr for
|
|
// AppBarLayout at the AppCompat level, check for these attributes here.
|
|
if (a.hasValue(R.styleable.AppBarLayout_android_keyboardNavigationCluster)) {
|
|
this.setKeyboardNavigationCluster(
|
|
a.getBoolean(R.styleable.AppBarLayout_android_keyboardNavigationCluster, false));
|
|
}
|
|
if (a.hasValue(R.styleable.AppBarLayout_android_touchscreenBlocksFocus)) {
|
|
this.setTouchscreenBlocksFocus(
|
|
a.getBoolean(R.styleable.AppBarLayout_android_touchscreenBlocksFocus, false));
|
|
}
|
|
}
|
|
a.recycle();
|
|
|
|
ViewCompat.setOnApplyWindowInsetsListener(
|
|
this,
|
|
new android.support.v4.view.OnApplyWindowInsetsListener() {
|
|
@Override
|
|
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
|
|
return onWindowInsetChanged(insets);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a listener that will be called when the offset of this {@link AppBarLayout} changes.
|
|
*
|
|
* @param listener The listener that will be called when the offset changes.]
|
|
* @see #removeOnOffsetChangedListener(OnOffsetChangedListener)
|
|
*/
|
|
public void addOnOffsetChangedListener(OnOffsetChangedListener listener) {
|
|
if (listeners == null) {
|
|
listeners = new ArrayList<>();
|
|
}
|
|
if (listener != null && !listeners.contains(listener)) {
|
|
listeners.add(listener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the previously added {@link OnOffsetChangedListener}.
|
|
*
|
|
* @param listener the listener to remove.
|
|
*/
|
|
public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) {
|
|
if (listeners != null && listener != null) {
|
|
listeners.remove(listener);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
invalidateScrollRanges();
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
super.onLayout(changed, l, t, r, b);
|
|
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;
|
|
}
|
|
}
|
|
|
|
updateCollapsible();
|
|
}
|
|
|
|
private void updateCollapsible() {
|
|
boolean haveCollapsibleChild = false;
|
|
for (int i = 0, z = getChildCount(); i < z; i++) {
|
|
if (((LayoutParams) getChildAt(i).getLayoutParams()).isCollapsible()) {
|
|
haveCollapsibleChild = true;
|
|
break;
|
|
}
|
|
}
|
|
setCollapsibleState(haveCollapsibleChild);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Sets whether this {@link AppBarLayout} is expanded or not, animating if it has already been
|
|
* laid out.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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) {
|
|
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 OnOffsetChangedListener listener = listeners.get(i);
|
|
if (listener != null) {
|
|
listener.onOffsetChanged(this, offset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 android.support.design.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;
|
|
}
|
|
|
|
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
|
|
}
|
|
)
|
|
@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;
|
|
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} and {@link #SCROLL_FLAG_SNAP }.
|
|
* @see #getScrollFlags()
|
|
* @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags
|
|
*/
|
|
public void setScrollFlags(@ScrollFlags int flags) {
|
|
scrollFlags = flags;
|
|
}
|
|
|
|
/**
|
|
* Returns the scrolling flags.
|
|
*
|
|
* @see #setScrollFlags(int)
|
|
* @attr ref android.support.design.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 android.support.design.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 android.support.design.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.
|
|
*/
|
|
public static class Behavior extends HeaderBehavior<AppBarLayout> {
|
|
private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
|
|
private static final int INVALID_POSITION = -1;
|
|
|
|
/** Callback to allow control over any {@link AppBarLayout} dragging. */
|
|
public abstract static class DragCallback {
|
|
/**
|
|
* Allows control over whether the given {@link AppBarLayout} can be dragged or not.
|
|
*
|
|
* <p>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 AppBarLayout appBarLayout);
|
|
}
|
|
|
|
private int offsetDelta;
|
|
|
|
@NestedScrollType
|
|
private int lastStartedType;
|
|
|
|
private ValueAnimator offsetAnimator;
|
|
|
|
private int offsetToChildIndexOnLayout = INVALID_POSITION;
|
|
private boolean offsetToChildIndexOnLayoutIsMinHeight;
|
|
private float offsetToChildIndexOnLayoutPerc;
|
|
|
|
private WeakReference<View> lastNestedScrollingChildRef;
|
|
private DragCallback onDragCallback;
|
|
|
|
public Behavior() {}
|
|
|
|
public Behavior(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
}
|
|
|
|
@Override
|
|
public boolean onStartNestedScroll(
|
|
CoordinatorLayout parent,
|
|
AppBarLayout child,
|
|
View directTargetChild,
|
|
View target,
|
|
int nestedScrollAxes,
|
|
int type) {
|
|
// 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()
|
|
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
|
|
|
|
if (started && offsetAnimator != null) {
|
|
// Cancel any offset animation
|
|
offsetAnimator.cancel();
|
|
}
|
|
|
|
// A new nested scroll has started so clear out the previous ref
|
|
lastNestedScrollingChildRef = null;
|
|
|
|
// Track the last started type so we know if a fling is about to happen once scrolling ends
|
|
lastStartedType = type;
|
|
|
|
return started;
|
|
}
|
|
|
|
@Override
|
|
public void onNestedPreScroll(
|
|
CoordinatorLayout coordinatorLayout,
|
|
AppBarLayout child,
|
|
View target,
|
|
int dx,
|
|
int dy,
|
|
int[] consumed,
|
|
int type) {
|
|
if (dy != 0) {
|
|
int min;
|
|
int 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;
|
|
}
|
|
if (min != max) {
|
|
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
|
|
stopNestedScrollIfNeeded(dy, child, target, type);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onNestedScroll(
|
|
CoordinatorLayout coordinatorLayout,
|
|
AppBarLayout child,
|
|
View target,
|
|
int dxConsumed,
|
|
int dyConsumed,
|
|
int dxUnconsumed,
|
|
int dyUnconsumed,
|
|
int type) {
|
|
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);
|
|
stopNestedScrollIfNeeded(dyUnconsumed, child, target, type);
|
|
}
|
|
}
|
|
|
|
private void stopNestedScrollIfNeeded(int dy, AppBarLayout child, View target, int type) {
|
|
if (type == ViewCompat.TYPE_NON_TOUCH) {
|
|
final int curOffset = getTopBottomOffsetForScrollingSibling();
|
|
if ((dy < 0 && curOffset == 0)
|
|
|| (dy > 0 && curOffset == -child.getDownNestedScrollRange())) {
|
|
ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStopNestedScroll(
|
|
CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
|
|
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
|
|
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
|
|
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
|
|
// (ViewCompat.TYPE_TOUCH) ends
|
|
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
|
// If we haven't been flung, or a fling is ending
|
|
snapToChildIfNeeded(coordinatorLayout, abl);
|
|
}
|
|
|
|
// Keep a reference to the previous nested scrolling child
|
|
lastNestedScrollingChildRef = new WeakReference<>(target);
|
|
}
|
|
|
|
/**
|
|
* Set a callback to control any {@link AppBarLayout} dragging.
|
|
*
|
|
* @param callback the callback to use, or {@code null} to use the default behavior.
|
|
*/
|
|
public void setDragCallback(@Nullable DragCallback callback) {
|
|
onDragCallback = callback;
|
|
}
|
|
|
|
private void animateOffsetTo(
|
|
final CoordinatorLayout coordinatorLayout,
|
|
final AppBarLayout child,
|
|
final int offset,
|
|
float velocity) {
|
|
final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
|
|
|
|
final int duration;
|
|
velocity = Math.abs(velocity);
|
|
if (velocity > 0) {
|
|
duration = 3 * Math.round(1000 * (distance / velocity));
|
|
} else {
|
|
final float distanceRatio = (float) distance / child.getHeight();
|
|
duration = (int) ((distanceRatio + 1) * 150);
|
|
}
|
|
|
|
animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
|
|
}
|
|
|
|
private void animateOffsetWithDuration(
|
|
final CoordinatorLayout coordinatorLayout,
|
|
final AppBarLayout child,
|
|
final int offset,
|
|
final int duration) {
|
|
final int currentOffset = getTopBottomOffsetForScrollingSibling();
|
|
if (currentOffset == offset) {
|
|
if (offsetAnimator != null && offsetAnimator.isRunning()) {
|
|
offsetAnimator.cancel();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (offsetAnimator == null) {
|
|
offsetAnimator = new ValueAnimator();
|
|
offsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
|
|
offsetAnimator.addUpdateListener(
|
|
new ValueAnimator.AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animator) {
|
|
setHeaderTopBottomOffset(
|
|
coordinatorLayout, child, (int) animator.getAnimatedValue());
|
|
}
|
|
});
|
|
} else {
|
|
offsetAnimator.cancel();
|
|
}
|
|
|
|
offsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
|
|
offsetAnimator.setIntValues(currentOffset, offset);
|
|
offsetAnimator.start();
|
|
}
|
|
|
|
private int getChildIndexOnOffset(AppBarLayout abl, final int offset) {
|
|
for (int i = 0, count = abl.getChildCount(); i < count; i++) {
|
|
View child = abl.getChildAt(i);
|
|
if (child.getTop() <= -offset && child.getBottom() >= -offset) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private void snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl) {
|
|
final int offset = getTopBottomOffsetForScrollingSibling();
|
|
final int offsetChildIndex = getChildIndexOnOffset(abl, offset);
|
|
if (offsetChildIndex >= 0) {
|
|
final View offsetChild = abl.getChildAt(offsetChildIndex);
|
|
final LayoutParams lp = (LayoutParams) offsetChild.getLayoutParams();
|
|
final int flags = lp.getScrollFlags();
|
|
|
|
if ((flags & LayoutParams.FLAG_SNAP) == LayoutParams.FLAG_SNAP) {
|
|
// We're set the snap, so animate the offset to the nearest edge
|
|
int snapTop = -offsetChild.getTop();
|
|
int snapBottom = -offsetChild.getBottom();
|
|
|
|
if (offsetChildIndex == abl.getChildCount() - 1) {
|
|
// If this is the last child, we need to take the top inset into account
|
|
snapBottom += abl.getTopInset();
|
|
}
|
|
|
|
if (checkFlag(flags, LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED)) {
|
|
// If the view is set only exit until it is collapsed, we'll abide by that
|
|
snapBottom += ViewCompat.getMinimumHeight(offsetChild);
|
|
} else if (checkFlag(
|
|
flags, LayoutParams.FLAG_QUICK_RETURN | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS)) {
|
|
// If it's set to always enter collapsed, it actually has two states. We
|
|
// select the state and then snap within the state
|
|
final int seam = snapBottom + ViewCompat.getMinimumHeight(offsetChild);
|
|
if (offset < seam) {
|
|
snapTop = seam;
|
|
} else {
|
|
snapBottom = seam;
|
|
}
|
|
}
|
|
|
|
final int newOffset = offset < (snapBottom + snapTop) / 2 ? snapBottom : snapTop;
|
|
animateOffsetTo(
|
|
coordinatorLayout,
|
|
abl,
|
|
MathUtils.constrain(newOffset, -abl.getTotalScrollRange(), 0),
|
|
0);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean checkFlag(final int flags, final int check) {
|
|
return (flags & check) == check;
|
|
}
|
|
|
|
@Override
|
|
public boolean onMeasureChild(
|
|
CoordinatorLayout parent,
|
|
AppBarLayout child,
|
|
int parentWidthMeasureSpec,
|
|
int widthUsed,
|
|
int parentHeightMeasureSpec,
|
|
int heightUsed) {
|
|
final CoordinatorLayout.LayoutParams lp =
|
|
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
|
|
if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
|
|
// If the view is set to wrap on it's height, CoordinatorLayout by default will
|
|
// cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
|
|
// what we actually want, so we measure it ourselves with an unspecified spec to
|
|
// allow the child to be larger than it's parent
|
|
parent.onMeasureChild(
|
|
child,
|
|
parentWidthMeasureSpec,
|
|
widthUsed,
|
|
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
|
|
heightUsed);
|
|
return true;
|
|
}
|
|
|
|
// Let the parent handle it as normal
|
|
return super.onMeasureChild(
|
|
parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
|
|
}
|
|
|
|
@Override
|
|
public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
|
|
boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
|
|
|
|
// The priority for actions here is (first which is true wins):
|
|
// 1. forced pending actions
|
|
// 2. offsets for restorations
|
|
// 3. non-forced pending actions
|
|
final int pendingAction = abl.getPendingAction();
|
|
if (offsetToChildIndexOnLayout >= 0 && (pendingAction & PENDING_ACTION_FORCE) == 0) {
|
|
View child = abl.getChildAt(offsetToChildIndexOnLayout);
|
|
int offset = -child.getBottom();
|
|
if (offsetToChildIndexOnLayoutIsMinHeight) {
|
|
offset += ViewCompat.getMinimumHeight(child) + abl.getTopInset();
|
|
} else {
|
|
offset += Math.round(child.getHeight() * offsetToChildIndexOnLayoutPerc);
|
|
}
|
|
setHeaderTopBottomOffset(parent, abl, offset);
|
|
} else if (pendingAction != PENDING_ACTION_NONE) {
|
|
final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0;
|
|
if ((pendingAction & PENDING_ACTION_COLLAPSED) != 0) {
|
|
final int offset = -abl.getUpNestedPreScrollRange();
|
|
if (animate) {
|
|
animateOffsetTo(parent, abl, offset, 0);
|
|
} else {
|
|
setHeaderTopBottomOffset(parent, abl, offset);
|
|
}
|
|
} else if ((pendingAction & PENDING_ACTION_EXPANDED) != 0) {
|
|
if (animate) {
|
|
animateOffsetTo(parent, abl, 0, 0);
|
|
} else {
|
|
setHeaderTopBottomOffset(parent, abl, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally reset any pending states
|
|
abl.resetPendingAction();
|
|
offsetToChildIndexOnLayout = INVALID_POSITION;
|
|
|
|
// We may have changed size, so let's constrain the top and bottom offset correctly,
|
|
// just in case we're out of the bounds
|
|
setTopAndBottomOffset(
|
|
MathUtils.constrain(getTopAndBottomOffset(), -abl.getTotalScrollRange(), 0));
|
|
|
|
// Update the AppBarLayout's drawable state for any elevation changes. This is needed so that
|
|
// the elevation is set in the first layout, so that we don't get a visual jump pre-N (due to
|
|
// the draw dispatch skip)
|
|
updateAppBarLayoutDrawableState(
|
|
parent, abl, getTopAndBottomOffset(), 0 /* direction */, true /* forceJump */);
|
|
|
|
// Make sure we dispatch the offset update
|
|
abl.dispatchOffsetUpdates(getTopAndBottomOffset());
|
|
|
|
return handled;
|
|
}
|
|
|
|
@Override
|
|
boolean canDragView(AppBarLayout view) {
|
|
if (onDragCallback != null) {
|
|
// If there is a drag callback set, it's in control
|
|
return onDragCallback.canDrag(view);
|
|
}
|
|
|
|
// Else we'll use the default behaviour of seeing if it can scroll down
|
|
if (lastNestedScrollingChildRef != null) {
|
|
// If we have a reference to a scrolling view, check it
|
|
final View scrollingView = lastNestedScrollingChildRef.get();
|
|
return scrollingView != null
|
|
&& scrollingView.isShown()
|
|
&& !scrollingView.canScrollVertically(-1);
|
|
} else {
|
|
// Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) {
|
|
// At the end of a manual fling, check to see if we need to snap to the edge-child
|
|
snapToChildIfNeeded(parent, layout);
|
|
}
|
|
|
|
@Override
|
|
int getMaxDragOffset(AppBarLayout view) {
|
|
return -view.getDownNestedScrollRange();
|
|
}
|
|
|
|
@Override
|
|
int getScrollRangeForDragFling(AppBarLayout view) {
|
|
return view.getTotalScrollRange();
|
|
}
|
|
|
|
@Override
|
|
int setHeaderTopBottomOffset(
|
|
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;
|
|
|
|
final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
|
|
|
|
// Update how much dy we have consumed
|
|
consumed = curOffset - newOffset;
|
|
// Update the stored sibling offset
|
|
offsetDelta = 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
|
|
appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());
|
|
|
|
// Update the AppBarLayout's drawable state (for any elevation changes)
|
|
updateAppBarLayoutDrawableState(
|
|
coordinatorLayout,
|
|
appBarLayout,
|
|
newOffset,
|
|
newOffset < curOffset ? -1 : 1,
|
|
false /* forceJump */);
|
|
}
|
|
} else {
|
|
// Reset the offset delta
|
|
offsetDelta = 0;
|
|
}
|
|
|
|
return consumed;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
boolean isOffsetAnimatorRunning() {
|
|
return offsetAnimator != null && offsetAnimator.isRunning();
|
|
}
|
|
|
|
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 plus margin
|
|
childScrollableHeight += child.getHeight() + childLp.topMargin + childLp.bottomMargin;
|
|
|
|
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 (ViewCompat.getFitsSystemWindows(child)) {
|
|
childScrollableHeight -= layout.getTopInset();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private void updateAppBarLayoutDrawableState(
|
|
final CoordinatorLayout parent,
|
|
final AppBarLayout layout,
|
|
final int offset,
|
|
final int direction,
|
|
final boolean forceJump) {
|
|
final View child = getAppBarChildOnOffset(layout, offset);
|
|
if (child != null) {
|
|
final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
|
|
final int flags = childLp.getScrollFlags();
|
|
boolean collapsed = false;
|
|
|
|
if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
|
|
final int minHeight = ViewCompat.getMinimumHeight(child);
|
|
|
|
if (direction > 0
|
|
&& (flags
|
|
& (LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
|
|
| LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED))
|
|
!= 0) {
|
|
// We're set to enter always collapsed so we are only collapsed when
|
|
// being scrolled down, and in a collapsed offset
|
|
collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
|
|
} else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
|
|
// We're set to exit until collapsed, so any offset which results in
|
|
// the minimum height (or less) being shown is collapsed
|
|
collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
|
|
}
|
|
}
|
|
|
|
final boolean changed = layout.setCollapsedState(collapsed);
|
|
|
|
if (Build.VERSION.SDK_INT >= 11
|
|
&& (forceJump || (changed && shouldJumpElevationState(parent, layout)))) {
|
|
// If the collapsed state changed, we may need to
|
|
// jump to the current state if we have an overlapping view
|
|
layout.jumpDrawablesToCurrentState();
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean shouldJumpElevationState(CoordinatorLayout parent, AppBarLayout layout) {
|
|
// We should jump the elevated state if we have a dependent scrolling view which has
|
|
// an overlapping top (i.e. overlaps us)
|
|
final List<View> dependencies = parent.getDependents(layout);
|
|
for (int i = 0, size = dependencies.size(); i < size; i++) {
|
|
final View dependency = dependencies.get(i);
|
|
final CoordinatorLayout.LayoutParams lp =
|
|
(CoordinatorLayout.LayoutParams) dependency.getLayoutParams();
|
|
final CoordinatorLayout.Behavior behavior = lp.getBehavior();
|
|
|
|
if (behavior instanceof ScrollingViewBehavior) {
|
|
return ((ScrollingViewBehavior) behavior).getOverlayTop() != 0;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static View getAppBarChildOnOffset(final 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);
|
|
if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
|
|
return child;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
int getTopBottomOffsetForScrollingSibling() {
|
|
return getTopAndBottomOffset() + offsetDelta;
|
|
}
|
|
|
|
@Override
|
|
public Parcelable onSaveInstanceState(CoordinatorLayout parent, AppBarLayout abl) {
|
|
final Parcelable superState = super.onSaveInstanceState(parent, abl);
|
|
final int offset = getTopAndBottomOffset();
|
|
|
|
// Try and find the first visible child...
|
|
for (int i = 0, count = abl.getChildCount(); i < count; i++) {
|
|
View child = abl.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.firstVisibleChildAtMinimumHeight =
|
|
visBottom == (ViewCompat.getMinimumHeight(child) + abl.getTopInset());
|
|
ss.firstVisibleChildPercentageShown = 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());
|
|
offsetToChildIndexOnLayout = ss.firstVisibleChildIndex;
|
|
offsetToChildIndexOnLayoutPerc = ss.firstVisibleChildPercentageShown;
|
|
offsetToChildIndexOnLayoutIsMinHeight = ss.firstVisibleChildAtMinimumHeight;
|
|
} else {
|
|
super.onRestoreInstanceState(parent, appBarLayout, state);
|
|
offsetToChildIndexOnLayout = INVALID_POSITION;
|
|
}
|
|
}
|
|
|
|
protected static class SavedState extends AbsSavedState {
|
|
int firstVisibleChildIndex;
|
|
float firstVisibleChildPercentageShown;
|
|
boolean firstVisibleChildAtMinimumHeight;
|
|
|
|
public SavedState(Parcel source, ClassLoader loader) {
|
|
super(source, loader);
|
|
firstVisibleChildIndex = source.readInt();
|
|
firstVisibleChildPercentageShown = source.readFloat();
|
|
firstVisibleChildAtMinimumHeight = 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(firstVisibleChildPercentageShown);
|
|
dest.writeByte((byte) (firstVisibleChildAtMinimumHeight ? 1 : 0));
|
|
}
|
|
|
|
public static final Creator<SavedState> CREATOR =
|
|
new ClassLoaderCreator<SavedState>() {
|
|
@Override
|
|
public SavedState createFromParcel(Parcel source, ClassLoader loader) {
|
|
return new SavedState(source, loader);
|
|
}
|
|
|
|
@Override
|
|
public SavedState createFromParcel(Parcel source) {
|
|
return new SavedState(source, null);
|
|
}
|
|
|
|
@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 HeaderScrollingViewBehavior {
|
|
|
|
public ScrollingViewBehavior() {}
|
|
|
|
public ScrollingViewBehavior(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
|
|
final TypedArray a =
|
|
context.obtainStyledAttributes(attrs, R.styleable.ScrollingViewBehavior_Layout);
|
|
setOverlayTop(
|
|
a.getDimensionPixelSize(R.styleable.ScrollingViewBehavior_Layout_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 onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
|
|
offsetChildAsNeeded(child, dependency);
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onRequestChildRectangleOnScreen(
|
|
CoordinatorLayout parent, View child, Rect rectangle, boolean immediate) {
|
|
final AppBarLayout header = findFirstDependency(parent.getDependencies(child));
|
|
if (header != null) {
|
|
// Offset the rect by the child's left/top
|
|
rectangle.offset(child.getLeft(), child.getTop());
|
|
|
|
final Rect parentRect = tempRect1;
|
|
parentRect.set(0, 0, parent.getWidth(), parent.getHeight());
|
|
|
|
if (!parentRect.contains(rectangle)) {
|
|
// If the rectangle can not be fully seen the visible bounds, collapse
|
|
// the AppBarLayout
|
|
header.setExpanded(false, !immediate);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void offsetChildAsNeeded(View child, View dependency) {
|
|
final CoordinatorLayout.Behavior behavior =
|
|
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
|
|
if (behavior instanceof Behavior) {
|
|
// Offset the child, pinning it to the bottom the header-dependency, maintaining
|
|
// any vertical gap and overlap
|
|
final Behavior ablBehavior = (Behavior) behavior;
|
|
ViewCompat.offsetTopAndBottom(
|
|
child,
|
|
(dependency.getBottom() - child.getTop())
|
|
+ ablBehavior.offsetDelta
|
|
+ getVerticalLayoutGap()
|
|
- getOverlapPixelsForOffset(dependency));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
float getOverlapRatioForOffset(final View header) {
|
|
if (header instanceof AppBarLayout) {
|
|
final AppBarLayout abl = (AppBarLayout) header;
|
|
final int totalScrollRange = abl.getTotalScrollRange();
|
|
final int preScrollDown = abl.getDownNestedPreScrollRange();
|
|
final int offset = getAppBarLayoutOffset(abl);
|
|
|
|
if (preScrollDown != 0 && (totalScrollRange + offset) <= preScrollDown) {
|
|
// If we're in a pre-scroll down. Don't use the offset at all.
|
|
return 0;
|
|
} else {
|
|
final int availScrollRange = totalScrollRange - preScrollDown;
|
|
if (availScrollRange != 0) {
|
|
// Else we'll use a interpolated ratio of the overlap, depending on offset
|
|
return 1f + (offset / (float) availScrollRange);
|
|
}
|
|
}
|
|
}
|
|
return 0f;
|
|
}
|
|
|
|
private static int getAppBarLayoutOffset(AppBarLayout abl) {
|
|
final CoordinatorLayout.Behavior behavior =
|
|
((CoordinatorLayout.LayoutParams) abl.getLayoutParams()).getBehavior();
|
|
if (behavior instanceof Behavior) {
|
|
return ((Behavior) behavior).getTopBottomOffsetForScrollingSibling();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
AppBarLayout findFirstDependency(List<View> 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;
|
|
}
|
|
|
|
@Override
|
|
int getScrollRange(View v) {
|
|
if (v instanceof AppBarLayout) {
|
|
return ((AppBarLayout) v).getTotalScrollRange();
|
|
} else {
|
|
return super.getScrollRange(v);
|
|
}
|
|
}
|
|
}
|
|
}
|