Home | History | Annotate | Download | only in widget
      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 
     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 }