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

Commit ac5f6af1 authored by Hyunyoung Song's avatar Hyunyoung Song
Browse files

Move fast scrolling logic to BaseRecyclerView

- This change has no effect on actual functionality but to make it easier
for widget tray to inherit the goodness of the 1) fast scroller
functionality 2) unify the scroll look and feel with the all apps view

b/21375339

Change-Id: Ib859b1c3352c0b69f16549ded8f20eb82cf58ba8
parent 0b068759
Loading
Loading
Loading
Loading
+265 −7
Original line number Diff line number Diff line
@@ -16,15 +16,28 @@

package com.android.launcher3;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import com.android.launcher3.util.Thunk;

/**
 * A base {@link RecyclerView}, which will NOT intercept a touch sequence unless the scrolling
 * velocity is below a predefined threshold.
 * A base {@link RecyclerView}, which does the following:
 * <ul>
 *   <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
 *   <li> Enable fast scroller.
 * </ul>
 */
public class BaseRecyclerView extends RecyclerView
        implements RecyclerView.OnItemTouchListener {
@@ -35,6 +48,53 @@ public class BaseRecyclerView extends RecyclerView
    @Thunk int mDy = 0;
    private float mDeltaThreshold;

    //
    // Keeps track of variables required for the second function of this class: fast scroller.
    //

    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;

    /**
     * The current scroll state of the recycler view.  We use this in updateVerticalScrollbarBounds()
     * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
     * that we can calculate what the scroll bar looks like, and where to jump to from the fast
     * scroller.
     */
    public static class ScrollPositionState {
        // The index of the first visible row
        public int rowIndex;
        // The offset of the first visible row
        public int rowTopOffset;
        // The height of a given row (they are currently all the same height)
        public int rowHeight;
    }
    // Should be maintained inside overriden method #updateVerticalScrollbarBounds
    public ScrollPositionState scrollPosState = new ScrollPositionState();
    public Rect verticalScrollbarBounds = new Rect();

    private boolean mDraggingFastScroller;

    private Drawable mScrollbar;
    private Drawable mFastScrollerBg;
    private Rect mTmpFastScrollerInvalidateRect = new Rect();
    private Rect mFastScrollerBounds = new Rect();

    private String mFastScrollSectionName;
    private Paint mFastScrollTextPaint;
    private Rect mFastScrollTextBounds = new Rect();
    private float mFastScrollAlpha;

    private int mDownX;
    private int mDownY;
    private int mLastX;
    private int mLastY;
    private int mScrollbarWidth;
    private int mScrollbarMinHeight;
    private int mScrollbarInset;
    private Rect mBackgroundPadding = new Rect();



    public BaseRecyclerView(Context context) {
        this(context, null);
    }
@@ -49,6 +109,24 @@ public class BaseRecyclerView extends RecyclerView

        ScrollListener listener = new ScrollListener();
        setOnScrollListener(listener);

        Resources res = context.getResources();
        int fastScrollerSize = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_popup_size);
        mScrollbar = res.getDrawable(R.drawable.all_apps_scrollbar_thumb);
        mFastScrollerBg = res.getDrawable(R.drawable.all_apps_fastscroll_bg);
        mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
        mFastScrollTextPaint = new Paint();
        mFastScrollTextPaint.setColor(Color.WHITE);
        mFastScrollTextPaint.setAntiAlias(true);
        mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
                R.dimen.all_apps_fast_scroll_text_size));
        mScrollbarWidth = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_width);
        mScrollbarMinHeight =
                res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_min_height);
        mScrollbarInset =
                res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_scrubber_touch_inset);
        setFastScrollerAlpha(mFastScrollAlpha);
        setOverScrollMode(View.OVER_SCROLL_NEVER);
    }

    private class ScrollListener extends OnScrollListener {
@@ -68,17 +146,74 @@ public class BaseRecyclerView extends RecyclerView
        addOnItemTouchListener(this);
    }

    /**
     * We intercept the touch handling only to support fast scrolling when initiated from the
     * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
     */
    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
        return handleTouchEvent(ev);
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
        handleTouchEvent(ev);
    }

    /**
     * Handles the touch event and determines whether to show the fast scroller (or updates it if
     * it is already showing).
     */
    private boolean handleTouchEvent(MotionEvent ev) {
        ViewConfiguration config = ViewConfiguration.get(getContext());

        int action = ev.getAction();
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // Keep track of the down positions
                mDownX = mLastX = x;
                mDownY = mLastY = y;
                if (shouldStopScroll(ev)) {
                    stopScroll();
                }
        return false;
                break;
            case MotionEvent.ACTION_MOVE:
                // Check if we are scrolling
                if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
                        Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    mDraggingFastScroller = true;
                    animateFastScrollerVisibility(true);
                }
                if (mDraggingFastScroller) {
                    mLastX = x;
                    mLastY = y;

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
        // Do nothing.
                    // Scroll to the right position, and update the section name
                    int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
                    int bottom = getHeight() - getPaddingBottom() -
                            (mFastScrollerBg.getBounds().height() / 2);
                    float boundedY = (float) Math.max(top, Math.min(bottom, y));
                    mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
                            (bottom - top));

                    // Combine the old and new fast scroller bounds to create the full invalidate
                    // rect
                    mTmpFastScrollerInvalidateRect.set(mFastScrollerBounds);
                    updateFastScrollerBounds();
                    mTmpFastScrollerInvalidateRect.union(mFastScrollerBounds);
                    invalidateFastScroller(mTmpFastScrollerInvalidateRect);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mDraggingFastScroller = false;
                animateFastScrollerVisibility(false);
                break;
        }
        return mDraggingFastScroller;
    }

    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -99,4 +234,127 @@ public class BaseRecyclerView extends RecyclerView
        }
        return false;
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        drawVerticalScrubber(canvas);
        drawFastScrollerPopup(canvas);
    }

    /**
     * Draws the vertical scrollbar.
     */
    private void drawVerticalScrubber(Canvas canvas) {
        updateVerticalScrollbarBounds();

        // Draw the scroll bar
        int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
        canvas.translate(verticalScrollbarBounds.left, verticalScrollbarBounds.top);
        mScrollbar.setBounds(0, 0, mScrollbarWidth, verticalScrollbarBounds.height());
        mScrollbar.draw(canvas);
        canvas.restoreToCount(restoreCount);
    }

    /**
     * Draws the fast scroller popup.
     */
    private void drawFastScrollerPopup(Canvas canvas) {
        if (mFastScrollAlpha > 0f && mFastScrollSectionName != null && !mFastScrollSectionName.isEmpty()) {
            // Draw the fast scroller popup
            int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
            canvas.translate(mFastScrollerBounds.left, mFastScrollerBounds.top);
            mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
            mFastScrollerBg.draw(canvas);
            mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
            mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
                    mFastScrollSectionName.length(), mFastScrollTextBounds);
            float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName);
            canvas.drawText(mFastScrollSectionName,
                    (mFastScrollerBounds.width() - textWidth) / 2,
                    mFastScrollerBounds.height() -
                            (mFastScrollerBounds.height() - mFastScrollTextBounds.height()) / 2,
                    mFastScrollTextPaint);
            canvas.restoreToCount(restoreCount);
        }
    }

    /**
     * Returns the scroll bar width.
     */
    public int getScrollbarWidth() {
        return mScrollbarWidth;
    }

    /**
     * Sets the fast scroller alpha.
     */
    public void setFastScrollerAlpha(float alpha) {
        mFastScrollAlpha = alpha;
        invalidateFastScroller(mFastScrollerBounds);
    }

    /**
     * Maps the touch (from 0..1) to the adapter position that should be visible.
     * <p>Override in each subclass of this base class.
     */
    public String scrollToPositionAtProgress(float touchFraction) {
        return null;
    }

    /**
     * Updates the bounds for the scrollbar.
     * <p>Override in each subclass of this base class.
     */
    public void updateVerticalScrollbarBounds() {};

    /**
     * Animates the visibility of the fast scroller popup.
     */
    private void animateFastScrollerVisibility(boolean visible) {
        ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
        anim.setDuration(visible ? 200 : 150);
        anim.start();
    }

    /**
     * Invalidates the fast scroller popup.
     */
    protected void invalidateFastScroller(Rect bounds) {
        invalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
    }

    /**
     * Returns whether a given point is near the scrollbar.
     */
    private boolean isPointNearScrollbar(int x, int y) {
        // Check if we are scrolling
        updateVerticalScrollbarBounds();
        verticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
        return verticalScrollbarBounds.contains(x, y);
    }

    /**
     * Updates the bounds for the fast scroller.
     */
    private void updateFastScrollerBounds() {
        if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
            int x;
            int y;

            // Calculate the position for the fast scroller popup
            Rect bgBounds = mFastScrollerBg.getBounds();
            if (Utilities.isRtl(getResources())) {
                x = mBackgroundPadding.left + getScrollBarSize();
            } else {
                x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
            }
            y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
            y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
                    bgBounds.height()));
            mFastScrollerBounds.set(x, y, x + bgBounds.width(), y + bgBounds.height());
        } else {
            mFastScrollerBounds.setEmpty();
        }
    }
}
 No newline at end of file
