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.content.res.TypedArray; 21import android.graphics.Canvas; 22import android.graphics.Rect; 23import android.graphics.Typeface; 24import android.graphics.drawable.ColorDrawable; 25import android.graphics.drawable.Drawable; 26import android.support.annotation.ColorInt; 27import android.support.annotation.DrawableRes; 28import android.support.annotation.IntDef; 29import android.support.annotation.IntRange; 30import android.support.annotation.NonNull; 31import android.support.annotation.Nullable; 32import android.support.annotation.StyleRes; 33import android.support.design.R; 34import android.support.v4.content.ContextCompat; 35import android.support.v4.graphics.drawable.DrawableCompat; 36import android.support.v4.view.GravityCompat; 37import android.support.v4.view.ViewCompat; 38import android.support.v4.view.WindowInsetsCompat; 39import android.support.v7.widget.Toolbar; 40import android.text.TextUtils; 41import android.util.AttributeSet; 42import android.view.Gravity; 43import android.view.View; 44import android.view.ViewGroup; 45import android.view.ViewParent; 46import android.widget.FrameLayout; 47 48import java.lang.annotation.Retention; 49import java.lang.annotation.RetentionPolicy; 50 51/** 52 * CollapsingToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar. 53 * It is designed to be used as a direct child of a {@link AppBarLayout}. 54 * CollapsingToolbarLayout contains the following features: 55 * 56 * <h4>Collapsing title</h4> 57 * A title which is larger when the layout is fully visible but collapses and becomes smaller as 58 * the layout is scrolled off screen. You can set the title to display via 59 * {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the 60 * {@code collapsedTextAppearance} and {@code expandedTextAppearance} attributes. 61 * 62 * <h4>Content scrim</h4> 63 * A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold. 64 * You can change this via {@link #setContentScrim(Drawable)}. 65 * 66 * <h4>Status bar scrim</h4> 67 * A scrim which is show or hidden behind the status bar when the scroll position has hit a certain 68 * threshold. You can change this via {@link #setStatusBarScrim(Drawable)}. This only works 69 * on {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} devices when we set to fit system 70 * windows. 71 * 72 * <h4>Parallax scrolling children</h4> 73 * Child views can opt to be scrolled within this layout in a parallax fashion. 74 * See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and 75 * {@link LayoutParams#setParallaxMultiplier(float)}. 76 * 77 * <h4>Pinned position children</h4> 78 * Child views can opt to be pinned in space globally. This is useful when implementing a 79 * collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is 80 * moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}. 81 * 82 * <p><strong>Do not manually add views to the Toolbar at run time</strong>. 83 * We will add a 'dummy view' to the Toolbar which allows us to work out the available space 84 * for the title. This can interfere with any views which you add.</p> 85 * 86 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance 87 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance 88 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_contentScrim 89 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin 90 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart 91 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd 92 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom 93 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_statusBarScrim 94 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_toolbarId 95 */ 96public class CollapsingToolbarLayout extends FrameLayout { 97 98 private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600; 99 100 private boolean mRefreshToolbar = true; 101 private int mToolbarId; 102 private Toolbar mToolbar; 103 private View mToolbarDirectChild; 104 private View mDummyView; 105 106 private int mExpandedMarginStart; 107 private int mExpandedMarginTop; 108 private int mExpandedMarginEnd; 109 private int mExpandedMarginBottom; 110 111 private final Rect mTmpRect = new Rect(); 112 private final CollapsingTextHelper mCollapsingTextHelper; 113 private boolean mCollapsingTitleEnabled; 114 private boolean mDrawCollapsingTitle; 115 116 private Drawable mContentScrim; 117 private Drawable mStatusBarScrim; 118 private int mScrimAlpha; 119 private boolean mScrimsAreShown; 120 private ValueAnimatorCompat mScrimAnimator; 121 private long mScrimAnimationDuration; 122 private int mScrimVisibleHeightTrigger = -1; 123 124 private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener; 125 126 private int mCurrentOffset; 127 128 private WindowInsetsCompat mLastInsets; 129 130 public CollapsingToolbarLayout(Context context) { 131 this(context, null); 132 } 133 134 public CollapsingToolbarLayout(Context context, AttributeSet attrs) { 135 this(context, attrs, 0); 136 } 137 138 public CollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) { 139 super(context, attrs, defStyleAttr); 140 141 ThemeUtils.checkAppCompatTheme(context); 142 143 mCollapsingTextHelper = new CollapsingTextHelper(this); 144 mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR); 145 146 TypedArray a = context.obtainStyledAttributes(attrs, 147 R.styleable.CollapsingToolbarLayout, defStyleAttr, 148 R.style.Widget_Design_CollapsingToolbar); 149 150 mCollapsingTextHelper.setExpandedTextGravity( 151 a.getInt(R.styleable.CollapsingToolbarLayout_expandedTitleGravity, 152 GravityCompat.START | Gravity.BOTTOM)); 153 mCollapsingTextHelper.setCollapsedTextGravity( 154 a.getInt(R.styleable.CollapsingToolbarLayout_collapsedTitleGravity, 155 GravityCompat.START | Gravity.CENTER_VERTICAL)); 156 157 mExpandedMarginStart = mExpandedMarginTop = mExpandedMarginEnd = mExpandedMarginBottom = 158 a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_expandedTitleMargin, 0); 159 160 if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart)) { 161 mExpandedMarginStart = a.getDimensionPixelSize( 162 R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart, 0); 163 } 164 if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd)) { 165 mExpandedMarginEnd = a.getDimensionPixelSize( 166 R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd, 0); 167 } 168 if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop)) { 169 mExpandedMarginTop = a.getDimensionPixelSize( 170 R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop, 0); 171 } 172 if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom)) { 173 mExpandedMarginBottom = a.getDimensionPixelSize( 174 R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom, 0); 175 } 176 177 mCollapsingTitleEnabled = a.getBoolean( 178 R.styleable.CollapsingToolbarLayout_titleEnabled, true); 179 setTitle(a.getText(R.styleable.CollapsingToolbarLayout_title)); 180 181 // First load the default text appearances 182 mCollapsingTextHelper.setExpandedTextAppearance( 183 R.style.TextAppearance_Design_CollapsingToolbar_Expanded); 184 mCollapsingTextHelper.setCollapsedTextAppearance( 185 android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Title); 186 187 // Now overlay any custom text appearances 188 if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance)) { 189 mCollapsingTextHelper.setExpandedTextAppearance( 190 a.getResourceId( 191 R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance, 0)); 192 } 193 if (a.hasValue(R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance)) { 194 mCollapsingTextHelper.setCollapsedTextAppearance( 195 a.getResourceId( 196 R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance, 0)); 197 } 198 199 mScrimVisibleHeightTrigger = a.getInt( 200 R.styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger, -1); 201 202 mScrimAnimationDuration = a.getInt( 203 R.styleable.CollapsingToolbarLayout_scrimAnimationDuration, 204 DEFAULT_SCRIM_ANIMATION_DURATION); 205 206 setContentScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_contentScrim)); 207 setStatusBarScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_statusBarScrim)); 208 209 mToolbarId = a.getResourceId(R.styleable.CollapsingToolbarLayout_toolbarId, -1); 210 211 a.recycle(); 212 213 setWillNotDraw(false); 214 215 ViewCompat.setOnApplyWindowInsetsListener(this, 216 new android.support.v4.view.OnApplyWindowInsetsListener() { 217 @Override 218 public WindowInsetsCompat onApplyWindowInsets(View v, 219 WindowInsetsCompat insets) { 220 return setWindowInsets(insets); 221 } 222 }); 223 } 224 225 @Override 226 protected void onAttachedToWindow() { 227 super.onAttachedToWindow(); 228 229 // Add an OnOffsetChangedListener if possible 230 final ViewParent parent = getParent(); 231 if (parent instanceof AppBarLayout) { 232 if (mOnOffsetChangedListener == null) { 233 mOnOffsetChangedListener = new OffsetUpdateListener(); 234 } 235 ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener); 236 } 237 238 // We're attached, so lets request an inset dispatch 239 ViewCompat.requestApplyInsets(this); 240 } 241 242 @Override 243 protected void onDetachedFromWindow() { 244 // Remove our OnOffsetChangedListener if possible and it exists 245 final ViewParent parent = getParent(); 246 if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) { 247 ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener); 248 } 249 250 super.onDetachedFromWindow(); 251 } 252 253 private WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) { 254 if (mLastInsets != insets) { 255 mLastInsets = insets; 256 requestLayout(); 257 } 258 return insets.consumeSystemWindowInsets(); 259 } 260 261 @Override 262 public void draw(Canvas canvas) { 263 super.draw(canvas); 264 265 // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below. 266 // Instead, we draw it here, before our collapsing text. 267 ensureToolbar(); 268 if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) { 269 mContentScrim.mutate().setAlpha(mScrimAlpha); 270 mContentScrim.draw(canvas); 271 } 272 273 // Let the collapsing text helper draw its text 274 if (mCollapsingTitleEnabled && mDrawCollapsingTitle) { 275 mCollapsingTextHelper.draw(canvas); 276 } 277 278 // Now draw the status bar scrim 279 if (mStatusBarScrim != null && mScrimAlpha > 0) { 280 final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 281 if (topInset > 0) { 282 mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(), 283 topInset - mCurrentOffset); 284 mStatusBarScrim.mutate().setAlpha(mScrimAlpha); 285 mStatusBarScrim.draw(canvas); 286 } 287 } 288 } 289 290 @Override 291 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 292 // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present), 293 // but in front of any other children which are behind it. To do this we intercept the 294 // drawChild() call, and draw our scrim first when drawing the toolbar 295 ensureToolbar(); 296 if (child == mToolbar && mContentScrim != null && mScrimAlpha > 0) { 297 mContentScrim.mutate().setAlpha(mScrimAlpha); 298 mContentScrim.draw(canvas); 299 } 300 301 // Carry on drawing the child... 302 return super.drawChild(canvas, child, drawingTime); 303 } 304 305 @Override 306 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 307 super.onSizeChanged(w, h, oldw, oldh); 308 if (mContentScrim != null) { 309 mContentScrim.setBounds(0, 0, w, h); 310 } 311 } 312 313 private void ensureToolbar() { 314 if (!mRefreshToolbar) { 315 return; 316 } 317 318 // First clear out the current Toolbar 319 mToolbar = null; 320 mToolbarDirectChild = null; 321 322 if (mToolbarId != -1) { 323 // If we have an ID set, try and find it and it's direct parent to us 324 mToolbar = (Toolbar) findViewById(mToolbarId); 325 if (mToolbar != null) { 326 mToolbarDirectChild = findDirectChild(mToolbar); 327 } 328 } 329 330 if (mToolbar == null) { 331 // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find 332 // one from our direct children 333 Toolbar toolbar = null; 334 for (int i = 0, count = getChildCount(); i < count; i++) { 335 final View child = getChildAt(i); 336 if (child instanceof Toolbar) { 337 toolbar = (Toolbar) child; 338 break; 339 } 340 } 341 mToolbar = toolbar; 342 } 343 344 updateDummyView(); 345 mRefreshToolbar = false; 346 } 347 348 /** 349 * Returns the direct child of this layout, which itself is the ancestor of the 350 * given view. 351 */ 352 private View findDirectChild(final View descendant) { 353 View directChild = descendant; 354 for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) { 355 if (p instanceof View) { 356 directChild = (View) p; 357 } 358 } 359 return directChild; 360 } 361 362 private void updateDummyView() { 363 if (!mCollapsingTitleEnabled && mDummyView != null) { 364 // If we have a dummy view and we have our title disabled, remove it from its parent 365 final ViewParent parent = mDummyView.getParent(); 366 if (parent instanceof ViewGroup) { 367 ((ViewGroup) parent).removeView(mDummyView); 368 } 369 } 370 if (mCollapsingTitleEnabled && mToolbar != null) { 371 if (mDummyView == null) { 372 mDummyView = new View(getContext()); 373 } 374 if (mDummyView.getParent() == null) { 375 mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 376 } 377 } 378 } 379 380 @Override 381 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 382 ensureToolbar(); 383 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 384 } 385 386 @Override 387 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 388 super.onLayout(changed, left, top, right, bottom); 389 390 // Update the collapsed bounds by getting it's transformed bounds. This needs to be done 391 // before the children are offset below 392 if (mCollapsingTitleEnabled && mDummyView != null) { 393 // We only draw the title if the dummy view is being displayed (Toolbar removes 394 // views if there is no space) 395 mDrawCollapsingTitle = ViewCompat.isAttachedToWindow(mDummyView) 396 && mDummyView.getVisibility() == VISIBLE; 397 398 if (mDrawCollapsingTitle) { 399 final boolean isRtl = ViewCompat.getLayoutDirection(this) 400 == ViewCompat.LAYOUT_DIRECTION_RTL; 401 402 // Update the collapsed bounds 403 int bottomOffset = 0; 404 if (mToolbarDirectChild != null && mToolbarDirectChild != this) { 405 final LayoutParams lp = (LayoutParams) mToolbarDirectChild.getLayoutParams(); 406 bottomOffset = lp.bottomMargin; 407 } 408 ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect); 409 mCollapsingTextHelper.setCollapsedBounds( 410 mTmpRect.left + (isRtl 411 ? mToolbar.getTitleMarginEnd() 412 : mToolbar.getTitleMarginStart()), 413 bottom + mToolbar.getTitleMarginTop() - mTmpRect.height() - bottomOffset, 414 mTmpRect.right + (isRtl 415 ? mToolbar.getTitleMarginStart() 416 : mToolbar.getTitleMarginEnd()), 417 bottom - bottomOffset - mToolbar.getTitleMarginBottom()); 418 419 // Update the expanded bounds 420 mCollapsingTextHelper.setExpandedBounds( 421 isRtl ? mExpandedMarginEnd : mExpandedMarginStart, 422 mTmpRect.bottom + mExpandedMarginTop, 423 right - left - (isRtl ? mExpandedMarginStart : mExpandedMarginEnd), 424 bottom - top - mExpandedMarginBottom); 425 // Now recalculate using the new bounds 426 mCollapsingTextHelper.recalculate(); 427 } 428 } 429 430 // Update our child view offset helpers 431 for (int i = 0, z = getChildCount(); i < z; i++) { 432 final View child = getChildAt(i); 433 434 if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) { 435 final int insetTop = mLastInsets.getSystemWindowInsetTop(); 436 if (child.getTop() < insetTop) { 437 // If the child isn't set to fit system windows but is drawing within the inset 438 // offset it down 439 ViewCompat.offsetTopAndBottom(child, insetTop); 440 } 441 } 442 443 getViewOffsetHelper(child).onViewLayout(); 444 } 445 446 // Finally, set our minimum height to enable proper AppBarLayout collapsing 447 if (mToolbar != null) { 448 if (mCollapsingTitleEnabled && TextUtils.isEmpty(mCollapsingTextHelper.getText())) { 449 // If we do not currently have a title, try and grab it from the Toolbar 450 mCollapsingTextHelper.setText(mToolbar.getTitle()); 451 } 452 if (mToolbarDirectChild == null || mToolbarDirectChild == this) { 453 setMinimumHeight(getHeightWithMargins(mToolbar)); 454 } else { 455 setMinimumHeight(getHeightWithMargins(mToolbarDirectChild)); 456 } 457 } 458 } 459 460 private static int getHeightWithMargins(@NonNull final View view) { 461 final ViewGroup.LayoutParams lp = view.getLayoutParams(); 462 if (lp instanceof MarginLayoutParams) { 463 final MarginLayoutParams mlp = (MarginLayoutParams) lp; 464 return view.getHeight() + mlp.topMargin + mlp.bottomMargin; 465 } 466 return view.getHeight(); 467 } 468 469 private static ViewOffsetHelper getViewOffsetHelper(View view) { 470 ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(R.id.view_offset_helper); 471 if (offsetHelper == null) { 472 offsetHelper = new ViewOffsetHelper(view); 473 view.setTag(R.id.view_offset_helper, offsetHelper); 474 } 475 return offsetHelper; 476 } 477 478 /** 479 * Sets the title to be displayed by this view, if enabled. 480 * 481 * @see #setTitleEnabled(boolean) 482 * @see #getTitle() 483 * 484 * @attr ref R.styleable#CollapsingToolbarLayout_title 485 */ 486 public void setTitle(@Nullable CharSequence title) { 487 mCollapsingTextHelper.setText(title); 488 } 489 490 /** 491 * Returns the title currently being displayed by this view. If the title is not enabled, then 492 * this will return {@code null}. 493 * 494 * @attr ref R.styleable#CollapsingToolbarLayout_title 495 */ 496 @Nullable 497 public CharSequence getTitle() { 498 return mCollapsingTitleEnabled ? mCollapsingTextHelper.getText() : null; 499 } 500 501 /** 502 * Sets whether this view should display its own title. 503 * 504 * <p>The title displayed by this view will shrink and grow based on the scroll offset.</p> 505 * 506 * @see #setTitle(CharSequence) 507 * @see #isTitleEnabled() 508 * 509 * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled 510 */ 511 public void setTitleEnabled(boolean enabled) { 512 if (enabled != mCollapsingTitleEnabled) { 513 mCollapsingTitleEnabled = enabled; 514 updateDummyView(); 515 requestLayout(); 516 } 517 } 518 519 /** 520 * Returns whether this view is currently displaying its own title. 521 * 522 * @see #setTitleEnabled(boolean) 523 * 524 * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled 525 */ 526 public boolean isTitleEnabled() { 527 return mCollapsingTitleEnabled; 528 } 529 530 /** 531 * Set whether the content scrim and/or status bar scrim should be shown or not. Any change 532 * in the vertical scroll may overwrite this value. Any visibility change will be animated if 533 * this view has already been laid out. 534 * 535 * @param shown whether the scrims should be shown 536 * 537 * @see #getStatusBarScrim() 538 * @see #getContentScrim() 539 */ 540 public void setScrimsShown(boolean shown) { 541 setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode()); 542 } 543 544 /** 545 * Set whether the content scrim and/or status bar scrim should be shown or not. Any change 546 * in the vertical scroll may overwrite this value. 547 * 548 * @param shown whether the scrims should be shown 549 * @param animate whether to animate the visibility change 550 * 551 * @see #getStatusBarScrim() 552 * @see #getContentScrim() 553 */ 554 public void setScrimsShown(boolean shown, boolean animate) { 555 if (mScrimsAreShown != shown) { 556 if (animate) { 557 animateScrim(shown ? 0xFF : 0x0); 558 } else { 559 setScrimAlpha(shown ? 0xFF : 0x0); 560 } 561 mScrimsAreShown = shown; 562 } 563 } 564 565 private void animateScrim(int targetAlpha) { 566 ensureToolbar(); 567 if (mScrimAnimator == null) { 568 mScrimAnimator = ViewUtils.createAnimator(); 569 mScrimAnimator.setDuration(mScrimAnimationDuration); 570 mScrimAnimator.setInterpolator( 571 targetAlpha > mScrimAlpha 572 ? AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR 573 : AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR); 574 mScrimAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 575 @Override 576 public void onAnimationUpdate(ValueAnimatorCompat animator) { 577 setScrimAlpha(animator.getAnimatedIntValue()); 578 } 579 }); 580 } else if (mScrimAnimator.isRunning()) { 581 mScrimAnimator.cancel(); 582 } 583 584 mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha); 585 mScrimAnimator.start(); 586 } 587 588 private void setScrimAlpha(int alpha) { 589 if (alpha != mScrimAlpha) { 590 final Drawable contentScrim = mContentScrim; 591 if (contentScrim != null && mToolbar != null) { 592 ViewCompat.postInvalidateOnAnimation(mToolbar); 593 } 594 mScrimAlpha = alpha; 595 ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this); 596 } 597 } 598 599 /** 600 * Set the drawable to use for the content scrim from resources. Providing null will disable 601 * the scrim functionality. 602 * 603 * @param drawable the drawable to display 604 * 605 * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 606 * @see #getContentScrim() 607 */ 608 public void setContentScrim(@Nullable Drawable drawable) { 609 if (mContentScrim != drawable) { 610 if (mContentScrim != null) { 611 mContentScrim.setCallback(null); 612 } 613 mContentScrim = drawable != null ? drawable.mutate() : null; 614 if (mContentScrim != null) { 615 mContentScrim.setBounds(0, 0, getWidth(), getHeight()); 616 mContentScrim.setCallback(this); 617 mContentScrim.setAlpha(mScrimAlpha); 618 } 619 ViewCompat.postInvalidateOnAnimation(this); 620 } 621 } 622 623 /** 624 * Set the color to use for the content scrim. 625 * 626 * @param color the color to display 627 * 628 * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 629 * @see #getContentScrim() 630 */ 631 public void setContentScrimColor(@ColorInt int color) { 632 setContentScrim(new ColorDrawable(color)); 633 } 634 635 /** 636 * Set the drawable to use for the content scrim from resources. 637 * 638 * @param resId drawable resource id 639 * 640 * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 641 * @see #getContentScrim() 642 */ 643 public void setContentScrimResource(@DrawableRes int resId) { 644 setContentScrim(ContextCompat.getDrawable(getContext(), resId)); 645 646 } 647 648 /** 649 * Returns the drawable which is used for the foreground scrim. 650 * 651 * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 652 * @see #setContentScrim(Drawable) 653 */ 654 @Nullable 655 public Drawable getContentScrim() { 656 return mContentScrim; 657 } 658 659 /** 660 * Set the drawable to use for the status bar scrim from resources. 661 * Providing null will disable the scrim functionality. 662 * 663 * <p>This scrim is only shown when we have been given a top system inset.</p> 664 * 665 * @param drawable the drawable to display 666 * 667 * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 668 * @see #getStatusBarScrim() 669 */ 670 public void setStatusBarScrim(@Nullable Drawable drawable) { 671 if (mStatusBarScrim != drawable) { 672 if (mStatusBarScrim != null) { 673 mStatusBarScrim.setCallback(null); 674 } 675 mStatusBarScrim = drawable != null ? drawable.mutate() : null; 676 if (mStatusBarScrim != null) { 677 if (mStatusBarScrim.isStateful()) { 678 mStatusBarScrim.setState(getDrawableState()); 679 } 680 DrawableCompat.setLayoutDirection(mStatusBarScrim, 681 ViewCompat.getLayoutDirection(this)); 682 mStatusBarScrim.setVisible(getVisibility() == VISIBLE, false); 683 mStatusBarScrim.setCallback(this); 684 mStatusBarScrim.setAlpha(mScrimAlpha); 685 } 686 ViewCompat.postInvalidateOnAnimation(this); 687 } 688 } 689 690 @Override 691 protected void drawableStateChanged() { 692 super.drawableStateChanged(); 693 694 final int[] state = getDrawableState(); 695 boolean changed = false; 696 697 Drawable d = mStatusBarScrim; 698 if (d != null && d.isStateful()) { 699 changed |= d.setState(state); 700 } 701 d = mContentScrim; 702 if (d != null && d.isStateful()) { 703 changed |= d.setState(state); 704 } 705 706 if (changed) { 707 invalidate(); 708 } 709 } 710 711 @Override 712 protected boolean verifyDrawable(Drawable who) { 713 return super.verifyDrawable(who) || who == mContentScrim || who == mStatusBarScrim; 714 } 715 716 @Override 717 public void setVisibility(int visibility) { 718 super.setVisibility(visibility); 719 720 final boolean visible = visibility == VISIBLE; 721 if (mStatusBarScrim != null && mStatusBarScrim.isVisible() != visible) { 722 mStatusBarScrim.setVisible(visible, false); 723 } 724 if (mContentScrim != null && mContentScrim.isVisible() != visible) { 725 mContentScrim.setVisible(visible, false); 726 } 727 } 728 729 /** 730 * Set the color to use for the status bar scrim. 731 * 732 * <p>This scrim is only shown when we have been given a top system inset.</p> 733 * 734 * @param color the color to display 735 * 736 * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 737 * @see #getStatusBarScrim() 738 */ 739 public void setStatusBarScrimColor(@ColorInt int color) { 740 setStatusBarScrim(new ColorDrawable(color)); 741 } 742 743 /** 744 * Set the drawable to use for the content scrim from resources. 745 * 746 * @param resId drawable resource id 747 * 748 * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 749 * @see #getStatusBarScrim() 750 */ 751 public void setStatusBarScrimResource(@DrawableRes int resId) { 752 setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId)); 753 } 754 755 /** 756 * Returns the drawable which is used for the status bar scrim. 757 * 758 * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 759 * @see #setStatusBarScrim(Drawable) 760 */ 761 @Nullable 762 public Drawable getStatusBarScrim() { 763 return mStatusBarScrim; 764 } 765 766 /** 767 * Sets the text color and size for the collapsed title from the specified 768 * TextAppearance resource. 769 * 770 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance 771 */ 772 public void setCollapsedTitleTextAppearance(@StyleRes int resId) { 773 mCollapsingTextHelper.setCollapsedTextAppearance(resId); 774 } 775 776 /** 777 * Sets the text color of the collapsed title. 778 * 779 * @param color The new text color in ARGB format 780 */ 781 public void setCollapsedTitleTextColor(@ColorInt int color) { 782 mCollapsingTextHelper.setCollapsedTextColor(color); 783 } 784 785 /** 786 * Sets the horizontal alignment of the collapsed title and the vertical gravity that will 787 * be used when there is extra space in the collapsed bounds beyond what is required for 788 * the title itself. 789 * 790 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity 791 */ 792 public void setCollapsedTitleGravity(int gravity) { 793 mCollapsingTextHelper.setCollapsedTextGravity(gravity); 794 } 795 796 /** 797 * Returns the horizontal and vertical alignment for title when collapsed. 798 * 799 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity 800 */ 801 public int getCollapsedTitleGravity() { 802 return mCollapsingTextHelper.getCollapsedTextGravity(); 803 } 804 805 /** 806 * Sets the text color and size for the expanded title from the specified 807 * TextAppearance resource. 808 * 809 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance 810 */ 811 public void setExpandedTitleTextAppearance(@StyleRes int resId) { 812 mCollapsingTextHelper.setExpandedTextAppearance(resId); 813 } 814 815 /** 816 * Sets the text color of the expanded title. 817 * 818 * @param color The new text color in ARGB format 819 */ 820 public void setExpandedTitleColor(@ColorInt int color) { 821 mCollapsingTextHelper.setExpandedTextColor(color); 822 } 823 824 /** 825 * Sets the horizontal alignment of the expanded title and the vertical gravity that will 826 * be used when there is extra space in the expanded bounds beyond what is required for 827 * the title itself. 828 * 829 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity 830 */ 831 public void setExpandedTitleGravity(int gravity) { 832 mCollapsingTextHelper.setExpandedTextGravity(gravity); 833 } 834 835 /** 836 * Returns the horizontal and vertical alignment for title when expanded. 837 * 838 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity 839 */ 840 public int getExpandedTitleGravity() { 841 return mCollapsingTextHelper.getExpandedTextGravity(); 842 } 843 844 /** 845 * Set the typeface to use for the collapsed title. 846 * 847 * @param typeface typeface to use, or {@code null} to use the default. 848 */ 849 public void setCollapsedTitleTypeface(@Nullable Typeface typeface) { 850 mCollapsingTextHelper.setCollapsedTypeface(typeface); 851 } 852 853 /** 854 * Returns the typeface used for the collapsed title. 855 */ 856 @NonNull 857 public Typeface getCollapsedTitleTypeface() { 858 return mCollapsingTextHelper.getCollapsedTypeface(); 859 } 860 861 /** 862 * Set the typeface to use for the expanded title. 863 * 864 * @param typeface typeface to use, or {@code null} to use the default. 865 */ 866 public void setExpandedTitleTypeface(@Nullable Typeface typeface) { 867 mCollapsingTextHelper.setExpandedTypeface(typeface); 868 } 869 870 /** 871 * Returns the typeface used for the expanded title. 872 */ 873 @NonNull 874 public Typeface getExpandedTitleTypeface() { 875 return mCollapsingTextHelper.getExpandedTypeface(); 876 } 877 878 /** 879 * Sets the expanded title margins. 880 * 881 * @param start the starting title margin in pixels 882 * @param top the top title margin in pixels 883 * @param end the ending title margin in pixels 884 * @param bottom the bottom title margin in pixels 885 * 886 * @see #getExpandedTitleMarginStart() 887 * @see #getExpandedTitleMarginTop() 888 * @see #getExpandedTitleMarginEnd() 889 * @see #getExpandedTitleMarginBottom() 890 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin 891 */ 892 public void setExpandedTitleMargin(int start, int top, int end, int bottom) { 893 mExpandedMarginStart = start; 894 mExpandedMarginTop = top; 895 mExpandedMarginEnd = end; 896 mExpandedMarginBottom = bottom; 897 requestLayout(); 898 } 899 900 /** 901 * @return the starting expanded title margin in pixels 902 * 903 * @see #setExpandedTitleMarginStart(int) 904 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart 905 */ 906 public int getExpandedTitleMarginStart() { 907 return mExpandedMarginStart; 908 } 909 910 /** 911 * Sets the starting expanded title margin in pixels. 912 * 913 * @param margin the starting title margin in pixels 914 * @see #getExpandedTitleMarginStart() 915 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart 916 */ 917 public void setExpandedTitleMarginStart(int margin) { 918 mExpandedMarginStart = margin; 919 requestLayout(); 920 } 921 922 /** 923 * @return the top expanded title margin in pixels 924 * @see #setExpandedTitleMarginTop(int) 925 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop 926 */ 927 public int getExpandedTitleMarginTop() { 928 return mExpandedMarginTop; 929 } 930 931 /** 932 * Sets the top expanded title margin in pixels. 933 * 934 * @param margin the top title margin in pixels 935 * @see #getExpandedTitleMarginTop() 936 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop 937 */ 938 public void setExpandedTitleMarginTop(int margin) { 939 mExpandedMarginTop = margin; 940 requestLayout(); 941 } 942 943 /** 944 * @return the ending expanded title margin in pixels 945 * @see #setExpandedTitleMarginEnd(int) 946 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd 947 */ 948 public int getExpandedTitleMarginEnd() { 949 return mExpandedMarginEnd; 950 } 951 952 /** 953 * Sets the ending expanded title margin in pixels. 954 * 955 * @param margin the ending title margin in pixels 956 * @see #getExpandedTitleMarginEnd() 957 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd 958 */ 959 public void setExpandedTitleMarginEnd(int margin) { 960 mExpandedMarginEnd = margin; 961 requestLayout(); 962 } 963 964 /** 965 * @return the bottom expanded title margin in pixels 966 * @see #setExpandedTitleMarginBottom(int) 967 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom 968 */ 969 public int getExpandedTitleMarginBottom() { 970 return mExpandedMarginBottom; 971 } 972 973 /** 974 * Sets the bottom expanded title margin in pixels. 975 * 976 * @param margin the bottom title margin in pixels 977 * @see #getExpandedTitleMarginBottom() 978 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom 979 */ 980 public void setExpandedTitleMarginBottom(int margin) { 981 mExpandedMarginBottom = margin; 982 requestLayout(); 983 } 984 985 /** 986 * Set the amount of visible height in pixels used to define when to trigger a scrim 987 * visibility change. 988 * 989 * <p>If the visible height of this view is less than the given value, the scrims will be 990 * made visible, otherwise they are hidden.</p> 991 * 992 * @param height value in pixels used to define when to trigger a scrim visibility change 993 * 994 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimVisibleHeightTrigger 995 */ 996 public void setScrimVisibleHeightTrigger(@IntRange(from = 0) final int height) { 997 if (mScrimVisibleHeightTrigger != height) { 998 mScrimVisibleHeightTrigger = height; 999 // Update the scrim visibilty 1000 updateScrimVisibility(); 1001 } 1002 } 1003 1004 /** 1005 * Returns the amount of visible height in pixels used to define when to trigger a scrim 1006 * visibility change. 1007 * 1008 * @see #setScrimTriggerOffset(int) 1009 */ 1010 public int getScrimVisibleHeightTrigger() { 1011 if (mScrimVisibleHeightTrigger >= 0) { 1012 // If we have one explictly set, return it 1013 return mScrimVisibleHeightTrigger; 1014 } 1015 1016 // Otherwise we'll use the default computed value 1017 final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 1018 1019 final int minHeight = ViewCompat.getMinimumHeight(this); 1020 if (minHeight > 0) { 1021 // If we have a minHeight set, lets use 2 * minHeight (capped at our height) 1022 return Math.min((minHeight * 2) + insetTop, getHeight()); 1023 } 1024 1025 // If we reach here then we don't have a min height set. Instead we'll take a 1026 // guess at 1/3 of our height being visible 1027 return getHeight() / 3; 1028 } 1029 1030 /** 1031 * Set the duration used for scrim visibility animations. 1032 * 1033 * @param duration the duration to use in milliseconds 1034 * 1035 * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimAnimationDuration 1036 */ 1037 public void setScrimAnimationDuration(@IntRange(from = 0) final long duration) { 1038 mScrimAnimationDuration = duration; 1039 } 1040 1041 /** 1042 * Returns the duration in milliseconds used for scrim visibility animations. 1043 */ 1044 public long getScrimAnimationDuration() { 1045 return mScrimAnimationDuration; 1046 } 1047 1048 @Override 1049 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1050 return p instanceof LayoutParams; 1051 } 1052 1053 @Override 1054 protected LayoutParams generateDefaultLayoutParams() { 1055 return new LayoutParams(super.generateDefaultLayoutParams()); 1056 } 1057 1058 @Override 1059 public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { 1060 return new LayoutParams(getContext(), attrs); 1061 } 1062 1063 @Override 1064 protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1065 return new LayoutParams(p); 1066 } 1067 1068 public static class LayoutParams extends FrameLayout.LayoutParams { 1069 1070 private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f; 1071 1072 /** @hide */ 1073 @IntDef({ 1074 COLLAPSE_MODE_OFF, 1075 COLLAPSE_MODE_PIN, 1076 COLLAPSE_MODE_PARALLAX 1077 }) 1078 @Retention(RetentionPolicy.SOURCE) 1079 @interface CollapseMode {} 1080 1081 /** 1082 * The view will act as normal with no collapsing behavior. 1083 */ 1084 public static final int COLLAPSE_MODE_OFF = 0; 1085 1086 /** 1087 * The view will pin in place until it reaches the bottom of the 1088 * {@link CollapsingToolbarLayout}. 1089 */ 1090 public static final int COLLAPSE_MODE_PIN = 1; 1091 1092 /** 1093 * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)} 1094 * to change the multiplier used. 1095 */ 1096 public static final int COLLAPSE_MODE_PARALLAX = 2; 1097 1098 int mCollapseMode = COLLAPSE_MODE_OFF; 1099 float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER; 1100 1101 public LayoutParams(Context c, AttributeSet attrs) { 1102 super(c, attrs); 1103 1104 TypedArray a = c.obtainStyledAttributes(attrs, 1105 R.styleable.CollapsingToolbarLayout_Layout); 1106 mCollapseMode = a.getInt( 1107 R.styleable.CollapsingToolbarLayout_Layout_layout_collapseMode, 1108 COLLAPSE_MODE_OFF); 1109 setParallaxMultiplier(a.getFloat( 1110 R.styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier, 1111 DEFAULT_PARALLAX_MULTIPLIER)); 1112 a.recycle(); 1113 } 1114 1115 public LayoutParams(int width, int height) { 1116 super(width, height); 1117 } 1118 1119 public LayoutParams(int width, int height, int gravity) { 1120 super(width, height, gravity); 1121 } 1122 1123 public LayoutParams(ViewGroup.LayoutParams p) { 1124 super(p); 1125 } 1126 1127 public LayoutParams(MarginLayoutParams source) { 1128 super(source); 1129 } 1130 1131 public LayoutParams(FrameLayout.LayoutParams source) { 1132 super(source); 1133 } 1134 1135 /** 1136 * Set the collapse mode. 1137 * 1138 * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} 1139 * or {@link #COLLAPSE_MODE_PARALLAX}. 1140 */ 1141 public void setCollapseMode(@CollapseMode int collapseMode) { 1142 mCollapseMode = collapseMode; 1143 } 1144 1145 /** 1146 * Returns the requested collapse mode. 1147 * 1148 * @return the current mode. One of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} 1149 * or {@link #COLLAPSE_MODE_PARALLAX}. 1150 */ 1151 @CollapseMode 1152 public int getCollapseMode() { 1153 return mCollapseMode; 1154 } 1155 1156 /** 1157 * Set the parallax scroll multiplier used in conjunction with 1158 * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all, 1159 * {@code 1.0f} indicates normal scroll movement. 1160 * 1161 * @param multiplier the multiplier. 1162 * 1163 * @see #getParallaxMultiplier() 1164 */ 1165 public void setParallaxMultiplier(float multiplier) { 1166 mParallaxMult = multiplier; 1167 } 1168 1169 /** 1170 * Returns the parallax scroll multiplier used in conjunction with 1171 * {@link #COLLAPSE_MODE_PARALLAX}. 1172 * 1173 * @see #setParallaxMultiplier(float) 1174 */ 1175 public float getParallaxMultiplier() { 1176 return mParallaxMult; 1177 } 1178 } 1179 1180 /** 1181 * Show or hide the scrims if needed 1182 */ 1183 final void updateScrimVisibility() { 1184 if (mContentScrim != null || mStatusBarScrim != null) { 1185 setScrimsShown(getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger()); 1186 } 1187 } 1188 1189 private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener { 1190 @Override 1191 public void onOffsetChanged(AppBarLayout layout, int verticalOffset) { 1192 mCurrentOffset = verticalOffset; 1193 1194 final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 1195 1196 for (int i = 0, z = getChildCount(); i < z; i++) { 1197 final View child = getChildAt(i); 1198 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1199 final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child); 1200 1201 switch (lp.mCollapseMode) { 1202 case LayoutParams.COLLAPSE_MODE_PIN: 1203 if (getHeight() - insetTop + verticalOffset >= child.getHeight()) { 1204 offsetHelper.setTopAndBottomOffset(-verticalOffset); 1205 } 1206 break; 1207 case LayoutParams.COLLAPSE_MODE_PARALLAX: 1208 offsetHelper.setTopAndBottomOffset( 1209 Math.round(-verticalOffset * lp.mParallaxMult)); 1210 break; 1211 } 1212 } 1213 1214 // Show or hide the scrims if needed 1215 updateScrimVisibility(); 1216 1217 if (mStatusBarScrim != null && insetTop > 0) { 1218 ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this); 1219 } 1220 1221 // Update the collapsing text's fraction 1222 final int expandRange = getHeight() - ViewCompat.getMinimumHeight( 1223 CollapsingToolbarLayout.this) - insetTop; 1224 mCollapsingTextHelper.setExpansionFraction( 1225 Math.abs(verticalOffset) / (float) expandRange); 1226 } 1227 } 1228} 1229