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