diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt
new file mode 100644
index 000000000..0a2ebf741
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt
@@ -0,0 +1,195 @@
+package io.legado.app.ui.widget.recycler.scroller
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.ViewGroup
+import androidx.annotation.ColorInt
+import androidx.recyclerview.widget.RecyclerView
+import io.legado.app.R
+
+class FastScrollRecyclerView : RecyclerView {
+
+ private var mFastScroller: FastScroller? = null
+
+ constructor(context: Context) : super(context) {
+
+ layout(context, null)
+
+ layoutParams =
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+
+ }
+
+
+ @JvmOverloads
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) {
+
+ layout(context, attrs)
+
+ }
+
+
+ override fun setAdapter(adapter: Adapter<*>?) {
+
+ super.setAdapter(adapter)
+
+ if (adapter is FastScroller.SectionIndexer) {
+ setSectionIndexer(adapter as FastScroller.SectionIndexer?)
+ } else if (adapter == null) {
+ setSectionIndexer(null)
+ }
+
+ }
+
+
+ override fun setVisibility(visibility: Int) {
+
+ super.setVisibility(visibility)
+ mFastScroller?.visibility = visibility
+
+ }
+
+
+ /**
+ * Set the [FastScroller.SectionIndexer] for the [FastScroller].
+ *
+ * @param sectionIndexer The SectionIndexer that provides section text for the FastScroller
+ */
+ fun setSectionIndexer(sectionIndexer: FastScroller.SectionIndexer?) {
+
+ mFastScroller?.setSectionIndexer(sectionIndexer)
+
+ }
+
+
+ /**
+ * Set the enabled state of fast scrolling.
+ *
+ * @param enabled True to enable fast scrolling, false otherwise
+ */
+ fun setFastScrollEnabled(enabled: Boolean) {
+
+ mFastScroller!!.isEnabled = enabled
+
+ }
+
+
+ /**
+ * Hide the scrollbar when not scrolling.
+ *
+ * @param hideScrollbar True to hide the scrollbar, false to show
+ */
+ fun setHideScrollbar(hideScrollbar: Boolean) {
+
+ mFastScroller?.setFadeScrollbar(hideScrollbar)
+
+ }
+
+ /**
+ * Display a scroll track while scrolling.
+ *
+ * @param visible True to show scroll track, false to hide
+ */
+ fun setTrackVisible(visible: Boolean) {
+
+ mFastScroller?.setTrackVisible(visible)
+
+ }
+
+ /**
+ * Set the color of the scroll track.
+ *
+ * @param color The color for the scroll track
+ */
+ fun setTrackColor(@ColorInt color: Int) {
+
+ mFastScroller?.setTrackColor(color)
+
+ }
+
+
+ /**
+ * Set the color for the scroll handle.
+ *
+ * @param color The color for the scroll handle
+ */
+ fun setHandleColor(@ColorInt color: Int) {
+
+ mFastScroller?.setHandleColor(color)
+
+ }
+
+
+ /**
+ * Show the section bubble while scrolling.
+ *
+ * @param visible True to show the bubble, false to hide
+ */
+ fun setBubbleVisible(visible: Boolean) {
+
+ mFastScroller?.setBubbleVisible(visible)
+
+ }
+
+
+ /**
+ * Set the background color of the index bubble.
+ *
+ * @param color The background color for the index bubble
+ */
+ fun setBubbleColor(@ColorInt color: Int) {
+
+ mFastScroller?.setBubbleColor(color)
+
+ }
+
+
+ /**
+ * Set the text color of the index bubble.
+ *
+ * @param color The text color for the index bubble
+ */
+ fun setBubbleTextColor(@ColorInt color: Int) {
+ mFastScroller?.setBubbleTextColor(color)
+ }
+
+
+ /**
+ * Set the fast scroll state change listener.
+ *
+ * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events
+ */
+ fun setFastScrollStateChangeListener(fastScrollStateChangeListener: FastScrollStateChangeListener) {
+
+ mFastScroller?.setFastScrollStateChangeListener(fastScrollStateChangeListener)
+
+ }
+
+
+ override fun onAttachedToWindow() {
+
+ super.onAttachedToWindow()
+
+ mFastScroller?.attachRecyclerView(this)
+
+ val parent = parent
+ if (parent is ViewGroup) {
+ parent.addView(mFastScroller)
+ mFastScroller?.setLayoutParams(parent)
+ }
+ }
+
+
+ override fun onDetachedFromWindow() {
+ mFastScroller?.detachRecyclerView()
+ super.onDetachedFromWindow()
+ }
+
+
+ private fun layout(context: Context, attrs: AttributeSet?) {
+ mFastScroller = FastScroller(context, attrs)
+ mFastScroller?.id = R.id.fast_scroller
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt
new file mode 100644
index 000000000..55afa8370
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt
@@ -0,0 +1,14 @@
+package io.legado.app.ui.widget.recycler.scroller
+
+interface FastScrollStateChangeListener {
+
+ /**
+ * Called when fast scrolling begins
+ */
+ fun onFastScrollStart(fastScroller: FastScroller)
+
+ /**
+ * Called when fast scrolling ends
+ */
+ fun onFastScrollStop(fastScroller: FastScroller)
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt
new file mode 100644
index 000000000..3b5acac27
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt
@@ -0,0 +1,518 @@
+package io.legado.app.ui.widget.recycler.scroller
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewPropertyAnimator
+import android.widget.*
+import androidx.annotation.ColorInt
+import androidx.annotation.IdRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.GravityCompat
+import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.StaggeredGridLayoutManager
+import io.legado.app.R
+import io.legado.app.lib.theme.accentColor
+import io.legado.app.utils.ColorUtil
+import io.legado.app.utils.getCompatColor
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+
+class FastScroller : LinearLayout {
+ @ColorInt
+ private var mBubbleColor: Int = 0
+ @ColorInt
+ private var mHandleColor: Int = 0
+ private var mBubbleHeight: Int = 0
+ private var mHandleHeight: Int = 0
+ private var mViewHeight: Int = 0
+ private var mFadeScrollbar: Boolean = false
+ private var mShowBubble: Boolean = false
+ private var mSectionIndexer: SectionIndexer? = null
+ private var mScrollbarAnimator: ViewPropertyAnimator? = null
+ private var mBubbleAnimator: ViewPropertyAnimator? = null
+ private var mRecyclerView: RecyclerView? = null
+ private var mBubbleView: TextView? = null
+ private var mHandleView: ImageView? = null
+ private var mTrackView: ImageView? = null
+ private var mScrollbar: View? = null
+ private var mBubbleImage: Drawable? = null
+ private var mHandleImage: Drawable? = null
+ private var mTrackImage: Drawable? = null
+ private var mFastScrollStateChangeListener: FastScrollStateChangeListener? = null
+ private val mScrollbarHider = Runnable { this.hideScrollbar() }
+
+ private val mScrollListener = object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ if (!mHandleView!!.isSelected && isEnabled) {
+ setViewPositions(getScrollProportion(recyclerView))
+ }
+ }
+
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ super.onScrollStateChanged(recyclerView, newState)
+ if (isEnabled) {
+ when (newState) {
+ RecyclerView.SCROLL_STATE_DRAGGING -> {
+ handler.removeCallbacks(mScrollbarHider)
+ cancelAnimation(mScrollbarAnimator)
+ if (!isViewVisible(mScrollbar)) {
+ showScrollbar()
+ }
+ }
+ RecyclerView.SCROLL_STATE_IDLE -> if (mFadeScrollbar && !mHandleView!!.isSelected) {
+ handler.postDelayed(mScrollbarHider, sScrollbarHideDelay.toLong())
+ }
+ }
+ }
+ }
+ }
+
+ constructor(context: Context) : super(context) {
+ layout(context, null)
+ layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
+ }
+
+ @JvmOverloads
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) {
+ layout(context, attrs)
+ layoutParams = generateLayoutParams(attrs)
+ }
+
+ override fun setLayoutParams(params: ViewGroup.LayoutParams) {
+ params.width = LayoutParams.WRAP_CONTENT
+ super.setLayoutParams(params)
+ }
+
+ fun setLayoutParams(viewGroup: ViewGroup) {
+ @IdRes val recyclerViewId = if (mRecyclerView != null) mRecyclerView!!.id else View.NO_ID
+ val marginTop = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top)
+ val marginBottom = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom)
+ if (recyclerViewId == View.NO_ID) {
+ throw IllegalArgumentException("RecyclerView must have a view ID")
+ }
+ when (viewGroup) {
+ is ConstraintLayout -> {
+ val constraintSet = ConstraintSet()
+ @IdRes val layoutId = id
+ constraintSet.clone(viewGroup)
+ constraintSet.connect(layoutId, ConstraintSet.TOP, recyclerViewId, ConstraintSet.TOP)
+ constraintSet.connect(layoutId, ConstraintSet.BOTTOM, recyclerViewId, ConstraintSet.BOTTOM)
+ constraintSet.connect(layoutId, ConstraintSet.END, recyclerViewId, ConstraintSet.END)
+ constraintSet.applyTo(viewGroup)
+ val layoutParams = layoutParams as ConstraintLayout.LayoutParams
+ layoutParams.setMargins(0, marginTop, 0, marginBottom)
+ setLayoutParams(layoutParams)
+ }
+ is CoordinatorLayout -> {
+ val layoutParams = layoutParams as CoordinatorLayout.LayoutParams
+ layoutParams.anchorId = recyclerViewId
+ layoutParams.anchorGravity = GravityCompat.END
+ layoutParams.setMargins(0, marginTop, 0, marginBottom)
+ setLayoutParams(layoutParams)
+ }
+ is FrameLayout -> {
+ val layoutParams = layoutParams as FrameLayout.LayoutParams
+ layoutParams.gravity = GravityCompat.END
+ layoutParams.setMargins(0, marginTop, 0, marginBottom)
+ setLayoutParams(layoutParams)
+ }
+ is RelativeLayout -> {
+ val layoutParams = layoutParams as RelativeLayout.LayoutParams
+ val endRule = RelativeLayout.ALIGN_END
+ layoutParams.addRule(RelativeLayout.ALIGN_TOP, recyclerViewId)
+ layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId)
+ layoutParams.addRule(endRule, recyclerViewId)
+ layoutParams.setMargins(0, marginTop, 0, marginBottom)
+ setLayoutParams(layoutParams)
+ }
+ else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout")
+ }
+ updateViewHeights()
+ }
+
+ fun setSectionIndexer(sectionIndexer: SectionIndexer?) {
+ mSectionIndexer = sectionIndexer
+ }
+
+ fun attachRecyclerView(recyclerView: RecyclerView) {
+ mRecyclerView = recyclerView
+ if (mRecyclerView != null) {
+ mRecyclerView!!.addOnScrollListener(mScrollListener)
+ post {
+ // set initial positions for bubble and handle
+ setViewPositions(getScrollProportion(mRecyclerView))
+ }
+ }
+ }
+
+ fun detachRecyclerView() {
+ if (mRecyclerView != null) {
+ mRecyclerView!!.removeOnScrollListener(mScrollListener)
+ mRecyclerView = null
+ }
+ }
+
+ /**
+ * Hide the scrollbar when not scrolling.
+ *
+ * @param fadeScrollbar True to hide the scrollbar, false to show
+ */
+ fun setFadeScrollbar(fadeScrollbar: Boolean) {
+ mFadeScrollbar = fadeScrollbar
+ mScrollbar!!.visibility = if (fadeScrollbar) View.GONE else View.VISIBLE
+ }
+
+ /**
+ * Show the section bubble while scrolling.
+ *
+ * @param visible True to show the bubble, false to hide
+ */
+ fun setBubbleVisible(visible: Boolean) {
+ mShowBubble = visible
+ }
+
+ /**
+ * Display a scroll track while scrolling.
+ *
+ * @param visible True to show scroll track, false to hide
+ */
+ fun setTrackVisible(visible: Boolean) {
+ mTrackView!!.visibility = if (visible) View.VISIBLE else View.GONE
+ }
+
+ /**
+ * Set the color of the scroll track.
+ *
+ * @param color The color for the scroll track
+ */
+ fun setTrackColor(@ColorInt color: Int) {
+ if (mTrackImage == null) {
+ val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_track)
+ if (drawable != null) {
+ mTrackImage = DrawableCompat.wrap(drawable)
+ }
+ }
+ DrawableCompat.setTint(mTrackImage!!, color)
+ mTrackView!!.setImageDrawable(mTrackImage)
+ }
+
+ /**
+ * Set the color for the scroll handle.
+ *
+ * @param color The color for the scroll handle
+ */
+ fun setHandleColor(@ColorInt color: Int) {
+ mHandleColor = color
+ if (mHandleImage == null) {
+ val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_handle)
+ if (drawable != null) {
+ mHandleImage = DrawableCompat.wrap(drawable)
+ }
+ }
+ DrawableCompat.setTint(mHandleImage!!, mHandleColor)
+ mHandleView!!.setImageDrawable(mHandleImage)
+ }
+
+ /**
+ * Set the background color of the index bubble.
+ *
+ * @param color The background color for the index bubble
+ */
+ fun setBubbleColor(@ColorInt color: Int) {
+ mBubbleColor = color
+ if (mBubbleImage == null) {
+ val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_bubble)
+ if (drawable != null) {
+ mBubbleImage = DrawableCompat.wrap(drawable)
+ }
+ }
+ DrawableCompat.setTint(mBubbleImage!!, mBubbleColor)
+ mBubbleView!!.background = mBubbleImage
+ }
+
+ /**
+ * Set the text color of the index bubble.
+ *
+ * @param color The text color for the index bubble
+ */
+ fun setBubbleTextColor(@ColorInt color: Int) {
+ mBubbleView!!.setTextColor(color)
+ }
+
+ /**
+ * Set the fast scroll state change listener.
+ *
+ * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events
+ */
+ fun setFastScrollStateChangeListener(fastScrollStateChangeListener: FastScrollStateChangeListener) {
+ mFastScrollStateChangeListener = fastScrollStateChangeListener
+ }
+
+ override fun setEnabled(enabled: Boolean) {
+ super.setEnabled(enabled)
+ visibility = if (enabled) View.VISIBLE else View.GONE
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ if (event.x < mHandleView!!.x - ViewCompat.getPaddingStart(mHandleView!!)) {
+ return false
+ }
+ requestDisallowInterceptTouchEvent(true)
+ setHandleSelected(true)
+ handler.removeCallbacks(mScrollbarHider)
+ cancelAnimation(mScrollbarAnimator)
+ cancelAnimation(mBubbleAnimator)
+ if (!isViewVisible(mScrollbar)) {
+ showScrollbar()
+ }
+ if (mShowBubble && mSectionIndexer != null) {
+ showBubble()
+ }
+ if (mFastScrollStateChangeListener != null) {
+ mFastScrollStateChangeListener!!.onFastScrollStart(this)
+ }
+ val y = event.y
+ setViewPositions(y)
+ setRecyclerViewPosition(y)
+ return true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ val y = event.y
+ setViewPositions(y)
+ setRecyclerViewPosition(y)
+ return true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ requestDisallowInterceptTouchEvent(false)
+ setHandleSelected(false)
+ if (mFadeScrollbar) {
+ handler.postDelayed(mScrollbarHider, sScrollbarHideDelay.toLong())
+ }
+ hideBubble()
+ if (mFastScrollStateChangeListener != null) {
+ mFastScrollStateChangeListener!!.onFastScrollStop(this)
+ }
+ return true
+ }
+ }
+ return super.onTouchEvent(event)
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ mViewHeight = h
+ }
+
+ private fun setRecyclerViewPosition(y: Float) {
+ if (mRecyclerView != null && mRecyclerView!!.adapter != null) {
+ val itemCount = mRecyclerView!!.adapter!!.itemCount
+ val proportion: Float = when {
+ mHandleView!!.y == 0f -> 0f
+ mHandleView!!.y + mHandleHeight >= mViewHeight - sTrackSnapRange -> 1f
+ else -> y / mViewHeight.toFloat()
+ }
+ var scrolledItemCount = (proportion * itemCount).roundToInt()
+ if (isLayoutReversed(mRecyclerView!!.layoutManager!!)) {
+ scrolledItemCount = itemCount - scrolledItemCount
+ }
+ val targetPos = getValueInRange(0, itemCount - 1, scrolledItemCount)
+ mRecyclerView!!.layoutManager!!.scrollToPosition(targetPos)
+ if (mShowBubble && mSectionIndexer != null) {
+ mBubbleView!!.text = mSectionIndexer!!.getSectionText(targetPos)
+ }
+ }
+ }
+
+ private fun getScrollProportion(recyclerView: RecyclerView?): Float {
+ if (recyclerView == null) {
+ return 0f
+ }
+ val verticalScrollOffset = recyclerView.computeVerticalScrollOffset()
+ val verticalScrollRange = recyclerView.computeVerticalScrollRange()
+ val rangeDiff = (verticalScrollRange - mViewHeight).toFloat()
+ val proportion = verticalScrollOffset.toFloat() / if (rangeDiff > 0) rangeDiff else 1f
+ return mViewHeight * proportion
+ }
+
+ private fun getValueInRange(min: Int, max: Int, value: Int): Int {
+ val minimum = max(min, value)
+ return min(minimum, max)
+ }
+
+ private fun setViewPositions(y: Float) {
+ mBubbleHeight = mBubbleView!!.height
+ mHandleHeight = mHandleView!!.height
+ val bubbleY = getValueInRange(0, mViewHeight - mBubbleHeight - mHandleHeight / 2, (y - mBubbleHeight).toInt())
+ val handleY = getValueInRange(0, mViewHeight - mHandleHeight, (y - mHandleHeight / 2).toInt())
+ if (mShowBubble) {
+ mBubbleView!!.y = bubbleY.toFloat()
+ }
+ mHandleView!!.y = handleY.toFloat()
+ }
+
+ private fun updateViewHeights() {
+ val measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+ mBubbleView!!.measure(measureSpec, measureSpec)
+ mBubbleHeight = mBubbleView!!.measuredHeight
+ mHandleView!!.measure(measureSpec, measureSpec)
+ mHandleHeight = mHandleView!!.measuredHeight
+ }
+
+ private fun isLayoutReversed(layoutManager: RecyclerView.LayoutManager): Boolean {
+ if (layoutManager is LinearLayoutManager) {
+ return layoutManager.reverseLayout
+ } else if (layoutManager is StaggeredGridLayoutManager) {
+ return layoutManager.reverseLayout
+ }
+ return false
+ }
+
+ private fun isViewVisible(view: View?): Boolean {
+ return view != null && view.visibility == View.VISIBLE
+ }
+
+ private fun cancelAnimation(animator: ViewPropertyAnimator?) {
+ animator?.cancel()
+ }
+
+ private fun showBubble() {
+ if (!isViewVisible(mBubbleView)) {
+ mBubbleView!!.visibility = View.VISIBLE
+ mBubbleAnimator = mBubbleView!!.animate().alpha(1f)
+ .setDuration(sBubbleAnimDuration.toLong())
+ .setListener(object : AnimatorListenerAdapter() {
+
+ // adapter required for new alpha value to stick
+ })
+ }
+ }
+
+ private fun hideBubble() {
+ if (isViewVisible(mBubbleView)) {
+ mBubbleAnimator = mBubbleView!!.animate().alpha(0f)
+ .setDuration(sBubbleAnimDuration.toLong())
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ super.onAnimationEnd(animation)
+ mBubbleView!!.visibility = View.GONE
+ mBubbleAnimator = null
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ super.onAnimationCancel(animation)
+ mBubbleView!!.visibility = View.GONE
+ mBubbleAnimator = null
+ }
+ })
+ }
+ }
+
+ private fun showScrollbar() {
+ if (mRecyclerView!!.computeVerticalScrollRange() - mViewHeight > 0) {
+ val transX = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat()
+ mScrollbar!!.translationX = transX
+ mScrollbar!!.visibility = View.VISIBLE
+ mScrollbarAnimator = mScrollbar!!.animate().translationX(0f).alpha(1f)
+ .setDuration(sScrollbarAnimDuration.toLong())
+ .setListener(object : AnimatorListenerAdapter() {
+
+ // adapter required for new alpha value to stick
+ })
+ }
+ }
+
+ private fun hideScrollbar() {
+ val transX = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat()
+ mScrollbarAnimator = mScrollbar!!.animate().translationX(transX).alpha(0f)
+ .setDuration(sScrollbarAnimDuration.toLong())
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ super.onAnimationEnd(animation)
+ mScrollbar!!.visibility = View.GONE
+ mScrollbarAnimator = null
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ super.onAnimationCancel(animation)
+ mScrollbar!!.visibility = View.GONE
+ mScrollbarAnimator = null
+ }
+ })
+ }
+
+ private fun setHandleSelected(selected: Boolean) {
+ mHandleView!!.isSelected = selected
+ DrawableCompat.setTint(mHandleImage!!, if (selected) mBubbleColor else mHandleColor)
+ }
+
+ private fun layout(context: Context, attrs: AttributeSet?) {
+ View.inflate(context, R.layout.view_fastscroller, this)
+ clipChildren = false
+ orientation = HORIZONTAL
+ mBubbleView = findViewById(R.id.fastscroll_bubble)
+ mHandleView = findViewById(R.id.fastscroll_handle)
+ mTrackView = findViewById(R.id.fastscroll_track)
+ mScrollbar = findViewById(R.id.fastscroll_scrollbar)
+ @ColorInt var bubbleColor = ColorUtil.adjustAlpha(context.accentColor, 0.8f)
+ @ColorInt var handleColor = context.accentColor
+ @ColorInt var trackColor = context.getCompatColor(R.color.transparent30)
+ @ColorInt var textColor = if (ColorUtil.isColorLight(bubbleColor)) Color.BLACK else Color.WHITE
+ var fadeScrollbar = true
+ var showBubble = false
+ var showTrack = true
+ if (attrs != null) {
+ val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, 0, 0)
+ if (typedArray != null) {
+ try {
+ bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor)
+ handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor)
+ trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor)
+ textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor)
+ fadeScrollbar = typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar)
+ showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble)
+ showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack)
+ } finally {
+ typedArray.recycle()
+ }
+ }
+ }
+ setTrackColor(trackColor)
+ setHandleColor(handleColor)
+ setBubbleColor(bubbleColor)
+ setBubbleTextColor(textColor)
+ setFadeScrollbar(fadeScrollbar)
+ setBubbleVisible(showBubble)
+ setTrackVisible(showTrack)
+ }
+
+ interface SectionIndexer {
+ fun getSectionText(position: Int): String
+ }
+
+ companion object {
+ private const val sBubbleAnimDuration = 100
+ private const val sScrollbarAnimDuration = 300
+ private const val sScrollbarHideDelay = 1000
+ private const val sTrackSnapRange = 5
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/utils/ColorUtil.kt b/app/src/main/java/io/legado/app/utils/ColorUtil.kt
new file mode 100644
index 000000000..bce3ea27c
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/ColorUtil.kt
@@ -0,0 +1,80 @@
+package io.legado.app.utils
+
+import android.graphics.Color
+
+import androidx.annotation.ColorInt
+import androidx.annotation.FloatRange
+
+object ColorUtil {
+
+ fun intToString(intColor: Int): String {
+ return String.format("#%06X", 0xFFFFFF and intColor)
+ }
+
+
+ fun stripAlpha(@ColorInt color: Int): Int {
+ return -0x1000000 or color
+ }
+
+ @ColorInt
+ fun shiftColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 2.0) by: Float): Int {
+ if (by == 1f) return color
+ val alpha = Color.alpha(color)
+ val hsv = FloatArray(3)
+ Color.colorToHSV(color, hsv)
+ hsv[2] *= by // value component
+ return (alpha shl 24) + (0x00ffffff and Color.HSVToColor(hsv))
+ }
+
+ @ColorInt
+ fun darkenColor(@ColorInt color: Int): Int {
+ return shiftColor(color, 0.9f)
+ }
+
+ @ColorInt
+ fun lightenColor(@ColorInt color: Int): Int {
+ return shiftColor(color, 1.1f)
+ }
+
+ fun isColorLight(@ColorInt color: Int): Boolean {
+ val darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
+ return darkness < 0.4
+ }
+
+ @ColorInt
+ fun invertColor(@ColorInt color: Int): Int {
+ val r = 255 - Color.red(color)
+ val g = 255 - Color.green(color)
+ val b = 255 - Color.blue(color)
+ return Color.argb(Color.alpha(color), r, g, b)
+ }
+
+ @ColorInt
+ fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int {
+ val alpha = Math.round(Color.alpha(color) * factor)
+ val red = Color.red(color)
+ val green = Color.green(color)
+ val blue = Color.blue(color)
+ return Color.argb(alpha, red, green, blue)
+ }
+
+ @ColorInt
+ fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int {
+ val a = Math.min(255, Math.max(0, (alpha * 255).toInt())) shl 24
+ val rgb = 0x00ffffff and baseColor
+ return a + rgb
+ }
+
+ /**
+ * Taken from CollapsingToolbarLayout's CollapsingTextHelper class.
+ */
+ fun blendColors(color1: Int, color2: Int, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Int {
+ val inverseRatio = 1f - ratio
+ val a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio
+ val r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio
+ val g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio
+ val b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio
+ return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())
+ }
+
+}
diff --git a/app/src/main/res/layout/view_fastscroller.xml b/app/src/main/res/layout/view_fastscroller.xml
new file mode 100644
index 000000000..7256c640f
--- /dev/null
+++ b/app/src/main/res/layout/view_fastscroller.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index bb25a8ab0..016946f36 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -7,4 +7,5 @@
+
\ No newline at end of file