diff --git a/api/current.txt b/api/current.txt index e7c86a08a..3cfe2571a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5,6 +5,7 @@ package android.support.design.widget { ctor public AppBarLayout(android.content.Context, android.util.AttributeSet); method public void addOnOffsetChangedListener(android.support.design.widget.AppBarLayout.OnOffsetChangedListener); method public float getTargetElevation(); + method public final int getTotalScrollRange(); method public void removeOnOffsetChangedListener(android.support.design.widget.AppBarLayout.OnOffsetChangedListener); method public void setTargetElevation(float); } @@ -56,13 +57,18 @@ package android.support.design.widget { ctor public CollapsingToolbarLayout(android.content.Context); ctor public CollapsingToolbarLayout(android.content.Context, android.util.AttributeSet); ctor public CollapsingToolbarLayout(android.content.Context, android.util.AttributeSet, int); - method public int getForegroundScrimColor(); + method public android.graphics.drawable.Drawable getContentScrim(); + method public android.graphics.drawable.Drawable getStatusBarScrim(); method public void setCollapsedTitleTextAppearance(int); method public void setCollapsedTitleTextColor(int); + method public void setContentScrim(android.graphics.drawable.Drawable); + method public void setContentScrimColor(int); + method public void setContentScrimResource(int); method public void setExpandedTitleColor(int); method public void setExpandedTitleTextAppearance(int); - method public void setForegroundScrimColor(int); - method public void setForegroundScrimColorResource(int); + method public void setStatusBarScrim(android.graphics.drawable.Drawable); + method public void setStatusBarScrimColor(int); + method public void setStatusBarScrimResource(int); method public void setTitle(java.lang.CharSequence); } @@ -89,12 +95,17 @@ package android.support.design.widget { method public void dispatchDependentViewsChanged(android.view.View); method public boolean doViewsOverlap(android.view.View, android.view.View); method public java.util.List getDependencies(android.view.View); + method public android.graphics.drawable.Drawable getStatusBarBackground(); method public boolean isPointInChildBounds(android.view.View, int, int); method public void onAttachedToWindow(); method public void onDetachedFromWindow(); + method public void onDraw(android.graphics.Canvas); method protected void onLayout(boolean, int, int, int, int); method public void onLayoutChild(android.view.View, int); method public void onMeasureChild(android.view.View, int, int, int, int); + method public void setStatusBarBackground(android.graphics.drawable.Drawable); + method public void setStatusBarBackgroundColor(int); + method public void setStatusBarBackgroundResource(int); } public static abstract class CoordinatorLayout.Behavior { @@ -106,6 +117,7 @@ package android.support.design.widget { method public static java.lang.Object getTag(android.view.View); method public boolean isDirty(android.support.design.widget.CoordinatorLayout, V); method public boolean layoutDependsOn(android.support.design.widget.CoordinatorLayout, V, android.view.View); + method public android.support.v4.view.WindowInsetsCompat onApplyWindowInsets(android.support.design.widget.CoordinatorLayout, V, android.support.v4.view.WindowInsetsCompat); method public boolean onDependentViewChanged(android.support.design.widget.CoordinatorLayout, V, android.view.View); method public boolean onInterceptTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent); method public boolean onLayoutChild(android.support.design.widget.CoordinatorLayout, V, int); diff --git a/base/android/support/design/widget/CoordinatorLayoutInsetsHelper.java b/base/android/support/design/widget/CoordinatorLayoutInsetsHelper.java new file mode 100644 index 000000000..1788c6569 --- /dev/null +++ b/base/android/support/design/widget/CoordinatorLayoutInsetsHelper.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.design.widget; + +import android.support.v4.view.OnApplyWindowInsetsListener; +import android.view.View; + +interface CoordinatorLayoutInsetsHelper { + + void setupForWindowInsets(View view, OnApplyWindowInsetsListener insetsListener); + +} diff --git a/lollipop/android/support/design/widget/CoordinatorLayoutInsetsHelperLollipop.java b/lollipop/android/support/design/widget/CoordinatorLayoutInsetsHelperLollipop.java new file mode 100644 index 000000000..661505044 --- /dev/null +++ b/lollipop/android/support/design/widget/CoordinatorLayoutInsetsHelperLollipop.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.design.widget; + +import android.support.v4.view.OnApplyWindowInsetsListener; +import android.support.v4.view.ViewCompat; +import android.view.View; + +class CoordinatorLayoutInsetsHelperLollipop implements CoordinatorLayoutInsetsHelper { + + public void setupForWindowInsets(View view, OnApplyWindowInsetsListener insetsListener) { + if (ViewCompat.getFitsSystemWindows(view)) { + // First apply the insets listener + ViewCompat.setOnApplyWindowInsetsListener(view, insetsListener); + // Now set the sys ui flags to enable us to lay out in the window insets + view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + } + +} diff --git a/res-public/values/public_attrs.xml b/res-public/values/public_attrs.xml index 30f3612d3..b2ad8fd03 100644 --- a/res-public/values/public_attrs.xml +++ b/res-public/values/public_attrs.xml @@ -19,6 +19,7 @@ + @@ -28,7 +29,6 @@ - @@ -38,6 +38,8 @@ + + diff --git a/res-public/values/public_styles.xml b/res-public/values/public_styles.xml index 426679fed..f65e5e342 100644 --- a/res-public/values/public_styles.xml +++ b/res-public/values/public_styles.xml @@ -21,6 +21,7 @@ + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index a090d8881..e2afe9978 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -94,6 +94,8 @@ layout_keyline="index" where index is a 0-based index into this array. --> + + @@ -207,9 +209,19 @@ + + - + + + + diff --git a/res/values/styles.xml b/res/values/styles.xml index c3fb502bb..e083f049c 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -97,7 +97,7 @@ 20dp @style/TextAppearance.AppCompat.Headline @style/TextAppearance.AppCompat.Widget.ActionBar.Title - @android:color/transparent + ?attr/colorPrimaryDark + + diff --git a/src/android/support/design/widget/AppBarLayout.java b/src/android/support/design/widget/AppBarLayout.java index ea5ede42d..bb7782132 100644 --- a/src/android/support/design/widget/AppBarLayout.java +++ b/src/android/support/design/widget/AppBarLayout.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.support.annotation.IntDef; import android.support.design.R; import android.support.v4.view.ViewCompat; +import android.support.v4.view.WindowInsetsCompat; import android.support.v4.widget.ScrollerCompat; import android.util.AttributeSet; import android.view.View; @@ -118,6 +119,8 @@ public class AppBarLayout extends LinearLayout { private float mTargetElevation; + private WindowInsetsCompat mLastInsets; + private final List> mListeners; public AppBarLayout(Context context) { @@ -140,6 +143,16 @@ public class AppBarLayout extends LinearLayout { mListeners = new ArrayList<>(); ViewCompat.setElevation(this, mTargetElevation); + + ViewCompat.setOnApplyWindowInsetsListener(this, + new android.support.v4.view.OnApplyWindowInsetsListener() { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, + WindowInsetsCompat insets) { + setWindowInsets(insets); + return insets.consumeSystemWindowInsets(); + } + }); } /** @@ -238,11 +251,11 @@ public class AppBarLayout extends LinearLayout { } /** - * Return the scroll range of any children + * Returns the scroll range of all children. * * @return the scroll range in px */ - final int getTotalScrollRange() { + public final int getTotalScrollRange() { if (mTotalScrollRange != INVALID_SCROLL_RANGE) { return mTotalScrollRange; } @@ -262,9 +275,10 @@ public class AppBarLayout extends LinearLayout { if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { // For a collapsing scroll, we to take the collapsed height into account. - // We also return the range straight away since later views can't scroll beneath + // We also break straight away since later views can't scroll beneath // us - return range - ViewCompat.getMinimumHeight(child); + range -= ViewCompat.getMinimumHeight(child); + break; } } else { // As soon as a view doesn't have the scroll flag, we end the range calculation. @@ -272,7 +286,8 @@ public class AppBarLayout extends LinearLayout { break; } } - return mTotalScrollRange = range; + final int top = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; + return mTotalScrollRange = (range - top); } final boolean hasScrollableChildren() { @@ -361,16 +376,17 @@ public class AppBarLayout extends LinearLayout { } final int getMinimumHeightForVisibleOverlappingContent() { + final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; final int minHeight = ViewCompat.getMinimumHeight(this); if (minHeight != 0) { // If this layout has a min height, use it (doubled) - return minHeight * 2; + return (minHeight * 2) + topInset; } // Otherwise, we'll use twice the min height of our last child final int childCount = getChildCount(); return childCount >= 1 - ? ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) * 2 + ? (ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) * 2) + topInset : 0; } @@ -398,6 +414,21 @@ public class AppBarLayout extends LinearLayout { return mTargetElevation; } + private void setWindowInsets(WindowInsetsCompat insets) { + // Invalidate the total scroll range... + mTotalScrollRange = INVALID_SCROLL_RANGE; + mLastInsets = insets; + + // Now dispatch them to our children + for (int i = 0, z = getChildCount(); i < z; i++) { + final View child = getChildAt(i); + insets = ViewCompat.dispatchApplyWindowInsets(child, insets); + if (insets.isConsumed()) { + break; + } + } + } + public static class LayoutParams extends LinearLayout.LayoutParams { /** @hide */ @@ -886,15 +917,15 @@ public class AppBarLayout extends LinearLayout { } final AppBarLayout appBar = findFirstAppBarLayout(dependencies); - if (appBar != null) { - if (appBar.getMeasuredWidth() == 0 || appBar.getMeasuredHeight() == 0) { - // If the AppBar hasn't been measured yet, we need to do it now - parent.onMeasureChild(appBar, parentWidthMeasureSpec, - widthUsed, parentHeightMeasureSpec, heightUsed); + if (appBar != null && ViewCompat.isLaidOut(appBar)) { + if (ViewCompat.getFitsSystemWindows(appBar)) { + // If the AppBarLayout is fitting system windows then we need to also, + // otherwise we'll get CoL's compatible layout functionality + ViewCompat.setFitsSystemWindows(child, true); } final int scrollRange = appBar.getTotalScrollRange(); - final int height = MeasureSpec.getSize(parentHeightMeasureSpec) + final int height = parent.getHeight() - appBar.getMeasuredHeight() + scrollRange; final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); diff --git a/src/android/support/design/widget/CollapsingToolbarLayout.java b/src/android/support/design/widget/CollapsingToolbarLayout.java index 8744aa5c6..e63432200 100644 --- a/src/android/support/design/widget/CollapsingToolbarLayout.java +++ b/src/android/support/design/widget/CollapsingToolbarLayout.java @@ -19,21 +19,22 @@ package android.support.design.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Rect; -import android.support.annotation.ColorRes; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.support.design.R; -import android.support.v4.graphics.ColorUtils; +import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; +import android.support.v4.view.WindowInsetsCompat; import android.support.v7.widget.Toolbar; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.animation.Animation; -import android.view.animation.Transformation; import android.widget.FrameLayout; import java.lang.annotation.Retention; @@ -52,7 +53,7 @@ import java.lang.annotation.RetentionPolicy; * *

Foreground scrim

* A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold. - * You can change the color via {@link #setForegroundScrimColor(int)}. + * You can change this via {@link #setContentScrim(Drawable)}. * *

Parallax scrolling children

* Child views can opt to be scrolled within this layout in a parallax fashion. @@ -66,11 +67,12 @@ import java.lang.annotation.RetentionPolicy; * * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance - * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor + * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_contentScrim * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom + * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_statusBarScrim */ public class CollapsingToolbarLayout extends FrameLayout { @@ -86,13 +88,18 @@ public class CollapsingToolbarLayout extends FrameLayout { private final Rect mRect = new Rect(); private final CollapsingTextHelper mCollapsingTextHelper; - private int mForegroundScrimColor; - private int mCurrentForegroundColor; - private boolean mScrimIsShown; + private Drawable mContentScrim; + private Drawable mStatusBarScrim; + private int mScrimAlpha; + private boolean mScrimsAreShown; private ValueAnimatorCompat mScrimAnimator; private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener; + private int mCurrentOffset; + + private WindowInsetsCompat mLastInsets; + public CollapsingToolbarLayout(Context context) { this(context, null); } @@ -150,12 +157,31 @@ public class CollapsingToolbarLayout extends FrameLayout { R.style.TextAppearance_AppCompat_Widget_ActionBar_Title); mCollapsingTextHelper.setCollapsedTextAppearance(tp); - mForegroundScrimColor = a.getColor( - R.styleable.CollapsingToolbarLayout_foregroundScrimColor, 0); + setContentScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_contentScrim)); + setStatusBarScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_statusBarScrim)); a.recycle(); setWillNotDraw(false); + + ViewCompat.setOnApplyWindowInsetsListener(this, + new android.support.v4.view.OnApplyWindowInsetsListener() { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, + WindowInsetsCompat insets) { + mLastInsets = insets; + + for (int i = 0, z = getChildCount(); i < z; i++) { + View child = getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.height != LayoutParams.MATCH_PARENT) { + lp.topMargin = insets.getSystemWindowInsetTop(); + } + } + return insets.consumeSystemWindowInsets(); + } + }); } @Override @@ -200,12 +226,24 @@ public class CollapsingToolbarLayout extends FrameLayout { // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below. // Instead, we draw it here, before our collapsing text. - if (mToolbar == null && Color.alpha(mCurrentForegroundColor) > 0) { - canvas.drawColor(mCurrentForegroundColor); + if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) { + mContentScrim.mutate().setAlpha(mScrimAlpha); + mContentScrim.draw(canvas); } // Let the collapsing text helper draw it's text mCollapsingTextHelper.draw(canvas); + + // Now draw the status bar scrim + if (mStatusBarScrim != null && mScrimAlpha > 0) { + final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; + if (topInset > 0) { + mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(), + topInset - mCurrentOffset); + mStatusBarScrim.mutate().setAlpha(mScrimAlpha); + mStatusBarScrim.draw(canvas); + } + } } @Override @@ -213,14 +251,23 @@ public class CollapsingToolbarLayout extends FrameLayout { // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present), // but in front of any other children which are behind it. To do this we intercept the // drawChild() call, and draw our scrim first when drawing the toolbar - if (child == mToolbar && Color.alpha(mCurrentForegroundColor) > 0) { - canvas.drawColor(mCurrentForegroundColor); + if (child == mToolbar && mContentScrim != null && mScrimAlpha > 0) { + mContentScrim.mutate().setAlpha(mScrimAlpha); + mContentScrim.draw(canvas); } // Carry on drawing the child... return super.drawChild(canvas, child, drawingTime); } + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mContentScrim != null) { + mContentScrim.setBounds(0, 0, w, h); + } + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); @@ -264,16 +311,16 @@ public class CollapsingToolbarLayout extends FrameLayout { } private void showScrim() { - if (!mScrimIsShown) { - animateScrim(Color.alpha(mForegroundScrimColor)); - mScrimIsShown = true; + if (!mScrimsAreShown) { + animateScrim(255); + mScrimsAreShown = true; } } private void hideScrim() { - if (mScrimIsShown) { + if (mScrimsAreShown) { animateScrim(0); - mScrimIsShown = false; + mScrimsAreShown = false; } } @@ -285,18 +332,14 @@ public class CollapsingToolbarLayout extends FrameLayout { mScrimAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimatorCompat animator) { - final int newColor = ColorUtils.setAlphaComponent( - mForegroundScrimColor, animator.getAnimatedIntValue()); - - if (newColor != mCurrentForegroundColor) { - mCurrentForegroundColor = newColor; - - // We need to manually invalidate ourselves and the Toolbar to ensure the scrim - // is drawn - invalidate(); - if (mToolbar != null) { - mToolbar.invalidate(); + final int newAlpha = animator.getAnimatedIntValue(); + if (newAlpha != mScrimAlpha) { + final Drawable contentScrim = mContentScrim; + if (contentScrim != null && mToolbar != null) { + ViewCompat.postInvalidateOnAnimation(mToolbar); } + mScrimAlpha = newAlpha; + ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this); } } }); @@ -304,44 +347,126 @@ public class CollapsingToolbarLayout extends FrameLayout { mScrimAnimator.cancel(); } - mScrimAnimator.setIntValues(Color.alpha(mCurrentForegroundColor), targetAlpha); + mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha); mScrimAnimator.start(); } /** - * Set the color to use for the foreground scrim. Providing a transparent color which disable - * the scrim. + * Set the drawable to use for the content scrim from resources. Providing null will disable + * the scrim functionality. * - * @param color ARGB color to use + * @param drawable the drawable to display * - * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor - * @see #getForegroundScrimColor() + * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim + * @see #getContentScrim() */ - public void setForegroundScrimColor(int color) { - mForegroundScrimColor = color; + public void setContentScrim(@Nullable Drawable drawable) { + if (mContentScrim != drawable) { + if (mContentScrim != null) { + mContentScrim.setCallback(null); + } + + mContentScrim = drawable; + drawable.setBounds(0, 0, getWidth(), getHeight()); + drawable.setCallback(this); + drawable.mutate().setAlpha(mScrimAlpha); + ViewCompat.postInvalidateOnAnimation(this); + } } /** - * Set the color to use for the foreground scrim from resources. Providing a transparent color - * which disable the scrim. + * Set the color to use for the content scrim. * - * @param resId color resource id + * @param color the color to display * - * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor - * @see #getForegroundScrimColor() + * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim + * @see #getContentScrim() */ - public void setForegroundScrimColorResource(@ColorRes int resId) { - mForegroundScrimColor = getResources().getColor(resId); + public void setContentScrimColor(int color) { + setContentScrim(new ColorDrawable(color)); } /** - * Returns the color which is used for the foreground scrim. + * Set the drawable to use for the content scrim from resources. * - * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_foregroundScrimColor - * @see #setForegroundScrimColor(int) + * @param resId drawable resource id + * + * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim + * @see #getContentScrim() */ - public int getForegroundScrimColor() { - return mForegroundScrimColor; + public void setContentScrimResource(@DrawableRes int resId) { + setContentScrim(ContextCompat.getDrawable(getContext(), resId)); + + } + + /** + * Returns the drawable which is used for the foreground scrim. + * + * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim + * @see #setContentScrim(Drawable) + */ + public Drawable getContentScrim() { + return mContentScrim; + } + + /** + * Set the drawable to use for the status bar scrim from resources. + * Providing null will disable the scrim functionality. + * + *

This scrim is only shown when we have been given a top system inset.

+ * + * @param drawable the drawable to display + * + * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim + * @see #getStatusBarScrim() + */ + public void setStatusBarScrim(@Nullable Drawable drawable) { + if (mStatusBarScrim != drawable) { + if (mStatusBarScrim != null) { + mStatusBarScrim.setCallback(null); + } + + mStatusBarScrim = drawable; + drawable.setCallback(this); + drawable.mutate().setAlpha(mScrimAlpha); + ViewCompat.postInvalidateOnAnimation(this); + } + } + + /** + * Set the color to use for the status bar scrim. + * + *

This scrim is only shown when we have been given a top system inset.

+ * + * @param color the color to display + * + * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim + * @see #getStatusBarScrim() + */ + public void setStatusBarScrimColor(int color) { + setStatusBarScrim(new ColorDrawable(color)); + } + + /** + * Set the drawable to use for the content scrim from resources. + * + * @param resId drawable resource id + * + * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim + * @see #getStatusBarScrim() + */ + public void setStatusBarScrimResource(@DrawableRes int resId) { + setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId)); + } + + /** + * Returns the drawable which is used for the status bar scrim. + * + * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim + * @see #setStatusBarScrim(Drawable) + */ + public Drawable getStatusBarScrim() { + return mStatusBarScrim; } /** @@ -382,6 +507,9 @@ public class CollapsingToolbarLayout extends FrameLayout { mCollapsingTextHelper.setExpandedTextColor(color); } + /** + * The additional offset used to define when to trigger the scrim visibility change. + */ final int getScrimTriggerOffset() { return 2 * ViewCompat.getMinimumHeight(this); } @@ -521,7 +649,10 @@ public class CollapsingToolbarLayout extends FrameLayout { private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener { @Override public void onOffsetChanged(AppBarLayout layout, int verticalOffset) { - int pinnedHeight = 0; + mCurrentOffset = verticalOffset; + + final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; + final int scrollRange = layout.getTotalScrollRange(); for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); @@ -530,10 +661,9 @@ public class CollapsingToolbarLayout extends FrameLayout { switch (lp.mCollapseMode) { case LayoutParams.COLLAPSE_MODE_PIN: - if (getHeight() + verticalOffset >= child.getHeight()) { + if (getHeight() - insetTop + verticalOffset >= child.getHeight()) { offsetHelper.setTopAndBottomOffset(-verticalOffset); } - pinnedHeight += child.getHeight(); break; case LayoutParams.COLLAPSE_MODE_PARALLAX: offsetHelper.setTopAndBottomOffset( @@ -542,22 +672,26 @@ public class CollapsingToolbarLayout extends FrameLayout { } } - // Show or hide the scrim if needed - if (Color.alpha(mForegroundScrimColor) > 0) { - if (getHeight() + verticalOffset < getScrimTriggerOffset()) { + // Show or hide the scrims if needed + if (mContentScrim != null || mStatusBarScrim != null) { + if (getHeight() + verticalOffset < getScrimTriggerOffset() + insetTop) { showScrim(); } else { hideScrim(); } } + if (mStatusBarScrim != null && insetTop > 0) { + ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this); + } + // Update the collapsing text's fraction final int expandRange = getHeight() - ViewCompat.getMinimumHeight( - CollapsingToolbarLayout.this); + CollapsingToolbarLayout.this) - insetTop; mCollapsingTextHelper.setExpansionFraction( Math.abs(verticalOffset) / (float) expandRange); - if (pinnedHeight > 0 && (getHeight() + verticalOffset) == pinnedHeight) { + if (Math.abs(verticalOffset) == scrollRange) { // If we have some pinned children, and we're offset to only show those views, // we want to be elevate ViewCompat.setElevation(layout, layout.getTargetElevation()); diff --git a/src/android/support/design/widget/CoordinatorLayout.java b/src/android/support/design/widget/CoordinatorLayout.java index bcda20a99..0003daada 100644 --- a/src/android/support/design/widget/CoordinatorLayout.java +++ b/src/android/support/design/widget/CoordinatorLayout.java @@ -23,13 +23,17 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.os.Build; import android.os.SystemClock; import android.support.design.R; +import android.support.v4.content.ContextCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; import android.support.v4.view.ViewCompat; +import android.support.v4.view.WindowInsetsCompat; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -82,8 +86,10 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen static { if (Build.VERSION.SDK_INT >= 21) { TOP_SORTED_CHILDREN_COMPARATOR = new ViewElevationComparator(); + INSETS_HELPER = new CoordinatorLayoutInsetsHelperLollipop(); } else { TOP_SORTED_CHILDREN_COMPARATOR = null; + INSETS_HELPER = null; } } @@ -113,6 +119,7 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen }; static final Comparator TOP_SORTED_CHILDREN_COMPARATOR; + static final CoordinatorLayoutInsetsHelper INSETS_HELPER; private final List mDependencySortedChildren = new ArrayList(); private final List mTempList1 = new ArrayList<>(); @@ -134,6 +141,10 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen private OnPreDrawListener mOnPreDrawListener; private boolean mNeedsPreDrawListener; + private WindowInsetsCompat mLastInsets; + private boolean mDrawStatusBarBackground; + private Drawable mStatusBarBackground; + private final NestedScrollingParentHelper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); @@ -149,7 +160,7 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen super(context, attrs, defStyleAttr); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout, - defStyleAttr, 0); + defStyleAttr, R.style.Widget_Design_CoordinatorLayout); final int keylineArrayRes = a.getResourceId(R.styleable.CoordinatorLayout_keylines, 0); if (keylineArrayRes != 0) { final Resources res = context.getResources(); @@ -160,7 +171,12 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen mKeylines[i] *= density; } } + mStatusBarBackground = a.getDrawable(R.styleable.CoordinatorLayout_statusBarBackground); a.recycle(); + + if (INSETS_HELPER != null) { + INSETS_HELPER.setupForWindowInsets(this, new ApplyInsetsListener()); + } } @Override @@ -191,6 +207,57 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen mIsAttachedToWindow = false; } + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param bg Background drawable to draw behind the status bar + */ + public void setStatusBarBackground(Drawable bg) { + mStatusBarBackground = bg; + invalidate(); + } + + /** + * Gets the drawable used to draw in the insets area for the status bar. + * + * @return The status bar background drawable, or null if none set + */ + public Drawable getStatusBarBackground() { + return mStatusBarBackground; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param resId Resource id of a background drawable to draw behind the status bar + */ + public void setStatusBarBackgroundResource(int resId) { + setStatusBarBackground(resId != 0 ? ContextCompat.getDrawable(getContext(), resId) : null); + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param color Color to use as a background drawable to draw behind the status bar + * in 0xAARRGGBB format. + */ + public void setStatusBarBackgroundColor(int color) { + setStatusBarBackground(new ColorDrawable(color)); + } + + private void setWindowInsets(WindowInsetsCompat insets) { + if (mLastInsets != insets) { + mLastInsets = insets; + mDrawStatusBarBackground = insets != null && insets.getSystemWindowInsetTop() > 0; + setWillNotDraw(!mDrawStatusBarBackground && getBackground() == null); + dispatchChildApplyWindowInsets(insets); + requestLayout(); + } + } + /** * Reset all Behavior-related tracking records either to clean up or in preparation * for a new event stream. This should be called when attached or detached from a window, @@ -518,6 +585,8 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); final int widthPadding = paddingLeft + paddingRight; final int heightPadding = paddingTop + paddingBottom; @@ -525,6 +594,8 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen int heightUsed = getSuggestedMinimumHeight(); int childState = 0; + final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this); + final int childCount = mDependencySortedChildren.size(); for (int i = 0; i < childCount; i++) { final View child = mDependencySortedChildren.get(i); @@ -545,11 +616,27 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen } } + int childWidthMeasureSpec = widthMeasureSpec; + int childHeightMeasureSpec = heightMeasureSpec; + if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) { + // We're set to handle insets but this child isn't, so we will measure the + // child as if there are no insets + final int horizInsets = mLastInsets.getSystemWindowInsetLeft() + + mLastInsets.getSystemWindowInsetRight(); + final int vertInsets = mLastInsets.getSystemWindowInsetTop() + + mLastInsets.getSystemWindowInsetBottom(); + + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + widthSize - horizInsets, widthMode); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + heightSize - vertInsets, heightMode); + } + final Behavior b = lp.getBehavior(); - if (b == null || !b.onMeasureChild(this, child, widthMeasureSpec, keylineWidthUsed, - heightMeasureSpec, 0)) { - onMeasureChild(child, widthMeasureSpec, keylineWidthUsed, - heightMeasureSpec, 0); + if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed, + childHeightMeasureSpec, 0)) { + onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed, + childHeightMeasureSpec, 0); } widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() + @@ -568,6 +655,35 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen setMeasuredDimension(width, height); } + private void dispatchChildApplyWindowInsets(WindowInsetsCompat insets) { + if (insets.isConsumed()) { + return; + } + + for (int i = 0, z = getChildCount(); i < z; i++) { + final View child = getChildAt(i); + if (ViewCompat.getFitsSystemWindows(child)) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final Behavior b = lp.getBehavior(); + + if (b != null) { + // If the view has a behavior, let it try first + insets = b.onApplyWindowInsets(this, child, insets); + if (insets.isConsumed()) { + // If it consumed the insets, break + break; + } + } + + // Now let the view try and consume them + insets = ViewCompat.dispatchApplyWindowInsets(child, insets); + if (insets.isConsumed()) { + break; + } + } + } + } + /** * Called to lay out each individual child view unless a * {@link CoordinatorLayout.Behavior Behavior} is present. The Behavior may choose to @@ -608,6 +724,18 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen } } + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + if (mDrawStatusBarBackground && mStatusBarBackground != null) { + final int inset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; + if (inset > 0) { + mStatusBarBackground.setBounds(0, 0, getWidth(), inset); + mStatusBarBackground.draw(c); + } + } + } + /** * Mark the last known child position rect for the given child view. * This will be used when checking if a child view's position has changed between frames. @@ -851,6 +979,17 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen getPaddingTop() + lp.topMargin, getWidth() - getPaddingRight() - lp.rightMargin, getHeight() - getPaddingBottom() - lp.bottomMargin); + + if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this) + && !ViewCompat.getFitsSystemWindows(child)) { + // If we're set to handle insets but this child isn't, then it has been measured as + // if there are no insets. We need to lay it out to match. + parent.left += mLastInsets.getSystemWindowInsetLeft(); + parent.top += mLastInsets.getSystemWindowInsetTop(); + parent.right -= mLastInsets.getSystemWindowInsetRight(); + parent.bottom -= mLastInsets.getSystemWindowInsetBottom(); + } + final Rect out = mTempRect2; GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), parent, out, layoutDirection); @@ -1885,6 +2024,25 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen float velocityX, float velocityY) { return false; } + + /** + * Called when the window insets have changed. + * + *

Any Behavior associated with the direct child of the CoordinatorLayout may elect + * to handle the window inset change on behalf of it's associated view. + *

+ * + * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is + * associated with + * @param child the child view of the CoordinatorLayout this Behavior is associated with + * @param insets the new window insets. + * + * @return The insets supplied, minus any insets that were consumed + */ + public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, + V child, WindowInsetsCompat insets) { + return insets; + } } /** @@ -2247,4 +2405,12 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen return true; } } + + final class ApplyInsetsListener implements android.support.v4.view.OnApplyWindowInsetsListener { + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + setWindowInsets(insets); + return insets.consumeSystemWindowInsets(); + } + } }