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

Commit d2eb49e4 authored by Winson's avatar Winson Committed by Winson Chung
Browse files

Tweaking fast scroller to follow touch closer.

- Issue: The fast scroller currently does not follow the touch input
  because fundamentally, the fixed scrollbar height and the mapping
  of the scroll space to the scrollbar space is fundamentally incompatible.
- This CL changes the fast scroller to allow it to detach when the user
  fast-scrolls, then re-attaches after the user scrolls the screen and
  the current scroll position for the scrollbar picks up the thumb position.
- Since the scroll position and the fast scroller thumb is now detached,
  we can change the distribution of the fast scroll letters to make it
  independent of the rows for each section and instead uniformly distribute
  it along the scrollbar, which allows for more stability.
- There are edge cases where this fails, especially when there are few
  apps, which we can investigate further.
  
Bug: 20035978

Change-Id: I8322f862107e6f330deff692885233706564bffd
parent 97b0d08d
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -3,8 +3,8 @@
}

-keep class com.android.launcher3.BaseRecyclerViewFastScrollBar {
  public void setWidth(int);
  public int getWidth();
  public void setThumbWidth(int);
  public int getThumbWidth();
  public void setTrackAlpha(int);
  public int getTrackAlpha();
}
+23 −17
Original line number Diff line number Diff line
@@ -92,7 +92,13 @@ public abstract class BaseRecyclerView extends RecyclerView
            // TODO(winsonc): If we want to animate the section heads while scrolling, we can
            //                initiate that here if the recycler view scroll state is not
            //                RecyclerView.SCROLL_STATE_IDLE.

            onUpdateScrollbar(dy);
        }
    }

    public void reset() {
        mScrollbar.reattachThumbToScroll();
    }

    @Override
@@ -143,7 +149,7 @@ public abstract class BaseRecyclerView extends RecyclerView
                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
                break;
        }
        return mScrollbar.isDragging();
        return mScrollbar.isDraggingThumb();
    }

    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -185,12 +191,10 @@ public abstract class BaseRecyclerView extends RecyclerView
     *   AvailableScrollHeight = Total height of the all items - last page height
     *
     * This assumes that all rows are the same height.
     *
     * @param yOffset the offset from the top of the recycler view to start tracking.
     */
    protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) {
    protected int getAvailableScrollHeight(int rowCount, int rowHeight) {
        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
        int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom();
        int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom();
        int availableScrollHeight = scrollHeight - visibleHeight;
        return availableScrollHeight;
    }
@@ -222,7 +226,7 @@ public abstract class BaseRecyclerView extends RecyclerView
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        onUpdateScrollbar();
        onUpdateScrollbar(0);
        mScrollbar.draw(canvas);
    }

@@ -234,24 +238,21 @@ public abstract class BaseRecyclerView extends RecyclerView
     * @param scrollPosState the current scroll position
     * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
     *                 all rows are the same height)
     * @param yOffset the offset to start tracking in the recycler view (only used for all apps)
     */
    protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
            int rowCount, int yOffset) {
        int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight,
                yOffset);
        int availableScrollBarHeight = getAvailableScrollBarHeight();

            int rowCount) {
        // Only show the scrollbar if there is height to be scrolled
        int availableScrollBarHeight = getAvailableScrollBarHeight();
        int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight);
        if (availableScrollHeight <= 0) {
            mScrollbar.setScrollbarThumbOffset(-1, -1);
            mScrollbar.setThumbOffset(-1, -1);
            return;
        }

        // Calculate the current scroll position, the scrollY of the recycler view accounts for the
        // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
        // padding)
        int scrollY = getPaddingTop() + yOffset +
        int scrollY = getPaddingTop() +
                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
        int scrollBarY = mBackgroundPadding.top +
                (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
@@ -261,9 +262,9 @@ public abstract class BaseRecyclerView extends RecyclerView
        if (Utilities.isRtl(getResources())) {
            scrollBarX = mBackgroundPadding.left;
        } else {
            scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth();
            scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
        }
        mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY);
        mScrollbar.setThumbOffset(scrollBarX, scrollBarY);
    }

    /**
@@ -276,10 +277,15 @@ public abstract class BaseRecyclerView extends RecyclerView
     * Updates the bounds for the scrollbar.
     * <p>Override in each subclass of this base class.
     */
    public abstract void onUpdateScrollbar();
    public abstract void onUpdateScrollbar(int dy);

    /**
     * <p>Override in each subclass of this base class.
     */
    public void onFastScrollCompleted() {}

    /**
     * Returns information about the item that the recycler view is currently scrolled to.
     */
    protected abstract void getCurScrollState(ScrollPositionState stateOut);
}
 No newline at end of file
