方法一:引用三方控件:
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);