Add ability to snap AppBarLayout children to edges
BUG: 23792717
Change-Id: Ic373fb30c1c3b97f2e1c845c496e97e766f078ec
diff --git a/design/api/current.txt b/design/api/current.txt
index 30da82a..2506089 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -49,6 +49,7 @@
field public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 8; // 0x8
field public static final int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED = 2; // 0x2
field public static final int SCROLL_FLAG_SCROLL = 1; // 0x1
+ field public static final int SCROLL_FLAG_SNAP = 16; // 0x10
}
public static abstract interface AppBarLayout.OnOffsetChangedListener {
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index 785308a..4d4d3686 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -201,6 +201,10 @@
reached the end of it's scroll range, the remainder of this view will be scrolled
into view. -->
<flag name="enterAlwaysCollapsed" value="0x8"/>
+
+ <!-- Upon a scroll ending, if the view is only partially visible then it will be
+ snapped and scrolled to it's closest edge. -->
+ <flag name="snap" value="0x10"/>
</attr>
<!-- An interpolator to use when scrolling this View. Only takes effect when View
diff --git a/design/src/android/support/design/widget/AppBarLayout.java b/design/src/android/support/design/widget/AppBarLayout.java
index 262caa0..327a3cc 100644
--- a/design/src/android/support/design/widget/AppBarLayout.java
+++ b/design/src/android/support/design/widget/AppBarLayout.java
@@ -492,7 +492,8 @@
SCROLL_FLAG_SCROLL,
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,
SCROLL_FLAG_ENTER_ALWAYS,
- SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED
+ SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,
+ SCROLL_FLAG_SNAP
})
@Retention(RetentionPolicy.SOURCE)
public @interface ScrollFlags {}
@@ -532,9 +533,18 @@
public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 0x8;
/**
- * Internal flag which allows quick checking of 'quick return'
+ * Upon a scroll ending, if the view is only partially visible then it will be snapped
+ * and scrolled to it's closest edge. For example, if the view only has it's bottom 25%
+ * displayed, it will be scrolled off screen completely. Conversely, if it's bottom 75%
+ * is visible then it will be scrolled fully into view.
+ */
+ public static final int SCROLL_FLAG_SNAP = 0x10;
+
+ /**
+ * Internal flags which allows quick checking features
*/
static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS;
+ static final int FLAG_SNAP = SCROLL_FLAG_SCROLL | SCROLL_FLAG_SNAP;
int mScrollFlags = SCROLL_FLAG_SCROLL;
Interpolator mScrollInterpolator;
@@ -582,8 +592,8 @@
* Set the scrolling flags.
*
* @param flags bitwise int of {@link #SCROLL_FLAG_SCROLL},
- * {@link #SCROLL_FLAG_EXIT_UNTIL_COLLAPSED}, {@link #SCROLL_FLAG_ENTER_ALWAYS}
- * and {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED}.
+ * {@link #SCROLL_FLAG_EXIT_UNTIL_COLLAPSED}, {@link #SCROLL_FLAG_ENTER_ALWAYS},
+ * {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED} and {@link #SCROLL_FLAG_SNAP }.
*
* @see #getScrollFlags()
*
@@ -641,6 +651,7 @@
private int mOffsetDelta;
private boolean mSkipNestedPreScroll;
+ private boolean mWasFlung;
private Runnable mFlingRunnable;
private ScrollerCompat mScroller;
@@ -719,10 +730,16 @@
}
@Override
- public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
+ public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
View target) {
- // Reset the skip flag
+ if (!mWasFlung) {
+ // If we haven't been flung then let's see if the current view has been set to snap
+ snapToChildIfNeeded(coordinatorLayout, abl);
+ }
+
+ // Reset the flags
mSkipNestedPreScroll = false;
+ mWasFlung = false;
// Keep a reference to the previous nested scrolling child
mLastNestedScrollingChildRef = new WeakReference<>(target);
}
@@ -840,41 +857,39 @@
public boolean onNestedFling(final CoordinatorLayout coordinatorLayout,
final AppBarLayout child, View target, float velocityX, float velocityY,
boolean consumed) {
+ boolean flung = false;
+
if (!consumed) {
// It has been consumed so let's fling ourselves
- return fling(coordinatorLayout, child, -child.getTotalScrollRange(), 0, -velocityY);
+ flung = fling(coordinatorLayout, child, -child.getTotalScrollRange(),
+ 0, -velocityY);
} else {
// If we're scrolling up and the child also consumed the fling. We'll fake scroll
// upto our 'collapsed' offset
- int targetScroll;
if (velocityY < 0) {
// We're scrolling down
- targetScroll = -child.getTotalScrollRange()
+ final int targetScroll = -child.getTotalScrollRange()
+ child.getDownNestedPreScrollRange();
-
- if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
- // If we're currently expanded more than the target scroll, we'll return false
- // now. This is so that we don't 'scroll' the wrong way.
- return false;
+ if (getTopBottomOffsetForScrollingSibling() < targetScroll) {
+ // If we're currently not expanded more than the target scroll, we'll
+ // animate a fling
+ animateOffsetTo(coordinatorLayout, child, targetScroll);
+ flung = true;
}
} else {
// We're scrolling up
- targetScroll = -child.getUpNestedPreScrollRange();
-
- if (getTopBottomOffsetForScrollingSibling() < targetScroll) {
- // If we're currently expanded less than the target scroll, we'll return
- // false now. This is so that we don't 'scroll' the wrong way.
- return false;
+ final int targetScroll = -child.getUpNestedPreScrollRange();
+ if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
+ // If we're currently not expanded less than the target scroll, we'll
+ // animate a fling
+ animateOffsetTo(coordinatorLayout, child, targetScroll);
+ flung = true;
}
}
-
- if (getTopBottomOffsetForScrollingSibling() != targetScroll) {
- animateOffsetTo(coordinatorLayout, child, targetScroll);
- return true;
- }
}
- return false;
+ mWasFlung = flung;
+ return flung;
}
private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
@@ -923,6 +938,31 @@
}
}
+ private View getChildOnOffset(AppBarLayout abl, final int offset) {
+ for (int i = 0, count = abl.getChildCount(); i < count; i++) {
+ View child = abl.getChildAt(i);
+ if (child.getTop() <= -offset && child.getBottom() >= -offset) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private void snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl) {
+ final int offset = getTopBottomOffsetForScrollingSibling();
+ final View offsetChild = getChildOnOffset(abl, offset);
+ if (offsetChild != null) {
+ final LayoutParams lp = (LayoutParams) offsetChild.getLayoutParams();
+ if ((lp.getScrollFlags() & LayoutParams.FLAG_SNAP) == LayoutParams.FLAG_SNAP) {
+ // We're set the snap, so animate the offset to the nearest edge
+ final int childTop = -offsetChild.getTop();
+ final int childBottom = -offsetChild.getBottom();
+ animateOffsetTo(coordinatorLayout, abl,
+ offset < (childBottom + childTop) / 2 ? childBottom : childTop);
+ }
+ }
+ }
+
private class FlingRunnable implements Runnable {
private final CoordinatorLayout mParent;
private final AppBarLayout mLayout;