+33 −5
Original line number Diff line number Diff line
@@ -56,9 +56,12 @@ public class BaseRecyclerViewFastScrollBar {
    private int mThumbMaxWidth;
    @Thunk int mThumbWidth;
    @Thunk int mThumbHeight;
    private float mLastTouchY;
    // The inset is the buffer around which a point will still register as a click on the scrollbar
    private int mTouchInset;
    private boolean mIsDragging;
    private boolean mIsThumbDetached;
    private boolean mCanThumbDetach;

    // This is the offset from the top of the scrollbar when the user first starts touching.  To
    // prevent jumping, this offset is applied as the user scrolls.
@@ -84,7 +87,15 @@ public class BaseRecyclerViewFastScrollBar {
        mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
    }

    public void setScrollbarThumbOffset(int x, int y) {
    public void setDetachThumbOnFastScroll() {
        mCanThumbDetach = true;
    }

    public void reattachThumbToScroll() {
        mIsThumbDetached = false;
    }

    public void setThumbOffset(int x, int y) {
        if (mThumbOffset.x == x && mThumbOffset.y == y) {
            return;
        }
@@ -95,8 +106,12 @@ public class BaseRecyclerViewFastScrollBar {
        mRv.invalidate(mInvalidateRect);
    }

    public Point getThumbOffset() {
        return mThumbOffset;
    }

    // Setter/getter for the search bar width for animations
    public void setWidth(int width) {
    public void setThumbWidth(int width) {
        mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight());
        mThumbWidth = width;
        mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
@@ -104,7 +119,7 @@ public class BaseRecyclerViewFastScrollBar {
        mRv.invalidate(mInvalidateRect);
    }

    public int getWidth() {
    public int getThumbWidth() {
        return mThumbWidth;
    }

@@ -127,10 +142,18 @@ public class BaseRecyclerViewFastScrollBar {
        return mThumbMaxWidth;
    }

    public boolean isDragging() {
    public float getLastTouchY() {
        return mLastTouchY;
    }

    public boolean isDraggingThumb() {
        return mIsDragging;
    }

    public boolean isThumbDetached() {
        return mIsThumbDetached;
    }

    /**
     * Handles the touch event and determines whether to show the fast scroller (or updates it if
     * it is already showing).
@@ -152,6 +175,9 @@ public class BaseRecyclerViewFastScrollBar {
                        Math.abs(y - downY) > config.getScaledTouchSlop()) {
                    mRv.getParent().requestDisallowInterceptTouchEvent(true);
                    mIsDragging = true;
                    if (mCanThumbDetach) {
                        mIsThumbDetached = true;
                    }
                    mTouchOffset += (lastY - downY);
                    mPopup.animateVisibility(true);
                    animateScrollbar(true);
@@ -166,11 +192,13 @@ public class BaseRecyclerViewFastScrollBar {
                    mPopup.setSectionName(sectionName);
                    mPopup.animateVisibility(!sectionName.isEmpty());
                    mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY));
                    mLastTouchY = boundedY;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mTouchOffset = 0;
                mLastTouchY = 0;
                if (mIsDragging) {
                    mIsDragging = false;
                    mPopup.animateVisibility(false);
@@ -205,7 +233,7 @@ public class BaseRecyclerViewFastScrollBar {
        }
        ObjectAnimator trackAlphaAnim = ObjectAnimator.ofInt(this, "trackAlpha",
                isScrolling ? MAX_TRACK_ALPHA : 0);
        ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "width",
        ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
                isScrolling ? mThumbMaxWidth : mThumbMinWidth);
        ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
                mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);
+2 −1
Original line number Diff line number Diff line
@@ -555,8 +555,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
    @Override
    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
        if (toWorkspace) {
            // Reset the search bar after transitioning home
            // Reset the search bar and base recycler view after transitioning home
            mSearchBarController.reset();
            mAppsRecyclerView.reset();
        }
    }

+59 −9
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView
    public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr);
        mScrollbar.setDetachThumbOnFastScroll();
    }

    /**
@@ -168,8 +169,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView
        }

        // Map the touch position back to the scroll of the recycler view
        getCurScrollState(mScrollPosState, mApps.getAdapterItems());
        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
        getCurScrollState(mScrollPosState);
        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
        if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
            layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
@@ -216,24 +217,73 @@ public class AllAppsRecyclerView extends BaseRecyclerView
     * Updates the bounds for the scrollbar.
     */
    @Override
    public void onUpdateScrollbar() {
    public void onUpdateScrollbar(int dy) {
        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();

        // Skip early if there are no items or we haven't been measured
        if (items.isEmpty() || mNumAppsPerRow == 0) {
            mScrollbar.setScrollbarThumbOffset(-1, -1);
            mScrollbar.setThumbOffset(-1, -1);
            return;
        }

        // Find the index and height of the first visible row (all rows have the same height)
        int rowCount = mApps.getNumAppRows();
        getCurScrollState(mScrollPosState, items);
        getCurScrollState(mScrollPosState);
        if (mScrollPosState.rowIndex < 0) {
            mScrollbar.setScrollbarThumbOffset(-1, -1);
            mScrollbar.setThumbOffset(-1, -1);
            return;
        }

        synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
        // Only show the scrollbar if there is height to be scrolled
        int availableScrollBarHeight = getAvailableScrollBarHeight();
        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight);
        if (availableScrollHeight <= 0) {
            mScrollbar.setThumbOffset(-1, -1);
            return;
        }

        // Calculate the current scroll position, the scrollY of the recycler view accounts for the
        // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
        // padding)
        int scrollY = getPaddingTop() +
                (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
        int scrollBarY = mBackgroundPadding.top +
                (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);

        if (mScrollbar.isThumbDetached()) {
            int scrollBarX;
            if (Utilities.isRtl(getResources())) {
                scrollBarX = mBackgroundPadding.left;
            } else {
                scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
            }

            if (mScrollbar.isDraggingThumb()) {
                // If the thumb is detached, then just update the thumb to the current
                // touch position
                mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY());
            } else {
                int thumbScrollY = mScrollbar.getThumbOffset().y;
                int diffScrollY = scrollBarY - thumbScrollY;
                if (diffScrollY * dy > 0f) {
                    // User is scrolling in the same direction the thumb needs to catch up to the
                    // current scroll position.
                    thumbScrollY += dy < 0 ? Math.max(dy, diffScrollY) : Math.min(dy, diffScrollY);
                    thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
                    mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
                    if (scrollBarY == thumbScrollY) {
                        mScrollbar.reattachThumbToScroll();
                    }
                } else {
                    // User is scrolling in an opposite direction to the direction that the thumb
                    // needs to catch up to the scroll position.  Do nothing except for updating
                    // the scroll bar x to match the thumb width.
                    mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
                }
            }
        } else {
            synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount);
        }
    }

    /**
@@ -285,13 +335,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView
    /**
     * Returns the current scroll state of the apps rows.
     */
    private void getCurScrollState(ScrollPositionState stateOut,
            List<AlphabeticalAppsList.AdapterItem> items) {
    protected void getCurScrollState(ScrollPositionState stateOut) {
        stateOut.rowIndex = -1;
        stateOut.rowTopOffset = -1;
        stateOut.rowHeight = -1;

        // Return early if there are no items or we haven't been measured
        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
        if (items.isEmpty() || mNumAppsPerRow == 0) {
            return;
        }
Loading