Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 764f652b authored by Brian Attwell's avatar Brian Attwell
Browse files

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
parent f2d3bd5d
Loading
Loading
Loading
Loading
+51 −8
Original line number Diff line number Diff line
@@ -4,15 +4,10 @@ import android.animation.LayoutTransition;
import android.app.ActionBar;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.LoaderManager;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Loader;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CallLog;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
@@ -20,6 +15,7 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ListView;

import com.android.contacts.common.GeoUtil;
@@ -34,7 +30,7 @@ import com.android.dialer.calllog.ContactInfoHelper;
import com.android.dialer.list.ShortcutCardsAdapter.SwipeableShortcutCard;
import com.android.dialer.util.DialerUtils;
import com.android.dialer.widget.OverlappingPaneLayout;
import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideListener;
import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideCallbacks;
import com.android.dialerbind.analytics.AnalyticsFragment;
import com.android.dialerbind.ObjectFactory;

@@ -108,7 +104,7 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand
     */
    private long mCurrentCallShortcutDate = 0;

    private PanelSlideListener mPanelSlideListener = new PanelSlideListener() {
    private PanelSlideCallbacks mPanelSlideCallbacks = new PanelSlideCallbacks() {
        @Override
        public void onPanelSlide(View panel, float slideOffset) {
            // For every 1 percent that the panel is slid upwards, clip 1 percent off the top
@@ -152,8 +148,35 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand
            }
            mIsPanelOpen = false;
        }

        @Override
        public void onPanelFlingReachesEdge(int velocityY) {
            if (getCurrentListView() != null) {
                getCurrentListView().fling(velocityY);
            }
        }

        @Override
        public boolean isScrollableChildUnscrolled() {
            final AbsListView listView = getCurrentListView();
            return listView != null && (listView.getChildCount() == 0
                    || listView.getChildAt(0).getTop() == listView.getPaddingTop());
        }
    };

    private AbsListView getCurrentListView() {
        final int position = mViewPager.getCurrentItem();
        switch (getRtlPosition(position)) {
            case TAB_INDEX_SPEED_DIAL:
                return mSpeedDialFragment == null ? null : mSpeedDialFragment.getListView();
            case TAB_INDEX_RECENTS:
                return mRecentsFragment == null ? null : mRecentsFragment.getListView();
            case TAB_INDEX_ALL_CONTACTS:
                return mAllContactsFragment == null ? null : mAllContactsFragment.getListView();
        }
        throw new IllegalStateException("No fragment at position " + position);
    }

    public class ViewPagerAdapter extends FragmentPagerAdapter {
        public ViewPagerAdapter(FragmentManager fm) {
            super(fm);
@@ -177,6 +200,26 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand
            throw new IllegalStateException("No fragment at position " + position);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            // On rotation the FragmentManager handles rotation. Therefore getItem() isn't called.
            // Copy the fragments that the FragmentManager finds so that we can store them in
            // instance variables for later.
            final Fragment fragment = (Fragment) super.instantiateItem(container, position);
            switch (getRtlPosition(position)) {
                case TAB_INDEX_SPEED_DIAL:
                    mSpeedDialFragment = (SpeedDialFragment) fragment;
                    return mSpeedDialFragment;
                case TAB_INDEX_RECENTS:
                    mRecentsFragment = (CallLogFragment) fragment;
                    return mRecentsFragment;
                case TAB_INDEX_ALL_CONTACTS:
                    mAllContactsFragment = (AllContactsFragment) fragment;
                    return mAllContactsFragment;
            }
            return super.instantiateItem(container, position);
        }

        @Override
        public int getCount() {
            return TAB_INDEX_COUNT;
@@ -360,7 +403,7 @@ public class ListsFragment extends AnalyticsFragment implements CallLogQueryHand
        // the framework better supports nested scrolling.
        paneLayout.setCapturableView(mViewPagerTabs);
        paneLayout.openPane();
        paneLayout.setPanelSlideListener(mPanelSlideListener);
        paneLayout.setPanelSlideCallbacks(mPanelSlideCallbacks);
        paneLayout.setIntermediatePinnedOffset(
                ((HostInterface) getActivity()).getActionBarHeight());

+4 −0
Original line number Diff line number Diff line
@@ -420,4 +420,8 @@ public class SpeedDialFragment extends AnalyticsFragment implements OnItemClickL
    public void cacheOffsetsForDatasetChange() {
        saveOffsets(0);
    }

    public AbsListView getListView() {
        return mListView;
    }
}
+187 −39
Original line number Diff line number Diff line
@@ -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 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 mChildCannotConsumeScroll;
    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,9 +990,11 @@ public class OverlappingPaneLayout extends ViewGroup {
        }
        if (mIsInNestedScroll) {
            mDragHelper.stopNestedScroll(mSlideableView);
        }
            mInNestedPreScrollDownwards = false;
            mInNestedPreScrollUpwards = 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
@@ -979,21 +1047,97 @@ public class OverlappingPaneLayout extends ViewGroup {
            invalidate();
        }

        @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;
+75 −12
Original line number Diff line number Diff line
@@ -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;

@@ -201,6 +200,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);
    }

    /**
@@ -701,6 +702,46 @@ public class ViewDragHelper {
        setDragState(STATE_SETTLING);
    }

    /**
     * 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
@@ -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)