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