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.content.res.TypedArray; 21 import android.os.Build; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.support.annotation.IntDef; 25 import android.support.annotation.NonNull; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.VisibleForTesting; 28 import android.support.design.R; 29 import android.support.v4.os.ParcelableCompat; 30 import android.support.v4.os.ParcelableCompatCreatorCallbacks; 31 import android.support.v4.view.AbsSavedState; 32 import android.support.v4.view.ViewCompat; 33 import android.support.v4.view.WindowInsetsCompat; 34 import android.util.AttributeSet; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.animation.Interpolator; 38 import android.widget.LinearLayout; 39 40 import java.lang.annotation.Retention; 41 import java.lang.annotation.RetentionPolicy; 42 import java.lang.ref.WeakReference; 43 import java.util.ArrayList; 44 import java.util.List; 45 46 /** 47 * AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of 48 * material designs app bar concept, namely scrolling gestures. 49 * <p> 50 * Children should provide their desired scrolling behavior through 51 * {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute: 52 * {@code app:layout_scrollFlags}. 53 * 54 * <p> 55 * This view depends heavily on being used as a direct child within a {@link CoordinatorLayout}. 56 * If you use AppBarLayout within a different {@link ViewGroup}, most of it's functionality will 57 * not work. 58 * <p> 59 * AppBarLayout also requires a separate scrolling sibling in order to know when to scroll. 60 * The binding is done through the {@link ScrollingViewBehavior} behavior class, meaning that you 61 * should set your scrolling view's behavior to be an instance of {@link ScrollingViewBehavior}. 62 * A string resource containing the full class name is available. 63 * 64 * <pre> 65 * <android.support.design.widget.CoordinatorLayout 66 * xmlns:android="http://schemas.android.com/apk/res/android" 67 * xmlns:app="http://schemas.android.com/apk/res-auto" 68 * android:layout_width="match_parent" 69 * android:layout_height="match_parent"> 70 * 71 * <android.support.v4.widget.NestedScrollView 72 * android:layout_width="match_parent" 73 * android:layout_height="match_parent" 74 * app:layout_behavior="@string/appbar_scrolling_view_behavior"> 75 * 76 * <!-- Your scrolling content --> 77 * 78 * </android.support.v4.widget.NestedScrollView> 79 * 80 * <android.support.design.widget.AppBarLayout 81 * android:layout_height="wrap_content" 82 * android:layout_width="match_parent"> 83 * 84 * <android.support.v7.widget.Toolbar 85 * ... 86 * app:layout_scrollFlags="scroll|enterAlways"/> 87 * 88 * <android.support.design.widget.TabLayout 89 * ... 90 * app:layout_scrollFlags="scroll|enterAlways"/> 91 * 92 * </android.support.design.widget.AppBarLayout> 93 * 94 * </android.support.design.widget.CoordinatorLayout> 95 * </pre> 96 * 97 * @see <a href="http://www.google.com/design/spec/layout/structure.html#structure-app-bar"> 98 * http://www.google.com/design/spec/layout/structure.html#structure-app-bar</a> 99 */ 100 @CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class) 101 public class AppBarLayout extends LinearLayout { 102 103 private static final int PENDING_ACTION_NONE = 0x0; 104 private static final int PENDING_ACTION_EXPANDED = 0x1; 105 private static final int PENDING_ACTION_COLLAPSED = 0x2; 106 private static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4; 107 108 /** 109 * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical 110 * offset changes. 111 */ 112 public interface OnOffsetChangedListener { 113 /** 114 * Called when the {@link AppBarLayout}'s layout offset has been changed. This allows 115 * child views to implement custom behavior based on the offset (for instance pinning a 116 * view at a certain y value). 117 * 118 * @param appBarLayout the {@link AppBarLayout} which offset has changed 119 * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px 120 */ onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset)121 void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset); 122 } 123 124 private static final int INVALID_SCROLL_RANGE = -1; 125 126 private int mTotalScrollRange = INVALID_SCROLL_RANGE; 127 private int mDownPreScrollRange = INVALID_SCROLL_RANGE; 128 private int mDownScrollRange = INVALID_SCROLL_RANGE; 129 130 private boolean mHaveChildWithInterpolator; 131 132 private int mPendingAction = PENDING_ACTION_NONE; 133 134 private WindowInsetsCompat mLastInsets; 135 136 private List<OnOffsetChangedListener> mListeners; 137 138 private boolean mCollapsible; 139 private boolean mCollapsed; 140 141 private final int[] mTmpStatesArray = new int[2]; 142 AppBarLayout(Context context)143 public AppBarLayout(Context context) { 144 this(context, null); 145 } 146 AppBarLayout(Context context, AttributeSet attrs)147 public AppBarLayout(Context context, AttributeSet attrs) { 148 super(context, attrs); 149 setOrientation(VERTICAL); 150 151 ThemeUtils.checkAppCompatTheme(context); 152 153 if (Build.VERSION.SDK_INT >= 21) { 154 // Use the bounds view outline provider so that we cast a shadow, even without a 155 // background 156 ViewUtilsLollipop.setBoundsViewOutlineProvider(this); 157 158 // If we're running on API 21+, we should reset any state list animator from our 159 // default style 160 ViewUtilsLollipop.setStateListAnimatorFromAttrs(this, attrs, 0, 161 R.style.Widget_Design_AppBarLayout); 162 } 163 164 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppBarLayout, 165 0, R.style.Widget_Design_AppBarLayout); 166 setBackgroundDrawable(a.getDrawable(R.styleable.AppBarLayout_android_background)); 167 if (a.hasValue(R.styleable.AppBarLayout_expanded)) { 168 setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false)); 169 } 170 if (Build.VERSION.SDK_INT >= 21 && a.hasValue(R.styleable.AppBarLayout_elevation)) { 171 ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator( 172 this, a.getDimensionPixelSize(R.styleable.AppBarLayout_elevation, 0)); 173 } 174 a.recycle(); 175 176 ViewCompat.setOnApplyWindowInsetsListener(this, 177 new android.support.v4.view.OnApplyWindowInsetsListener() { 178 @Override 179 public WindowInsetsCompat onApplyWindowInsets(View v, 180 WindowInsetsCompat insets) { 181 return onWindowInsetChanged(insets); 182 } 183 }); 184 } 185 186 /** 187 * Add a listener that will be called when the offset of this {@link AppBarLayout} changes. 188 * 189 * @param listener The listener that will be called when the offset changes.] 190 * 191 * @see #removeOnOffsetChangedListener(OnOffsetChangedListener) 192 */ addOnOffsetChangedListener(OnOffsetChangedListener listener)193 public void addOnOffsetChangedListener(OnOffsetChangedListener listener) { 194 if (mListeners == null) { 195 mListeners = new ArrayList<>(); 196 } 197 if (listener != null && !mListeners.contains(listener)) { 198 mListeners.add(listener); 199 } 200 } 201 202 /** 203 * Remove the previously added {@link OnOffsetChangedListener}. 204 * 205 * @param listener the listener to remove. 206 */ removeOnOffsetChangedListener(OnOffsetChangedListener listener)207 public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) { 208 if (mListeners != null && listener != null) { 209 mListeners.remove(listener); 210 } 211 } 212 213 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)214 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 215 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 216 invalidateScrollRanges(); 217 } 218 219 @Override onLayout(boolean changed, int l, int t, int r, int b)220 protected void onLayout(boolean changed, int l, int t, int r, int b) { 221 super.onLayout(changed, l, t, r, b); 222 invalidateScrollRanges(); 223 224 mHaveChildWithInterpolator = false; 225 for (int i = 0, z = getChildCount(); i < z; i++) { 226 final View child = getChildAt(i); 227 final LayoutParams childLp = (LayoutParams) child.getLayoutParams(); 228 final Interpolator interpolator = childLp.getScrollInterpolator(); 229 230 if (interpolator != null) { 231 mHaveChildWithInterpolator = true; 232 break; 233 } 234 } 235 236 updateCollapsible(); 237 } 238 updateCollapsible()239 private void updateCollapsible() { 240 boolean haveCollapsibleChild = false; 241 for (int i = 0, z = getChildCount(); i < z; i++) { 242 if (((LayoutParams) getChildAt(i).getLayoutParams()).isCollapsible()) { 243 haveCollapsibleChild = true; 244 break; 245 } 246 } 247 setCollapsibleState(haveCollapsibleChild); 248 } 249 invalidateScrollRanges()250 private void invalidateScrollRanges() { 251 // Invalidate the scroll ranges 252 mTotalScrollRange = INVALID_SCROLL_RANGE; 253 mDownPreScrollRange = INVALID_SCROLL_RANGE; 254 mDownScrollRange = INVALID_SCROLL_RANGE; 255 } 256 257 @Override setOrientation(int orientation)258 public void setOrientation(int orientation) { 259 if (orientation != VERTICAL) { 260 throw new IllegalArgumentException("AppBarLayout is always vertical and does" 261 + " not support horizontal orientation"); 262 } 263 super.setOrientation(orientation); 264 } 265 266 /** 267 * Sets whether this {@link AppBarLayout} is expanded or not, animating if it has already 268 * been laid out. 269 * 270 * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a 271 * direct child of a {@link CoordinatorLayout}.</p> 272 * 273 * @param expanded true if the layout should be fully expanded, false if it should 274 * be fully collapsed 275 * 276 * @attr ref android.support.design.R.styleable#AppBarLayout_expanded 277 */ setExpanded(boolean expanded)278 public void setExpanded(boolean expanded) { 279 setExpanded(expanded, ViewCompat.isLaidOut(this)); 280 } 281 282 /** 283 * Sets whether this {@link AppBarLayout} is expanded or not. 284 * 285 * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a 286 * direct child of a {@link CoordinatorLayout}.</p> 287 * 288 * @param expanded true if the layout should be fully expanded, false if it should 289 * be fully collapsed 290 * @param animate Whether to animate to the new state 291 * 292 * @attr ref android.support.design.R.styleable#AppBarLayout_expanded 293 */ setExpanded(boolean expanded, boolean animate)294 public void setExpanded(boolean expanded, boolean animate) { 295 mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED) 296 | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0); 297 requestLayout(); 298 } 299 300 @Override checkLayoutParams(ViewGroup.LayoutParams p)301 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 302 return p instanceof LayoutParams; 303 } 304 305 @Override generateDefaultLayoutParams()306 protected LayoutParams generateDefaultLayoutParams() { 307 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 308 } 309 310 @Override generateLayoutParams(AttributeSet attrs)311 public LayoutParams generateLayoutParams(AttributeSet attrs) { 312 return new LayoutParams(getContext(), attrs); 313 } 314 315 @Override generateLayoutParams(ViewGroup.LayoutParams p)316 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 317 if (p instanceof LinearLayout.LayoutParams) { 318 return new LayoutParams((LinearLayout.LayoutParams) p); 319 } else if (p instanceof MarginLayoutParams) { 320 return new LayoutParams((MarginLayoutParams) p); 321 } 322 return new LayoutParams(p); 323 } 324 hasChildWithInterpolator()325 private boolean hasChildWithInterpolator() { 326 return mHaveChildWithInterpolator; 327 } 328 329 /** 330 * Returns the scroll range of all children. 331 * 332 * @return the scroll range in px 333 */ getTotalScrollRange()334 public final int getTotalScrollRange() { 335 if (mTotalScrollRange != INVALID_SCROLL_RANGE) { 336 return mTotalScrollRange; 337 } 338 339 int range = 0; 340 for (int i = 0, z = getChildCount(); i < z; i++) { 341 final View child = getChildAt(i); 342 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 343 final int childHeight = child.getMeasuredHeight(); 344 final int flags = lp.mScrollFlags; 345 346 if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) { 347 // We're set to scroll so add the child's height 348 range += childHeight + lp.topMargin + lp.bottomMargin; 349 350 if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { 351 // For a collapsing scroll, we to take the collapsed height into account. 352 // We also break straight away since later views can't scroll beneath 353 // us 354 range -= ViewCompat.getMinimumHeight(child); 355 break; 356 } 357 } else { 358 // As soon as a view doesn't have the scroll flag, we end the range calculation. 359 // This is because views below can not scroll under a fixed view. 360 break; 361 } 362 } 363 return mTotalScrollRange = Math.max(0, range - getTopInset()); 364 } 365 hasScrollableChildren()366 private boolean hasScrollableChildren() { 367 return getTotalScrollRange() != 0; 368 } 369 370 /** 371 * Return the scroll range when scrolling up from a nested pre-scroll. 372 */ getUpNestedPreScrollRange()373 private int getUpNestedPreScrollRange() { 374 return getTotalScrollRange(); 375 } 376 377 /** 378 * Return the scroll range when scrolling down from a nested pre-scroll. 379 */ getDownNestedPreScrollRange()380 private int getDownNestedPreScrollRange() { 381 if (mDownPreScrollRange != INVALID_SCROLL_RANGE) { 382 // If we already have a valid value, return it 383 return mDownPreScrollRange; 384 } 385 386 int range = 0; 387 for (int i = getChildCount() - 1; i >= 0; i--) { 388 final View child = getChildAt(i); 389 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 390 final int childHeight = child.getMeasuredHeight(); 391 final int flags = lp.mScrollFlags; 392 393 if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) { 394 // First take the margin into account 395 range += lp.topMargin + lp.bottomMargin; 396 // The view has the quick return flag combination... 397 if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) { 398 // If they're set to enter collapsed, use the minimum height 399 range += ViewCompat.getMinimumHeight(child); 400 } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { 401 // Only enter by the amount of the collapsed height 402 range += childHeight - ViewCompat.getMinimumHeight(child); 403 } else { 404 // Else use the full height 405 range += childHeight; 406 } 407 } else if (range > 0) { 408 // If we've hit an non-quick return scrollable view, and we've already hit a 409 // quick return view, return now 410 break; 411 } 412 } 413 return mDownPreScrollRange = Math.max(0, range); 414 } 415 416 /** 417 * Return the scroll range when scrolling down from a nested scroll. 418 */ getDownNestedScrollRange()419 private int getDownNestedScrollRange() { 420 if (mDownScrollRange != INVALID_SCROLL_RANGE) { 421 // If we already have a valid value, return it 422 return mDownScrollRange; 423 } 424 425 int range = 0; 426 for (int i = 0, z = getChildCount(); i < z; i++) { 427 final View child = getChildAt(i); 428 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 429 int childHeight = child.getMeasuredHeight(); 430 childHeight += lp.topMargin + lp.bottomMargin; 431 432 final int flags = lp.mScrollFlags; 433 434 if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) { 435 // We're set to scroll so add the child's height 436 range += childHeight; 437 438 if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { 439 // For a collapsing exit scroll, we to take the collapsed height into account. 440 // We also break the range straight away since later views can't scroll 441 // beneath us 442 range -= ViewCompat.getMinimumHeight(child) + getTopInset(); 443 break; 444 } 445 } else { 446 // As soon as a view doesn't have the scroll flag, we end the range calculation. 447 // This is because views below can not scroll under a fixed view. 448 break; 449 } 450 } 451 return mDownScrollRange = Math.max(0, range); 452 } 453 dispatchOffsetUpdates(int offset)454 private void dispatchOffsetUpdates(int offset) { 455 // Iterate backwards through the list so that most recently added listeners 456 // get the first chance to decide 457 if (mListeners != null) { 458 for (int i = 0, z = mListeners.size(); i < z; i++) { 459 final OnOffsetChangedListener listener = mListeners.get(i); 460 if (listener != null) { 461 listener.onOffsetChanged(this, offset); 462 } 463 } 464 } 465 } 466 getMinimumHeightForVisibleOverlappingContent()467 final int getMinimumHeightForVisibleOverlappingContent() { 468 final int topInset = getTopInset(); 469 final int minHeight = ViewCompat.getMinimumHeight(this); 470 if (minHeight != 0) { 471 // If this layout has a min height, use it (doubled) 472 return (minHeight * 2) + topInset; 473 } 474 475 // Otherwise, we'll use twice the min height of our last child 476 final int childCount = getChildCount(); 477 final int lastChildMinHeight = childCount >= 1 478 ? ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) : 0; 479 if (lastChildMinHeight != 0) { 480 return (lastChildMinHeight * 2) + topInset; 481 } 482 483 // If we reach here then we don't have a min height explicitly set. Instead we'll take a 484 // guess at 1/3 of our height being visible 485 return getHeight() / 3; 486 } 487 488 @Override onCreateDrawableState(int extraSpace)489 protected int[] onCreateDrawableState(int extraSpace) { 490 final int[] extraStates = mTmpStatesArray; 491 final int[] states = super.onCreateDrawableState(extraSpace + extraStates.length); 492 493 extraStates[0] = mCollapsible ? R.attr.state_collapsible : -R.attr.state_collapsible; 494 extraStates[1] = mCollapsible && mCollapsed 495 ? R.attr.state_collapsed : -R.attr.state_collapsed; 496 497 return mergeDrawableStates(states, extraStates); 498 } 499 setCollapsibleState(boolean collapsible)500 private void setCollapsibleState(boolean collapsible) { 501 if (mCollapsible != collapsible) { 502 mCollapsible = collapsible; 503 refreshDrawableState(); 504 } 505 } 506 setCollapsedState(boolean collapsed)507 private void setCollapsedState(boolean collapsed) { 508 if (mCollapsed != collapsed) { 509 mCollapsed = collapsed; 510 refreshDrawableState(); 511 } 512 } 513 514 /** 515 * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now 516 * controlled via a {@link android.animation.StateListAnimator}. If a target 517 * elevation is set, either by this method or the {@code app:elevation} attibute, 518 * a new state list animator is created which uses the given {@code elevation} value. 519 * 520 * @attr ref android.support.design.R.styleable#AppBarLayout_elevation 521 */ 522 @Deprecated setTargetElevation(float elevation)523 public void setTargetElevation(float elevation) { 524 if (Build.VERSION.SDK_INT >= 21) { 525 ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(this, elevation); 526 } 527 } 528 529 /** 530 * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now 531 * controlled via a {@link android.animation.StateListAnimator}. This method now 532 * always returns 0. 533 */ 534 @Deprecated getTargetElevation()535 public float getTargetElevation() { 536 return 0; 537 } 538 getPendingAction()539 private int getPendingAction() { 540 return mPendingAction; 541 } 542 resetPendingAction()543 private void resetPendingAction() { 544 mPendingAction = PENDING_ACTION_NONE; 545 } 546 547 @VisibleForTesting getTopInset()548 final int getTopInset() { 549 return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 550 } 551 onWindowInsetChanged(final WindowInsetsCompat insets)552 private WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) { 553 WindowInsetsCompat newInsets = null; 554 555 if (ViewCompat.getFitsSystemWindows(this)) { 556 // If we're set to fit system windows, keep the insets 557 newInsets = insets; 558 } 559 560 // If our insets have changed, keep them and invalidate the scroll ranges... 561 if (newInsets != mLastInsets) { 562 mLastInsets = newInsets; 563 invalidateScrollRanges(); 564 } 565 566 return insets; 567 } 568 569 public static class LayoutParams extends LinearLayout.LayoutParams { 570 571 /** @hide */ 572 @IntDef(flag=true, value={ 573 SCROLL_FLAG_SCROLL, 574 SCROLL_FLAG_EXIT_UNTIL_COLLAPSED, 575 SCROLL_FLAG_ENTER_ALWAYS, 576 SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED, 577 SCROLL_FLAG_SNAP 578 }) 579 @Retention(RetentionPolicy.SOURCE) 580 public @interface ScrollFlags {} 581 582 /** 583 * The view will be scroll in direct relation to scroll events. This flag needs to be 584 * set for any of the other flags to take effect. If any sibling views 585 * before this one do not have this flag, then this value has no effect. 586 */ 587 public static final int SCROLL_FLAG_SCROLL = 0x1; 588 589 /** 590 * When exiting (scrolling off screen) the view will be scrolled until it is 591 * 'collapsed'. The collapsed height is defined by the view's minimum height. 592 * 593 * @see ViewCompat#getMinimumHeight(View) 594 * @see View#setMinimumHeight(int) 595 */ 596 public static final int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED = 0x2; 597 598 /** 599 * When entering (scrolling on screen) the view will scroll on any downwards 600 * scroll event, regardless of whether the scrolling view is also scrolling. This 601 * is commonly referred to as the 'quick return' pattern. 602 */ 603 public static final int SCROLL_FLAG_ENTER_ALWAYS = 0x4; 604 605 /** 606 * An additional flag for 'enterAlways' which modifies the returning view to 607 * only initially scroll back to it's collapsed height. Once the scrolling view has 608 * reached the end of it's scroll range, the remainder of this view will be scrolled 609 * into view. The collapsed height is defined by the view's minimum height. 610 * 611 * @see ViewCompat#getMinimumHeight(View) 612 * @see View#setMinimumHeight(int) 613 */ 614 public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 0x8; 615 616 /** 617 * Upon a scroll ending, if the view is only partially visible then it will be snapped 618 * and scrolled to it's closest edge. For example, if the view only has it's bottom 25% 619 * displayed, it will be scrolled off screen completely. Conversely, if it's bottom 75% 620 * is visible then it will be scrolled fully into view. 621 */ 622 public static final int SCROLL_FLAG_SNAP = 0x10; 623 624 /** 625 * Internal flags which allows quick checking features 626 */ 627 static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS; 628 static final int FLAG_SNAP = SCROLL_FLAG_SCROLL | SCROLL_FLAG_SNAP; 629 static final int COLLAPSIBLE_FLAGS = SCROLL_FLAG_EXIT_UNTIL_COLLAPSED 630 | SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED; 631 632 int mScrollFlags = SCROLL_FLAG_SCROLL; 633 Interpolator mScrollInterpolator; 634 LayoutParams(Context c, AttributeSet attrs)635 public LayoutParams(Context c, AttributeSet attrs) { 636 super(c, attrs); 637 TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AppBarLayout_Layout); 638 mScrollFlags = a.getInt(R.styleable.AppBarLayout_Layout_layout_scrollFlags, 0); 639 if (a.hasValue(R.styleable.AppBarLayout_Layout_layout_scrollInterpolator)) { 640 int resId = a.getResourceId( 641 R.styleable.AppBarLayout_Layout_layout_scrollInterpolator, 0); 642 mScrollInterpolator = android.view.animation.AnimationUtils.loadInterpolator( 643 c, resId); 644 } 645 a.recycle(); 646 } 647 LayoutParams(int width, int height)648 public LayoutParams(int width, int height) { 649 super(width, height); 650 } 651 LayoutParams(int width, int height, float weight)652 public LayoutParams(int width, int height, float weight) { 653 super(width, height, weight); 654 } 655 LayoutParams(ViewGroup.LayoutParams p)656 public LayoutParams(ViewGroup.LayoutParams p) { 657 super(p); 658 } 659 LayoutParams(MarginLayoutParams source)660 public LayoutParams(MarginLayoutParams source) { 661 super(source); 662 } 663 LayoutParams(LinearLayout.LayoutParams source)664 public LayoutParams(LinearLayout.LayoutParams source) { 665 super(source); 666 } 667 LayoutParams(LayoutParams source)668 public LayoutParams(LayoutParams source) { 669 super(source); 670 mScrollFlags = source.mScrollFlags; 671 mScrollInterpolator = source.mScrollInterpolator; 672 } 673 674 /** 675 * Set the scrolling flags. 676 * 677 * @param flags bitwise int of {@link #SCROLL_FLAG_SCROLL}, 678 * {@link #SCROLL_FLAG_EXIT_UNTIL_COLLAPSED}, {@link #SCROLL_FLAG_ENTER_ALWAYS}, 679 * {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED} and {@link #SCROLL_FLAG_SNAP }. 680 * 681 * @see #getScrollFlags() 682 * 683 * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags 684 */ setScrollFlags(@crollFlags int flags)685 public void setScrollFlags(@ScrollFlags int flags) { 686 mScrollFlags = flags; 687 } 688 689 /** 690 * Returns the scrolling flags. 691 * 692 * @see #setScrollFlags(int) 693 * 694 * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags 695 */ 696 @ScrollFlags getScrollFlags()697 public int getScrollFlags() { 698 return mScrollFlags; 699 } 700 701 /** 702 * Set the interpolator to when scrolling the view associated with this 703 * {@link LayoutParams}. 704 * 705 * @param interpolator the interpolator to use, or null to use normal 1-to-1 scrolling. 706 * 707 * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator 708 * @see #getScrollInterpolator() 709 */ setScrollInterpolator(Interpolator interpolator)710 public void setScrollInterpolator(Interpolator interpolator) { 711 mScrollInterpolator = interpolator; 712 } 713 714 /** 715 * Returns the {@link Interpolator} being used for scrolling the view associated with this 716 * {@link LayoutParams}. Null indicates 'normal' 1-to-1 scrolling. 717 * 718 * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator 719 * @see #setScrollInterpolator(Interpolator) 720 */ getScrollInterpolator()721 public Interpolator getScrollInterpolator() { 722 return mScrollInterpolator; 723 } 724 725 /** 726 * Returns true if the scroll flags are compatible for 'collapsing' 727 */ isCollapsible()728 private boolean isCollapsible() { 729 return (mScrollFlags & SCROLL_FLAG_SCROLL) == SCROLL_FLAG_SCROLL 730 && (mScrollFlags & COLLAPSIBLE_FLAGS) != 0; 731 } 732 } 733 734 /** 735 * The default {@link Behavior} for {@link AppBarLayout}. Implements the necessary nested 736 * scroll handling with offsetting. 737 */ 738 public static class Behavior extends HeaderBehavior<AppBarLayout> { 739 private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms 740 private static final int INVALID_POSITION = -1; 741 742 /** 743 * Callback to allow control over any {@link AppBarLayout} dragging. 744 */ 745 public static abstract class DragCallback { 746 /** 747 * Allows control over whether the given {@link AppBarLayout} can be dragged or not. 748 * 749 * <p>Dragging is defined as a direct touch on the AppBarLayout with movement. This 750 * call does not affect any nested scrolling.</p> 751 * 752 * @return true if we are in a position to scroll the AppBarLayout via a drag, false 753 * if not. 754 */ canDrag(@onNull AppBarLayout appBarLayout)755 public abstract boolean canDrag(@NonNull AppBarLayout appBarLayout); 756 } 757 758 private int mOffsetDelta; 759 760 private boolean mSkipNestedPreScroll; 761 private boolean mWasNestedFlung; 762 763 private ValueAnimatorCompat mOffsetAnimator; 764 765 private int mOffsetToChildIndexOnLayout = INVALID_POSITION; 766 private boolean mOffsetToChildIndexOnLayoutIsMinHeight; 767 private float mOffsetToChildIndexOnLayoutPerc; 768 769 private WeakReference<View> mLastNestedScrollingChildRef; 770 private DragCallback mOnDragCallback; 771 Behavior()772 public Behavior() {} 773 Behavior(Context context, AttributeSet attrs)774 public Behavior(Context context, AttributeSet attrs) { 775 super(context, attrs); 776 } 777 778 @Override onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes)779 public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, 780 View directTargetChild, View target, int nestedScrollAxes) { 781 // Return true if we're nested scrolling vertically, and we have scrollable children 782 // and the scrolling view is big enough to scroll 783 final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 784 && child.hasScrollableChildren() 785 && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight(); 786 787 if (started && mOffsetAnimator != null) { 788 // Cancel any offset animation 789 mOffsetAnimator.cancel(); 790 } 791 792 // A new nested scroll has started so clear out the previous ref 793 mLastNestedScrollingChildRef = null; 794 795 return started; 796 } 797 798 @Override onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed)799 public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, 800 View target, int dx, int dy, int[] consumed) { 801 if (dy != 0 && !mSkipNestedPreScroll) { 802 int min, max; 803 if (dy < 0) { 804 // We're scrolling down 805 min = -child.getTotalScrollRange(); 806 max = min + child.getDownNestedPreScrollRange(); 807 } else { 808 // We're scrolling up 809 min = -child.getUpNestedPreScrollRange(); 810 max = 0; 811 } 812 consumed[1] = scroll(coordinatorLayout, child, dy, min, max); 813 } 814 } 815 816 @Override onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)817 public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, 818 View target, int dxConsumed, int dyConsumed, 819 int dxUnconsumed, int dyUnconsumed) { 820 if (dyUnconsumed < 0) { 821 // If the scrolling view is scrolling down but not consuming, it's probably be at 822 // the top of it's content 823 scroll(coordinatorLayout, child, dyUnconsumed, 824 -child.getDownNestedScrollRange(), 0); 825 // Set the expanding flag so that onNestedPreScroll doesn't handle any events 826 mSkipNestedPreScroll = true; 827 } else { 828 // As we're no longer handling nested scrolls, reset the skip flag 829 mSkipNestedPreScroll = false; 830 } 831 } 832 833 @Override onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target)834 public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, 835 View target) { 836 if (!mWasNestedFlung) { 837 // If we haven't been flung then let's see if the current view has been set to snap 838 snapToChildIfNeeded(coordinatorLayout, abl); 839 } 840 841 // Reset the flags 842 mSkipNestedPreScroll = false; 843 mWasNestedFlung = false; 844 // Keep a reference to the previous nested scrolling child 845 mLastNestedScrollingChildRef = new WeakReference<>(target); 846 } 847 848 @Override onNestedFling(final CoordinatorLayout coordinatorLayout, final AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed)849 public boolean onNestedFling(final CoordinatorLayout coordinatorLayout, 850 final AppBarLayout child, View target, float velocityX, float velocityY, 851 boolean consumed) { 852 boolean flung = false; 853 854 if (!consumed) { 855 // It has been consumed so let's fling ourselves 856 flung = fling(coordinatorLayout, child, -child.getTotalScrollRange(), 857 0, -velocityY); 858 } else { 859 // If we're scrolling up and the child also consumed the fling. We'll fake scroll 860 // upto our 'collapsed' offset 861 if (velocityY < 0) { 862 // We're scrolling down 863 final int targetScroll = -child.getTotalScrollRange() 864 + child.getDownNestedPreScrollRange(); 865 if (getTopBottomOffsetForScrollingSibling() < targetScroll) { 866 // If we're currently not expanded more than the target scroll, we'll 867 // animate a fling 868 animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY); 869 flung = true; 870 } 871 } else { 872 // We're scrolling up 873 final int targetScroll = -child.getUpNestedPreScrollRange(); 874 if (getTopBottomOffsetForScrollingSibling() > targetScroll) { 875 // If we're currently not expanded less than the target scroll, we'll 876 // animate a fling 877 animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY); 878 flung = true; 879 } 880 } 881 } 882 883 mWasNestedFlung = flung; 884 return flung; 885 } 886 887 /** 888 * Set a callback to control any {@link AppBarLayout} dragging. 889 * 890 * @param callback the callback to use, or {@code null} to use the default behavior. 891 */ setDragCallback(@ullable DragCallback callback)892 public void setDragCallback(@Nullable DragCallback callback) { 893 mOnDragCallback = callback; 894 } 895 animateOffsetTo(final CoordinatorLayout coordinatorLayout, final AppBarLayout child, final int offset, float velocity)896 private void animateOffsetTo(final CoordinatorLayout coordinatorLayout, 897 final AppBarLayout child, final int offset, float velocity) { 898 final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset); 899 900 final int duration; 901 velocity = Math.abs(velocity); 902 if (velocity > 0) { 903 duration = 3 * Math.round(1000 * (distance / velocity)); 904 } else { 905 final float distanceRatio = (float) distance / child.getHeight(); 906 duration = (int) ((distanceRatio + 1) * 150); 907 } 908 909 animateOffsetWithDuration(coordinatorLayout, child, offset, duration); 910 } 911 animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout, final AppBarLayout child, final int offset, final int duration)912 private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout, 913 final AppBarLayout child, final int offset, final int duration) { 914 final int currentOffset = getTopBottomOffsetForScrollingSibling(); 915 if (currentOffset == offset) { 916 if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) { 917 mOffsetAnimator.cancel(); 918 } 919 return; 920 } 921 922 if (mOffsetAnimator == null) { 923 mOffsetAnimator = ViewUtils.createAnimator(); 924 mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR); 925 mOffsetAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 926 @Override 927 public void onAnimationUpdate(ValueAnimatorCompat animator) { 928 setHeaderTopBottomOffset(coordinatorLayout, child, 929 animator.getAnimatedIntValue()); 930 } 931 }); 932 } else { 933 mOffsetAnimator.cancel(); 934 } 935 936 mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION)); 937 mOffsetAnimator.setIntValues(currentOffset, offset); 938 mOffsetAnimator.start(); 939 } 940 getChildIndexOnOffset(AppBarLayout abl, final int offset)941 private int getChildIndexOnOffset(AppBarLayout abl, final int offset) { 942 for (int i = 0, count = abl.getChildCount(); i < count; i++) { 943 View child = abl.getChildAt(i); 944 if (child.getTop() <= -offset && child.getBottom() >= -offset) { 945 return i; 946 } 947 } 948 return -1; 949 } 950 snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl)951 private void snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl) { 952 final int offset = getTopBottomOffsetForScrollingSibling(); 953 final int offsetChildIndex = getChildIndexOnOffset(abl, offset); 954 if (offsetChildIndex >= 0) { 955 final View offsetChild = abl.getChildAt(offsetChildIndex); 956 final LayoutParams lp = (LayoutParams) offsetChild.getLayoutParams(); 957 final int flags = lp.getScrollFlags(); 958 959 if ((flags & LayoutParams.FLAG_SNAP) == LayoutParams.FLAG_SNAP) { 960 // We're set the snap, so animate the offset to the nearest edge 961 int snapTop = -offsetChild.getTop(); 962 int snapBottom = -offsetChild.getBottom(); 963 964 if (offsetChildIndex == abl.getChildCount() - 1) { 965 // If this is the last child, we need to take the top inset into account 966 snapBottom += abl.getTopInset(); 967 } 968 969 if (checkFlag(flags, LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED)) { 970 // If the view is set only exit until it is collapsed, we'll abide by that 971 snapBottom += ViewCompat.getMinimumHeight(offsetChild); 972 } else if (checkFlag(flags, LayoutParams.FLAG_QUICK_RETURN 973 | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS)) { 974 // If it's set to always enter collapsed, it actually has two states. We 975 // select the state and then snap within the state 976 final int seam = snapBottom + ViewCompat.getMinimumHeight(offsetChild); 977 if (offset < seam) { 978 snapTop = seam; 979 } else { 980 snapBottom = seam; 981 } 982 } 983 984 final int newOffset = offset < (snapBottom + snapTop) / 2 985 ? snapBottom 986 : snapTop; 987 animateOffsetTo(coordinatorLayout, abl, 988 MathUtils.constrain(newOffset, -abl.getTotalScrollRange(), 0), 0); 989 } 990 } 991 } 992 993 private static boolean checkFlag(final int flags, final int check) { 994 return (flags & check) == check; 995 } 996 997 @Override 998 public boolean onMeasureChild(CoordinatorLayout parent, AppBarLayout child, 999 int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, 1000 int heightUsed) { 1001 final CoordinatorLayout.LayoutParams lp = 1002 (CoordinatorLayout.LayoutParams) child.getLayoutParams(); 1003 if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) { 1004 // If the view is set to wrap on it's height, CoordinatorLayout by default will 1005 // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't 1006 // what we actually want, so we measure it ourselves with an unspecified spec to 1007 // allow the child to be larger than it's parent 1008 parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, 1009 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed); 1010 return true; 1011 } 1012 1013 // Let the parent handle it as normal 1014 return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed, 1015 parentHeightMeasureSpec, heightUsed); 1016 } 1017 1018 @Override 1019 public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, 1020 int layoutDirection) { 1021 boolean handled = super.onLayoutChild(parent, abl, layoutDirection); 1022 1023 final int pendingAction = abl.getPendingAction(); 1024 if (pendingAction != PENDING_ACTION_NONE) { 1025 final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0; 1026 if ((pendingAction & PENDING_ACTION_COLLAPSED) != 0) { 1027 final int offset = -abl.getUpNestedPreScrollRange(); 1028 if (animate) { 1029 animateOffsetTo(parent, abl, offset, 0); 1030 } else { 1031 setHeaderTopBottomOffset(parent, abl, offset); 1032 } 1033 } else if ((pendingAction & PENDING_ACTION_EXPANDED) != 0) { 1034 if (animate) { 1035 animateOffsetTo(parent, abl, 0, 0); 1036 } else { 1037 setHeaderTopBottomOffset(parent, abl, 0); 1038 } 1039 } 1040 } else if (mOffsetToChildIndexOnLayout >= 0) { 1041 View child = abl.getChildAt(mOffsetToChildIndexOnLayout); 1042 int offset = -child.getBottom(); 1043 if (mOffsetToChildIndexOnLayoutIsMinHeight) { 1044 offset += ViewCompat.getMinimumHeight(child); 1045 } else { 1046 offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc); 1047 } 1048 setTopAndBottomOffset(offset); 1049 } 1050 1051 // Finally reset any pending states 1052 abl.resetPendingAction(); 1053 mOffsetToChildIndexOnLayout = INVALID_POSITION; 1054 1055 // We may have changed size, so let's constrain the top and bottom offset correctly, 1056 // just in case we're out of the bounds 1057 setTopAndBottomOffset( 1058 MathUtils.constrain(getTopAndBottomOffset(), -abl.getTotalScrollRange(), 0)); 1059 1060 // Make sure we dispatch the offset update 1061 abl.dispatchOffsetUpdates(getTopAndBottomOffset()); 1062 1063 return handled; 1064 } 1065 1066 @Override canDragView(AppBarLayout view)1067 boolean canDragView(AppBarLayout view) { 1068 if (mOnDragCallback != null) { 1069 // If there is a drag callback set, it's in control 1070 return mOnDragCallback.canDrag(view); 1071 } 1072 1073 // Else we'll use the default behaviour of seeing if it can scroll down 1074 if (mLastNestedScrollingChildRef != null) { 1075 // If we have a reference to a scrolling view, check it 1076 final View scrollingView = mLastNestedScrollingChildRef.get(); 1077 return scrollingView != null && scrollingView.isShown() 1078 && !ViewCompat.canScrollVertically(scrollingView, -1); 1079 } else { 1080 // Otherwise we assume that the scrolling view hasn't been scrolled and can drag. 1081 return true; 1082 } 1083 } 1084 1085 @Override onFlingFinished(CoordinatorLayout parent, AppBarLayout layout)1086 void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) { 1087 // At the end of a manual fling, check to see if we need to snap to the edge-child 1088 snapToChildIfNeeded(parent, layout); 1089 } 1090 1091 @Override getMaxDragOffset(AppBarLayout view)1092 int getMaxDragOffset(AppBarLayout view) { 1093 return -view.getDownNestedScrollRange(); 1094 } 1095 1096 @Override getScrollRangeForDragFling(AppBarLayout view)1097 int getScrollRangeForDragFling(AppBarLayout view) { 1098 return view.getTotalScrollRange(); 1099 } 1100 1101 @Override setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout, AppBarLayout header, int newOffset, int minOffset, int maxOffset)1102 int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout, 1103 AppBarLayout header, int newOffset, int minOffset, int maxOffset) { 1104 final int curOffset = getTopBottomOffsetForScrollingSibling(); 1105 int consumed = 0; 1106 1107 if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) { 1108 // If we have some scrolling range, and we're currently within the min and max 1109 // offsets, calculate a new offset 1110 newOffset = MathUtils.constrain(newOffset, minOffset, maxOffset); 1111 AppBarLayout appBarLayout = (AppBarLayout) header; 1112 if (curOffset != newOffset) { 1113 final int interpolatedOffset = appBarLayout.hasChildWithInterpolator() 1114 ? interpolateOffset(appBarLayout, newOffset) 1115 : newOffset; 1116 1117 final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset); 1118 1119 // Update how much dy we have consumed 1120 consumed = curOffset - newOffset; 1121 // Update the stored sibling offset 1122 mOffsetDelta = newOffset - interpolatedOffset; 1123 1124 if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) { 1125 // If the offset hasn't changed and we're using an interpolated scroll 1126 // then we need to keep any dependent views updated. CoL will do this for 1127 // us when we move, but we need to do it manually when we don't (as an 1128 // interpolated scroll may finish early). 1129 coordinatorLayout.dispatchDependentViewsChanged(appBarLayout); 1130 } 1131 1132 // Dispatch the updates to any listeners 1133 appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset()); 1134 1135 // Update the AppBarLayout's drawable state (for any elevation changes) 1136 updateAppBarLayoutDrawableState(appBarLayout, newOffset, 1137 newOffset < curOffset ? -1 : 1); 1138 } 1139 } else { 1140 // Reset the offset delta 1141 mOffsetDelta = 0; 1142 } 1143 1144 return consumed; 1145 } 1146 interpolateOffset(AppBarLayout layout, final int offset)1147 private int interpolateOffset(AppBarLayout layout, final int offset) { 1148 final int absOffset = Math.abs(offset); 1149 1150 for (int i = 0, z = layout.getChildCount(); i < z; i++) { 1151 final View child = layout.getChildAt(i); 1152 final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams(); 1153 final Interpolator interpolator = childLp.getScrollInterpolator(); 1154 1155 if (absOffset >= child.getTop() && absOffset <= child.getBottom()) { 1156 if (interpolator != null) { 1157 int childScrollableHeight = 0; 1158 final int flags = childLp.getScrollFlags(); 1159 if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) { 1160 // We're set to scroll so add the child's height plus margin 1161 childScrollableHeight += child.getHeight() + childLp.topMargin 1162 + childLp.bottomMargin; 1163 1164 if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { 1165 // For a collapsing scroll, we to take the collapsed height 1166 // into account. 1167 childScrollableHeight -= ViewCompat.getMinimumHeight(child); 1168 } 1169 } 1170 1171 if (ViewCompat.getFitsSystemWindows(child)) { 1172 childScrollableHeight -= layout.getTopInset(); 1173 } 1174 1175 if (childScrollableHeight > 0) { 1176 final int offsetForView = absOffset - child.getTop(); 1177 final int interpolatedDiff = Math.round(childScrollableHeight * 1178 interpolator.getInterpolation( 1179 offsetForView / (float) childScrollableHeight)); 1180 1181 return Integer.signum(offset) * (child.getTop() + interpolatedDiff); 1182 } 1183 } 1184 1185 // If we get to here then the view on the offset isn't suitable for interpolated 1186 // scrolling. So break out of the loop 1187 break; 1188 } 1189 } 1190 1191 return offset; 1192 } 1193 updateAppBarLayoutDrawableState(final AppBarLayout layout, final int offset, final int direction)1194 private void updateAppBarLayoutDrawableState(final AppBarLayout layout, 1195 final int offset, final int direction) { 1196 final View child = getAppBarChildOnOffset(layout, offset); 1197 if (child != null) { 1198 final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams(); 1199 final int flags = childLp.getScrollFlags(); 1200 boolean collapsed = false; 1201 1202 if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) { 1203 final int minHeight = ViewCompat.getMinimumHeight(child); 1204 1205 if (direction > 0 && (flags & (LayoutParams.SCROLL_FLAG_ENTER_ALWAYS 1206 | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED)) != 0) { 1207 // We're set to enter always collapsed so we are only collapsed when 1208 // being scrolled down, and in a collapsed offset 1209 collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset(); 1210 } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { 1211 // We're set to exit until collapsed, so any offset which results in 1212 // the minimum height (or less) being shown is collapsed 1213 collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset(); 1214 } 1215 } 1216 1217 layout.setCollapsedState(collapsed); 1218 } 1219 } 1220 getAppBarChildOnOffset(final AppBarLayout layout, final int offset)1221 private static View getAppBarChildOnOffset(final AppBarLayout layout, final int offset) { 1222 final int absOffset = Math.abs(offset); 1223 for (int i = 0, z = layout.getChildCount(); i < z; i++) { 1224 final View child = layout.getChildAt(i); 1225 if (absOffset >= child.getTop() && absOffset <= child.getBottom()) { 1226 return child; 1227 } 1228 } 1229 return null; 1230 } 1231 1232 @Override getTopBottomOffsetForScrollingSibling()1233 int getTopBottomOffsetForScrollingSibling() { 1234 return getTopAndBottomOffset() + mOffsetDelta; 1235 } 1236 1237 @Override onSaveInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout)1238 public Parcelable onSaveInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout) { 1239 final Parcelable superState = super.onSaveInstanceState(parent, appBarLayout); 1240 final int offset = getTopAndBottomOffset(); 1241 1242 // Try and find the first visible child... 1243 for (int i = 0, count = appBarLayout.getChildCount(); i < count; i++) { 1244 View child = appBarLayout.getChildAt(i); 1245 final int visBottom = child.getBottom() + offset; 1246 1247 if (child.getTop() + offset <= 0 && visBottom >= 0) { 1248 final SavedState ss = new SavedState(superState); 1249 ss.firstVisibleChildIndex = i; 1250 ss.firstVisibileChildAtMinimumHeight = 1251 visBottom == ViewCompat.getMinimumHeight(child); 1252 ss.firstVisibileChildPercentageShown = visBottom / (float) child.getHeight(); 1253 return ss; 1254 } 1255 } 1256 1257 // Else we'll just return the super state 1258 return superState; 1259 } 1260 1261 @Override onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout, Parcelable state)1262 public void onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout, 1263 Parcelable state) { 1264 if (state instanceof SavedState) { 1265 final SavedState ss = (SavedState) state; 1266 super.onRestoreInstanceState(parent, appBarLayout, ss.getSuperState()); 1267 mOffsetToChildIndexOnLayout = ss.firstVisibleChildIndex; 1268 mOffsetToChildIndexOnLayoutPerc = ss.firstVisibileChildPercentageShown; 1269 mOffsetToChildIndexOnLayoutIsMinHeight = ss.firstVisibileChildAtMinimumHeight; 1270 } else { 1271 super.onRestoreInstanceState(parent, appBarLayout, state); 1272 mOffsetToChildIndexOnLayout = INVALID_POSITION; 1273 } 1274 } 1275 1276 protected static class SavedState extends AbsSavedState { 1277 int firstVisibleChildIndex; 1278 float firstVisibileChildPercentageShown; 1279 boolean firstVisibileChildAtMinimumHeight; 1280 SavedState(Parcel source, ClassLoader loader)1281 public SavedState(Parcel source, ClassLoader loader) { 1282 super(source, loader); 1283 firstVisibleChildIndex = source.readInt(); 1284 firstVisibileChildPercentageShown = source.readFloat(); 1285 firstVisibileChildAtMinimumHeight = source.readByte() != 0; 1286 } 1287 SavedState(Parcelable superState)1288 public SavedState(Parcelable superState) { 1289 super(superState); 1290 } 1291 1292 @Override writeToParcel(Parcel dest, int flags)1293 public void writeToParcel(Parcel dest, int flags) { 1294 super.writeToParcel(dest, flags); 1295 dest.writeInt(firstVisibleChildIndex); 1296 dest.writeFloat(firstVisibileChildPercentageShown); 1297 dest.writeByte((byte) (firstVisibileChildAtMinimumHeight ? 1 : 0)); 1298 } 1299 1300 public static final Parcelable.Creator<SavedState> CREATOR = 1301 ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { 1302 @Override 1303 public SavedState createFromParcel(Parcel source, ClassLoader loader) { 1304 return new SavedState(source, loader); 1305 } 1306 1307 @Override 1308 public SavedState[] newArray(int size) { 1309 return new SavedState[size]; 1310 } 1311 }); 1312 } 1313 } 1314 1315 /** 1316 * Behavior which should be used by {@link View}s which can scroll vertically and support 1317 * nested scrolling to automatically scroll any {@link AppBarLayout} siblings. 1318 */ 1319 public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior { 1320 ScrollingViewBehavior()1321 public ScrollingViewBehavior() {} 1322 ScrollingViewBehavior(Context context, AttributeSet attrs)1323 public ScrollingViewBehavior(Context context, AttributeSet attrs) { 1324 super(context, attrs); 1325 1326 final TypedArray a = context.obtainStyledAttributes(attrs, 1327 R.styleable.ScrollingViewBehavior_Layout); 1328 setOverlayTop(a.getDimensionPixelSize( 1329 R.styleable.ScrollingViewBehavior_Layout_behavior_overlapTop, 0)); 1330 a.recycle(); 1331 } 1332 1333 @Override layoutDependsOn(CoordinatorLayout parent, View child, View dependency)1334 public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { 1335 // We depend on any AppBarLayouts 1336 return dependency instanceof AppBarLayout; 1337 } 1338 1339 @Override onDependentViewChanged(CoordinatorLayout parent, View child, View dependency)1340 public boolean onDependentViewChanged(CoordinatorLayout parent, View child, 1341 View dependency) { 1342 offsetChildAsNeeded(parent, child, dependency); 1343 return false; 1344 } 1345 offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency)1346 private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) { 1347 final CoordinatorLayout.Behavior behavior = 1348 ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior(); 1349 if (behavior instanceof Behavior) { 1350 // Offset the child, pinning it to the bottom the header-dependency, maintaining 1351 // any vertical gap, and overlap 1352 final Behavior ablBehavior = (Behavior) behavior; 1353 final int offset = ablBehavior.getTopBottomOffsetForScrollingSibling(); 1354 ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop()) 1355 + ablBehavior.mOffsetDelta 1356 + getVerticalLayoutGap() 1357 - getOverlapPixelsForOffset(dependency)); 1358 } 1359 } 1360 1361 1362 @Override getOverlapRatioForOffset(final View header)1363 float getOverlapRatioForOffset(final View header) { 1364 if (header instanceof AppBarLayout) { 1365 final AppBarLayout abl = (AppBarLayout) header; 1366 final int totalScrollRange = abl.getTotalScrollRange(); 1367 final int preScrollDown = abl.getDownNestedPreScrollRange(); 1368 final int offset = getAppBarLayoutOffset(abl); 1369 1370 if (preScrollDown != 0 && (totalScrollRange + offset) <= preScrollDown) { 1371 // If we're in a pre-scroll down. Don't use the offset at all. 1372 return 0; 1373 } else { 1374 final int availScrollRange = totalScrollRange - preScrollDown; 1375 if (availScrollRange != 0) { 1376 // Else we'll use a interpolated ratio of the overlap, depending on offset 1377 return 1f + (offset / (float) availScrollRange); 1378 } 1379 } 1380 } 1381 return 0f; 1382 } 1383 getAppBarLayoutOffset(AppBarLayout abl)1384 private static int getAppBarLayoutOffset(AppBarLayout abl) { 1385 final CoordinatorLayout.Behavior behavior = 1386 ((CoordinatorLayout.LayoutParams) abl.getLayoutParams()).getBehavior(); 1387 if (behavior instanceof Behavior) { 1388 return ((Behavior) behavior).getTopBottomOffsetForScrollingSibling(); 1389 } 1390 return 0; 1391 } 1392 1393 @Override findFirstDependency(List<View> views)1394 View findFirstDependency(List<View> views) { 1395 for (int i = 0, z = views.size(); i < z; i++) { 1396 View view = views.get(i); 1397 if (view instanceof AppBarLayout) { 1398 return view; 1399 } 1400 } 1401 return null; 1402 } 1403 1404 @Override getScrollRange(View v)1405 int getScrollRange(View v) { 1406 if (v instanceof AppBarLayout) { 1407 return ((AppBarLayout) v).getTotalScrollRange(); 1408 } else { 1409 return super.getScrollRange(v); 1410 } 1411 } 1412 } 1413 } 1414