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