summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/widget
diff options
context:
space:
mode:
authorBrian Attwell <brianattwell@google.com>2014-09-22 14:17:11 -0700
committerBrian Attwell <brianattwell@google.com>2014-09-23 10:51:56 -0700
commit764f652b451d27282cfaf73407d31c9522e6cb0e (patch)
treee6f2a19c5863ab197bb2f5bcb3fc64fb5dcd540a /src/com/android/dialer/widget
parentf2d3bd5d0fc9fd8e28f440fce7489691e0aee46b (diff)
Improve scrolling, handle onNestedPreFling
This contains three main changes. 1 Carry momentum from flings in the header into the ListView. 2 The header now snaps into a semi collapsed state more often then it used to 3 The current scrolling direction is now a larger factor in deciding where the header position will snap to upon finishing a scroll I coupled ViewDragHelper a bit closer to OverlappingPaneLayout. At first I tried to avoid this. But I think this was a wasted effort. ViewDragHelper is specifically forked for OverlappingPaneLayout. Some behaviors I made sure to test manually: 1 When expanding/collapsing the header the direction of motion should determine where the header snaps to upon release. 2 Collapsing from fully open to intermediate (not previously possible) 3 Drag tabs up/down regardless of whether at top of ListView or not (unchanged) 4 Dragging and releasing the tabs should cause the same sort of snapping behavior as scrolling and releasing the nested ListView (this still isn't exactly the same. I don't think this is important enough to dig into more) 5 After fully expanding the header by grabbing on the tabs, you can collapse the header normally via nested scrolling. 6 Scroll down the ListView. Then expand the header by dragging the tabs. Now scroll up and down in the ListView a bit. 7 Quickly fling up, down, up, down, up, down, up, down, up, down. Should feel the same as scrolling a regular ListView. 8 Fling upwards, stop the fling prematurly then release. The header shouldn't do anything (fixing this was a matter of adding a scroll slop). Bug: 16462679 Change-Id: I272a838885ce9045d41aaef1168b0ee0a32ee31d
Diffstat (limited to 'src/com/android/dialer/widget')
-rw-r--r--src/com/android/dialer/widget/OverlappingPaneLayout.java226
-rw-r--r--src/com/android/dialer/widget/ViewDragHelper.java87
2 files changed, 262 insertions, 51 deletions
diff --git a/src/com/android/dialer/widget/OverlappingPaneLayout.java b/src/com/android/dialer/widget/OverlappingPaneLayout.java
index b6b9ec777..b81722942 100644
--- a/src/com/android/dialer/widget/OverlappingPaneLayout.java
+++ b/src/com/android/dialer/widget/OverlappingPaneLayout.java
@@ -33,6 +33,7 @@ import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
@@ -116,18 +117,27 @@ public class OverlappingPaneLayout extends ViewGroup {
/**
* Indicates that the layout is currently in the process of a nested pre-scroll operation where
- * the child scrolling view is being dragged downwards, and still has the ability to consume
- * scroll events itself. If so, we should open the pane up to the maximum offset defined in
- * {@link #mIntermediateOffset}, and no further, so that the child view can continue performing
- * its own scroll.
+ * the child scrolling view is being dragged downwards.
*/
- private boolean mInNestedPreScrollDownwards = false;
+ private boolean mInNestedPreScrollDownwards;
/**
- * Indicates whether or not a nested scrolling child is able to scroll internally at this point
- * in time.
+ * Indicates that the layout is currently in the process of a nested pre-scroll operation where
+ * the child scrolling view is being dragged upwards.
*/
- private boolean mChildCannotConsumeScroll;
+ private boolean mInNestedPreScrollUpwards;
+
+ /**
+ * Indicates that the layout is currently in the process of a fling initiated by a pre-fling
+ * from the child scrolling view.
+ */
+ private boolean mIsInNestedFling;
+
+ /**
+ * Indicates the direction of the pre fling. We need to store this information since
+ * OverScoller doesn't expose the direction of its velocity.
+ */
+ private boolean mInUpwardsPreFling;
/**
* Stores an offset used to represent a point somewhere in between the panel's fully closed
@@ -139,7 +149,7 @@ public class OverlappingPaneLayout extends ViewGroup {
private float mInitialMotionX;
private float mInitialMotionY;
- private PanelSlideListener mPanelSlideListener;
+ private PanelSlideCallbacks mPanelSlideCallbacks;
private final ViewDragHelper mDragHelper;
@@ -154,9 +164,18 @@ public class OverlappingPaneLayout extends ViewGroup {
private final Rect mTmpRect = new Rect();
/**
- * Listener for monitoring events about sliding panes.
+ * How many dips we need to scroll past a position before we can snap to the next position
+ * on release. Using this prevents accidentally snapping to positions.
+ *
+ * This is needed since vertical nested scrolling can be passed to this class even if the
+ * vertical scroll is less than the the nested list's touch slop.
*/
- public interface PanelSlideListener {
+ private final int mReleaseScrollSlop;
+
+ /**
+ * Callbacks for interacting with sliding panes.
+ */
+ public interface PanelSlideCallbacks {
/**
* Called when a sliding pane's position changes.
* @param panel The child view that was moved
@@ -176,6 +195,22 @@ public class OverlappingPaneLayout extends ViewGroup {
* @param panel The child view that was slid to a closed position
*/
public void onPanelClosed(View panel);
+
+ /**
+ * Called when a sliding pane is flung as far open/closed as it can be.
+ * @param velocityY Velocity of the panel once its fling goes as far as it can.
+ */
+ public void onPanelFlingReachesEdge(int velocityY);
+
+ /**
+ * Returns true if the second panel's contents haven't been scrolled at all. This value is
+ * used to determine whether or not we can fully expand the header on downwards scrolls.
+ *
+ * Instead of using this callback, it would be preferable to instead fully expand the header
+ * on a View#onNestedFlingOver() callback. The behavior would be nicer. Unfortunately,
+ * no such callback exists yet (b/17547693).
+ */
+ public boolean isScrollableChildUnscrolled();
}
public OverlappingPaneLayout(Context context) {
@@ -199,6 +234,8 @@ public class OverlappingPaneLayout extends ViewGroup {
mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density);
+
+ mReleaseScrollSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
/**
@@ -218,27 +255,21 @@ public class OverlappingPaneLayout extends ViewGroup {
mCapturableView = capturableView;
}
- public void setPanelSlideListener(PanelSlideListener listener) {
- mPanelSlideListener = listener;
+ public void setPanelSlideCallbacks(PanelSlideCallbacks listener) {
+ mPanelSlideCallbacks = listener;
}
void dispatchOnPanelSlide(View panel) {
- if (mPanelSlideListener != null) {
- mPanelSlideListener.onPanelSlide(panel, mSlideOffset);
- }
+ mPanelSlideCallbacks.onPanelSlide(panel, mSlideOffset);
}
void dispatchOnPanelOpened(View panel) {
- if (mPanelSlideListener != null) {
- mPanelSlideListener.onPanelOpened(panel);
- }
+ mPanelSlideCallbacks.onPanelOpened(panel);
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void dispatchOnPanelClosed(View panel) {
- if (mPanelSlideListener != null) {
- mPanelSlideListener.onPanelClosed(panel);
- }
+ mPanelSlideCallbacks.onPanelClosed(panel);
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
@@ -820,7 +851,7 @@ public class OverlappingPaneLayout extends ViewGroup {
@Override
public void computeScroll() {
- if (mDragHelper.continueSettling(true)) {
+ if (mDragHelper.continueSettling(/* deferCallbacks = */ false)) {
if (!mCanSlide) {
mDragHelper.abort();
return;
@@ -897,7 +928,6 @@ public class OverlappingPaneLayout extends ViewGroup {
final boolean startNestedScroll = (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
if (startNestedScroll) {
mIsInNestedScroll = true;
- mChildCannotConsumeScroll = true;
mDragHelper.startNestedScroll(mSlideableView);
}
if (DEBUG) {
@@ -915,19 +945,41 @@ public class OverlappingPaneLayout extends ViewGroup {
if (DEBUG) {
Log.d(TAG, "onNestedPreScroll: " + dy);
}
- mInNestedPreScrollDownwards =
- mChildCannotConsumeScroll && dy < 0 && mSlideOffsetPx <= mIntermediateOffset;
+
+ mInNestedPreScrollDownwards = dy < 0;
+ mInNestedPreScrollUpwards = dy > 0;
+ mIsInNestedFling = false;
mDragHelper.processNestedScroll(mSlideableView, 0, -dy, consumed);
}
@Override
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ if (!(velocityY > 0 && mSlideOffsetPx != 0
+ || velocityY < 0 && mSlideOffsetPx < mIntermediateOffset
+ || velocityY < 0 && mSlideOffsetPx < mSlideRange
+ && mPanelSlideCallbacks.isScrollableChildUnscrolled())) {
+ // No need to consume the fling if the fling won't collapse or expand the header.
+ // How far we are willing to expand the header depends on isScrollableChildUnscrolled().
+ return false;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "onNestedPreFling: " + velocityY);
+ }
+ mInUpwardsPreFling = velocityY > 0;
+ mIsInNestedFling = true;
+ mIsInNestedScroll = false;
+ mDragHelper.processNestedFling(mSlideableView, (int) -velocityY);
+ return true;
+ }
+
+ @Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
if (DEBUG) {
Log.d(TAG, "onNestedScroll: " + dyUnconsumed);
}
- mChildCannotConsumeScroll = false;
- mInNestedPreScrollDownwards = false;
+ mIsInNestedFling = false;
mDragHelper.processNestedScroll(mSlideableView, 0, -dyUnconsumed, null);
}
@@ -938,8 +990,10 @@ public class OverlappingPaneLayout extends ViewGroup {
}
if (mIsInNestedScroll) {
mDragHelper.stopNestedScroll(mSlideableView);
+ mInNestedPreScrollDownwards = false;
+ mInNestedPreScrollUpwards = false;
+ mIsInNestedScroll = false;
}
- mIsInNestedScroll = false;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
@@ -955,6 +1009,10 @@ public class OverlappingPaneLayout extends ViewGroup {
@Override
public void onViewDragStateChanged(int state) {
+ if (DEBUG) {
+ Log.d(TAG, "onViewDragStateChanged: " + state);
+ }
+
if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
if (mSlideOffset == 0) {
updateObscuredViewsVisibility(mSlideableView);
@@ -965,6 +1023,16 @@ public class OverlappingPaneLayout extends ViewGroup {
mPreservedOpenState = true;
}
}
+
+ if (mDragHelper.getVelocityMagnitude() > 0
+ && (mDragHelper.getCurrentScrollY() == 0
+ || mDragHelper.getCurrentScrollY() == mIntermediateOffset)
+ && mIsInNestedFling) {
+ mIsInNestedFling = false;
+ final int flingVelocity = !mInUpwardsPreFling ?
+ -mDragHelper.getVelocityMagnitude() : mDragHelper.getVelocityMagnitude();
+ mPanelSlideCallbacks.onPanelFlingReachesEdge(flingVelocity);
+ }
}
@Override
@@ -980,20 +1048,96 @@ public class OverlappingPaneLayout extends ViewGroup {
}
@Override
+ public void onViewFling(View releasedChild, float xVelocity, float yVelocity) {
+ if (releasedChild == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onViewFling: " + yVelocity);
+ }
+
+ // Flings won't always fully expand or collapse the header. Instead of performing the
+ // fling and then waiting for the fling to end before snapping into place, we
+ // immediately snap into place if we predict the fling won't fully expand or collapse
+ // the header.
+ int yOffsetPx = mDragHelper.predictFlingYOffset((int) yVelocity);
+ if (yVelocity < 0) {
+ // Only perform a fling if we know the fling will fully compress the header.
+ if (-yOffsetPx > mSlideOffsetPx) {
+ mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0,
+ mSlideRange, Integer.MAX_VALUE, (int) yVelocity);
+ } else {
+ mIsInNestedFling = false;
+ onViewReleased(releasedChild, xVelocity, yVelocity);
+ }
+ } else {
+ // Only perform a fling if we know the fling will expand the header as far
+ // as it can possible be expanded, given the isScrollableChildUnscrolled() value.
+ if (yOffsetPx + mSlideOffsetPx >= mSlideRange
+ && mPanelSlideCallbacks.isScrollableChildUnscrolled()) {
+ mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0,
+ Integer.MAX_VALUE, mSlideRange, (int) yVelocity);
+ } else if (yOffsetPx + mSlideOffsetPx >= mIntermediateOffset
+ && mSlideOffsetPx <= mIntermediateOffset
+ && !mPanelSlideCallbacks.isScrollableChildUnscrolled()) {
+ mDragHelper.flingCapturedView(releasedChild.getLeft(), /* minTop = */ 0,
+ Integer.MAX_VALUE, mIntermediateOffset, (int) yVelocity);
+ } else {
+ mIsInNestedFling = false;
+ onViewReleased(releasedChild, xVelocity, yVelocity);
+ }
+ }
+
+ mInNestedPreScrollDownwards = false;
+ mInNestedPreScrollUpwards = false;
+
+ // Without this invalidate, some calls to flingCapturedView can have no affect.
+ invalidate();
+ }
+
+ @Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ if (DEBUG) {
+ Log.d(TAG, "onViewReleased: "
+ + " unscrolled=" + mPanelSlideCallbacks.isScrollableChildUnscrolled()
+ + ", mInNestedPreScrollDownwards = " + mInNestedPreScrollDownwards
+ + ", mInNestedPreScrollUpwards = " + mInNestedPreScrollUpwards
+ + ", yvel=" + yvel);
+ }
if (releasedChild == null) {
return;
}
- final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams();
+ final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams();
int top = getPaddingTop() + lp.topMargin;
- if (mInNestedPreScrollDownwards) {
- // Snap to the closest pinnable position based on the current slide offset
- // (in pixels) [0 - mIntermediateoffset - mSlideRange]
- if (yvel > 0) {
+ // Decide where to snap to according to the current direction of motion and the current
+ // position. The velocity's magnitude has no bearing on this.
+ if (mInNestedPreScrollDownwards || yvel > 0) {
+ // Scrolling downwards
+ if (mSlideOffsetPx > mIntermediateOffset + mReleaseScrollSlop) {
+ top += mSlideRange;
+ } else if (mSlideOffsetPx > mReleaseScrollSlop) {
+ top += mIntermediateOffset;
+ } else {
+ // Offset is very close to 0
+ }
+ } else if (mInNestedPreScrollUpwards || yvel < 0) {
+ // Scrolling upwards
+ if (mSlideOffsetPx > mSlideRange - mReleaseScrollSlop) {
+ // Offset is very close to mSlideRange
top += mSlideRange;
- } else if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) {
+ } else if (mSlideOffsetPx > mIntermediateOffset - mReleaseScrollSlop) {
+ // Offset is between mIntermediateOffset and mSlideRange.
+ top += mIntermediateOffset;
+ } else {
+ // Offset is between 0 and mIntermediateOffset.
+ }
+ } else {
+ // Not moving upwards or downwards. This case can only be triggered when directly
+ // dragging the tabs. We don't bother to remember previous scroll direction
+ // when directly dragging the tabs.
+ if (0 <= mSlideOffsetPx && mSlideOffsetPx <= mIntermediateOffset / 2) {
// Offset is between 0 and mIntermediateOffset, but closer to 0
// Leave top unchanged
} else if (mIntermediateOffset / 2 <= mSlideOffsetPx
@@ -1005,8 +1149,6 @@ public class OverlappingPaneLayout extends ViewGroup {
// mSlideRange
top += mSlideRange;
}
- } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) {
- top += mSlideRange;
}
mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
@@ -1029,9 +1171,15 @@ public class OverlappingPaneLayout extends ViewGroup {
final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
final int newTop;
+ int previousTop = top - dy;
int topBound = getPaddingTop() + lp.topMargin;
- int bottomBound = topBound
- + (mInNestedPreScrollDownwards ? mIntermediateOffset : mSlideRange);
+ int bottomBound = topBound + (mPanelSlideCallbacks.isScrollableChildUnscrolled()
+ || !mIsInNestedScroll ? mSlideRange : mIntermediateOffset);
+ if (previousTop > bottomBound) {
+ // We were previously below the bottomBound, so loosen the bottomBound so that this
+ // makes sense. This can occur after the view was directly dragged by the tabs.
+ bottomBound = Math.max(bottomBound, mSlideRange);
+ }
newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
diff --git a/src/com/android/dialer/widget/ViewDragHelper.java b/src/com/android/dialer/widget/ViewDragHelper.java
index 91016d15b..e4fe12be2 100644
--- a/src/com/android/dialer/widget/ViewDragHelper.java
+++ b/src/com/android/dialer/widget/ViewDragHelper.java
@@ -27,7 +27,6 @@ import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
-import android.view.animation.Interpolator;
import java.util.Arrays;
@@ -202,6 +201,18 @@ public class ViewDragHelper {
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
/**
+ * Called when the child view has been released with a fling.
+ *
+ * <p>Calling code may decide to fling or otherwise release the view to let it
+ * settle into place.</p>
+ *
+ * @param releasedChild The captured child view now being released
+ * @param xvel X velocity of the fling.
+ * @param yvel Y velocity of the fling.
+ */
+ public void onViewFling(View releasedChild, float xvel, float yvel) {}
+
+ /**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
*
@@ -321,16 +332,6 @@ public class ViewDragHelper {
}
}
- /**
- * Interpolator defining the animation curve for mScroller
- */
- private static final Interpolator sInterpolator = new Interpolator() {
- public float getInterpolation(float t) {
- t -= 1.0f;
- return t * t * t * t * t + 1.0f;
- }
- };
-
private final Runnable mSetIdleRunnable = new Runnable() {
public void run() {
setDragState(STATE_IDLE);
@@ -389,7 +390,7 @@ public class ViewDragHelper {
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
- mScroller = ScrollerCompat.create(context, sInterpolator);
+ mScroller = ScrollerCompat.create(context);
}
/**
@@ -702,6 +703,46 @@ public class ViewDragHelper {
}
/**
+ * Settle the captured view based on standard free-moving fling behavior.
+ * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
+ * to continue the motion until it returns false.
+ *
+ * @param minLeft Minimum X position for the view's left edge
+ * @param minTop Minimum Y position for the view's top edge
+ * @param maxLeft Maximum X position for the view's left edge
+ * @param maxTop Maximum Y position for the view's top edge
+ * @param yvel the Y velocity to fling with
+ */
+ public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop, int yvel) {
+ if (!mReleaseInProgress) {
+ throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
+ "Callback#onViewReleased");
+ }
+ mScroller.abortAnimation();
+ mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), 0, yvel, minLeft, maxLeft,
+ minTop, maxTop);
+
+ setDragState(STATE_SETTLING);
+ }
+
+ /**
+ * Predict how far a fling with {@param yvel} will cause the view to travel from stand still.
+ * @return predicted y offset
+ */
+ public int predictFlingYOffset(int yvel) {
+ mScroller.abortAnimation();
+ mScroller.fling(0, 0, 0, yvel, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE,
+ Integer.MAX_VALUE);
+ final int finalY = mScroller.getFinalY();
+ mScroller.abortAnimation();
+ return finalY;
+ }
+
+ public int getCurrentScrollY() {
+ return mScroller.getCurrY();
+ }
+
+ /**
* Move the captured settling view by the appropriate amount for the current time.
* If <code>continueSettling</code> returns true, the caller should call it again
* on the next frame to continue.
@@ -750,6 +791,28 @@ public class ViewDragHelper {
return mDragState == STATE_SETTLING;
}
+ public void processNestedFling(View target, int yvel) {
+ mCapturedView = target;
+ dispatchViewFling(0, yvel);
+ }
+
+ public int getVelocityMagnitude() {
+ // Use Math.abs() to ensure this always returns an absolute value, even if the
+ // ScrollerCompat implementation changes.
+ return (int) Math.abs(mScroller.getCurrVelocity());
+ }
+
+ private void dispatchViewFling(float xvel, float yvel) {
+ mReleaseInProgress = true;
+ mCallback.onViewFling(mCapturedView, xvel, yvel);
+ mReleaseInProgress = false;
+
+ if (mDragState == STATE_DRAGGING) {
+ // onViewReleased didn't call a method that would have changed this. Go idle.
+ setDragState(STATE_IDLE);
+ }
+ }
+
/**
* Like all callback events this must happen on the UI thread, but release
* involves some extra semantics. During a release (mReleaseInProgress)