Using material fast scroll on manga details

This concludes the use of reddit's fastscroll

Also updated the fast scroll's scroll listener so that the scroll handle respects its margins (thus making it reach the bottom of the manga details
This commit is contained in:
Jay 2020-05-10 03:03:36 -04:00
parent 60c3f24515
commit 4dae06803c
7 changed files with 51 additions and 186 deletions

View File

@ -206,7 +206,6 @@ dependencies {
implementation("me.zhanghai.android.systemuihelper:library:1.0.0") implementation("me.zhanghai.android.systemuihelper:library:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0") implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.mthli:Slice:v1.2") implementation("com.github.mthli:Slice:v1.2")
implementation("com.reddit:indicator-fast-scroll:1.2.1")
implementation("com.github.kizitonwose:AndroidTagGroup:1.6.0") implementation("com.github.kizitonwose:AndroidTagGroup:1.6.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")

View File

@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.library
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.fastscroller.FastScroller import eu.davidea.fastscroller.FastScroller
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.dpToPxEnd import eu.kanade.tachiyomi.util.system.dpToPxEnd
import eu.kanade.tachiyomi.util.view.marginTop
import kotlin.math.abs
class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FastScroller(context, attrs) { FastScroller(context, attrs) {
@ -16,6 +19,7 @@ class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: Attr
) )
autoHideEnabled = true autoHideEnabled = true
ignoreTouchesOutsideHandle = true ignoreTouchesOutsideHandle = true
updateScrollListener()
} }
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
@ -30,4 +34,22 @@ class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: Attr
bubble.translationX = (-45f).dpToPxEnd bubble.translationX = (-45f).dpToPxEnd
} }
} }
private fun updateScrollListener() {
onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (!isEnabled || bubble == null || handle.isSelected) return
val verticalScrollOffset = recyclerView.computeVerticalScrollOffset()
val verticalScrollRange = recyclerView.computeVerticalScrollRange() - marginTop
val proportion =
verticalScrollOffset.toFloat() / (verticalScrollRange - height).toFloat()
setBubbleAndHandlePosition(height * proportion)
// If scroll amount is small, don't show it
if (minimumScrollThreshold == 0 || dy == 0 || abs(dy) > minimumScrollThreshold || scrollbarAnimator.isAnimating) {
showScrollbar()
if (autoHideEnabled) hideScrollbar()
}
}
}
}
} }

View File

