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

Commit 5d9fb0e9 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Simplifying fast scroller logic

> Using a separate view for drawing the popup. This allows us to use elevation
  property instead of drawing the shadow as bitmap.
> During the thumb animation, invalidating the full track width, instead of
  invalidating the track and thumb separately.
> The thumb path is calculated at 0,0 and drawn using canvas.translate().
   This avoids recalculating the path on every scroll.

Change-Id: I48741e5b4432df0d939016db284d7aaf52cc2aa6
parent 631ffbda
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -55,6 +55,22 @@
            android:focusable="true"
            android:theme="@style/CustomOverscroll.Light" />

        <!-- Fast scroller popup -->
        <TextView
            android:background="@drawable/container_fastscroll_popup_bg"
            android:layout_width="@dimen/container_fastscroll_popup_size"
            android:layout_height="@dimen/container_fastscroll_popup_size"
            android:textSize="@dimen/container_fastscroll_popup_text_size"
            android:layout_below="@+id/search_container"
            android:id="@+id/fast_scroller_popup"
            android:layout_alignParentEnd="true"
            android:gravity="center"
            android:alpha="0"
            android:elevation="3dp"
            android:saveEnabled="false"
            android:layout_marginEnd="@dimen/container_fastscroll_popup_margin"
            android:textColor="@android:color/white" />

        <FrameLayout
            android:id="@+id/search_container"
            android:layout_width="match_parent"
+16 −0
Original line number Diff line number Diff line
@@ -49,6 +49,22 @@
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <!-- Fast scroller popup -->
        <TextView
            android:background="@drawable/container_fastscroll_popup_bg"
            android:layout_width="@dimen/container_fastscroll_popup_size"
            android:layout_height="@dimen/container_fastscroll_popup_size"
            android:textSize="@dimen/container_fastscroll_popup_text_size"
            android:layout_below="@+id/search_container"
            android:id="@+id/fast_scroller_popup"
            android:elevation="3dp"
            android:gravity="center"
            android:alpha="0"
            android:saveEnabled="false"
            android:layout_gravity="top|end"
            android:layout_marginEnd="@dimen/container_fastscroll_popup_margin"
            android:textColor="@android:color/white" />

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
+1 −0
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@

    <dimen name="container_fastscroll_thumb_min_width">5dp</dimen>
    <dimen name="container_fastscroll_thumb_max_width">9dp</dimen>
    <dimen name="container_fastscroll_popup_margin">18dp</dimen>
    <dimen name="container_fastscroll_thumb_height">72dp</dimen>
    <dimen name="container_fastscroll_thumb_touch_inset">-24dp</dimen>
    <dimen name="container_fastscroll_popup_size">72dp</dimen>
+11 −14
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;

import com.android.launcher3.util.Thunk;


@@ -41,7 +43,7 @@ public abstract class BaseRecyclerView extends RecyclerView
    @Thunk int mDy = 0;
    private float mDeltaThreshold;

    protected BaseRecyclerViewFastScrollBar mScrollbar;
    protected final BaseRecyclerViewFastScrollBar mScrollbar;

    private int mDownX;
    private int mDownY;
@@ -92,6 +94,12 @@ public abstract class BaseRecyclerView extends RecyclerView
        addOnItemTouchListener(this);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mScrollbar.setPopupView(((ViewGroup) getParent()).findViewById(R.id.fast_scroller_popup));
    }

    /**
     * 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.
@@ -235,7 +243,7 @@ public abstract class BaseRecyclerView extends RecyclerView
        // Only show the scrollbar if there is height to be scrolled
        int availableScrollBarHeight = getAvailableScrollBarHeight();
        if (availableScrollHeight <= 0) {
            mScrollbar.setThumbOffset(-1, -1);
            mScrollbar.setThumbOffsetY(-1);
            return;
        }

@@ -246,18 +254,7 @@ public abstract class BaseRecyclerView extends RecyclerView
                (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);

        // Calculate the position and size of the scroll bar
        mScrollbar.setThumbOffset(getScrollBarX(), scrollBarY);
    }

    /**
     * @return the x position for the scrollbar thumb
     */
    protected int getScrollBarX() {
        if (Utilities.isRtl(getResources())) {
            return mBackgroundPadding.left;
        } else {
            return getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
        }
        mScrollbar.setThumbOffsetY(scrollBarY);
    }

    /**
+141 −129
Original line number Diff line number Diff line
@@ -15,21 +15,18 @@
 */
