• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.design.widget;
18 
19 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.Rect;
25 import android.os.Build;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.support.annotation.IntDef;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.Nullable;
31 import android.support.annotation.RequiresApi;
32 import android.support.annotation.RestrictTo;
33 import android.support.annotation.VisibleForTesting;
34 import android.support.design.R;
35 import android.support.v4.math.MathUtils;
36 import android.support.v4.util.ObjectsCompat;
37 import android.support.v4.view.AbsSavedState;
38 import android.support.v4.view.ViewCompat;
39 import android.support.v4.view.WindowInsetsCompat;
40 import android.util.AttributeSet;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.animation.Interpolator;
44 import android.widget.LinearLayout;
45 
46 import java.lang.annotation.Retention;
47 import java.lang.annotation.RetentionPolicy;
48 import java.lang.ref.WeakReference;
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 /**
53  * AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of
54  * material designs app bar concept, namely scrolling gestures.
55  * <p>
56  * Children should provide their desired scrolling behavior through
57  * {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute:
58  * {@code app:layout_scrollFlags}.
59  *
60  * <p>
61  * This view depends heavily on being used as a direct child within a {@link CoordinatorLayout}.
62  * If you use AppBarLayout within a different {@link ViewGroup}, most of it's functionality will
63  * not work.
64  * <p>
65  * AppBarLayout also requires a separate scrolling sibling in order to know when to scroll.
66  * The binding is done through the {@link ScrollingViewBehavior} behavior class, meaning that you
67  * should set your scrolling view's behavior to be an instance of {@link ScrollingViewBehavior}.
68  * A string resource containing the full class name is available.
69  *
70  * <pre>
71  * &lt;android.support.design.widget.CoordinatorLayout
72  *         xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
73  *         xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
74  *         android:layout_width=&quot;match_parent&quot;
75  *         android:layout_height=&quot;match_parent&quot;&gt;
76  *
77  *     &lt;android.support.v4.widget.NestedScrollView
78  *             android:layout_width=&quot;match_parent&quot;
79  *             android:layout_height=&quot;match_parent&quot;
80  *             app:layout_behavior=&quot;@string/appbar_scrolling_view_behavior&quot;&gt;
81  *
82  *         &lt;!-- Your scrolling content --&gt;
83  *
84  *     &lt;/android.support.v4.widget.NestedScrollView&gt;
85  *
86  *     &lt;android.support.design.widget.AppBarLayout
87  *             android:layout_height=&quot;wrap_content&quot;
88  *             android:layout_width=&quot;match_parent&quot;&gt;
89  *
90  *         &lt;android.support.v7.widget.Toolbar
91  *                 ...
92  *                 app:layout_scrollFlags=&quot;scroll|enterAlways&quot;/&gt;
93  *
94  *         &lt;android.support.design.widget.TabLayout
95  *                 ...
96  *                 app:layout_scrollFlags=&quot;scroll|enterAlways&quot;/&gt;
97  *
98  *     &lt;/android.support.design.widget.AppBarLayout&gt;
99  *
100  * &lt;/android.support.design.widget.CoordinatorLayout&gt;
101  * </pre>
102  *
103  * @see <a href="http://www.google.com/design/spec/layout/structure.html#structure-app-bar">
104  *     http://www.google.com/design/spec/layout/structure.html#structure-app-bar</a>
105  */
106 @CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
107 public class AppBarLayout extends LinearLayout {
108 
109     static final int PENDING_ACTION_NONE = 0x0;
110     static final int PENDING_ACTION_EXPANDED = 0x1;
111     static final int PENDING_ACTION_COLLAPSED = 0x2;
112     static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4;
113     static final int PENDING_ACTION_FORCE = 0x8;
114 
115     /**
116      * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
117      * offset changes.
118      */
119     public interface OnOffsetChangedListener {
120         /**
121          * Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
122          * child views to implement custom behavior based on the offset (for instance pinning a
123          * view at a certain y value).
124          *
125          * @param appBarLayout the {@link AppBarLayout} which offset has changed
126          * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
127          */
onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset)128         void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
129     }
130 
131     private static final int INVALID_SCROLL_RANGE = -1;
132 
133     private int mTotalScrollRange = INVALID_SCROLL_RANGE;
134     private int mDownPreScrollRange = INVALID_SCROLL_RANGE;
135     private int mDownScrollRange = INVALID_SCROLL_RANGE;
136 
137     private boolean mHaveChildWithInterpolator;
138 
139     private int mPendingAction = PENDING_ACTION_NONE;
140 
141     private WindowInsetsCompat mLastInsets;
142 
143     private List<OnOffsetChangedListener> mListeners;
144 
145     private boolean mCollapsible;
146     private boolean mCollapsed;
147 
148     private int[] mTmpStatesArray;
149 
AppBarLayout(Context context)150     public AppBarLayout(Context context) {
151         this(context, null);
152     }
153 
AppBarLayout(Context context, AttributeSet attrs)154     public AppBarLayout(Context context, AttributeSet attrs) {
155         super(context, attrs);
156         setOrientation(VERTICAL);
157 
158         ThemeUtils.checkAppCompatTheme(context);
159 
160         if (Build.VERSION.SDK_INT >= 21) {
161             // Use the bounds view outline provider so that we cast a shadow, even without a
162             // background
163             ViewUtilsLollipop.setBoundsViewOutlineProvider(this);
164 
165             // If we're running on API 21+, we should reset any state list animator from our
166             // default style
167             ViewUtilsLollipop.setStateListAnimatorFromAttrs(this, attrs, 0,
168                     R.style.Widget_Design_AppBarLayout);
169         }
170 
171         final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppBarLayout,
172                 0, R.style.Widget_Design_AppBarLayout);
173         ViewCompat.setBackground(this, a.getDrawable(R.styleable.AppBarLayout_android_background));
174         if (a.hasValue(R.styleable.AppBarLayout_expanded)) {
175             setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false), false, false);
176         }
177         if (Build.VERSION.SDK_INT >= 21 && a.hasValue(R.styleable.AppBarLayout_elevation)) {
178             ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(
179                     this, a.getDimensionPixelSize(R.styleable.AppBarLayout_elevation, 0));
180         }
181         if (Build.VERSION.SDK_INT >= 26) {
182             // In O+, we have these values set in the style. Since there is no defStyleAttr for
183             // AppBarLayout at the AppCompat level, check for these attributes here.
184             if (a.hasValue(R.styleable.AppBarLayout_android_keyboardNavigationCluster)) {
185                 this.setKeyboardNavigationCluster(a.getBoolean(
186                         R.styleable.AppBarLayout_android_keyboardNavigationCluster, false));
187             }
188             if (a.hasValue(R.styleable.AppBarLayout_android_touchscreenBlocksFocus)) {
189                 this.setTouchscreenBlocksFocus(a.getBoolean(
190                         R.styleable.AppBarLayout_android_touchscreenBlocksFocus, false));
191             }
192         }
193         a.recycle();
194 
195         ViewCompat.setOnApplyWindowInsetsListener(this,
196                 new android.support.v4.view.OnApplyWindowInsetsListener() {
197                     @Override
198                     public WindowInsetsCompat onApplyWindowInsets(View v,
199                             WindowInsetsCompat insets) {
200                         return onWindowInsetChanged(insets);
201                     }
202                 });
203     }
204 
205     /**
206      * Add a listener that will be called when the offset of this {@link AppBarLayout} changes.
207      *
208      * @param listener The listener that will be called when the offset changes.]
209      *
210      * @see #removeOnOffsetChangedListener(OnOffsetChangedListener)
211      */
addOnOffsetChangedListener(OnOffsetChangedListener listener)212     public void addOnOffsetChangedListener(OnOffsetChangedListener listener) {
213         if (mListeners == null) {
214             mListeners = new ArrayList<>();
215         }
216         if (listener != null && !mListeners.contains(listener)) {
217             mListeners.add(listener);
218         }
219     }
220 
221     /**
222      * Remove the previously added {@link OnOffsetChangedListener}.
223      *
224      * @param listener the listener to remove.
225      */
removeOnOffsetChangedListener(OnOffsetChangedListener listener)226     public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) {
227         if (mListeners != null && listener != null) {
228             mListeners.remove(listener);
229         }
230     }
231 
232     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)233     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
234         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
235         invalidateScrollRanges();
236     }
237 
238     @Override
onLayout(boolean changed, int l, int t, int r, int b)239     protected void onLayout(boolean changed, int l, int t, int r, int b) {
240         super.onLayout(changed, l, t, r, b);
241         invalidateScrollRanges();
242 
243         mHaveChildWithInterpolator = false;
244         for (int i = 0, z = getChildCount(); i < z; i++) {
245             final View child = getChildAt(i);
246             final LayoutParams childLp = (LayoutParams) child.getLayoutParams();
247             final Interpolator interpolator = childLp.getScrollInterpolator();
248 
249             if (interpolator != null) {
250                 mHaveChildWithInterpolator = true;
251                 break;
252             }
253         }
254 
255         updateCollapsible();
256     }
257 
updateCollapsible()258     private void updateCollapsible() {
259         boolean haveCollapsibleChild = false;
260         for (int i = 0, z = getChildCount(); i < z; i++) {
261             if (((LayoutParams) getChildAt(i).getLayoutParams()).isCollapsible()) {
262                 haveCollapsibleChild = true;
263                 break;
264             }
265         }
266         setCollapsibleState(haveCollapsibleChild);
267     }
268 
invalidateScrollRanges()269     private void invalidateScrollRanges() {
270         // Invalidate the scroll ranges
271         mTotalScrollRange = INVALID_SCROLL_RANGE;
272         mDownPreScrollRange = INVALID_SCROLL_RANGE;
273         mDownScrollRange = INVALID_SCROLL_RANGE;
274     }
275 
276     @Override
setOrientation(int orientation)277     public void setOrientation(int orientation) {
278         if (orientation != VERTICAL) {
279             throw new IllegalArgumentException("AppBarLayout is always vertical and does"
280                     + " not support horizontal orientation");
281         }
282         super.setOrientation(orientation);
283     }
284 
285     /**
286      * Sets whether this {@link AppBarLayout} is expanded or not, animating if it has already
287      * been laid out.
288      *
289      * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a
290      * direct child of a {@link CoordinatorLayout}.</p>
291      *
292      * @param expanded true if the layout should be fully expanded, false if it should
293      *                 be fully collapsed
294      *
295      * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
296      */
setExpanded(boolean expanded)297     public void setExpanded(boolean expanded) {
298         setExpanded(expanded, ViewCompat.isLaidOut(this));
299     }
300 
301     /**
302      * Sets whether this {@link AppBarLayout} is expanded or not.
303      *
304      * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a
305      * direct child of a {@link CoordinatorLayout}.</p>
306      *
307      * @param expanded true if the layout should be fully expanded, false if it should
308      *                 be fully collapsed
309      * @param animate Whether to animate to the new state
310      *
311      * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
312      */
setExpanded(boolean expanded, boolean animate)313     public void setExpanded(boolean expanded, boolean animate) {
314         setExpanded(expanded, animate, true);
315     }
316 
setExpanded(boolean expanded, boolean animate, boolean force)317     private void setExpanded(boolean expanded, boolean animate, boolean force) {
318         mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
319                 | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0)
320                 | (force ? PENDING_ACTION_FORCE : 0);
321         requestLayout();
322     }
323 
324     @Override
checkLayoutParams(ViewGroup.LayoutParams p)325     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
326         return p instanceof LayoutParams;
327     }
328 
329     @Override
generateDefaultLayoutParams()330     protected LayoutParams generateDefaultLayoutParams() {
331         return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
332     }
333 
334     @Override
generateLayoutParams(AttributeSet attrs)335     public LayoutParams generateLayoutParams(AttributeSet attrs) {
336         return new LayoutParams(getContext(), attrs);
337     }
338 
339     @Override
generateLayoutParams(ViewGroup.LayoutParams p)340     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
341         if (Build.VERSION.SDK_INT >= 19 && p instanceof LinearLayout.LayoutParams) {
342             return new LayoutParams((LinearLayout.LayoutParams) p);
343         } else if (p instanceof MarginLayoutParams) {
344             return new LayoutParams((MarginLayoutParams) p);
345         }
346         return new LayoutParams(p);
347     }
348 
hasChildWithInterpolator()349     boolean hasChildWithInterpolator() {
350         return mHaveChildWithInterpolator;
351     }
352 
353     /**
354      * Returns the scroll range of all children.
355      *
356      * @return the scroll range in px
357      */
getTotalScrollRange()358     public final int getTotalScrollRange() {
359         if (mTotalScrollRange != INVALID_SCROLL_RANGE) {
360             return mTotalScrollRange;
361         }
362 
363         int range = 0;
364         for (int i = 0, z = getChildCount(); i < z; i++) {
365             final View child = getChildAt(i);
366             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
367             final int childHeight = child.getMeasuredHeight();
368             final int flags = lp.mScrollFlags;
369 
370             if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
371                 // We're set to scroll so add the child's height
372                 range += childHeight + lp.topMargin + lp.bottomMargin;
373 
374                 if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
375                     // For a collapsing scroll, we to take the collapsed height into account.
376                     // We also break straight away since later views can't scroll beneath
377                     // us
378                     range -= ViewCompat.getMinimumHeight(child);
379                     break;
380                 }
381             } else {
382                 // As soon as a view doesn't have the scroll flag, we end the range calculation.
383                 // This is because views below can not scroll under a fixed view.
384                 break;
385             }
386         }
387         return mTotalScrollRange = Math.max(0, range - getTopInset());
388     }
389 
hasScrollableChildren()390     boolean hasScrollableChildren() {
391         return getTotalScrollRange() != 0;
392     }
393 
394     /**
395      * Return the scroll range when scrolling up from a nested pre-scroll.
396      */
getUpNestedPreScrollRange()397     int getUpNestedPreScrollRange() {
398         return getTotalScrollRange();
399     }
400 
401     /**
402      * Return the scroll range when scrolling down from a nested pre-scroll.
403      */
getDownNestedPreScrollRange()404     int getDownNestedPreScrollRange() {
405         if (mDownPreScrollRange != INVALID_SCROLL_RANGE) {
406             // If we already have a valid value, return it
407             return mDownPreScrollRange;
408         }
409 
410         int range = 0;
411         for (int i = getChildCount() - 1; i >= 0; i--) {
412             final View child = getChildAt(i);
413             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
414             final int childHeight = child.getMeasuredHeight();
415             final int flags = lp.mScrollFlags;
416 
417             if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
418                 // First take the margin into account
419                 range += lp.topMargin + lp.bottomMargin;
420                 // The view has the quick return flag combination...
421                 if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
422                     // If they're set to enter collapsed, use the minimum height
423                     range += ViewCompat.getMinimumHeight(child);
424                 } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
425                     // Only enter by the amount of the collapsed height
426                     range += childHeight - ViewCompat.getMinimumHeight(child);
427                 } else {
428                     // Else use the full height (minus the top inset)
429                     range += childHeight - getTopInset();
430                 }
431             } else if (range > 0) {
432                 // If we've hit an non-quick return scrollable view, and we've already hit a
433                 // quick return view, return now
434                 break;
435             }
436         }
437         return mDownPreScrollRange = Math.max(0, range);
438     }
439 
440     /**
441      * Return the scroll range when scrolling down from a nested scroll.
442      */
getDownNestedScrollRange()443     int getDownNestedScrollRange() {
444         if (mDownScrollRange != INVALID_SCROLL_RANGE) {
445             // If we already have a valid value, return it
446             return mDownScrollRange;
447         }
448 
449         int range = 0;
450         for (int i = 0, z = getChildCount(); i < z; i++) {
451             final View child = getChildAt(i);
452             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
453             int childHeight = child.getMeasuredHeight();
454             childHeight += lp.topMargin + lp.bottomMargin;
455 
456             final int flags = lp.mScrollFlags;
457 
458             if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
459                 // We're set to scroll so add the child's height
460                 range += childHeight;
461 
462                 if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
463                     // For a collapsing exit scroll, we to take the collapsed height into account.
464                     // We also break the range straight away since later views can't scroll
465                     // beneath us
466                     range -= ViewCompat.getMinimumHeight(child) + getTopInset();
467                     break;
468                 }
469             } else {
470                 // As soon as a view doesn't have the scroll flag, we end the range calculation.
471                 // This is because views below can not scroll under a fixed view.
472                 break;
473             }
474         }
475         return mDownScrollRange = Math.max(0, range);
476     }
477 
dispatchOffsetUpdates(int offset)478     void dispatchOffsetUpdates(int offset) {
479         // Iterate backwards through the list so that most recently added listeners
480         // get the first chance to decide
481         if (mListeners != null) {
482             for (int i = 0, z = mListeners.size(); i < z; i++) {
483                 final OnOffsetChangedListener listener = mListeners.get(i);
484                 if (listener != null) {
485                     listener.onOffsetChanged(this, offset);
486                 }
487             }
488         }
489     }
490 
getMinimumHeightForVisibleOverlappingContent()491     final int getMinimumHeightForVisibleOverlappingContent() {
492         final int topInset = getTopInset();
493         final int minHeight = ViewCompat.getMinimumHeight(this);
494         if (minHeight != 0) {
495             // If this layout has a min height, use it (doubled)
496             return (minHeight * 2) + topInset;
497         }
498 
499         // Otherwise, we'll use twice the min height of our last child
500         final int childCount = getChildCount();
501         final int lastChildMinHeight = childCount >= 1
502                 ? ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) : 0;
503         if (lastChildMinHeight != 0) {
504             return (lastChildMinHeight * 2) + topInset;
505         }
506 
507         // If we reach here then we don't have a min height explicitly set. Instead we'll take a
508         // guess at 1/3 of our height being visible
509         return getHeight() / 3;
510     }
511 
512     @Override
onCreateDrawableState(int extraSpace)513     protected int[] onCreateDrawableState(int extraSpace) {
514         if (mTmpStatesArray == null) {
515             // Note that we can't allocate this at the class level (in declaration) since
516             // some paths in super View constructor are going to call this method before
517             // that
518             mTmpStatesArray = new int[2];
519         }
520         final int[] extraStates = mTmpStatesArray;
521         final int[] states = super.onCreateDrawableState(extraSpace + extraStates.length);
522 
523         extraStates[0] = mCollapsible ? R.attr.state_collapsible : -R.attr.state_collapsible;
524         extraStates[1] = mCollapsible && mCollapsed
525                 ? R.attr.state_collapsed : -R.attr.state_collapsed;
526 
527         return mergeDrawableStates(states, extraStates);
528     }
529 
530     /**
531      * Sets whether the AppBarLayout has collapsible children or not.
532      *
533      * @return true if the collapsible state changed
534      */
setCollapsibleState(boolean collapsible)535     private boolean setCollapsibleState(boolean collapsible) {
536         if (mCollapsible != collapsible) {
537             mCollapsible = collapsible;
538             refreshDrawableState();
539             return true;
540         }
541         return false;
542     }
543 
544     /**
545      * Sets whether the AppBarLayout is in a collapsed state or not.
546      *
547      * @return true if the collapsed state changed
548      */
setCollapsedState(boolean collapsed)549     boolean setCollapsedState(boolean collapsed) {
550         if (mCollapsed != collapsed) {
551             mCollapsed = collapsed;
552             refreshDrawableState();
553             return true;
554         }
555         return false;
556     }
557 
558     /**
559      * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now
560      * controlled via a {@link android.animation.StateListAnimator}. If a target
561      * elevation is set, either by this method or the {@code app:elevation} attribute,
562      * a new state list animator is created which uses the given {@code elevation} value.
563      *
564      * @attr ref android.support.design.R.styleable#AppBarLayout_elevation
565      */
566     @Deprecated
setTargetElevation(float elevation)567     public void setTargetElevation(float elevation) {
568         if (Build.VERSION.SDK_INT >= 21) {
569             ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(this, elevation);
570         }
571     }
572 
573     /**
574      * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now
575      * controlled via a {@link android.animation.StateListAnimator}. This method now
576      * always returns 0.
577      */
578     @Deprecated
getTargetElevation()579     public float getTargetElevation() {
580         return 0;
581     }
582 
getPendingAction()583     int getPendingAction() {
584         return mPendingAction;
585     }
586 
resetPendingAction()587     void resetPendingAction() {
588         mPendingAction = PENDING_ACTION_NONE;
589     }
590 
591     @VisibleForTesting
getTopInset()592     final int getTopInset() {
593         return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
594     }
595 
onWindowInsetChanged(final WindowInsetsCompat insets)596     WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {
597         WindowInsetsCompat newInsets = null;
598 
599         if (ViewCompat.getFitsSystemWindows(this)) {
600             // If we're set to fit system windows, keep the insets
601             newInsets = insets;
602         }
603 
604         // If our insets have changed, keep them and invalidate the scroll ranges...
605         if (!ObjectsCompat.equals(mLastInsets, newInsets)) {
606             mLastInsets = newInsets;
607             invalidateScrollRanges();
608         }
609 
610         return insets;
611     }
612 
613     public static class LayoutParams extends LinearLayout.LayoutParams {
614 
615         /** @hide */
616         @RestrictTo(LIBRARY_GROUP)
617         @IntDef(flag=true, value={
618                 SCROLL_FLAG_SCROLL,
619                 SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,
620                 SCROLL_FLAG_ENTER_ALWAYS,
621                 SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,
622                 SCROLL_FLAG_SNAP
623         })
624         @Retention(RetentionPolicy.SOURCE)
625         public @interface ScrollFlags {}
626 
627         /**
628          * The view will be scroll in direct relation to scroll events. This flag needs to be
629          * set for any of the other flags to take effect. If any sibling views
630          * before this one do not have this flag, then this value has no effect.
631          */
632         public static final int SCROLL_FLAG_SCROLL = 0x1;
633 
634         /**
635          * When exiting (scrolling off screen) the view will be scrolled until it is
636          * 'collapsed'. The collapsed height is defined by the view's minimum height.
637          *
638          * @see ViewCompat#getMinimumHeight(View)
639          * @see View#setMinimumHeight(int)
640          */
641         public static final int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED = 0x2;
642 
643         /**
644          * When entering (scrolling on screen) the view will scroll on any downwards
645          * scroll event, regardless of whether the scrolling view is also scrolling. This
646          * is commonly referred to as the 'quick return' pattern.
647          */
648         public static final int SCROLL_FLAG_ENTER_ALWAYS = 0x4;
649 
650         /**
651          * An additional flag for 'enterAlways' which modifies the returning view to
652          * only initially scroll back to it's collapsed height. Once the scrolling view has
653          * reached the end of it's scroll range, the remainder of this view will be scrolled
654          * into view. The collapsed height is defined by the view's minimum height.
655          *
656          * @see ViewCompat#getMinimumHeight(View)
657          * @see View#setMinimumHeight(int)
658          */
659         public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 0x8;
660 
661         /**
662          * Upon a scroll ending, if the view is only partially visible then it will be snapped
663          * and scrolled to it's closest edge. For example, if the view only has it's bottom 25%
664          * displayed, it will be scrolled off screen completely. Conversely, if it's bottom 75%
665          * is visible then it will be scrolled fully into view.
666          */
667         public static final int SCROLL_FLAG_SNAP = 0x10;
668 
669         /**
670          * Internal flags which allows quick checking features
671          */
672         static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS;
673         static final int FLAG_SNAP = SCROLL_FLAG_SCROLL | SCROLL_FLAG_SNAP;
674         static final int COLLAPSIBLE_FLAGS = SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
675                 | SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED;
676 
677         int mScrollFlags = SCROLL_FLAG_SCROLL;
678         Interpolator mScrollInterpolator;
679 
LayoutParams(Context c, AttributeSet attrs)680         public LayoutParams(Context c, AttributeSet attrs) {
681             super(c, attrs);
682             TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AppBarLayout_Layout);
683             mScrollFlags = a.getInt(R.styleable.AppBarLayout_Layout_layout_scrollFlags, 0);
684             if (a.hasValue(R.styleable.AppBarLayout_Layout_layout_scrollInterpolator)) {
685                 int resId = a.getResourceId(
686                         R.styleable.AppBarLayout_Layout_layout_scrollInterpolator, 0);
687                 mScrollInterpolator = android.view.animation.AnimationUtils.loadInterpolator(
688                         c, resId);
689             }
690             a.recycle();
691         }
692 
LayoutParams(int width, int height)693         public LayoutParams(int width, int height) {
694             super(width, height);
695         }
696 
LayoutParams(int width, int height, float weight)697         public LayoutParams(int width, int height, float weight) {
698             super(width, height, weight);
699         }
700 
LayoutParams(ViewGroup.LayoutParams p)701         public LayoutParams(ViewGroup.LayoutParams p) {
702             super(p);
703         }
704 
LayoutParams(MarginLayoutParams source)705         public LayoutParams(MarginLayoutParams source) {
706             super(source);
707         }
708 
709         @RequiresApi(19)
LayoutParams(LinearLayout.LayoutParams source)710         public LayoutParams(LinearLayout.LayoutParams source) {
711             // The copy constructor called here only exists on API 19+.
712             super(source);
713         }
714 
715         @RequiresApi(19)
LayoutParams(LayoutParams source)716         public LayoutParams(LayoutParams source) {
717             // The copy constructor called here only exists on API 19+.
718             super(source);
719             mScrollFlags = source.mScrollFlags;
720             mScrollInterpolator = source.mScrollInterpolator;
721         }
722 
723         /**
724          * Set the scrolling flags.
725          *
726          * @param flags bitwise int of {@link #SCROLL_FLAG_SCROLL},
727          *             {@link #SCROLL_FLAG_EXIT_UNTIL_COLLAPSED}, {@link #SCROLL_FLAG_ENTER_ALWAYS},
728          *             {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED} and {@link #SCROLL_FLAG_SNAP }.
729          *
730          * @see #getScrollFlags()
731          *
732          * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags
733          */
setScrollFlags(@crollFlags int flags)734         public void setScrollFlags(@ScrollFlags int flags) {
735             mScrollFlags = flags;
736         }
737 
738         /**
739          * Returns the scrolling flags.
740          *
741          * @see #setScrollFlags(int)
742          *
743          * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags
744          */
745         @ScrollFlags
getScrollFlags()746         public int getScrollFlags() {
747             return mScrollFlags;
748         }
749 
750         /**
751          * Set the interpolator to when scrolling the view associated with this
752          * {@link LayoutParams}.
753          *
754          * @param interpolator the interpolator to use, or null to use normal 1-to-1 scrolling.
755          *
756          * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
757          * @see #getScrollInterpolator()
758          */
setScrollInterpolator(Interpolator interpolator)759         public void setScrollInterpolator(Interpolator interpolator) {
760             mScrollInterpolator = interpolator;
761         }
762 
763         /**
764          * Returns the {@link Interpolator} being used for scrolling the view associated with this
765          * {@link LayoutParams}. Null indicates 'normal' 1-to-1 scrolling.
766          *
767          * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
768          * @see #setScrollInterpolator(Interpolator)
769          */
getScrollInterpolator()770         public Interpolator getScrollInterpolator() {
771             return mScrollInterpolator;
772         }
773 
774         /**
775          * Returns true if the scroll flags are compatible for 'collapsing'
776          */
isCollapsible()777         boolean isCollapsible() {
778             return (mScrollFlags & SCROLL_FLAG_SCROLL) == SCROLL_FLAG_SCROLL
779                     && (mScrollFlags & COLLAPSIBLE_FLAGS) != 0;
780         }
781     }
782 
783     /**
784      * The default {@link Behavior} for {@link AppBarLayout}. Implements the necessary nested
785      * scroll handling with offsetting.
786      */
787     public static class Behavior extends HeaderBehavior<AppBarLayout> {
788         private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
789         private static final int INVALID_POSITION = -1;
790 
791         /**
792          * Callback to allow control over any {@link AppBarLayout} dragging.
793          */
794         public static abstract class DragCallback {
795             /**
796              * Allows control over whether the given {@link AppBarLayout} can be dragged or not.
797              *
798              * <p>Dragging is defined as a direct touch on the AppBarLayout with movement. This
799              * call does not affect any nested scrolling.</p>
800              *
801              * @return true if we are in a position to scroll the AppBarLayout via a drag, false
802              *         if not.
803              */
canDrag(@onNull AppBarLayout appBarLayout)804             public abstract boolean canDrag(@NonNull AppBarLayout appBarLayout);
805         }
806 
807         private int mOffsetDelta;
808         private ValueAnimator mOffsetAnimator;
809 
810         private int mOffsetToChildIndexOnLayout = INVALID_POSITION;
811         private boolean mOffsetToChildIndexOnLayoutIsMinHeight;
812         private float mOffsetToChildIndexOnLayoutPerc;
813 
814         private WeakReference<View> mLastNestedScrollingChildRef;
815         private DragCallback mOnDragCallback;
816 
Behavior()817         public Behavior() {}
818 
Behavior(Context context, AttributeSet attrs)819         public Behavior(Context context, AttributeSet attrs) {
820             super(context, attrs);
821         }
822 
823         @Override
onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type)824         public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
825                 View directTargetChild, View target, int nestedScrollAxes, int type) {
826             // Return true if we're nested scrolling vertically, and we have scrollable children
827             // and the scrolling view is big enough to scroll
828             final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
829                     && child.hasScrollableChildren()
830                     && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
831 
832             if (started && mOffsetAnimator != null) {
833                 // Cancel any offset animation
834                 mOffsetAnimator.cancel();
835             }
836 
837             // A new nested scroll has started so clear out the previous ref
838             mLastNestedScrollingChildRef = null;
839 
840             return started;
841         }
842 
843         @Override
onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type)844         public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
845                 View target, int dx, int dy, int[] consumed, int type) {
846             if (dy != 0) {
847                 int min, max;
848                 if (dy < 0) {
849                     // We're scrolling down
850                     min = -child.getTotalScrollRange();
851                     max = min + child.getDownNestedPreScrollRange();
852                 } else {
853                     // We're scrolling up
854                     min = -child.getUpNestedPreScrollRange();
855                     max = 0;
856                 }
857                 if (min != max) {
858                     consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
859                 }
860             }
861         }
862 
863         @Override
onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type)864         public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
865                 View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
866                 int type) {
867             if (dyUnconsumed < 0) {
868                 // If the scrolling view is scrolling down but not consuming, it's probably be at
869                 // the top of it's content
870                 scroll(coordinatorLayout, child, dyUnconsumed,
871                         -child.getDownNestedScrollRange(), 0);
872             }
873         }
874 
875         @Override
onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type)876         public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
877                 View target, int type) {
878             if (type == ViewCompat.TYPE_TOUCH) {
879                 // If we haven't been flung then let's see if the current view has been set to snap
880                 snapToChildIfNeeded(coordinatorLayout, abl);
881             }
882 
883             // Keep a reference to the previous nested scrolling child
884             mLastNestedScrollingChildRef = new WeakReference<>(target);
885         }
886 
887         /**
888          * Set a callback to control any {@link AppBarLayout} dragging.
889          *
890          * @param callback the callback to use, or {@code null} to use the default behavior.
891          */
setDragCallback(@ullable DragCallback callback)892         public void setDragCallback(@Nullable DragCallback callback) {
893             mOnDragCallback = callback;
894         }
895 
animateOffsetTo(final CoordinatorLayout coordinatorLayout, final AppBarLayout child, final int offset, float velocity)896         private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
897                 final AppBarLayout child, final int offset, float velocity) {
898             final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
899 
900             final int duration;
901             velocity = Math.abs(velocity);
902             if (velocity > 0) {
903                 duration = 3 * Math.round(1000 * (distance / velocity));
904             } else {
905                 final float distanceRatio = (float) distance / child.getHeight();
906                 duration = (int) ((distanceRatio + 1) * 150);
907             }
908 
909             animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
910         }
911 
animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout, final AppBarLayout child, final int offset, final int duration)912         private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
913                 final AppBarLayout child, final int offset, final int duration) {
914             final int currentOffset = getTopBottomOffsetForScrollingSibling();
915             if (currentOffset == offset) {
916                 if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
917                     mOffsetAnimator.cancel();
918                 }
919                 return;
920             }
921 
922             if (mOffsetAnimator == null) {
923                 mOffsetAnimator = new ValueAnimator();
924                 mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
925                 mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
926                     @Override
927                     public void onAnimationUpdate(ValueAnimator animation) {
928                         setHeaderTopBottomOffset(coordinatorLayout, child,
929                                 (int) animation.getAnimatedValue());
930                     }
931                 });
932             } else {
933                 mOffsetAnimator.cancel();
934             }
935 
936             mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
937             mOffsetAnimator.setIntValues(currentOffset, offset);
938             mOffsetAnimator.start();
939         }
940 
getChildIndexOnOffset(AppBarLayout abl, final int offset)941         private int getChildIndexOnOffset(AppBarLayout abl, final int offset) {
942             for (int i = 0, count = abl.getChildCount(); i < count; i++) {
943                 View child = abl.getChildAt(i);
944                 if (child.getTop() <= -offset && child.getBottom() >= -offset) {
945                     return i;
946                 }
947             }
948             return -1;
949         }
950 
snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl)951         private void snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl) {
952             final int offset = getTopBottomOffsetForScrollingSibling();
953             final int offsetChildIndex = getChildIndexOnOffset(abl, offset);
954             if (offsetChildIndex >= 0) {
955                 final View offsetChild = abl.getChildAt(offsetChildIndex);
956                 final LayoutParams lp = (LayoutParams) offsetChild.getLayoutParams();
957                 final int flags = lp.getScrollFlags();
958 
959                 if ((flags & LayoutParams.FLAG_SNAP) == LayoutParams.FLAG_SNAP) {
960                     // We're set the snap, so animate the offset to the nearest edge
961                     int snapTop = -offsetChild.getTop();
962                     int snapBottom = -offsetChild.getBottom();
963 
964                     if (offsetChildIndex == abl.getChildCount() - 1) {
965                         // If this is the last child, we need to take the top inset into account
966                         snapBottom += abl.getTopInset();
967                     }
968 
969                     if (checkFlag(flags, LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED)) {
970                         // If the view is set only exit until it is collapsed, we'll abide by that
971                         snapBottom += ViewCompat.getMinimumHeight(offsetChild);
972                     } else if (checkFlag(flags, LayoutParams.FLAG_QUICK_RETURN
973                             | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS)) {
974                         // If it's set to always enter collapsed, it actually has two states. We
975                         // select the state and then snap within the state
976                         final int seam = snapBottom + ViewCompat.getMinimumHeight(offsetChild);
977                         if (offset < seam) {
978                             snapTop = seam;
979                         } else {
980                             snapBottom = seam;
981                         }
982                     }
983 
984                     final int newOffset = offset < (snapBottom + snapTop) / 2
985                             ? snapBottom
986                             : snapTop;
987                     animateOffsetTo(coordinatorLayout, abl,
988                             MathUtils.clamp(newOffset, -abl.getTotalScrollRange(), 0), 0);
989                 }
990             }
991         }
992 
993         private static boolean checkFlag(final int flags, final int check) {
994             return (flags & check) == check;
995         }
996 
997         @Override
998         public boolean onMeasureChild(CoordinatorLayout parent, AppBarLayout child,
999                 int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
1000                 int heightUsed) {
1001             final CoordinatorLayout.LayoutParams lp =
1002                     (CoordinatorLayout.LayoutParams) child.getLayoutParams();
1003             if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
1004                 // If the view is set to wrap on it's height, CoordinatorLayout by default will
1005                 // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
1006                 // what we actually want, so we measure it ourselves with an unspecified spec to
1007                 // allow the child to be larger than it's parent
1008                 parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
1009                         MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed);
1010                 return true;
1011             }
1012 
1013             // Let the parent handle it as normal
1014             return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
1015                     parentHeightMeasureSpec, heightUsed);
1016         }
1017 
1018         @Override
1019         public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl,
1020                 int layoutDirection) {
1021             boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
1022 
1023             // The priority for for actions here is (first which is true wins):
1024             // 1. forced pending actions
1025             // 2. offsets for restorations
1026             // 3. non-forced pending actions
1027             final int pendingAction = abl.getPendingAction();
1028             if (mOffsetToChildIndexOnLayout >= 0 && (pendingAction & PENDING_ACTION_FORCE) == 0) {
1029                 View child = abl.getChildAt(mOffsetToChildIndexOnLayout);
1030                 int offset = -child.getBottom();
1031                 if (mOffsetToChildIndexOnLayoutIsMinHeight) {
1032                     offset += ViewCompat.getMinimumHeight(child) + abl.getTopInset();
1033                 } else {
1034                     offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
1035                 }
1036                 setHeaderTopBottomOffset(parent, abl, offset);
1037             } else if (pendingAction != PENDING_ACTION_NONE) {
1038                 final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0;
1039                 if ((pendingAction & PENDING_ACTION_COLLAPSED) != 0) {
1040                     final int offset = -abl.getUpNestedPreScrollRange();
1041                     if (animate) {
1042                         animateOffsetTo(parent, abl, offset, 0);
1043                     } else {
1044                         setHeaderTopBottomOffset(parent, abl, offset);
1045                     }
1046                 } else if ((pendingAction & PENDING_ACTION_EXPANDED) != 0) {
1047                     if (animate) {
1048                         animateOffsetTo(parent, abl, 0, 0);
1049                     } else {
1050                         setHeaderTopBottomOffset(parent, abl, 0);
1051                     }
1052                 }
1053             }
1054 
1055             // Finally reset any pending states
1056             abl.resetPendingAction();
1057             mOffsetToChildIndexOnLayout = INVALID_POSITION;
1058 
1059             // We may have changed size, so let's constrain the top and bottom offset correctly,
1060             // just in case we're out of the bounds
1061             setTopAndBottomOffset(
1062                     MathUtils.clamp(getTopAndBottomOffset(), -abl.getTotalScrollRange(), 0));
1063 
1064             // Update the AppBarLayout's drawable state for any elevation changes.
1065             // This is needed so that the elevation is set in the first layout, so that
1066             // we don't get a visual elevation jump pre-N (due to the draw dispatch skip)
1067             updateAppBarLayoutDrawableState(parent, abl, getTopAndBottomOffset(), 0, true);
1068 
1069             // Make sure we dispatch the offset update
1070             abl.dispatchOffsetUpdates(getTopAndBottomOffset());
1071 
1072             return handled;
1073         }
1074 
1075         @Override
canDragView(AppBarLayout view)1076         boolean canDragView(AppBarLayout view) {
1077             if (mOnDragCallback != null) {
1078                 // If there is a drag callback set, it's in control
1079                 return mOnDragCallback.canDrag(view);
1080             }
1081 
1082             // Else we'll use the default behaviour of seeing if it can scroll down
1083             if (mLastNestedScrollingChildRef != null) {
1084                 // If we have a reference to a scrolling view, check it
1085                 final View scrollingView = mLastNestedScrollingChildRef.get();
1086                 return scrollingView != null && scrollingView.isShown()
1087                         && !scrollingView.canScrollVertically(-1);
1088             } else {
1089                 // Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
1090                 return true;
1091             }
1092         }
1093 
1094         @Override
onFlingFinished(CoordinatorLayout parent, AppBarLayout layout)1095         void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) {
1096             // At the end of a manual fling, check to see if we need to snap to the edge-child
1097             snapToChildIfNeeded(parent, layout);
1098         }
1099 
1100         @Override
getMaxDragOffset(AppBarLayout view)1101         int getMaxDragOffset(AppBarLayout view) {
1102             return -view.getDownNestedScrollRange();
1103         }
1104 
1105         @Override
getScrollRangeForDragFling(AppBarLayout view)1106         int getScrollRangeForDragFling(AppBarLayout view) {
1107             return view.getTotalScrollRange();
1108         }
1109 
1110         @Override
setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset)1111         int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
1112                 AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
1113             final int curOffset = getTopBottomOffsetForScrollingSibling();
1114             int consumed = 0;
1115 
1116             if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
1117                 // If we have some scrolling range, and we're currently within the min and max
1118                 // offsets, calculate a new offset
1119                 newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
1120                 if (curOffset != newOffset) {
1121                     final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
1122                             ? interpolateOffset(appBarLayout, newOffset)
1123                             : newOffset;
1124 
1125                     final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
1126 
1127                     // Update how much dy we have consumed
1128                     consumed = curOffset - newOffset;
1129                     // Update the stored sibling offset
1130                     mOffsetDelta = newOffset - interpolatedOffset;
1131 
1132                     if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
1133                         // If the offset hasn't changed and we're using an interpolated scroll
1134                         // then we need to keep any dependent views updated. CoL will do this for
1135                         // us when we move, but we need to do it manually when we don't (as an
1136                         // interpolated scroll may finish early).
1137                         coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
1138                     }
1139 
1140                     // Dispatch the updates to any listeners
1141                     appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());
1142 
1143                     // Update the AppBarLayout's drawable state (for any elevation changes)
1144                     updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
1145                             newOffset < curOffset ? -1 : 1, false);
1146                 }
1147             } else {
1148                 // Reset the offset delta
1149                 mOffsetDelta = 0;
1150             }
1151 
1152             return consumed;
1153         }
1154 
1155         @VisibleForTesting
isOffsetAnimatorRunning()1156         boolean isOffsetAnimatorRunning() {
1157             return mOffsetAnimator != null && mOffsetAnimator.isRunning();
1158         }
1159 
interpolateOffset(AppBarLayout layout, final int offset)1160         private int interpolateOffset(AppBarLayout layout, final int offset) {
1161             final int absOffset = Math.abs(offset);
1162 
1163             for (int i = 0, z = layout.getChildCount(); i < z; i++) {
1164                 final View child = layout.getChildAt(i);
1165                 final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
1166                 final Interpolator interpolator = childLp.getScrollInterpolator();
1167 
1168                 if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
1169                     if (interpolator != null) {
1170                         int childScrollableHeight = 0;
1171                         final int flags = childLp.getScrollFlags();
1172                         if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
1173                             // We're set to scroll so add the child's height plus margin
1174                             childScrollableHeight += child.getHeight() + childLp.topMargin
1175                                     + childLp.bottomMargin;
1176 
1177                             if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
1178                                 // For a collapsing scroll, we to take the collapsed height
1179                                 // into account.
1180                                 childScrollableHeight -= ViewCompat.getMinimumHeight(child);
1181                             }
1182                         }
1183 
1184                         if (ViewCompat.getFitsSystemWindows(child)) {
1185                             childScrollableHeight -= layout.getTopInset();
1186                         }
1187 
1188                         if (childScrollableHeight > 0) {
1189                             final int offsetForView = absOffset - child.getTop();
1190                             final int interpolatedDiff = Math.round(childScrollableHeight *
1191                                     interpolator.getInterpolation(
1192                                             offsetForView / (float) childScrollableHeight));
1193 
1194                             return Integer.signum(offset) * (child.getTop() + interpolatedDiff);
1195                         }
1196                     }
1197 
1198                     // If we get to here then the view on the offset isn't suitable for interpolated
1199                     // scrolling. So break out of the loop
1200                     break;
1201                 }
1202             }
1203 
1204             return offset;
1205         }
1206 
updateAppBarLayoutDrawableState(final CoordinatorLayout parent, final AppBarLayout layout, final int offset, final int direction, final boolean forceJump)1207         private void updateAppBarLayoutDrawableState(final CoordinatorLayout parent,
1208                 final AppBarLayout layout, final int offset, final int direction,
1209                 final boolean forceJump) {
1210             final View child = getAppBarChildOnOffset(layout, offset);
1211             if (child != null) {
1212                 final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
1213                 final int flags = childLp.getScrollFlags();
1214                 boolean collapsed = false;
1215 
1216                 if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
1217                     final int minHeight = ViewCompat.getMinimumHeight(child);
1218 
1219                     if (direction > 0 && (flags & (LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
1220                             | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED)) != 0) {
1221                         // We're set to enter always collapsed so we are only collapsed when
1222                         // being scrolled down, and in a collapsed offset
1223                         collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
1224                     } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
1225                         // We're set to exit until collapsed, so any offset which results in
1226                         // the minimum height (or less) being shown is collapsed
1227                         collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
1228                     }
1229                 }
1230 
1231                 final boolean changed = layout.setCollapsedState(collapsed);
1232 
1233                 if (Build.VERSION.SDK_INT >= 11 && (forceJump
1234                         || (changed && shouldJumpElevationState(parent, layout)))) {
1235                     // If the collapsed state changed, we may need to
1236                     // jump to the current state if we have an overlapping view
1237                     layout.jumpDrawablesToCurrentState();
1238                 }
1239             }
1240         }
1241 
shouldJumpElevationState(CoordinatorLayout parent, AppBarLayout layout)1242         private boolean shouldJumpElevationState(CoordinatorLayout parent, AppBarLayout layout) {
1243             // We should jump the elevated state if we have a dependent scrolling view which has
1244             // an overlapping top (i.e. overlaps us)
1245             final List<View> dependencies = parent.getDependents(layout);
1246             for (int i = 0, size = dependencies.size(); i < size; i++) {
1247                 final View dependency = dependencies.get(i);
1248                 final CoordinatorLayout.LayoutParams lp =
1249                         (CoordinatorLayout.LayoutParams) dependency.getLayoutParams();
1250                 final CoordinatorLayout.Behavior behavior = lp.getBehavior();
1251 
1252                 if (behavior instanceof ScrollingViewBehavior) {
1253                     return ((ScrollingViewBehavior) behavior).getOverlayTop() != 0;
1254                 }
1255             }
1256             return false;
1257         }
1258 
getAppBarChildOnOffset(final AppBarLayout layout, final int offset)1259         private static View getAppBarChildOnOffset(final AppBarLayout layout, final int offset) {
1260             final int absOffset = Math.abs(offset);
1261             for (int i = 0, z = layout.getChildCount(); i < z; i++) {
1262                 final View child = layout.getChildAt(i);
1263                 if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
1264                     return child;
1265                 }
1266             }
1267             return null;
1268         }
1269 
1270         @Override
getTopBottomOffsetForScrollingSibling()1271         int getTopBottomOffsetForScrollingSibling() {
1272             return getTopAndBottomOffset() + mOffsetDelta;
1273         }
1274 
1275         @Override
onSaveInstanceState(CoordinatorLayout parent, AppBarLayout abl)1276         public Parcelable onSaveInstanceState(CoordinatorLayout parent, AppBarLayout abl) {
1277             final Parcelable superState = super.onSaveInstanceState(parent, abl);
1278             final int offset = getTopAndBottomOffset();
1279 
1280             // Try and find the first visible child...
1281             for (int i = 0, count = abl.getChildCount(); i < count; i++) {
1282                 View child = abl.getChildAt(i);
1283                 final int visBottom = child.getBottom() + offset;
1284 
1285                 if (child.getTop() + offset <= 0 && visBottom >= 0) {
1286                     final SavedState ss = new SavedState(superState);
1287                     ss.firstVisibleChildIndex = i;
1288                     ss.firstVisibleChildAtMinimumHeight =
1289                             visBottom == (ViewCompat.getMinimumHeight(child) + abl.getTopInset());
1290                     ss.firstVisibleChildPercentageShown = visBottom / (float) child.getHeight();
1291                     return ss;
1292                 }
1293             }
1294 
1295             // Else we'll just return the super state
1296             return superState;
1297         }
1298 
1299         @Override
onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout, Parcelable state)1300         public void onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout,
1301                 Parcelable state) {
1302             if (state instanceof SavedState) {
1303                 final SavedState ss = (SavedState) state;
1304                 super.onRestoreInstanceState(parent, appBarLayout, ss.getSuperState());
1305                 mOffsetToChildIndexOnLayout = ss.firstVisibleChildIndex;
1306                 mOffsetToChildIndexOnLayoutPerc = ss.firstVisibleChildPercentageShown;
1307                 mOffsetToChildIndexOnLayoutIsMinHeight = ss.firstVisibleChildAtMinimumHeight;
1308             } else {
1309                 super.onRestoreInstanceState(parent, appBarLayout, state);
1310                 mOffsetToChildIndexOnLayout = INVALID_POSITION;
1311             }
1312         }
1313 
1314         protected static class SavedState extends AbsSavedState {
1315             int firstVisibleChildIndex;
1316             float firstVisibleChildPercentageShown;
1317             boolean firstVisibleChildAtMinimumHeight;
1318 
SavedState(Parcel source, ClassLoader loader)1319             public SavedState(Parcel source, ClassLoader loader) {
1320                 super(source, loader);
1321                 firstVisibleChildIndex = source.readInt();
1322                 firstVisibleChildPercentageShown = source.readFloat();
1323                 firstVisibleChildAtMinimumHeight = source.readByte() != 0;
1324             }
1325 
SavedState(Parcelable superState)1326             public SavedState(Parcelable superState) {
1327                 super(superState);
1328             }
1329 
1330             @Override
writeToParcel(Parcel dest, int flags)1331             public void writeToParcel(Parcel dest, int flags) {
1332                 super.writeToParcel(dest, flags);
1333                 dest.writeInt(firstVisibleChildIndex);
1334                 dest.writeFloat(firstVisibleChildPercentageShown);
1335                 dest.writeByte((byte) (firstVisibleChildAtMinimumHeight ? 1 : 0));
1336             }
1337 
1338             public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
1339                 @Override
1340                 public SavedState createFromParcel(Parcel source, ClassLoader loader) {
1341                     return new SavedState(source, loader);
1342                 }
1343 
1344                 @Override
1345                 public SavedState createFromParcel(Parcel source) {
1346                     return new SavedState(source, null);
1347                 }
1348 
1349                 @Override
1350                 public SavedState[] newArray(int size) {
1351                     return new SavedState[size];
1352                 }
1353             };
1354         }
1355     }
1356 
1357     /**
1358      * Behavior which should be used by {@link View}s which can scroll vertically and support
1359      * nested scrolling to automatically scroll any {@link AppBarLayout} siblings.
1360      */
1361     public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior {
1362 
ScrollingViewBehavior()1363         public ScrollingViewBehavior() {}
1364 
ScrollingViewBehavior(Context context, AttributeSet attrs)1365         public ScrollingViewBehavior(Context context, AttributeSet attrs) {
1366             super(context, attrs);
1367 
1368             final TypedArray a = context.obtainStyledAttributes(attrs,
1369                     R.styleable.ScrollingViewBehavior_Layout);
1370             setOverlayTop(a.getDimensionPixelSize(
1371                     R.styleable.ScrollingViewBehavior_Layout_behavior_overlapTop, 0));
1372             a.recycle();
1373         }
1374 
1375         @Override
layoutDependsOn(CoordinatorLayout parent, View child, View dependency)1376         public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
1377             // We depend on any AppBarLayouts
1378             return dependency instanceof AppBarLayout;
1379         }
1380 
1381         @Override
onDependentViewChanged(CoordinatorLayout parent, View child, View dependency)1382         public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
1383                 View dependency) {
1384             offsetChildAsNeeded(parent, child, dependency);
1385             return false;
1386         }
1387 
1388         @Override
onRequestChildRectangleOnScreen(CoordinatorLayout parent, View child, Rect rectangle, boolean immediate)1389         public boolean onRequestChildRectangleOnScreen(CoordinatorLayout parent, View child,
1390                 Rect rectangle, boolean immediate) {
1391             final AppBarLayout header = findFirstDependency(parent.getDependencies(child));
1392             if (header != null) {
1393                 // Offset the rect by the child's left/top
1394                 rectangle.offset(child.getLeft(), child.getTop());
1395 
1396                 final Rect parentRect = mTempRect1;
1397                 parentRect.set(0, 0, parent.getWidth(), parent.getHeight());
1398 
1399                 if (!parentRect.contains(rectangle)) {
1400                     // If the rectangle can not be fully seen the visible bounds, collapse
1401                     // the AppBarLayout
1402                     header.setExpanded(false, !immediate);
1403                     return true;
1404                 }
1405             }
1406             return false;
1407         }
1408 
offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency)1409         private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
1410             final CoordinatorLayout.Behavior behavior =
1411                     ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
1412             if (behavior instanceof Behavior) {
1413                 // Offset the child, pinning it to the bottom the header-dependency, maintaining
1414                 // any vertical gap and overlap
1415                 final Behavior ablBehavior = (Behavior) behavior;
1416                 ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
1417                         + ablBehavior.mOffsetDelta
1418                         + getVerticalLayoutGap()
1419                         - getOverlapPixelsForOffset(dependency));
1420             }
1421         }
1422 
1423         @Override
getOverlapRatioForOffset(final View header)1424         float getOverlapRatioForOffset(final View header) {
1425             if (header instanceof AppBarLayout) {
1426                 final AppBarLayout abl = (AppBarLayout) header;
1427                 final int totalScrollRange = abl.getTotalScrollRange();
1428                 final int preScrollDown = abl.getDownNestedPreScrollRange();
1429                 final int offset = getAppBarLayoutOffset(abl);
1430 
1431                 if (preScrollDown != 0 && (totalScrollRange + offset) <= preScrollDown) {
1432                     // If we're in a pre-scroll down. Don't use the offset at all.
1433                     return 0;
1434                 } else {
1435                     final int availScrollRange = totalScrollRange - preScrollDown;
1436                     if (availScrollRange != 0) {
1437                         // Else we'll use a interpolated ratio of the overlap, depending on offset
1438                         return 1f + (offset / (float) availScrollRange);
1439                     }
1440                 }
1441             }
1442             return 0f;
1443         }
1444 
getAppBarLayoutOffset(AppBarLayout abl)1445         private static int getAppBarLayoutOffset(AppBarLayout abl) {
1446             final CoordinatorLayout.Behavior behavior =
1447                     ((CoordinatorLayout.LayoutParams) abl.getLayoutParams()).getBehavior();
1448             if (behavior instanceof Behavior) {
1449                 return ((Behavior) behavior).getTopBottomOffsetForScrollingSibling();
1450             }
1451             return 0;
1452         }
1453 
1454         @Override
findFirstDependency(List<View> views)1455         AppBarLayout findFirstDependency(List<View> views) {
1456             for (int i = 0, z = views.size(); i < z; i++) {
1457                 View view = views.get(i);
1458                 if (view instanceof AppBarLayout) {
1459                     return (AppBarLayout) view;
1460                 }
1461             }
1462             return null;
1463         }
1464 
1465         @Override
getScrollRange(View v)1466         int getScrollRange(View v) {
1467             if (v instanceof AppBarLayout) {
1468                 return ((AppBarLayout) v).getTotalScrollRange();
1469             } else {
1470                 return super.getScrollRange(v);
1471             }
1472         }
1473     }
1474 }
1475