@ -59,55 +59,34 @@ class MangaDetailsAdapter(
} }
} }
fun getSectionText(position: Int): String? { override fun onCreateBubbleText(position: Int): String {
val chapter = getItem(position) as? ChapterItem ?: return null
if (position == itemCount - 1) return "-"
return when (presenter.scrollType) {
MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS ->
presenter.getGroupNumber(chapter)?.toString() ?: "*"
MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS ->
if (chapter.chapter_number < 0) "*"
else (chapter.chapter_number / 100).toInt().toString()
MangaDetailsPresenter.TENS_OF_CHAPTERS ->
if (chapter.chapter_number < 0) "*"
else (chapter.chapter_number / 10).toInt().toString()
else -> null
}
}
fun getFullText(position: Int): String {
val chapter = val chapter =
getItem(position) as? ChapterItem ?: return recyclerView.context.getString(R.string.top) getItem(position) as? ChapterItem ?: return recyclerView.context.getString(R.string.top)
if (position == itemCount - 1) return recyclerView.context.getString(R.string.bottom)
return when (val scrollType = presenter.scrollType) { return when (val scrollType = presenter.scrollType) {
MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS -> { MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS -> {
val volume = presenter.getGroupNumber(chapter) val volume = presenter.getGroupNumber(chapter)
if (volume != null) recyclerView.context.getString( if (volume != null) {
recyclerView.context.getString(
if (scrollType == MangaDetailsPresenter.MULTIPLE_SEASONS) R.string.season_ if (scrollType == MangaDetailsPresenter.MULTIPLE_SEASONS) R.string.season_
else R.string.volume_, volume) else R.string.volume_, volume
else recyclerView.context.getString(R.string.unknown) )
} else {
recyclerView.context.getString(R.string.unknown)
}
} }
MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS -> recyclerView.context.getString(
R.string.chapters_, get100sRange(
chapter.chapter_number
)
)
MangaDetailsPresenter.TENS_OF_CHAPTERS -> recyclerView.context.getString( MangaDetailsPresenter.TENS_OF_CHAPTERS -> recyclerView.context.getString(
R.string.chapters_, get10sRange( R.string.chapters_, get10sRange(
chapter.chapter_number chapter.chapter_number
) )
) )
else -> recyclerView.context.getString(R.string.unknown) else -> if (chapter.chapter_number > 0) {
recyclerView.context.getString(
R.string.chapter_, decimalFormat.format(chapter.chapter_number)
)
} else {
chapter.name
} }
} }
private fun get100sRange(value: Float): String {
val number = value.toInt()
return if (number < 100) "0-99"
else {
val hundred = number / 100
"${hundred}00-${hundred}99"
}
} }
private fun get10sRange(value: Float): String { private fun get10sRange(value: Float): String {

View File

@ -53,8 +53,6 @@ import com.bumptech.glide.request.transition.Transition
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
import com.reddit.indicatorfastscroll.FastScrollerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -101,14 +99,10 @@ import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.getText import eu.kanade.tachiyomi.util.view.getText
import eu.kanade.tachiyomi.util.view.hide
import eu.kanade.tachiyomi.util.view.requestPermissionsSafe import eu.kanade.tachiyomi.util.view.requestPermissionsSafe
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setBackground
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.setStartTranslationX
import eu.kanade.tachiyomi.util.view.setStyle import eu.kanade.tachiyomi.util.view.setStyle
import eu.kanade.tachiyomi.util.view.show
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.updatePaddingRelative import eu.kanade.tachiyomi.util.view.updatePaddingRelative
@ -205,7 +199,7 @@ class MangaDetailsController : BaseController,
setRecycler(view) setRecycler(view)
setPaletteColor() setPaletteColor()
setFastScroller() adapter?.fastScroller = fast_scroller2
presenter.onCreate() presenter.onCreate()
swipe_refresh.isRefreshing = presenter.isLoading swipe_refresh.isRefreshing = presenter.isLoading
@ -259,17 +253,6 @@ class MangaDetailsController : BaseController,
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
val atTop = !recycler.canScrollVertically(-1) val atTop = !recycler.canScrollVertically(-1)
if (atTop) getHeader()?.backdrop?.translationY = 0f if (atTop) getHeader()?.backdrop?.translationY = 0f
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
scrollAnim?.cancel()
if (fast_scroller.translationX != 0f) {
showFastScroller()
}
}
RecyclerView.SCROLL_STATE_IDLE -> {
scrollAnim = fast_scroller.hide()
}
}
} }
}) })
} }
@ -281,51 +264,12 @@ class MangaDetailsController : BaseController,
swipe_refresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset) swipe_refresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset)
// 1dp extra to line up chapter header and manga header // 1dp extra to line up chapter header and manga header
getHeader()?.setTopHeight(headerHeight) getHeader()?.setTopHeight(headerHeight)
fast_scroll_layout.updateLayoutParams<ViewGroup.MarginLayoutParams> { fast_scroller2.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = headerHeight topMargin = headerHeight
bottomMargin = insets.systemWindowInsetBottom bottomMargin = insets.systemWindowInsetBottom
} }
} }
private fun setFastScroller() {
fast_scroller.setStartTranslationX(true)
fast_scroller.setBackground(true)
fast_scroller.setupWithRecyclerView(recycler, { position ->
val letter = adapter?.getSectionText(position)
when {
presenter.scrollType == 0 -> null
letter != null -> FastScrollItemIndicator.Text(letter)
else -> FastScrollItemIndicator.Icon(R.drawable.ic_star_24dp)
}
})
fast_scroller.useDefaultScroller = false
fast_scroller.itemIndicatorSelectedCallbacks += object :
FastScrollerView.ItemIndicatorSelectedCallback {
override fun onItemIndicatorSelected(
indicator: FastScrollItemIndicator,
indicatorCenterY: Int,
itemPosition: Int
) {
scrollAnim?.cancel()
scrollAnim = fast_scroller.hide(2000)
textAnim?.cancel()
textAnim = text_view_m.animate().alpha(0f).setDuration(250L).setStartDelay(1000)
textAnim?.start()
text_view_m.translationY = indicatorCenterY.toFloat() - text_view_m.height / 2
text_view_m.alpha = 1f
text_view_m.text = adapter?.getFullText(itemPosition)
val appbar = activity?.appbar
appbar?.y = 0f
(recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
itemPosition, headerHeight
)
colorToolbar(itemPosition > 0, false)
}
}
}
/** Set the toolbar to fully transparent or colored and translucent */ /** Set the toolbar to fully transparent or colored and translucent */
fun colorToolbar(isColor: Boolean, animate: Boolean = true) { fun colorToolbar(isColor: Boolean, animate: Boolean = true) {
if (isColor == toolbarIsColored) return if (isColor == toolbarIsColored) return
@ -594,12 +538,6 @@ class MangaDetailsController : BaseController,
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
private fun showFastScroller(animate: Boolean = true) {
if (presenter.scrollType != 0) {
fast_scroller.show(animate)
}
}
private fun addMangaHeader() { private fun addMangaHeader() {
if (adapter?.scrollableHeaders?.isEmpty() == true) { if (adapter?.scrollableHeaders?.isEmpty() == true) {
adapter?.removeAllScrollableHeaders() adapter?.removeAllScrollableHeaders()

View File

@ -251,7 +251,6 @@ class MangaDetailsPresenter(
scrollType = when { scrollType = when {
hasMultipleVolumes(chapters) -> MULTIPLE_VOLUMES hasMultipleVolumes(chapters) -> MULTIPLE_VOLUMES
hasMultipleSeasons(chapters) -> MULTIPLE_SEASONS hasMultipleSeasons(chapters) -> MULTIPLE_SEASONS
hasHundredsOfChapters(chapters) -> HUNDREDS_OF_CHAPTERS
hasTensOfChapters(chapters) -> TENS_OF_CHAPTERS hasTensOfChapters(chapters) -> TENS_OF_CHAPTERS
else -> 0 else -> 0
} }
@ -283,7 +282,7 @@ class MangaDetailsPresenter(
val volNum = getVolumeNumber(it) val volNum = getVolumeNumber(it)
if (volNum != null) { if (volNum != null) {
volumeSet.add(volNum) volumeSet.add(volNum)
if (volumeSet.size >= 3) return true if (volumeSet.size >= 2) return true
} }
} }
return false return false
@ -295,18 +294,14 @@ class MangaDetailsPresenter(
val volNum = getSeasonNumber(it) val volNum = getSeasonNumber(it)
if (volNum != null) { if (volNum != null) {
volumeSet.add(volNum) volumeSet.add(volNum)
if (volumeSet.size >= 3) return true if (volumeSet.size >= 2) return true
} }
} }
return false return false
} }
private fun hasHundredsOfChapters(chapters: List<ChapterItem>): Boolean {
return chapters.size > 300
}
private fun hasTensOfChapters(chapters: List<ChapterItem>): Boolean { private fun hasTensOfChapters(chapters: List<ChapterItem>): Boolean {
return chapters.size in 21..300 return chapters.size > 20
} }
/** /**
@ -878,7 +873,6 @@ class MangaDetailsPresenter(
companion object { companion object {
const val MULTIPLE_VOLUMES = 1 const val MULTIPLE_VOLUMES = 1
const val TENS_OF_CHAPTERS = 2 const val TENS_OF_CHAPTERS = 2
const val HUNDREDS_OF_CHAPTERS = 3 const val MULTIPLE_SEASONS = 3
const val MULTIPLE_SEASONS = 4
} }
} }

View File

@ -1,37 +0,0 @@
package eu.kanade.tachiyomi.util.view
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.ViewPropertyAnimator
import androidx.core.content.ContextCompat
import com.reddit.indicatorfastscroll.FastScrollerView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.dpToPxEnd
import eu.kanade.tachiyomi.util.system.getResourceColor
fun FastScrollerView.setBackground(hasBackground: Boolean) {
background = if (hasBackground) ContextCompat.getDrawable(
context, R.drawable.fast_scroll_background
) else null
textColor = ColorStateList.valueOf(
if (hasBackground) Color.WHITE
else context.getResourceColor(android.R.attr.textColorPrimary)
)
iconColor = textColor
}
fun FastScrollerView.setStartTranslationX(shouldHide: Boolean) {
translationX = if (shouldHide) 25f.dpToPxEnd else 0f
}
fun FastScrollerView.hide(duration: Long = 1000): ViewPropertyAnimator {
val scrollAnim = animate().setStartDelay(duration).setDuration(250)
.translationX(25f.dpToPxEnd)
scrollAnim.start()
return scrollAnim
}
fun FastScrollerView.show(animate: Boolean = true) {
if (animate) animate().setStartDelay(0).setDuration(100).translationX(0f).start()
else translationX = 0f
}

View File

@ -14,55 +14,25 @@
android:background="?android:colorBackground"> android:background="?android:colorBackground">
<LinearLayout <FrameLayout
android:id="@+id/linear_recycler_layout" android:id="@+id/linear_recycler_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="horizontal">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="0.25"
android:clipToPadding="false" android:clipToPadding="false"
tools:listitem="@layout/chapters_item" /> tools:listitem="@layout/chapters_item" />
</LinearLayout> </FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.constraintlayout.widget.ConstraintLayout <eu.kanade.tachiyomi.ui.library.MaterialFastScroll
android:id="@+id/fast_scroll_layout" android:id="@+id/fast_scroller2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
app:fastScrollerBubbleEnabled="true" />
<com.reddit.indicatorfastscroll.FastScrollerView
android:id="@+id/fast_scroller"
android:layout_width="25dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:elevation="10dp"
android:paddingStart="1dp"
android:paddingTop="8dp"
android:paddingEnd="0dp"
android:paddingBottom="8dp"
android:textColor="?android:attr/textColorPrimary"
app:iconColor="?android:attr/textColorPrimary"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_view_m"
style="@style/FloatingTextView"
android:layout_marginEnd="50dp"
android:alpha="0"
app:layout_constraintEnd_toStartOf="@id/fast_scroller"
app:layout_constraintTop_toTopOf="@id/fast_scroller"
tools:text="Volume 12"
tools:alpha="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View <View
android:id="@+id/full_backdrop" android:id="@+id/full_backdrop"