package com.android.launcher3;

import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import com.android.launcher3.util.Thunk;
import android.widget.TextView;

/**
 * The track and scrollbar that shows when you scroll the list.
@@ -40,29 +37,46 @@ public class BaseRecyclerViewFastScrollBar {
        void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
    }

    private static final Property<BaseRecyclerViewFastScrollBar, Integer> TRACK_WIDTH =
            new Property<BaseRecyclerViewFastScrollBar, Integer>(Integer.class, "width") {

                @Override
                public Integer get(BaseRecyclerViewFastScrollBar scrollBar) {
                    return scrollBar.mWidth;
                }

                @Override
                public void set(BaseRecyclerViewFastScrollBar scrollBar, Integer value) {
                    scrollBar.setTrackWidth(value);
                }
            };

    private final static int MAX_TRACK_ALPHA = 30;
    private final static int SCROLL_BAR_VIS_DURATION = 150;
    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;

    private final Rect mTmpRect = new Rect();
    private final BaseRecyclerView mRv;

    private final boolean mIsRtl;

    @Thunk BaseRecyclerView mRv;
    private BaseRecyclerViewFastScrollPopup mPopup;

    private AnimatorSet mScrollbarAnimator;

    private int mThumbInactiveColor;
    private int mThumbActiveColor;
    @Thunk Point mThumbOffset = new Point(-1, -1);
    @Thunk Paint mThumbPaint;
    private int mThumbMinWidth;
    private int mThumbMaxWidth;
    @Thunk int mThumbWidth;
    @Thunk int mThumbHeight;
    private int mThumbCurvature;
    private Path mThumbPath = new Path();
    private Paint mTrackPaint;
    private int mTrackWidth;
    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 final int mTouchInset;

    private final int mMinWidth;
    private final int mMaxWidth;

    // Current width of the track
    private int mWidth;
    private ObjectAnimator mWidthAnimator;

    private final Path mThumbPath = new Path();
    private final Paint mThumbPaint;
    private final int mThumbHeight;

    private final Paint mTrackPaint;

    private float mLastTouchY;
    private boolean mIsDragging;
    private boolean mIsThumbDetached;
    private boolean mCanThumbDetach;
@@ -70,27 +84,35 @@ public class BaseRecyclerViewFastScrollBar {

    // 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.
    private int mTouchOffset;
    private int mTouchOffsetY;
    private int mThumbOffsetY;

    private Rect mInvalidateRect = new Rect();
    private Rect mTmpRect = new Rect();
    // Fast scroller popup
    private TextView mPopupView;
    private boolean mPopupVisible;
    private String mPopupSectionName;

    public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
        mRv = rv;
        mPopup = new BaseRecyclerViewFastScrollPopup(rv, res);
        mTrackPaint = new Paint();
        mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
        mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
        mThumbActiveColor = mThumbInactiveColor = Utilities.getColorAccent(rv.getContext());

        mThumbPaint = new Paint();
        mThumbPaint.setAntiAlias(true);
        mThumbPaint.setColor(mThumbInactiveColor);
        mThumbPaint.setColor(Utilities.getColorAccent(rv.getContext()));
        mThumbPaint.setStyle(Paint.Style.FILL);
        mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
        mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);

        mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
        mMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
        mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
        mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
        mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
        mIsRtl = Utilities.isRtl(res);
        updateThumbPath();
    }

    public void setPopupView(View popup) {
        mPopupView = (TextView) popup;
    }

    public void setDetachThumbOnFastScroll() {
@@ -101,51 +123,54 @@ public class BaseRecyclerViewFastScrollBar {
        mIsThumbDetached = false;
    }

    public void setThumbOffset(int x, int y) {
        if (mThumbOffset.x == x && mThumbOffset.y == y) {
            return;
    private int getDrawLeft() {
        return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth);
    }
        mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
                mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
        mThumbOffset.set(x, y);
        updateThumbPath();
        mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
                mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
        mRv.invalidate(mInvalidateRect);

    public void setThumbOffsetY(int y) {
        if (mThumbOffsetY == y) {
            return;
        }

    public Point getThumbOffset() {
        return mThumbOffset;
        // Invalidate the previous and new thumb area
        int drawLeft = getDrawLeft();
        mTmpRect.set(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
        mThumbOffsetY = y;
        mTmpRect.union(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
        mRv.invalidate(mTmpRect);
    }

    // Setter/getter for the thumb bar width for animations
    public void setThumbWidth(int width) {
        mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
                mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
        mThumbWidth = width;
        updateThumbPath();
        mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
                mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
        mRv.invalidate(mInvalidateRect);
    public int getThumbOffsetY() {
        return mThumbOffsetY;
    }

    public int getThumbWidth() {
        return mThumbWidth;
    private void setTrackWidth(int width) {
        if (mWidth == width) {
            return;
        }
        int left = getDrawLeft();
        // Invalidate the whole scroll bar area.
        mRv.invalidate(left, 0, left + mMaxWidth, mRv.getVisibleHeight());

    // Setter/getter for the track bar width for animations
    public void setTrackWidth(int width) {
        mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
                mRv.getVisibleHeight());
        mTrackWidth = width;
        mWidth = width;
        updateThumbPath();
        mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
                mRv.getVisibleHeight());
        mRv.invalidate(mInvalidateRect);
    }

    public int getTrackWidth() {
        return mTrackWidth;
    /**
     * Updates the path for the thumb drawable.
     */
    private void updateThumbPath() {
        int smallWidth = mIsRtl ? mWidth : -mWidth;
        int largeWidth = mIsRtl ? mMaxWidth : -mMaxWidth;

        mThumbPath.reset();
        mThumbPath.moveTo(0, 0);
        mThumbPath.lineTo(0, mThumbHeight);             // Left edge
        mThumbPath.lineTo(smallWidth, mThumbHeight);    // bottom edge
        mThumbPath.cubicTo(smallWidth, mThumbHeight,    // right edge
                largeWidth, mThumbHeight / 2,
                smallWidth, 0);
        mThumbPath.close();
    }

    public int getThumbHeight() {
@@ -153,7 +178,7 @@ public class BaseRecyclerViewFastScrollBar {
    }

    public int getThumbMaxWidth() {
        return mThumbMaxWidth;
        return mMaxWidth;
    }

    public boolean isDraggingThumb() {
@@ -176,7 +201,7 @@ public class BaseRecyclerViewFastScrollBar {
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (isNearThumb(downX, downY)) {
                    mTouchOffset = downY - mThumbOffset.y;
                    mTouchOffsetY = downY - mThumbOffsetY;
                }
                break;
            case MotionEvent.ACTION_MOVE:
@@ -191,32 +216,35 @@ public class BaseRecyclerViewFastScrollBar {
                    if (mCanThumbDetach) {
                        mIsThumbDetached = true;
                    }
                    mTouchOffset += (lastY - downY);
                    mPopup.animateVisibility(true);
                    mTouchOffsetY += (lastY - downY);
                    animatePopupVisibility(true);
                    showActiveScrollbar(true);
                }
                if (mIsDragging) {
                    // Update the fastscroller section name at this touch position
                    int top = mRv.getBackgroundPadding().top;
                    int bottom = top + mRv.getVisibleHeight() - mThumbHeight;
                    float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset));
                    float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffsetY));
                    String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) /
                            (bottom - top));
                    mPopup.setSectionName(sectionName);
                    mPopup.animateVisibility(!sectionName.isEmpty());
                    mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
                    if (!sectionName.equals(mPopupSectionName)) {
                        mPopupSectionName = sectionName;
                        mPopupView.setText(sectionName);
                    }
                    animatePopupVisibility(!sectionName.isEmpty());
                    updatePopupY(lastY);
                    mLastTouchY = boundedY;
                    setThumbOffset(mRv.getScrollBarX(), (int) mLastTouchY);
                    setThumbOffsetY((int) mLastTouchY);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mTouchOffset = 0;
                mTouchOffsetY = 0;
                mLastTouchY = 0;
                mIgnoreDragGesture = false;
                if (mIsDragging) {
                    mIsDragging = false;
                    mPopup.animateVisibility(false);
                    animatePopupVisibility(false);
                    showActiveScrollbar(false);
                }
                break;
