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