1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.design.widget; 18 19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.content.res.TypedArray; 24import android.graphics.Rect; 25import android.os.Build; 26import android.os.Parcel; 27import android.os.Parcelable; 28import android.support.annotation.IntDef; 29import android.support.annotation.NonNull; 30import android.support.annotation.Nullable; 31import android.support.annotation.RequiresApi; 32import android.support.annotation.RestrictTo; 33import android.support.annotation.VisibleForTesting; 34import android.support.design.R; 35import android.support.v4.math.MathUtils; 36import android.support.v4.util.ObjectsCompat; 37import android.support.v4.view.AbsSavedState; 38import android.support.v4.view.ViewCompat; 39import android.support.v4.view.WindowInsetsCompat; 40import android.util.AttributeSet; 41import android.view.View; 42import android.view.ViewGroup; 43import android.view.animation.Interpolator; 44import android.widget.LinearLayout; 45 46import java.lang.annotation.Retention; 47import java.lang.annotation.RetentionPolicy; 48import java.lang.ref.WeakReference; 49import java.util.ArrayList; 50import 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) 107public 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 */ 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 150 public AppBarLayout(Context context) { 151 this(context, null); 152 } 153 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 */ 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 */ 226 public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) { 227 if (mListeners != null && listener != null) { 228 mListeners.remove(listener); 229 } 230 } 231 232 @Override 233 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 234 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 235 invalidateScrollRanges(); 236 } 237 238 @Override 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 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 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 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 */ 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 */ 313 public void setExpanded(boolean expanded, boolean animate) { 314 setExpanded(expanded, animate, true); 315 } 316 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 325 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 326 return p instanceof LayoutParams; 327 } 328 329 @Override 330 protected LayoutParams generateDefaultLayoutParams() { 331 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 332 } 333 334 @Override 335 public LayoutParams generateLayoutParams(AttributeSet attrs) { 336 return new LayoutParams(getContext(), attrs); 337 } 338 339 @Override 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 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 */ 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 390 boolean hasScrollableChildren() { 391 return getTotalScrollRange() != 0; 392 } 393 394 /** 395 * Return the scroll range when scrolling up from a nested pre-scroll. 396 */ 397 int getUpNestedPreScrollRange() { 398 return getTotalScrollRange(); 399 } 400 401 /** 402 * Return the scroll range when scrolling down from a nested pre-scroll. 403 */ 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 */ 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 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 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 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 */ 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 */ 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 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 579 public float getTargetElevation() { 580 return 0; 581 } 582 583 int getPendingAction() { 584 return mPendingAction; 585 } 586 587 void resetPendingAction() { 588 mPendingAction = PENDING_ACTION_NONE; 589 } 590 591 @VisibleForTesting 592 final int getTopInset() { 593 return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 594 } 595 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 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 693 public LayoutParams(int width, int height) { 694 super(width, height); 695 } 696 697 public LayoutParams(int width, int height, float weight) { 698 super(width, height, weight); 699 } 700 701 public LayoutParams(ViewGroup.LayoutParams p) { 702 super(p); 703 } 704 705 public LayoutParams(MarginLayoutParams source) { 706 super(source); 707 } 708 709 @RequiresApi(19) 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) 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 */ 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 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 */ 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 */ 770 public Interpolator getScrollInterpolator() { 771 return mScrollInterpolator; 772 } 773 774 /** 775 * Returns true if the scroll flags are compatible for 'collapsing' 776 */ 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 */ 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 817 public Behavior() {} 818 819 public Behavior(Context context, AttributeSet attrs) { 820 super(context, attrs); 821 } 822 823 @Override 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 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 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 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 */ 892 public void setDragCallback(@Nullable DragCallback callback) { 893 mOnDragCallback = callback; 894 } 895 896 private void animateOffsetTo(final CoordinatorLayout coordinatorLayout, 897 final AppBarLayout child, final int offset, float velocity) { 898 final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset); 899 900 final int duration; 901 velocity = Math.abs(velocity); 902 if (velocity > 0) { 903 duration = 3 * Math.round(1000 * (distance / velocity)); 904 } else { 905 final float distanceRatio = (float) distance / child.getHeight(); 906 duration = (int) ((distanceRatio + 1) * 150); 907 } 908 909 animateOffsetWithDuration(coordinatorLayout, child, offset, duration); 910 } 911 912 private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout, 913 final AppBarLayout child, final int offset, final int duration) { 914 final int currentOffset = getTopBottomOffsetForScrollingSibling(); 915 if (currentOffset == offset) { 916 if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) { 917 mOffsetAnimator.cancel(); 918 } 919 return; 920 } 921 922 if (mOffsetAnimator == null) { 923 mOffsetAnimator = 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 941 private int getChildIndexOnOffset(AppBarLayout abl, final int offset) { 942 for (int i = 0, count = abl.getChildCount(); i < count; i++) { 943 View child = abl.getChildAt(i); 944 if (child.getTop() <= -offset && child.getBottom() >= -offset) { 945 return i; 946 } 947 } 948 return -1; 949 } 950 951 private void snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl) { 952 final int offset = getTopBottomOffsetForScrollingSibling(); 953 final int offsetChildIndex = getChildIndexOnOffset(abl, offset); 954 if (offsetChildIndex >= 0) { 955 final View offsetChild = abl.getChildAt(offsetChildIndex); 956 final LayoutParams lp = (LayoutParams) offsetChild.getLayoutParams(); 957 final int flags = lp.getScrollFlags(); 958 959 if ((flags & LayoutParams.FLAG_SNAP) == LayoutParams.FLAG_SNAP) { 960 // We're set the snap, so animate the offset to the nearest edge 961 int snapTop = -offsetChild.getTop(); 962 int snapBottom = -offsetChild.getBottom(); 963 964 if (offsetChildIndex == abl.getChildCount() - 1) { 965 // If this is the last child, we need to take the top inset into account 966 snapBottom += abl.getTopInset(); 967 } 968 969 if (checkFlag(flags, LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED)) { 970 // If the view is set only exit until it is collapsed, we'll abide by that 971 snapBottom += ViewCompat.getMinimumHeight(offsetChild); 972 } else if (checkFlag(flags, LayoutParams.FLAG_QUICK_RETURN 973 | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS)) { 974 // If it's set to always enter collapsed, it actually has two states. We 975 // select the state and then snap within the state 976 final int seam = snapBottom + ViewCompat.getMinimumHeight(offsetChild); 977 if (offset < seam) { 978 snapTop = seam; 979 } else { 980 snapBottom = seam; 981 } 982 } 983 984 final int newOffset = offset < (snapBottom + snapTop) / 2 985 ? snapBottom 986 : snapTop; 987 animateOffsetTo(coordinatorLayout, abl, 988 MathUtils.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 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 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 1101 int getMaxDragOffset(AppBarLayout view) { 1102 return -view.getDownNestedScrollRange(); 1103 } 1104 1105 @Override 1106 int getScrollRangeForDragFling(AppBarLayout view) { 1107 return view.getTotalScrollRange(); 1108 } 1109 1110 @Override 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 1156 boolean isOffsetAnimatorRunning() { 1157 return mOffsetAnimator != null && mOffsetAnimator.isRunning(); 1158 } 1159 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 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 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 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 1271 int getTopBottomOffsetForScrollingSibling() { 1272 return getTopAndBottomOffset() + mOffsetDelta; 1273 } 1274 1275 @Override 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 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 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 1326 public SavedState(Parcelable superState) { 1327 super(superState); 1328 } 1329 1330 @Override 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 1363 public ScrollingViewBehavior() {} 1364 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 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 1382 public boolean onDependentViewChanged(CoordinatorLayout parent, View child, 1383 View dependency) { 1384 offsetChildAsNeeded(parent, child, dependency); 1385 return false; 1386 } 1387 1388 @Override 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 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 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 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 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 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