diff --git a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt index 9cfe22e8d..7f42d74d8 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt @@ -1,13 +1,21 @@ package io.legado.app.data.dao +import androidx.paging.DataSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.Query import io.legado.app.data.entities.BookChapter @Dao interface BookChapterDao { + @Query("select * from chapters where bookUrl = :bookUrl") + fun observeByBook(bookUrl: String): DataSource.Factory + + @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") + fun getChapter(bookUrl: String, index: Int): BookChapter? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg bookChapter: BookChapter) diff --git a/app/src/main/java/io/legado/app/model/WebBook.kt b/app/src/main/java/io/legado/app/model/WebBook.kt index 0b764b898..f8b7148c9 100644 --- a/app/src/main/java/io/legado/app/model/WebBook.kt +++ b/app/src/main/java/io/legado/app/model/WebBook.kt @@ -44,11 +44,11 @@ class WebBook(private val bookSource: BookSource) { } } - fun getContent(book: Book, bookChapter: BookChapter): Coroutine { + fun getContent(book: Book, bookChapter: BookChapter, nextChapterUrl: String? = null): Coroutine { return Coroutine.async { val analyzeUrl = AnalyzeUrl(book = book, ruleUrl = bookChapter.url, baseUrl = book.tocUrl) val response = analyzeUrl.getResponseAsync().await() - BookContent.analyzeContent(this, response, book, bookSource) + BookContent.analyzeContent(this, response, book, bookChapter, bookSource, nextChapterUrl) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webbook/BookContent.kt b/app/src/main/java/io/legado/app/model/webbook/BookContent.kt index a85ccb28c..a8e92444b 100644 --- a/app/src/main/java/io/legado/app/model/webbook/BookContent.kt +++ b/app/src/main/java/io/legado/app/model/webbook/BookContent.kt @@ -3,6 +3,7 @@ package io.legado.app.model.webbook import io.legado.app.App import io.legado.app.R 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.rule.ContentRule import io.legado.app.model.analyzeRule.AnalyzeRule @@ -20,7 +21,9 @@ object BookContent { coroutineScope: CoroutineScope, response: Response, book: Book, - bookSource: BookSource + bookChapter: BookChapter, + bookSource: BookSource, + nextChapterUrlF: String? = null ): String { val baseUrl: String = NetworkUtils.getUrl(response) val body: String? = response.body() @@ -38,7 +41,15 @@ object BookContent { content.append(contentData.content) if (contentData.nextUrl.size == 1) { 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)) { + if (!nextChapterUrl.isNullOrEmpty() + && NetworkUtils.getAbsoluteURL(baseUrl, nextUrl) + == NetworkUtils.getAbsoluteURL(baseUrl, nextChapterUrl) + ) break nextUrlList.add(nextUrl) AnalyzeUrl(ruleUrl = nextUrl, book = book).getResponse().execute() .body()?.let { nextBody -> diff --git a/app/src/main/java/io/legado/app/model/webbook/SourceDebug.kt b/app/src/main/java/io/legado/app/model/webbook/SourceDebug.kt index a51922f7b..c79b078f9 100644 --- a/app/src/main/java/io/legado/app/model/webbook/SourceDebug.kt +++ b/app/src/main/java/io/legado/app/model/webbook/SourceDebug.kt @@ -5,7 +5,6 @@ import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp import io.legado.app.help.coroutine.CompositeCoroutine -import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook import io.legado.app.utils.htmlFormat import io.legado.app.utils.isAbsUrl @@ -117,7 +116,8 @@ class SourceDebug(private val webBook: WebBook, callback: Callback) { if (it.isNotEmpty()) { printLog(debugSource, 1, "目录完成") 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 { printLog(debugSource, -1, "目录列表为空") } @@ -129,9 +129,9 @@ class SourceDebug(private val webBook: WebBook, callback: Callback) { tasks.add(chapterList) } - private fun contentDebug(book: Book, bookChapter: BookChapter) { + private fun contentDebug(book: Book, bookChapter: BookChapter, nextChapterUrl: String?) { printLog(debugSource, 1, "开始获取内容") - val content = webBook.getContent(book, bookChapter) + val content = webBook.getContent(book, bookChapter, nextChapterUrl) .onSuccess { content -> content?.let { printLog(debugSource, 1000, it) 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..85a04b8b3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt @@ -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 + } + +} 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/activity_book_source.xml b/app/src/main/res/layout/activity_book_source.xml index 4069bd48a..8297c7268 100644 --- a/app/src/main/res/layout/activity_book_source.xml +++ b/app/src/main/res/layout/activity_book_source.xml @@ -13,9 +13,15 @@ app:displayHomeAsUp="true" app:title="@string/book_source"/> - + + + + \ No newline at end of file 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