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 android.content.Context; 20import android.graphics.Rect; 21import android.support.design.widget.CoordinatorLayout.Behavior; 22import android.support.v4.view.GravityCompat; 23import android.support.v4.view.ViewCompat; 24import android.support.v4.view.WindowInsetsCompat; 25import android.util.AttributeSet; 26import android.view.Gravity; 27import android.view.View; 28import android.view.ViewGroup; 29 30import 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 */ 36abstract 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 44 public HeaderScrollingViewBehavior() {} 45 46 public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) { 47 super(context, attrs); 48 } 49 50 @Override 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 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 140 float getOverlapRatioForOffset(final View header) { 141 return 1f; 142 } 143 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 152 private static int resolveGravity(int gravity) { 153 return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity; 154 } 155 156 abstract View findFirstDependency(List<View> views); 157 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 */ 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 */ 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 */ 181 public final int getOverlayTop() { 182 return mOverlayTop; 183 } 184}