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