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