commit
b9f9663824
@ -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 |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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) |
||||||
|
} |
@ -0,0 +1,520 @@ |
|||||||
|
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 lateinit var mBubbleView: TextView |
||||||
|
private lateinit var mHandleView: ImageView |
||||||
|
private lateinit var mTrackView: ImageView |
||||||
|
private lateinit var mScrollbar: View |
||||||
|
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() { |
||||||
|
mRecyclerView?.let { mRecyclerView -> |
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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()) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:tools="http://schemas.android.com/tools"> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/fastscroll_bubble" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_gravity="end" |
||||||
|
android:gravity="center" |
||||||
|
android:maxLines="1" |
||||||
|
android:textSize="@dimen/fastscroll_bubble_textsize" |
||||||
|
android:visibility="gone" |
||||||
|
tools:background="@drawable/fastscroll_bubble" |
||||||
|
tools:text="A" |
||||||
|
tools:textColor="#ffffff" |
||||||
|
tools:visibility="visible" /> |
||||||
|
|
||||||
|
<FrameLayout |
||||||
|
android:id="@+id/fastscroll_scrollbar" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:paddingEnd="@dimen/fastscroll_scrollbar_padding_end" |
||||||
|
android:paddingLeft="@dimen/fastscroll_scrollbar_padding_start" |
||||||
|
android:paddingRight="@dimen/fastscroll_scrollbar_padding_end" |
||||||
|
android:paddingStart="@dimen/fastscroll_scrollbar_padding_start" |
||||||
|
android:visibility="gone" |
||||||
|
tools:visibility="visible"> |
||||||
|
|
||||||
|
<ImageView |
||||||
|
android:id="@+id/fastscroll_track" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:layout_gravity="center_horizontal" |
||||||
|
tools:ignore="ContentDescription" |
||||||
|
tools:src="@drawable/fastscroll_track" /> |
||||||
|
|
||||||
|
<ImageView |
||||||
|
android:id="@+id/fastscroll_handle" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_gravity="center_horizontal" |
||||||
|
tools:ignore="ContentDescription" |
||||||
|
tools:src="@drawable/fastscroll_handle" /> |
||||||
|
|
||||||
|
</FrameLayout> |
||||||
|
|
||||||
|
</merge> |
Loading…
Reference in new issue