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; 20import static android.support.v4.utils.ObjectUtils.objectEquals; 21 22import android.animation.ValueAnimator; 23import android.content.Context; 24import android.content.res.TypedArray; 25import android.graphics.Rect; 26import android.os.Build; 27import android.os.Parcel; 28import android.os.Parcelable; 29import android.support.annotation.IntDef; 30import android.support.annotation.NonNull; 31import android.support.annotation.Nullable; 32import android.support.annotation.RequiresApi; 33import android.support.annotation.RestrictTo; 34import android.support.annotation.VisibleForTesting; 35import android.support.design.R; 36import android.support.v4.math.MathUtils; 37import android.support.v4.os.BuildCompat; 38import android.support.v4.view.AbsSavedState; 39import android.support.v4.view.ViewCompat; 40import android.support.v4.view.WindowInsetsCompat; 41import android.util.AttributeSet; 42import android.view.View; 43import android.view.ViewGroup; 44import android.view.animation.Interpolator; 45import android.widget.LinearLayout; 46 47import java.lang.annotation.Retention; 48import java.lang.annotation.RetentionPolicy; 49import java.lang.ref.WeakReference; 50import java.util.ArrayList; 51import 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) 108public 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 */ 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 151 public AppBarLayout(Context context) { 152 this(context, null); 153 } 154 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 */ 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 */ 227 public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) { 228 if (mListeners != null && listener != null) { 229 mListeners.remove(listener); 230 } 231 } 232 233 @Override 234 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 235 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 236 invalidateScrollRanges(); 237 } 238 239 @Override 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 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 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 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 */ 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 */ 314 public void setExpanded(boolean expanded, boolean animate) { 315 setExpanded(expanded, animate, true); 316 } 317 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 326 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 327 return p instanceof LayoutParams; 328 } 329 330 @Override 331 protected LayoutParams generateDefaultLayoutParams() { 332 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 333 } 334 335 @Override 336 public LayoutParams generateLayoutParams(AttributeSet attrs) { 337 return new LayoutParams(getContext(), attrs); 338 } 339 340 @Override 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 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 */ 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 391 boolean hasScrollableChildren() { 392 return getTotalScrollRange() != 0; 393 } 394 395 /** 396 * Return the scroll range when scrolling up from a nested pre-scroll. 397 */ 398 int getUpNestedPreScrollRange() { 399 return getTotalScrollRange(); 400 } 401 402 /** 403 * Return the scroll range when scrolling down from a nested pre-scroll. 404 */ 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 */ 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 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 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 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 */ 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 */ 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 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 580 public float getTargetElevation() { 581 return 0; 582 } 583 584 int getPendingAction() { 585 return mPendingAction; 586 } 587 588 void resetPendingAction() { 589 mPendingAction = PENDING_ACTION_NONE; 590 } 591 592 @VisibleForTesting 593 final int getTopInset() { 594 return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 595 } 596 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 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 694 public LayoutParams(int width, int height) { 695 super(width, height); 696 } 697 698 public LayoutParams(int width, int height, float weight) { 699 super(width, height, weight); 700 } 701 702 public LayoutParams(ViewGroup.LayoutParams p) { 703 super(p); 704 } 705 706 public LayoutParams(MarginLayoutParams source) { 707 super(source); 708 } 709 710 @RequiresApi(19) 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) 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 */ 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 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 */ 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 */ 771 public Interpolator getScrollInterpolator() { 772 return mScrollInterpolator; 773 } 774 775 /** 776 * Returns true if the scroll flags are compatible for 'collapsing' 777 */ 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 */ 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 818 public Behavior() {} 819 820 public Behavior(Context context, AttributeSet attrs) { 821 super(context, attrs); 822 } 823 824 @Override 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 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 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 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 */ 893 public void setDragCallback(@Nullable DragCallback callback) { 894 mOnDragCallback = callback; 895 } 896 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 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 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 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 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 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 1102 int getMaxDragOffset(AppBarLayout view) { 1103 return -view.getDownNestedScrollRange(); 1104 } 1105 1106 @Override 1107 int getScrollRangeForDragFling(AppBarLayout view) { 1108 return view.getTotalScrollRange(); 1109 } 1110 1111 @Override 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 1157 boolean isOffsetAnimatorRunning() { 1158 return mOffsetAnimator != null && mOffsetAnimator.isRunning(); 1159 } 1160 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 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 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 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 1272 int getTopBottomOffsetForScrollingSibling() { 1273 return getTopAndBottomOffset() + mOffsetDelta; 1274 } 1275 1276 @Override 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 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 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 1327 public SavedState(Parcelable superState) { 1328 super(superState); 1329 } 1330 1331 @Override 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 1364 public ScrollingViewBehavior() {} 1365 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 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 1383 public boolean onDependentViewChanged(CoordinatorLayout parent, View child, 1384 View dependency) { 1385 offsetChildAsNeeded(parent, child, dependency); 1386 return false; 1387 } 1388 1389 @Override 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 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 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 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 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 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