Merge remote-tracking branch 'origin/master'

pull/32/head
Administrator 5 years ago
commit b9f9663824
  1. 8
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  2. 4
      app/src/main/java/io/legado/app/model/WebBook.kt
  3. 13
      app/src/main/java/io/legado/app/model/webbook/BookContent.kt
  4. 8
      app/src/main/java/io/legado/app/model/webbook/SourceDebug.kt
  5. 195
      app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt
  6. 14
      app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt
  7. 520
      app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt
  8. 80
      app/src/main/java/io/legado/app/utils/ColorUtil.kt
  9. 8
      app/src/main/res/layout/activity_book_source.xml
  10. 49
      app/src/main/res/layout/view_fastscroller.xml
  11. 1
      app/src/main/res/values/ids.xml

@ -1,13 +1,21 @@
package io.legado.app.data.dao package io.legado.app.data.dao
import androidx.paging.DataSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@Dao @Dao
interface BookChapterDao { interface BookChapterDao {
@Query("select * from chapters where bookUrl = :bookUrl")
fun observeByBook(bookUrl: String): DataSource.Factory<Int, BookChapter>
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
fun getChapter(bookUrl: String, index: Int): BookChapter?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookChapter: BookChapter) fun insert(vararg bookChapter: BookChapter)

@ -44,11 +44,11 @@ class WebBook(private val bookSource: BookSource) {
} }
} }
fun getContent(book: Book, bookChapter: BookChapter): Coroutine<String> { fun getContent(book: Book, bookChapter: BookChapter, nextChapterUrl: String? = null): Coroutine<String> {
return Coroutine.async { return Coroutine.async {
val analyzeUrl = AnalyzeUrl(book = book, ruleUrl = bookChapter.url, baseUrl = book.tocUrl) val analyzeUrl = AnalyzeUrl(book = book, ruleUrl = bookChapter.url, baseUrl = book.tocUrl)
val response = analyzeUrl.getResponseAsync().await() val response = analyzeUrl.getResponseAsync().await()
BookContent.analyzeContent(this, response, book, bookSource) BookContent.analyzeContent(this, response, book, bookChapter, bookSource, nextChapterUrl)
} }
} }
} }

@ -3,6 +3,7 @@ package io.legado.app.model.webbook
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.ContentRule import io.legado.app.data.entities.rule.ContentRule
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
@ -20,7 +21,9 @@ object BookContent {
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
response: Response<String>, response: Response<String>,
book: Book, book: Book,
bookSource: BookSource bookChapter: BookChapter,
bookSource: BookSource,
nextChapterUrlF: String? = null
): String { ): String {
val baseUrl: String = NetworkUtils.getUrl(response) val baseUrl: String = NetworkUtils.getUrl(response)
val body: String? = response.body() val body: String? = response.body()
@ -38,7 +41,15 @@ object BookContent {
content.append(contentData.content) content.append(contentData.content)
if (contentData.nextUrl.size == 1) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty())
nextChapterUrlF
else
App.db.bookChapterDao().getChapter(book.bookUrl, bookChapter.index + 1)?.url
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {
if (!nextChapterUrl.isNullOrEmpty()
&& NetworkUtils.getAbsoluteURL(baseUrl, nextUrl)
== NetworkUtils.getAbsoluteURL(baseUrl, nextChapterUrl)
) break
nextUrlList.add(nextUrl) nextUrlList.add(nextUrl)
AnalyzeUrl(ruleUrl = nextUrl, book = book).getResponse().execute() AnalyzeUrl(ruleUrl = nextUrl, book = book).getResponse().execute()
.body()?.let { nextBody -> .body()?.let { nextBody ->

@ -5,7 +5,6 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.utils.htmlFormat import io.legado.app.utils.htmlFormat
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
@ -117,7 +116,8 @@ class SourceDebug(private val webBook: WebBook, callback: Callback) {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
printLog(debugSource, 1, "目录完成") printLog(debugSource, 1, "目录完成")
printLog(debugSource, 1, "", showTime = false) printLog(debugSource, 1, "", showTime = false)
contentDebug(book, it[0]) val nextChapterUrl = if (it.size > 1) it[1].url else null
contentDebug(book, it[0], nextChapterUrl)
} else { } else {
printLog(debugSource, -1, "目录列表为空") printLog(debugSource, -1, "目录列表为空")
} }
@ -129,9 +129,9 @@ class SourceDebug(private val webBook: WebBook, callback: Callback) {
tasks.add(chapterList) tasks.add(chapterList)
} }
private fun contentDebug(book: Book, bookChapter: BookChapter) { private fun contentDebug(book: Book, bookChapter: BookChapter, nextChapterUrl: String?) {
printLog(debugSource, 1, "开始获取内容") printLog(debugSource, 1, "开始获取内容")
val content = webBook.getContent(book, bookChapter) val content = webBook.getContent(book, bookChapter, nextChapterUrl)
.onSuccess { content -> .onSuccess { content ->
content?.let { content?.let {
printLog(debugSource, 1000, it) printLog(debugSource, 1000, it)

@ -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())
}
}

@ -13,9 +13,15 @@
app:displayHomeAsUp="true" app:displayHomeAsUp="true"
app:title="@string/book_source"/> app:title="@string/book_source"/>
<androidx.recyclerview.widget.RecyclerView <FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout> </LinearLayout>

@ -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>

@ -7,4 +7,5 @@
<item name="tag1" type="id" /> <item name="tag1" type="id" />
<item name="tag2" type="id" /> <item name="tag2" type="id" />
<item name="fast_scroller" type="id" />
</resources> </resources>
Loading…
Cancel
Save