+55 −297

File changed.

Preview size limit exceeded, changes collapsed.

+3 −2
Original line number Diff line number Diff line
@@ -65,7 +65,7 @@ public class WidgetsContainerView extends BaseContainerView
    private IconCache mIconCache;

    /* Recycler view related member variables */
    private RecyclerView mView;
    private WidgetsRecyclerView mView;
    private WidgetsListAdapter mAdapter;

    /* Touch handling related member variables. */
@@ -100,7 +100,7 @@ public class WidgetsContainerView extends BaseContainerView

    @Override
    protected void onFinishInflate() {
        mView = (RecyclerView) findViewById(R.id.widgets_list_view);
        mView = (WidgetsRecyclerView) findViewById(R.id.widgets_list_view);
        mView.setAdapter(mAdapter);

        // This extends the layout space so that preloading happen for the {@link RecyclerView}
@@ -351,6 +351,7 @@ public class WidgetsContainerView extends BaseContainerView
     * Initialize the widget data model.
     */
    public void addWidgets(WidgetsModel model) {
        mView.setWidgets(model);
        mAdapter.setWidgetsModel(model);
        mAdapter.notifyDataSetChanged();
    }
+0 −1
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import android.widget.LinearLayout;

import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.IconCache;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
+72 −0
Original line number Diff line number Diff line
@@ -17,14 +17,23 @@
package com.android.launcher3.widget;

import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;

import com.android.launcher3.BaseRecyclerView;
import com.android.launcher3.model.WidgetsModel;

/**
 * The widgets recycler view.
 */
public class WidgetsRecyclerView extends BaseRecyclerView {

    private WidgetsModel mWidgets;
    private Rect mBackgroundPadding = new Rect();

    public WidgetsRecyclerView(Context context) {
        this(context, null);
    }
@@ -37,4 +46,67 @@ public class WidgetsRecyclerView extends BaseRecyclerView {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        addOnItemTouchListener(this);
    }

    public void updateBackgroundPadding(Drawable background) {
        background.getPadding(mBackgroundPadding);
    }

    /**
     * Sets the widget model in this view, used to determine the fast scroll position.
     */
    public void setWidgets(WidgetsModel widgets) {
        mWidgets = widgets;
    }

    /**
     * Maps the touch (from 0..1) to the adapter position that should be visible.
     */
    @Override
    public String scrollToPositionAtProgress(float touchFraction) {
        // Ensure that we have any sections
        return "";
    }

    /**
     * Updates the bounds for the scrollbar.
     */
    @Override
    public void updateVerticalScrollbarBounds() {
        int rowCount = mWidgets.getPackageSize();

        // Skip early if there are no items.
        if (rowCount == 0) {
            verticalScrollbarBounds.setEmpty();
            return;
        }

        int x, y;
        getCurScrollState(scrollPosState);
        if (scrollPosState.rowIndex < 0) {
            verticalScrollbarBounds.setEmpty();
        }
        // TODO
    }

    /**
     * Returns the current scroll state.
     */
    private void getCurScrollState(ScrollPositionState stateOut) {
        stateOut.rowIndex = -1;
        stateOut.rowTopOffset = -1;
        stateOut.rowHeight = -1;

        int rowCount = mWidgets.getPackageSize();

        // Return early if there are no items
        if (rowCount == 0) {
            return;
        }
        // TODO
    }
}
 No newline at end of file