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