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.os.Build; 22import android.os.Parcel; 23import android.os.Parcelable; 24import android.support.annotation.IntDef; 25import android.support.annotation.NonNull; 26import android.support.annotation.Nullable; 27import android.support.annotation.VisibleForTesting; 28import android.support.design.R; 29import android.support.v4.os.ParcelableCompat; 30import android.support.v4.os.ParcelableCompatCreatorCallbacks; 31import android.support.v4.view.AbsSavedState; 32import android.support.v4.view.ViewCompat; 33import android.support.v4.view.WindowInsetsCompat; 34import android.util.AttributeSet; 35import android.view.View; 36import android.view.ViewGroup; 37import android.view.animation.Interpolator; 38import android.widget.LinearLayout; 39 40import java.lang.annotation.Retention; 41import java.lang.annotation.RetentionPolicy; 42import java.lang.ref.WeakReference; 43import java.util.ArrayList; 44import 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) 101public 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 */ 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 143 public AppBarLayout(Context context) { 144 this(context, null); 145 } 146 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 */ 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 */ 207 public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) { 208 if (mListeners != null && listener != null) { 209 mListeners.remove(listener); 210 } 211 } 212 213 @Override 214 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 215 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 216 invalidateScrollRanges(); 217 } 218 219 @Override 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 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 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 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 */ 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 */ 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 301 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 302 return p instanceof LayoutParams; 303 } 304 305 @Override 306 protected LayoutParams generateDefaultLayoutParams() { 307 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 308 } 309 310 @Override 311 public LayoutParams generateLayoutParams(AttributeSet attrs) { 312 return new LayoutParams(getContext(), attrs); 313 } 314 315 @Override 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 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 */ 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 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 */ 373 private int getUpNestedPreScrollRange() { 374 return getTotalScrollRange(); 375 } 376 377 /** 378 * Return the scroll range when scrolling down from a nested pre-scroll. 379 */ 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 */ 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 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 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 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 500 private void setCollapsibleState(boolean collapsible) { 501 if (mCollapsible != collapsible) { 502 mCollapsible = collapsible; 503 refreshDrawableState(); 504 } 505 } 506 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 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 535 public float getTargetElevation() { 536 return 0; 537 } 538 539 private int getPendingAction() { 540 return mPendingAction; 541 } 542 543 private void resetPendingAction() { 544 mPendingAction = PENDING_ACTION_NONE; 545 } 546 547 @VisibleForTesting 548 final int getTopInset() { 549 return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 550 } 551 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 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 648 public LayoutParams(int width, int height) { 649 super(width, height); 650 } 651 652 public LayoutParams(int width, int height, float weight) { 653 super(width, height, weight); 654 } 655 656 public LayoutParams(ViewGroup.LayoutParams p) { 657 super(p); 658 } 659 660 public LayoutParams(MarginLayoutParams source) { 661 super(source); 662 } 663 664 public LayoutParams(LinearLayout.LayoutParams source) { 665 super(source); 666 } 667 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 */ 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 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 */ 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 */ 721 public Interpolator getScrollInterpolator() { 722 return mScrollInterpolator; 723 } 724 725 /** 726 * Returns true if the scroll flags are compatible for 'collapsing' 727 */ 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 */ 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 772 public Behavior() {} 773 774 public Behavior(Context context, AttributeSet attrs) { 775 super(context, attrs); 776 } 777 778 @Override 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 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 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 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 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 */ 892 public void setDragCallback(@Nullable DragCallback callback) { 893 mOnDragCallback = callback; 894 } 895 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 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 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 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 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 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 1092 int getMaxDragOffset(AppBarLayout view) { 1093 return -view.getDownNestedScrollRange(); 1094 } 1095 1096 @Override 1097 int getScrollRangeForDragFling(AppBarLayout view) { 1098 return view.getTotalScrollRange(); 1099 } 1100 1101 @Override 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 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 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 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 1233 int getTopBottomOffsetForScrollingSibling() { 1234 return getTopAndBottomOffset() + mOffsetDelta; 1235 } 1236 1237 @Override 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 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 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 1288 public SavedState(Parcelable superState) { 1289 super(superState); 1290 } 1291 1292 @Override 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 1321 public ScrollingViewBehavior() {} 1322 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 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 1340 public boolean onDependentViewChanged(CoordinatorLayout parent, View child, 1341 View dependency) { 1342 offsetChildAsNeeded(parent, child, dependency); 1343 return false; 1344 } 1345 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 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 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 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 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