@@ -224,74 +252,58 @@ public class BaseRecyclerViewFastScrollBar {
    }

    public void draw(Canvas canvas) {
        if (mThumbOffset.x < 0 || mThumbOffset.y < 0) {
        if (mThumbOffsetY < 0) {
            return;
        }

        // Draw the scroll bar track and thumb
        if (mTrackPaint.getAlpha() > 0) {
            canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
                    mRv.getVisibleHeight(), mTrackPaint);
        int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
        if (!mIsRtl) {
            canvas.translate(mRv.getWidth(), 0);
        }
        canvas.drawPath(mThumbPath, mThumbPaint);
        // Draw the track
        int thumbWidth = mIsRtl ? mWidth : -mWidth;
        canvas.drawRect(0, 0, thumbWidth, mRv.getVisibleHeight(), mTrackPaint);

        // Draw the popup
        mPopup.draw(canvas);
        canvas.translate(0, mThumbOffsetY);
        canvas.drawPath(mThumbPath, mThumbPaint);
        canvas.restoreToCount(saveCount);
    }

    /**
     * Animates the width and color of the scrollbar.
     * Animates the width of the scrollbar.
     */
    private void showActiveScrollbar(boolean isScrolling) {
        if (mScrollbarAnimator != null) {
            mScrollbarAnimator.cancel();
        }

        mScrollbarAnimator = new AnimatorSet();
        ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
                isScrolling ? mThumbMaxWidth : mThumbMinWidth);
        ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
                isScrolling ? mThumbMaxWidth : mThumbMinWidth);
        mScrollbarAnimator.playTogether(trackWidthAnim, thumbWidthAnim);
        if (mThumbActiveColor != mThumbInactiveColor) {
            ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
                    mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);
            colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animator) {
                    mThumbPaint.setColor((Integer) animator.getAnimatedValue());
                    mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
                            mThumbOffset.y + mThumbHeight);
                }
            });
            mScrollbarAnimator.play(colorAnimation);
        }
        mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
        mScrollbarAnimator.start();
        if (mWidthAnimator != null) {
            mWidthAnimator.cancel();
        }

    /**
     * Updates the path for the thumb drawable.
     */
    private void updateThumbPath() {
        mThumbCurvature = mThumbMaxWidth - mThumbWidth;
        mThumbPath.reset();
        mThumbPath.moveTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y);                    // tr
        mThumbPath.lineTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);     // br
        mThumbPath.lineTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight);                   // bl
        mThumbPath.cubicTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight,
                mThumbOffset.x - mThumbCurvature, mThumbOffset.y + mThumbHeight / 2,
                mThumbOffset.x, mThumbOffset.y);                                            // bl2tl
        mThumbPath.close();
        mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
                isScrolling ? mMaxWidth : mMinWidth);
        mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
        mWidthAnimator.start();
    }

    /**
     * Returns whether the specified points are near the scroll bar bounds.
     */
    public boolean isNearThumb(int x, int y) {
        mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
                mThumbOffset.y + mThumbHeight);
        int left = getDrawLeft();
        mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight);
        mTmpRect.inset(mTouchInset, mTouchInset);
        return mTmpRect.contains(x, y);
    }

    private void animatePopupVisibility(boolean visible) {
        if (mPopupVisible != visible) {
            mPopupVisible = visible;
            mPopupView.animate().cancel();
            mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
        }
    }

    private void updatePopupY(int lastTouchY) {
        int height = mPopupView.getHeight();
        float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height);
        top = Math.max(mMaxWidth, Math.min(top, mRv.getVisibleHeight() - mMaxWidth - height));
        mPopupView.setTranslationY(top);
    }
}
Loading