1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.design.widget;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.support.design.widget.CoordinatorLayout.Behavior;
22 import android.support.v4.view.GravityCompat;
23 import android.support.v4.view.ViewCompat;
24 import android.support.v4.view.WindowInsetsCompat;
25 import android.util.AttributeSet;
26 import android.view.Gravity;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 import java.util.List;
31 
32 /**
33  * The {@link Behavior} for a scrolling view that is positioned vertically below another view.
34  * See {@link HeaderBehavior}.
35  */
36 abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
37 
38     private final Rect mTempRect1 = new Rect();
39     private final Rect mTempRect2 = new Rect();
40 
41     private int mVerticalLayoutGap = 0;
42     private int mOverlayTop;
43 
HeaderScrollingViewBehavior()44     public HeaderScrollingViewBehavior() {}
45 
HeaderScrollingViewBehavior(Context context, AttributeSet attrs)46     public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {
47         super(context, attrs);
48     }
49 
50     @Override
onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)51     public boolean onMeasureChild(CoordinatorLayout parent, View child,
52             int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
53             int heightUsed) {
54         final int childLpHeight = child.getLayoutParams().height;
55         if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
56                 || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
57             // If the menu's height is set to match_parent/wrap_content then measure it
58             // with the maximum visible height
59 
60             final List<View> dependencies = parent.getDependencies(child);
61             final View header = findFirstDependency(dependencies);
62             if (header != null) {
63                 if (ViewCompat.getFitsSystemWindows(header)
64                         && !ViewCompat.getFitsSystemWindows(child)) {
65                     // If the header is fitting system windows then we need to also,
66                     // otherwise we'll get CoL's compatible measuring
67                     ViewCompat.setFitsSystemWindows(child, true);
68 
69                     if (ViewCompat.getFitsSystemWindows(child)) {
70                         // If the set succeeded, trigger a new layout and return true
71                         child.requestLayout();
72                         return true;
73                     }
74                 }
75 
76                 int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
77                 if (availableHeight == 0) {
78                     // If the measure spec doesn't specify a size, use the current height
79                     availableHeight = parent.getHeight();
80                 }
81 
82                 final int height = availableHeight - header.getMeasuredHeight()
83                         + getScrollRange(header);
84                 final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
85                         childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
86                                 ? View.MeasureSpec.EXACTLY
87                                 : View.MeasureSpec.AT_MOST);
88 
89                 // Now measure the scrolling view with the correct height
90                 parent.onMeasureChild(child, parentWidthMeasureSpec,
91                         widthUsed, heightMeasureSpec, heightUsed);
92 
93                 return true;
94             }
95         }
96         return false;
97     }
98 
99     @Override
layoutChild(final CoordinatorLayout parent, final View child, final int layoutDirection)100     protected void layoutChild(final CoordinatorLayout parent, final View child,
101             final int layoutDirection) {
102         final List<View> dependencies = parent.getDependencies(child);
103         final View header = findFirstDependency(dependencies);
104 
105         if (header != null) {
106             final CoordinatorLayout.LayoutParams lp =
107                     (CoordinatorLayout.LayoutParams) child.getLayoutParams();
108             final Rect available = mTempRect1;
109             available.set(parent.getPaddingLeft() + lp.leftMargin,
110                     header.getBottom() + lp.topMargin,
111                     parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
112                     parent.getHeight() + header.getBottom()
113                             - parent.getPaddingBottom() - lp.bottomMargin);
114 
115             final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
116             if (parentInsets != null && ViewCompat.getFitsSystemWindows(parent)
117                     && !ViewCompat.getFitsSystemWindows(child)) {
118                 // If we're set to handle insets but this child isn't, then it has been measured as
119                 // if there are no insets. We need to lay it out to match horizontally.
120                 // Top and bottom and already handled in the logic above
121                 available.left += parentInsets.getSystemWindowInsetLeft();
122                 available.right -= parentInsets.getSystemWindowInsetRight();
123             }
124 
125             final Rect out = mTempRect2;
126             GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
127                     child.getMeasuredHeight(), available, out, layoutDirection);
128 
129             final int overlap = getOverlapPixelsForOffset(header);
130 
131             child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
132             mVerticalLayoutGap = out.top - header.getBottom();
133         } else {
134             // If we don't have a dependency, let super handle it
135             super.layoutChild(parent, child, layoutDirection);
136             mVerticalLayoutGap = 0;
137         }
138     }
139 
getOverlapRatioForOffset(final View header)140     float getOverlapRatioForOffset(final View header) {
141         return 1f;
142     }
143 
getOverlapPixelsForOffset(final View header)144     final int getOverlapPixelsForOffset(final View header) {
145         return mOverlayTop == 0
146                 ? 0
147                 : MathUtils.constrain(Math.round(getOverlapRatioForOffset(header) * mOverlayTop),
148                         0, mOverlayTop);
149 
150     }
151 
resolveGravity(int gravity)152     private static int resolveGravity(int gravity) {
153         return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
154     }
155 
findFirstDependency(List<View> views)156     abstract View findFirstDependency(List<View> views);
157 
getScrollRange(View v)158     int getScrollRange(View v) {
159         return v.getMeasuredHeight();
160     }
161 
162     /**
163      * The gap between the top of the scrolling view and the bottom of the header layout in pixels.
164      */
getVerticalLayoutGap()165     final int getVerticalLayoutGap() {
166         return mVerticalLayoutGap;
167     }
168 
169     /**
170      * Set the distance that this view should overlap any {@link AppBarLayout}.
171      *
172      * @param overlayTop the distance in px
173      */
setOverlayTop(int overlayTop)174     public final void setOverlayTop(int overlayTop) {
175         mOverlayTop = overlayTop;
176     }
177 
178     /**
179      * Returns the distance that this view should overlap any {@link AppBarLayout}.
180      */
getOverlayTop()181     public final int getOverlayTop() {
182         return mOverlayTop;
183     }
184 }