recyclerView快速滑动功能

方法一:引用三方控件:

implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'

github地址为:GitHub - timusus/RecyclerView-FastScroll: A simple FastScroller for Android's RecyclerView

方法二:将三方控件的代码下载后放到自己的项目中(该目的是为了修改增加功能,例如修改滑动条长度),三方库代码如下:

FastScroller.java
/*
 * Copyright (c) 2016 Tim Malseed
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.android.car.dialer.fastscroll;


import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

import java.lang.annotation.Retention;

import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
import androidx.recyclerview.widget.RecyclerView;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import com.android.car.dialer.R;

@SuppressWarnings("WeakerAccess")
public class FastScroller {
    private static final int DEFAULT_AUTO_HIDE_DELAY = 1500;

    private FastScrollRecyclerView mRecyclerView;
    private FastScrollPopup mPopup;

    private int mThumbHeight;
    private int mThumbWidth;
    private Paint mThumb;

    private Paint mTrack;
    private int mTrackWidth;

    private Rect mTmpRect = new Rect();
    private Rect mInvalidateRect = new Rect();
    private Rect mInvalidateTmpRect = new Rect();

    // The inset is the buffer around which a point will still register as a click on the scrollbar
    private int mTouchInset;

    // 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 Point mThumbPosition = new Point(-1, -1);
    private Point mOffset = new Point(0, 0);

    private boolean mIsDragging;

    private Animator mAutoHideAnimator;
    private boolean mAnimatingShow;
    private int mAutoHideDelay = DEFAULT_AUTO_HIDE_DELAY;
    private boolean mAutoHideEnabled = true;
    private final Runnable mHideRunnable;

    private int mThumbActiveColor;
    private int mThumbInactiveColor = 0x79000000;
    private boolean mThumbInactiveState;

    private int mTouchSlop;

    private int mLastY;

    @Retention(SOURCE)
    @IntDef({PopupTextVerticalAlignmentMode.TEXT_BOUNDS, PopupTextVerticalAlignmentMode.FONT_METRICS})
    public @interface PopupTextVerticalAlignmentMode {
        int TEXT_BOUNDS = 0;
        int FONT_METRICS = 1;
    }
    @IntDef({PopupPosition.ADJACENT, PopupPosition.CENTER})
    public @interface PopupPosition {
        int ADJACENT = 0;
        int CENTER = 1;
    }

    public FastScroller(Context context, FastScrollRecyclerView recyclerView, AttributeSet attrs) {

        Resources resources = context.getResources();

        mRecyclerView = recyclerView;
        mPopup = new FastScrollPopup(resources, recyclerView);

        mThumbHeight = Utils.toPixels(resources, 116);
        mThumbWidth = Utils.toPixels(resources, 10);
        mTrackWidth = Utils.toPixels(resources, 10);

        mTouchInset = Utils.toPixels(resources, -24);

        mThumb = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTrack = new Paint(Paint.ANTI_ALIAS_FLAG);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        TypedArray typedArray = context.getTheme().obtainStyledAttributes(
                attrs, R.styleable.FastScrollRecyclerView, 0, 0);
        try {
            mAutoHideEnabled = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_fastScrollAutoHide, true);
            mAutoHideDelay = typedArray.getInteger(R.styleable.FastScrollRecyclerView_fastScrollAutoHideDelay, DEFAULT_AUTO_HIDE_DELAY);
            mThumbInactiveState = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_fastScrollEnableThumbInactiveColor, true);
            mThumbActiveColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollThumbColor, 0x79000000);
            mThumbInactiveColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollThumbInactiveColor, 0x79000000);

            int trackColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollTrackColor, 0x28000000);
            int popupBgColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollPopupBgColor, 0xff000000);
            int popupTextColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollPopupTextColor, 0xffffffff);
            int popupTextSize = typedArray.getDimensionPixelSize(R.styleable.FastScrollRecyclerView_fastScrollPopupTextSize, Utils.toScreenPixels(resources, 32));
            int popupBackgroundSize = typedArray.getDimensionPixelSize(R.styleable.FastScrollRecyclerView_fastScrollPopupBackgroundSize, Utils.toPixels(resources, 62));
            @PopupTextVerticalAlignmentMode int popupTextVerticalAlignmentMode = typedArray.getInteger(R.styleable.FastScrollRecyclerView_fastScrollPopupTextVerticalAlignmentMode, PopupTextVerticalAlignmentMode.TEXT_BOUNDS);
            @PopupPosition int popupPosition = typedArray.getInteger(R.styleable.FastScrollRecyclerView_fastScrollPopupPosition, PopupPosition.ADJACENT);

            mThumbWidth =
                typedArray.getDimensionPixelSize(R.styleable.FastScrollRecyclerView_fastScrollThumbWidth, mThumbWidth);
            mTrackWidth =
                typedArray.getDimensionPixelSize(R.styleable.FastScrollRecyclerView_fastScrollTrackWidth, mTrackWidth);

            mTrack.setColor(trackColor);
            mThumb.setColor(mThumbInactiveState ? mThumbInactiveColor : mThumbActiveColor);
            mPopup.setBgColor(popupBgColor);
            mPopup.setTextColor(popupTextColor);
            mPopup.setTextSize(popupTextSize);
            mPopup.setBackgroundSize(popupBackgroundSize);
            mPopup.setPopupTextVerticalAlignmentMode(popupTextVerticalAlignmentMode);
            mPopup.setPopupPosition(popupPosition);
        } finally {
            typedArray.recycle();
        }

        mHideRunnable = new Runnable() {
            @Override
            public void run() {
                if (!mIsDragging) {
                    if (mAutoHideAnimator != null) {
                        mAutoHideAnimator.cancel();
                    }
                    mAutoHideAnimator = ObjectAnimator.ofInt(FastScroller.this, "offsetX", (Utils.isRtl(mRecyclerView.getResources()) ? -1 : 1) * getWidth());
                    mAutoHideAnimator.setInterpolator(new FastOutLinearInInterpolator());
                    mAutoHideAnimator.setDuration(200);
                    mAutoHideAnimator.start();
                }
            }
        };

        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                if (!mRecyclerView.isInEditMode()) {
                    show();
                }
            }
        });

        if (mAutoHideEnabled) {
            postAutoHideDelayed();
        }
    }

    public int getThumbHeight() {
        return mThumbHeight;
    }

    public void setThumbHeight(int height){
        mThumbHeight = height;

    }

    public int getWidth() {
        return Math.max(mTrackWidth, mThumbWidth);
    }

    public boolean isDragging() {
        return mIsDragging;
    }

    /**
     * Handles the touch event and determines whether to show the fast scroller (or updates it if
     * it is already showing).
     */
    public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY,
                                 OnFastScrollStateChangeListener stateChangeListener) {
        int action = ev.getAction();
        int y = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (isNearPoint(downX, downY)) {
                    mTouchOffset = downY - mThumbPosition.y;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                // Check if we should start scrolling
                if (!mIsDragging && isNearPoint(downX, downY) &&
                        Math.abs(y - downY) > mTouchSlop) {
                    mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
                    mIsDragging = true;
                    mTouchOffset += (lastY - downY);
                    mPopup.animateVisibility(true);
                    if (stateChangeListener != null) {
                        stateChangeListener.onFastScrollStart();
                    }
                    if (mThumbInactiveState) {
                        mThumb.setColor(mThumbActiveColor);
                    }
                }
                if (mIsDragging) {
                    if (mLastY == 0 || Math.abs(mLastY - y) >= mTouchSlop) {
                        mLastY = y;
                        // Update the fastscroller section name at this touch position
                        boolean layoutManagerReversed = mRecyclerView.isLayoutManagerReversed();
                        int bottom = mRecyclerView.getHeight() - mThumbHeight;
                        float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffset));

                        // Represents the amount the thumb has scrolled divided by its total scroll range
                        float touchFraction = boundedY / bottom;
                        if (layoutManagerReversed) {
                            touchFraction = 1 - touchFraction;
                        }

                        String sectionName = mRecyclerView.scrollToPositionAtProgress(touchFraction);
                        mPopup.setSectionName(sectionName);
                        mPopup.animateVisibility(!sectionName.isEmpty());
                        mRecyclerView.invalidate(mPopup.updateFastScrollerBounds(mRecyclerView, mThumbPosition.y));
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mTouchOffset = 0;
                mLastY = 0;
                if (mIsDragging) {
                    mIsDragging = false;
                    mPopup.animateVisibility(false);
                    if (stateChangeListener != null) {
                        stateChangeListener.onFastScrollStop();
                    }
                }
                if (mThumbInactiveState) {
                    mThumb.setColor(mThumbInactiveColor);
                }
                break;
        }
    }

    RectF rect = new RectF();

    public void draw(Canvas canvas) {

        if (mThumbPosition.x < 0 || mThumbPosition.y < 0) {
            return;
        }

        //Background
        rect.set(mThumbPosition.x + mOffset.x + (mThumbWidth - mTrackWidth),
                mOffset.y + mRecyclerView.getPaddingTop(),
                mThumbPosition.x + mOffset.x + mTrackWidth + (mThumbWidth - mTrackWidth),
                mRecyclerView.getHeight() + mOffset.y - mRecyclerView.getPaddingBottom());
        canvas.drawRoundRect(rect,
                mTrackWidth,
                mTrackWidth,
                mTrack);

        //Handle
        rect.set(mThumbPosition.x + mOffset.x + (mThumbWidth - mTrackWidth) / 2,
                mThumbPosition.y + mOffset.y,
                mThumbPosition.x + mOffset.x + mThumbWidth + (mThumbWidth - mTrackWidth) / 2,
                mThumbPosition.y + mOffset.y + mThumbHeight);
        canvas.drawRoundRect(rect,
                mThumbWidth,
                mThumbWidth,
                mThumb);

        //Popup
        mPopup.draw(canvas);

    }

    /**
     * Returns whether the specified points are near the scroll bar bounds.
     */
    private boolean isNearPoint(int x, int y) {
        mTmpRect.set(mThumbPosition.x, mThumbPosition.y, mThumbPosition.x + mTrackWidth,
                mThumbPosition.y + mThumbHeight);
        mTmpRect.inset(mTouchInset, mTouchInset);
        return mTmpRect.contains(x, y);
    }

    public void setThumbPosition(int x, int y) {
        if (mThumbPosition.x == x && mThumbPosition.y == y) {
            return;
        }
        // do not create new objects here, this is called quite often
        mInvalidateRect.set(mThumbPosition.x + mOffset.x, mOffset.y, mThumbPosition.x + mOffset.x + mTrackWidth, mRecyclerView.getHeight() + mOffset.y);
        mThumbPosition.set(x, y);
        mInvalidateTmpRect.set(mThumbPosition.x + mOffset.x, mOffset.y, mThumbPosition.x + mOffset.x + mTrackWidth, mRecyclerView.getHeight() + mOffset.y);
        mInvalidateRect.union(mInvalidateTmpRect);
        mRecyclerView.invalidate(mInvalidateRect);
    }


    public void setOffset(int x, int y) {
        if (mOffset.x == x && mOffset.y == y) {
            return;
        }
        // do not create new objects here, this is called quite often
        mInvalidateRect.set(mThumbPosition.x + mOffset.x, mOffset.y, mThumbPosition.x + mOffset.x + mTrackWidth, mRecyclerView.getHeight() + mOffset.y);
        mOffset.set(x, y);
        mInvalidateTmpRect.set(mThumbPosition.x + mOffset.x, mOffset.y, mThumbPosition.x + mOffset.x + mTrackWidth, mRecyclerView.getHeight() + mOffset.y);
        mInvalidateRect.union(mInvalidateTmpRect);
        mRecyclerView.invalidate(mInvalidateRect);
    }

    // Setter/getter for the popup alpha for animations
    @Keep
    public void setOffsetX(int x) {
        setOffset(x, mOffset.y);
    }

    @Keep
    public int getOffsetX() {
        return mOffset.x;
    }

    public void show() {
        if (!mAnimatingShow) {
            if (mAutoHideAnimator != null) {
                mAutoHideAnimator.cancel();
            }
            mAutoHideAnimator = ObjectAnimator.ofInt(this, "offsetX", 0);
            mAutoHideAnimator.setInterpolator(new LinearOutSlowInInterpolator());
            mAutoHideAnimator.setDuration(150);
            mAutoHideAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationCancel(Animator animation) {
                    super.onAnimationCancel(animation);
                    mAnimatingShow = false;
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mAnimatingShow = false;
                }
            });
            mAnimatingShow = true;
            mAutoHideAnimator.start();
        }
        if (mAutoHideEnabled) {
            postAutoHideDelayed();
        } else {
            cancelAutoHide();
        }
    }

    protected void postAutoHideDelayed() {
        if (mRecyclerView != null) {
            cancelAutoHide();
            mRecyclerView.postDelayed(mHideRunnable, mAutoHideDelay);
        }
    }

    protected void cancelAutoHide() {
        if (mRecyclerView != null) {
            mRecyclerView.removeCallbacks(mHideRunnable);
        }
    }

    public void setThumbColor(@ColorInt int color) {
        mThumbActiveColor = color;
        mThumb.setColor(color);
        mRecyclerView.invalidate(mInvalidateRect);
    }

    public void setTrackColor(@ColorInt int color) {
        mTrack.setColor(color);
        mRecyclerView.invalidate(mInvalidateRect);
    }

    public void setPopupBgColor(@ColorInt int color) {
        mPopup.setBgColor(color);
    }

    public void setPopupTextColor(@ColorInt int color) {
        mPopup.setTextColor(color);
    }

    public void setPopupTypeface(Typeface typeface) {
        mPopup.setTypeface(typeface);
    }

    public void setPopupTextSize(int size) {
        mPopup.setTextSize(size);
    }

    public void setAutoHideDelay(int hideDelay) {
        mAutoHideDelay = hideDelay;
        if (mAutoHideEnabled) {
            postAutoHideDelayed();
        }
    }

    public void setAutoHideEnabled(boolean autoHideEnabled) {
        mAutoHideEnabled = autoHideEnabled;
        if (autoHideEnabled) {
            postAutoHideDelayed();
        } else {
            cancelAutoHide();
        }
    }

    public void setPopupPosition(@PopupPosition int popupPosition) {
        mPopup.setPopupPosition(popupPosition);
    }

    public void setThumbInactiveColor(@ColorInt int color) {
        mThumbInactiveColor = color;
        enableThumbInactiveColor(true);
    }

    public void enableThumbInactiveColor(boolean enableInactiveColor) {
        mThumbInactiveState = enableInactiveColor;
        mThumb.setColor(mThumbInactiveState ? mThumbInactiveColor : mThumbActiveColor);
    }

    @Deprecated
    public void setThumbInactiveColor(boolean thumbInactiveColor) {
        enableThumbInactiveColor(thumbInactiveColor);
    }
}
FastScrollPopup.java
/*
 * Copyright (c) 2016 Tim Malseed
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.android.car.dialer.fastscroll;

import android.animation.ObjectAnimator;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.text.TextUtils;


import androidx.annotation.Keep;

public class FastScrollPopup {

    private FastScrollRecyclerView mRecyclerView;

    private Resources mRes;

    private int mBackgroundSize;
    private int mCornerRadius;

    private Path mBackgroundPath = new Path();
    private RectF mBackgroundRect = new RectF();
    private Paint mBackgroundPaint;
    private int mBackgroundColor = 0xff000000;

    private Rect mInvalidateRect = new Rect();
    private Rect mTmpRect = new Rect();

    // The absolute bounds of the fast scroller bg
    private Rect mBgBounds = new Rect();

    private String mSectionName;

    private Paint mTextPaint;
    private Rect mTextBounds = new Rect();

    private float mAlpha = 1;

    private ObjectAnimator mAlphaAnimator;
    private boolean mVisible;

    @FastScroller.PopupTextVerticalAlignmentMode private int mTextVerticalAlignmentMode;
    @FastScroller.PopupPosition private int mPosition;

    FastScrollPopup(Resources resources, FastScrollRecyclerView recyclerView) {

        mRes = resources;

        mRecyclerView = recyclerView;

        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setAlpha(0);

        setTextSize(Utils.toScreenPixels(mRes, 32));
        setBackgroundSize(Utils.toPixels(mRes, 62));
    }

    public void setBgColor(int color) {
        mBackgroundColor = color;
        mBackgroundPaint.setColor(color);
        mRecyclerView.invalidate(mBgBounds);
    }

    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        mRecyclerView.invalidate(mBgBounds);
    }

    public void setTextSize(int size) {
        mTextPaint.setTextSize(size);
        mRecyclerView.invalidate(mBgBounds);
    }

    public void setBackgroundSize(int size) {
        mBackgroundSize = size;
        mCornerRadius = mBackgroundSize / 2;
        mRecyclerView.invalidate(mBgBounds);
    }

    public void setTypeface(Typeface typeface) {
        mTextPaint.setTypeface(typeface);
        mRecyclerView.invalidate(mBgBounds);
    }

    /**
     * Animates the visibility of the fast scroller popup.
     */
    public void animateVisibility(boolean visible) {
        if (mVisible != visible) {
            mVisible = visible;
            if (mAlphaAnimator != null) {
                mAlphaAnimator.cancel();
            }
            mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", visible ? 1f : 0f);
            mAlphaAnimator.setDuration(visible ? 200 : 150);
            mAlphaAnimator.start();
        }
    }

    // Setter/getter for the popup alpha for animations
    @Keep
    public void setAlpha(float alpha) {
        mAlpha = alpha;
        mRecyclerView.invalidate(mBgBounds);
    }

    @Keep
    public float getAlpha() {
        return mAlpha;
    }

    public void setPopupTextVerticalAlignmentMode(@FastScroller.PopupTextVerticalAlignmentMode int mode) {
        mTextVerticalAlignmentMode = mode;
    }

    @FastScroller.PopupTextVerticalAlignmentMode
    public int getPopupTextVerticalAlignmentMode() {
        return mTextVerticalAlignmentMode;
    }

    public void setPopupPosition(@FastScroller.PopupPosition int position) {
        mPosition = position;
    }

    @FastScroller.PopupPosition
    public int getPopupPosition() {
        return mPosition;
    }

    private float[] createRadii() {
        if (mPosition == FastScroller.PopupPosition.CENTER) {
            return new float[]{mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius};
        }

        if (Utils.isRtl(mRes)) {
            return new float[]{mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 0, 0};
        } else {
            return new float[]{mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 0, 0, mCornerRadius, mCornerRadius};
        }
    }

    public void draw(Canvas canvas) {
        if (isVisible()) {
            // Draw the fast scroller popup
            int restoreCount = canvas.save();
            canvas.translate(mBgBounds.left, mBgBounds.top);
            mTmpRect.set(mBgBounds);
            mTmpRect.offsetTo(0, 0);

            mBackgroundPath.reset();
            mBackgroundRect.set(mTmpRect);

            float[] radii = createRadii();
            float baselinePosition;
            if (mTextVerticalAlignmentMode == FastScroller.PopupTextVerticalAlignmentMode.FONT_METRICS) {
                Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
                baselinePosition = (mBgBounds.height() - fontMetrics.ascent - fontMetrics.descent) / 2f;
            } else {
                baselinePosition = (mBgBounds.height() + mTextBounds.height()) / 2f;
            }

            mBackgroundPath.addRoundRect(mBackgroundRect, radii, Path.Direction.CW);

            mBackgroundPaint.setAlpha((int) (Color.alpha(mBackgroundColor) * mAlpha));
            mTextPaint.setAlpha((int) (mAlpha * 255));
            canvas.drawPath(mBackgroundPath, mBackgroundPaint);
            canvas.drawText(
                    mSectionName,
                    (mBgBounds.width() - mTextBounds.width()) / 2f,
                    baselinePosition,
                    mTextPaint
            );
            canvas.restoreToCount(restoreCount);
        }
    }

    public void setSectionName(String sectionName) {
        if (!sectionName.equals(mSectionName)) {
            mSectionName = sectionName;
            mTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTextBounds);
            // Update the width to use measureText since that is more accurate
            mTextBounds.right = (int) (mTextBounds.left + mTextPaint.measureText(sectionName));
        }
    }

    /**
     * Updates the bounds for the fast scroller.
     *
     * @return the invalidation rect for this update.
     */
    public Rect updateFastScrollerBounds(FastScrollRecyclerView recyclerView, int thumbOffsetY) {
        mInvalidateRect.set(mBgBounds);

        if (isVisible()) {
            // Calculate the dimensions and position of the fast scroller popup
            int edgePadding = recyclerView.getScrollBarWidth();
            int bgPadding = Math.round((mBackgroundSize - mTextBounds.height()) / 10f) * 5;
            int bgHeight = mBackgroundSize;
            int bgWidth = Math.max(mBackgroundSize, mTextBounds.width() + (2 * bgPadding));
            if (mPosition == FastScroller.PopupPosition.CENTER) {
                mBgBounds.left = (recyclerView.getWidth() - bgWidth) / 2;
                mBgBounds.right = mBgBounds.left + bgWidth;
                mBgBounds.top = (recyclerView.getHeight() - bgHeight) / 2;
            } else {
                if (Utils.isRtl(mRes)) {
                    mBgBounds.left = (2 * recyclerView.getScrollBarWidth());
                    mBgBounds.right = mBgBounds.left + bgWidth;
                } else {
                    mBgBounds.right = recyclerView.getWidth() - (2 * recyclerView.getScrollBarWidth());
                    mBgBounds.left = mBgBounds.right - bgWidth;
                }
                mBgBounds.top = recyclerView.getPaddingTop() - recyclerView.getPaddingBottom() + thumbOffsetY - bgHeight + recyclerView.getScrollBarThumbHeight() / 2;
                mBgBounds.top = Math.max(recyclerView.getPaddingTop() + edgePadding, Math.min(mBgBounds.top, recyclerView.getPaddingTop() + recyclerView.getHeight() - edgePadding - bgHeight));
            }
            mBgBounds.bottom = mBgBounds.top + bgHeight;
        } else {
            mBgBounds.setEmpty();
        }

        // Combine the old and new fast scroller bounds to create the full invalidate rect
        mInvalidateRect.union(mBgBounds);
        return mInvalidateRect;
    }

    public boolean isVisible() {
        return (mAlpha > 0f) && (!TextUtils.isEmpty(mSectionName));
    }
}
FastScrollRecyclerView.java
/*
 * Copyright (c) 2016 Tim Malseed
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.android.car.dialer.fastscroll;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.MotionEvent;
import android.view.View;


import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.android.car.dialer.R;

public class FastScrollRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener {

    private static final String TAG = "FastScrollRecyclerView";

    private FastScroller mScrollbar;

    private boolean mFastScrollEnabled = true;

    /**
     * The current scroll state of the recycler view.  We use this in onUpdateScrollbar()
     * 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
        int rowIndex;
        // The offset of the first visible row
        int rowTopOffset;
        // The height of a given row (they are currently all the same height)
        int rowHeight;
    }

    private ScrollPositionState mScrollPosState = new ScrollPositionState();

    private int mDownX;
    private int mDownY;
    private int mLastY;

    private SparseIntArray mScrollOffsets;

    private ScrollOffsetInvalidator mScrollOffsetInvalidator;
    private OnFastScrollStateChangeListener mStateChangeListener;

    public FastScrollRecyclerView(Context context) {
        this(context, null);
    }

    public FastScrollRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.getTheme().obtainStyledAttributes(
                attrs, R.styleable.FastScrollRecyclerView, 0, 0);
        try {
            mFastScrollEnabled = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_fastScrollThumbEnabled, true);
        } finally {
            typedArray.recycle();
        }

        mScrollbar = new FastScroller(context, this, attrs);
        mScrollOffsetInvalidator = new ScrollOffsetInvalidator();
        mScrollOffsets = new SparseIntArray();
    }

    public int getScrollBarWidth() {
        return mScrollbar.getWidth();
    }

    public int getScrollBarThumbHeight() {
        return mScrollbar.getThumbHeight();
    }

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

    @Override
    public void setAdapter(Adapter adapter) {
        if (getAdapter() != null) {
            getAdapter().unregisterAdapterDataObserver(mScrollOffsetInvalidator);
        }

        if (adapter != null) {
            adapter.registerAdapterDataObserver(mScrollOffsetInvalidator);
        }

        super.setAdapter(adapter);
    }

    /**
     * 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(@NonNull RecyclerView rv, @NonNull MotionEvent ev) {
        return handleTouchEvent(ev);
    }

    @Override
    public void onTouchEvent(@NonNull RecyclerView rv, @NonNull 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) {
        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 = x;
                mDownY = mLastY = y;
                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener);
                break;
            case MotionEvent.ACTION_MOVE:
                mLastY = y;
                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener);
                break;
        }
        return mScrollbar.isDragging();
    }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    }

    /**
     * Returns the available scroll height:
     * AvailableScrollHeight = Total height of the all items - last page height
     *
     * @param yOffset the offset from the top of the recycler view to start tracking.
     */
    protected int getAvailableScrollHeight(int adapterHeight, int yOffset) {
        int visibleHeight = getHeight();
        int scrollHeight = getPaddingTop() + yOffset + adapterHeight + getPaddingBottom();
        return scrollHeight - visibleHeight;
    }

    /**
     * Returns the available scroll bar height:
     * AvailableScrollBarHeight = Total height of the visible view - thumb height
     */
    protected int getAvailableScrollBarHeight() {
        int visibleHeight = getHeight() - getPaddingTop() - getPaddingBottom();
        return visibleHeight - mScrollbar.getThumbHeight();
    }

    @Override
    public void draw(Canvas c) {
        super.draw(c);
        if (mFastScrollEnabled) {
            onUpdateScrollbar();
            mScrollbar.draw(c);
        }
    }

    /**
     * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
     * this by mapping the available scroll area of the recycler view to the available space for the
     * scroll bar.
     *
     * @param scrollPosState the current scroll position
     * @param rowCount       the number of rows, used to calculate the total scroll height (assumes that
     */
    protected void updateThumbPosition(ScrollPositionState scrollPosState, int rowCount) {
        int availableScrollHeight;
        int availableScrollBarHeight;
        int scrolledPastHeight;

        if (getAdapter() instanceof MeasurableAdapter) {
            availableScrollHeight = getAvailableScrollHeight(calculateAdapterHeight(), 0);
            scrolledPastHeight = calculateScrollDistanceToPosition(scrollPosState.rowIndex);
        } else {
            availableScrollHeight = getAvailableScrollHeight(rowCount * scrollPosState.rowHeight, 0);
            scrolledPastHeight = scrollPosState.rowIndex * scrollPosState.rowHeight;
        }

        availableScrollBarHeight = getAvailableScrollBarHeight();

        // Only show the scrollbar if there is height to be scrolled
        if (availableScrollHeight <= 0) {
            mScrollbar.setThumbPosition(-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 = Math.min(availableScrollHeight, getPaddingTop() + scrolledPastHeight);
        if (isLayoutManagerReversed()) {
            scrollY = scrollY + scrollPosState.rowTopOffset - availableScrollBarHeight;
        } else {
            scrollY = scrollY - scrollPosState.rowTopOffset;
        }
        int scrollBarY = (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
        if (isLayoutManagerReversed()) {
            scrollBarY = availableScrollBarHeight - scrollBarY + getPaddingBottom();
        } else {
            scrollBarY += getPaddingTop();
        }

        // Calculate the position and size of the scroll bar
        int scrollBarX;
        if (Utils.isRtl(getResources())) {
            scrollBarX = 0;
        } else {
            scrollBarX = getWidth() - mScrollbar.getWidth();
        }
        mScrollbar.setThumbPosition(scrollBarX, scrollBarY);
    }

    /**
     * Maps the touch (from 0..1) to the adapter position that should be visible.
     */
    public String scrollToPositionAtProgress(float touchFraction) {
        int itemCount = getAdapter().getItemCount();
        if (itemCount == 0) {
            return "";
        }
        int spanCount = 1;
        int rowCount = itemCount;
        if (getLayoutManager() instanceof GridLayoutManager) {
            spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount();
            rowCount = (int) Math.ceil((double) rowCount / spanCount);
        }

        // Stop the scroller if it is scrolling
        stopScroll();

        getCurScrollState(mScrollPosState);

        float itemPos;
        int availableScrollHeight;

        int scrollPosition;
        int scrollOffset;

        if (getAdapter() instanceof MeasurableAdapter) {
            itemPos = findItemPosition(touchFraction);
            availableScrollHeight = getAvailableScrollHeight(calculateAdapterHeight(), 0);
            int passedHeight = (int) (availableScrollHeight * touchFraction);
            scrollPosition = findMeasureAdapterFirstVisiblePosition(passedHeight);
            scrollOffset = calculateScrollDistanceToPosition(scrollPosition) - passedHeight;
        } else {
            itemPos = findItemPosition(touchFraction);
            availableScrollHeight = getAvailableScrollHeight(rowCount * mScrollPosState.rowHeight, 0);

            //The exact position of our desired item
            int exactItemPos = (int) (availableScrollHeight * touchFraction);

            //The offset used here is kind of hard to explain.
            //If the position we wish to scroll to is, say, position 10.5, we scroll to position 10,
            //and then offset by 0.5 * rowHeight. This is how we achieve smooth scrolling.
            scrollPosition = spanCount * exactItemPos / mScrollPosState.rowHeight;
            scrollOffset = -(exactItemPos % mScrollPosState.rowHeight);
        }

        LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
        layoutManager.scrollToPositionWithOffset(scrollPosition, scrollOffset);

        if (!(getAdapter() instanceof SectionedAdapter)) {
            return "";
        }

        int posInt = (int) ((touchFraction == 1) ? getAdapter().getItemCount() - 1 : itemPos);

        SectionedAdapter sectionedAdapter = (SectionedAdapter) getAdapter();
        return sectionedAdapter.getSectionName(posInt);
    }

    @SuppressWarnings("unchecked")
    private int findMeasureAdapterFirstVisiblePosition(int passedHeight) {
        if (getAdapter() instanceof MeasurableAdapter) {
            MeasurableAdapter measurableAdapter = (MeasurableAdapter) getAdapter();
            for (int i = 0; i < getAdapter().getItemCount(); i++) {
                int top = calculateScrollDistanceToPosition(i);
                int bottom = top + measurableAdapter.getViewTypeHeight(this, findViewHolderForAdapterPosition(i), getAdapter().getItemViewType(i));
                if (i == getAdapter().getItemCount() - 1) {
                    if (passedHeight >= top && passedHeight <= bottom) {
                        return i;
                    }
                } else {
                    if (passedHeight >= top && passedHeight < bottom) {
                        return i;
                    }
                }
            }
            int low = calculateScrollDistanceToPosition(0);
            int height = calculateScrollDistanceToPosition(getAdapter().getItemCount() - 1)
                    + measurableAdapter.getViewTypeHeight(this, findViewHolderForAdapterPosition(getAdapter().getItemCount() - 1), getAdapter().getItemViewType(getAdapter().getItemCount() - 1));
            throw new IllegalStateException(String.format("Invalid passed height: %d, [low: %d, height: %d]", passedHeight, low, height));
        } else {
            throw new IllegalStateException("findMeasureAdapterFirstVisiblePosition() should only be called where the RecyclerView.Adapter is an instance of MeasurableAdapter");
        }

    }

    @SuppressWarnings("unchecked")
    private float findItemPosition(float touchFraction) {

        if (getAdapter() instanceof MeasurableAdapter) {
            MeasurableAdapter measurer = (MeasurableAdapter) getAdapter();
            int viewTop = (int) (touchFraction * calculateAdapterHeight());

            for (int i = 0; i < getAdapter().getItemCount(); i++) {
                int top = calculateScrollDistanceToPosition(i);
                int bottom = top + measurer.getViewTypeHeight(this, findViewHolderForAdapterPosition(i), getAdapter().getItemViewType(i));
                if (i == getAdapter().getItemCount() - 1) {
                    if (viewTop >= top && viewTop <= bottom) {
                        return i;
                    }
                } else {
                    if (viewTop >= top && viewTop < bottom) {
                        return i;
                    }
                }
            }

            // Should never happen
            Log.w(TAG, "Failed to find a view at the provided scroll fraction (" + touchFraction + ")");
            return touchFraction * getAdapter().getItemCount();
        } else {
            return getAdapter().getItemCount() * touchFraction;
        }
    }

    /**
     * Updates the bounds for the scrollbar.
     */
    public void onUpdateScrollbar() {

        if (getAdapter() == null) {
            return;
        }

        int rowCount = getAdapter().getItemCount();
        if (getLayoutManager() instanceof GridLayoutManager) {
            int spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount();
            rowCount = (int) Math.ceil((double) rowCount / spanCount);
        }
        // Skip early if, there are no items.
        if (rowCount == 0) {
            mScrollbar.setThumbPosition(-1, -1);
            return;
        }

        // Skip early if, there no child laid out in the container.
        getCurScrollState(mScrollPosState);
        if (mScrollPosState.rowIndex < 0) {
            mScrollbar.setThumbPosition(-1, -1);
            return;
        }

        updateThumbPosition(mScrollPosState, rowCount);
    }

    protected boolean isLayoutManagerReversed() {
        if (getLayoutManager() instanceof LinearLayoutManager) {
            return ((LinearLayoutManager) getLayoutManager()).getReverseLayout();
        }
        return false;
    }

    /**
     * Returns the current scroll state of the apps rows.
     */
    @SuppressWarnings("unchecked")
    private void getCurScrollState(ScrollPositionState stateOut) {
        stateOut.rowIndex = -1;
        stateOut.rowTopOffset = -1;
        stateOut.rowHeight = -1;

        int itemCount = getAdapter().getItemCount();

        // Return early if there are no items, or no children.
        if (itemCount == 0 || getChildCount() == 0) {
            return;
        }

        View child = getChildAt(0);

        stateOut.rowIndex = getChildAdapterPosition(child);
        if (getLayoutManager() instanceof GridLayoutManager) {
            stateOut.rowIndex = stateOut.rowIndex / ((GridLayoutManager) getLayoutManager()).getSpanCount();
        }
        if (getAdapter() instanceof MeasurableAdapter) {
            stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
            stateOut.rowHeight = ((MeasurableAdapter) getAdapter()).getViewTypeHeight(this, findViewHolderForAdapterPosition(stateOut.rowIndex), getAdapter().getItemViewType(stateOut.rowIndex));
        } else {
            stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
            stateOut.rowHeight = child.getHeight() + getLayoutManager().getTopDecorationHeight(child)
                    + getLayoutManager().getBottomDecorationHeight(child);
        }
    }

    /**
     * Calculates the total height of all views above a position in the recycler view. This method
     * should only be called when the attached adapter implements {@link MeasurableAdapter}.
     *
     * @param adapterIndex The index in the adapter to find the total height above the
     *                     corresponding view
     * @return The total height of all views above {@code adapterIndex} in pixels
     */
    @SuppressWarnings("unchecked")
    private int calculateScrollDistanceToPosition(int adapterIndex) {
        if (!(getAdapter() instanceof MeasurableAdapter)) {
            throw new IllegalStateException("calculateScrollDistanceToPosition() should only be called where the RecyclerView.Adapter is an instance of MeasurableAdapter");
        }

        if (mScrollOffsets.indexOfKey(adapterIndex) >= 0) {
            return mScrollOffsets.get(adapterIndex);
        }

        int totalHeight = 0;
        MeasurableAdapter measurer = (MeasurableAdapter) getAdapter();

        // TODO Take grid layouts into account

        for (int i = 0; i < adapterIndex; i++) {
            mScrollOffsets.put(i, totalHeight);
            int viewType = getAdapter().getItemViewType(i);
            totalHeight += measurer.getViewTypeHeight(this, findViewHolderForAdapterPosition(i), viewType);
        }

        mScrollOffsets.put(adapterIndex, totalHeight);
        return totalHeight;
    }

    /**
     * Calculates the total height of the recycler view. This method should only be called when the
     * attached adapter implements {@link MeasurableAdapter}.
     *
     * @return The total height of all rows in the RecyclerView
     */
    private int calculateAdapterHeight() {
        if (!(getAdapter() instanceof MeasurableAdapter)) {
            throw new IllegalStateException("calculateAdapterHeight() should only be called where the RecyclerView.Adapter is an instance of MeasurableAdapter");
        }
        return calculateScrollDistanceToPosition(getAdapter().getItemCount());
    }

    public void showScrollbar() {
        mScrollbar.show();
    }

    public void setThumbColor(@ColorInt int color) {
        mScrollbar.setThumbColor(color);
    }

    public void setTrackColor(@ColorInt int color) {
        mScrollbar.setTrackColor(color);
    }

    public void setPopupBgColor(@ColorInt int color) {
        mScrollbar.setPopupBgColor(color);
    }

    public void setPopupTextColor(@ColorInt int color) {
        mScrollbar.setPopupTextColor(color);
    }

    public void setPopupTextSize(int textSize) {
        mScrollbar.setPopupTextSize(textSize);
    }

    public void setPopUpTypeface(Typeface typeface) {
        mScrollbar.setPopupTypeface(typeface);
    }

    public void setAutoHideDelay(int hideDelay) {
        mScrollbar.setAutoHideDelay(hideDelay);
    }

    public void setAutoHideEnabled(boolean autoHideEnabled) {
        mScrollbar.setAutoHideEnabled(autoHideEnabled);
    }

    public void setOnFastScrollStateChangeListener(OnFastScrollStateChangeListener stateChangeListener) {
        mStateChangeListener = stateChangeListener;
    }

    @Deprecated
    public void setStateChangeListener(OnFastScrollStateChangeListener stateChangeListener) {
        setOnFastScrollStateChangeListener(stateChangeListener);
    }

    public void setThumbInactiveColor(@ColorInt int color) {
        mScrollbar.setThumbInactiveColor(color);
    }
    public void setThumbHeight( float height) {
        mScrollbar.setThumbHeight((int)height);
    }

    public void allowThumbInactiveColor(boolean allowInactiveColor) {
        mScrollbar.enableThumbInactiveColor(allowInactiveColor);
    }

    @Deprecated
    public void setThumbInactiveColor(boolean allowInactiveColor) {
        allowThumbInactiveColor(allowInactiveColor);
    }

    public void setFastScrollEnabled(boolean fastScrollEnabled) {
        mFastScrollEnabled = fastScrollEnabled;
    }

    @Deprecated
    public void setThumbEnabled(boolean thumbEnabled) {
        setFastScrollEnabled(thumbEnabled);
    }

    public void setPopupPosition(@FastScroller.PopupPosition int popupPosition) {
        mScrollbar.setPopupPosition(popupPosition);
    }

    private class ScrollOffsetInvalidator extends AdapterDataObserver {
        private void invalidateAllScrollOffsets() {
            mScrollOffsets.clear();
        }

        @Override
        public void onChanged() {
            invalidateAllScrollOffsets();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            invalidateAllScrollOffsets();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            invalidateAllScrollOffsets();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            invalidateAllScrollOffsets();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            invalidateAllScrollOffsets();
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            invalidateAllScrollOffsets();
        }
    }

    public interface SectionedAdapter {
        @NonNull
        String getSectionName(int position);
    }

    /**
     * FastScrollRecyclerView by default assumes that all items in a RecyclerView will have
     * ItemViews with the same heights so that the total height of all views in the RecyclerView
     * can be calculated. If your list uses different view heights, then make your adapter implement
     * this interface.
     */
    public interface MeasurableAdapter<VH extends ViewHolder> {
        /**
         * Gets the height of a specific view type, including item decorations
         *
         * @param recyclerView The recyclerView that this item view will be placed in
         * @param viewHolder   The viewHolder that corresponds to this item view
         * @param viewType     The view type to get the height of
         * @return The height of a single view for the given view type in pixels
         */
        int getViewTypeHeight(RecyclerView recyclerView, @Nullable VH viewHolder, int viewType);
    }
}
OnFastScrollStateChangeListener.java
package com.android.car.dialer.fastscroll;

public interface OnFastScrollStateChangeListener {

    /**
     * Called when fast scrolling begins
     */
    void onFastScrollStart();

    /**
     * Called when fast scrolling ends
     */
    void onFastScrollStop();
}
Utils.java
/*
 * Copyright (c) 2016 Tim Malseed
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.android.car.dialer.fastscroll;

import android.annotation.TargetApi;
import android.content.res.Resources;
import android.os.Build;
import android.util.TypedValue;
import android.view.View;

public class Utils {

    /**
     * Converts dp to px
     *
     * @param res Resources
     * @param dp  the value in dp
     * @return int
     */
    public static int toPixels(Resources res, float dp) {
        return (int) (dp * res.getDisplayMetrics().density);
    }

    /**
     * Converts sp to px
     *
     * @param res Resources
     * @param sp  the value in sp
     * @return int
     */
    public static int toScreenPixels(Resources res, float sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, res.getDisplayMetrics());
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    public static boolean isRtl(Resources res) {
        return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) &&
                (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
    }
}
values/attrs.xml
<declare-styleable name="FastScrollRecyclerView">
    <attr name="fastScrollThumbColor" format="reference|color" />
    <attr name="fastScrollThumbInactiveColor" format="reference|color" />
    <attr name="fastScrollThumbWidth" format="reference|dimension" />
    <attr name="fastScrollTrackColor" format="reference|color" />
    <attr name="fastScrollTrackWidth" format="reference|dimension" />
    <attr name="fastScrollPopupBgColor" format="reference|color" />
    <attr name="fastScrollPopupTextColor" format="reference|color" />
    <attr name="fastScrollPopupTextSize" format="reference|dimension" />
    <attr name="fastScrollPopupTextVerticalAlignmentMode" format="enum">
        <enum name="text_bounds" value="0"/>
        <enum name="font_metrics" value="1"/>
    </attr>
    <attr name="fastScrollPopupBackgroundSize" format="reference|dimension" />
    <attr name="fastScrollPopupPosition" format="enum">
        <enum name="adjacent" value="0"/>
        <enum name="center" value="1"/>
    </attr>
    <attr name="fastScrollAutoHide" format="reference|boolean" />
    <attr name="fastScrollAutoHideDelay" format="reference|integer" />
    <attr name="fastScrollEnableThumbInactiveColor" format="reference|boolean" />
    <attr name="fastScrollThumbEnabled" format="reference|boolean" />
</declare-styleable>

当上面步骤做完后,就可以直接使用,用FastScrollRecyclerView代替recyclerView,如下:

<com.android.car.dialer.fastscroll.FastScrollRecyclerView
    android:id="@+id/contact_recycle"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginEnd="20dp"
        //滑动条没滑动时是否自动隐藏
    app:fastScrollAutoHide="false"
        //隐藏前需要的时间
    app:fastScrollAutoHideDelay="1500"
    app:fastScrollEnableThumbInactiveColor="true"
    app:fastScrollThumbInactiveColor="#40ffffff"
//Thumb width and color
    app:fastScrollThumbWidth="6dp"
    app:fastScrollThumbColor="#40ffffff"
    app:fastScrollThumbEnabled="true"
//Track width and background
    app:fastScrollTrackWidth="8dp"
    app:fastScrollTrackColor="@color/transparent"
//Popup background size
    app:fastScrollPopupBackgroundSize="0dp"
//Popup background
    app:fastScrollPopupBgColor="@color/transparent"
//Popup position
    app:fastScrollPopupPosition="adjacent"
//Popup text color
    app:fastScrollPopupTextColor="@android:color/transparent"
//Popup text size
    app:fastScrollPopupTextSize="0dp"
//Popup text vertical alignment mode
    app:fastScrollPopupTextVerticalAlignmentMode="font_metrics"
    />

其余用法与原生recyclerView一致,如下:

        mContactRecycle.setLayoutManager(new LinearLayoutManager(getContext()));
        mContactRecycle.setAdapter(mContactAdapter);

        //设置分割线
//        DividerItemDecoration itemDecoration = new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL);
//        itemDecoration.setDrawable(getResources().getDrawable(R.drawable.list_divider));
//        mContactRecycle.addItemDecoration(itemDecoration);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值