Smooth auto scrolling #319
This commit is contained in:
@@ -40,7 +40,6 @@ import org.koitharu.kotatsu.databinding.ActivityReaderBinding
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.ui.config.PageSwitchTimer
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigBottomSheet
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||
@@ -70,16 +69,16 @@ class ReaderActivity :
|
||||
|
||||
private val viewModel: ReaderViewModel by viewModels()
|
||||
|
||||
override var pageSwitchDelay: Float
|
||||
get() = pageSwitchTimer.delaySec
|
||||
override var autoScrollSpeed: Float
|
||||
get() = scrollTimer.speed
|
||||
set(value) {
|
||||
pageSwitchTimer.delaySec = value
|
||||
scrollTimer.speed = value
|
||||
}
|
||||
|
||||
override val readerMode: ReaderMode?
|
||||
get() = readerManager.currentMode
|
||||
|
||||
private lateinit var pageSwitchTimer: PageSwitchTimer
|
||||
private lateinit var scrollTimer: ScrollTimer
|
||||
private lateinit var touchHelper: GridTouchHelper
|
||||
private lateinit var controlDelegate: ReaderControlDelegate
|
||||
private var gestureInsets: Insets = Insets.NONE
|
||||
@@ -92,7 +91,7 @@ class ReaderActivity :
|
||||
readerManager = ReaderManager(supportFragmentManager, R.id.container)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
touchHelper = GridTouchHelper(this, this)
|
||||
pageSwitchTimer = PageSwitchTimer(this, this)
|
||||
scrollTimer = ScrollTimer(this, this)
|
||||
controlDelegate = ReaderControlDelegate(settings, this, this)
|
||||
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
|
||||
binding.slider.setLabelFormatter(PageLabelFormatter())
|
||||
@@ -134,7 +133,6 @@ class ReaderActivity :
|
||||
|
||||
override fun onUserInteraction() {
|
||||
super.onUserInteraction()
|
||||
pageSwitchTimer.onUserInteraction()
|
||||
idlingDetector.onUserInteraction()
|
||||
}
|
||||
|
||||
@@ -337,6 +335,10 @@ class ReaderActivity :
|
||||
readerManager.currentReader?.switchPageBy(delta)
|
||||
}
|
||||
|
||||
override fun scrollBy(delta: Int): Boolean {
|
||||
return readerManager.currentReader?.scrollBy(delta) ?: false
|
||||
}
|
||||
|
||||
override fun toggleUiVisibility() {
|
||||
setUiIsVisible(!binding.appbarTop.isVisible)
|
||||
}
|
||||
|
||||
@@ -42,18 +42,22 @@ class ReaderControlDelegate(
|
||||
listener.toggleUiVisibility()
|
||||
view.playSoundEffect(SoundEffectConstants.CLICK)
|
||||
}
|
||||
|
||||
GridTouchHelper.AREA_TOP -> if (isTapSwitchEnabled) {
|
||||
listener.switchPageBy(-1)
|
||||
view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP)
|
||||
}
|
||||
|
||||
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
|
||||
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
|
||||
view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT)
|
||||
}
|
||||
|
||||
GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) {
|
||||
listener.switchPageBy(1)
|
||||
view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN)
|
||||
}
|
||||
|
||||
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
|
||||
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
|
||||
view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT)
|
||||
@@ -68,12 +72,14 @@ class ReaderControlDelegate(
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) {
|
||||
listener.switchPageBy(1)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_SPACE,
|
||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
|
||||
@@ -82,10 +88,12 @@ class ReaderControlDelegate(
|
||||
listener.switchPageBy(1)
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_PAGE_UP,
|
||||
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
@@ -93,14 +101,17 @@ class ReaderControlDelegate(
|
||||
listener.switchPageBy(-1)
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
listener.toggleUiVisibility()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
@@ -128,6 +139,8 @@ class ReaderControlDelegate(
|
||||
|
||||
fun switchPageBy(delta: Int)
|
||||
|
||||
fun scrollBy(delta: Int): Boolean
|
||||
|
||||
fun toggleUiVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
private const val MIN_SPEED = 0.1
|
||||
private const val MAX_DELAY = 80L
|
||||
private const val MAX_SWITCH_DELAY = 20_000L
|
||||
|
||||
class ScrollTimer(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val listener: ReaderControlDelegate.OnInteractionListener,
|
||||
) {
|
||||
|
||||
private var job: Job? = null
|
||||
private var delayMs: Long = 10L
|
||||
private var pageSwitchDelay: Long = 100L
|
||||
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
var speed: Float = 0f
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
onSpeedChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSpeedChanged() {
|
||||
if (speed < MIN_SPEED) {
|
||||
delayMs = 0L
|
||||
pageSwitchDelay = 0L
|
||||
} else {
|
||||
val speedFactor = 1 - speed + MIN_SPEED
|
||||
delayMs = (MAX_DELAY * speedFactor).roundToLong()
|
||||
pageSwitchDelay = (MAX_SWITCH_DELAY * speedFactor).roundToLong()
|
||||
}
|
||||
if ((job == null) != (delayMs == 0L)) {
|
||||
restartJob()
|
||||
}
|
||||
}
|
||||
|
||||
private fun restartJob() {
|
||||
job?.cancel()
|
||||
if (delayMs == 0L) {
|
||||
job = null
|
||||
return
|
||||
}
|
||||
job = lifecycleOwner.lifecycle.coroutineScope.launch {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
var accumulator = 0L
|
||||
while (isActive) {
|
||||
delay(delayMs)
|
||||
if (!listener.scrollBy(1)) {
|
||||
accumulator += delayMs
|
||||
}
|
||||
if (accumulator >= pageSwitchDelay) {
|
||||
listener.switchPageBy(1)
|
||||
accumulator -= pageSwitchDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.slider.LabelFormatter
|
||||
import kotlin.math.roundToLong
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -14,7 +13,9 @@ import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderControlDelegate
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@Deprecated("")
|
||||
class PageSwitchTimer(
|
||||
private val listener: ReaderControlDelegate.OnInteractionListener,
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.core.view.isGone
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -23,7 +24,6 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
|
||||
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
@@ -32,7 +32,7 @@ class ReaderConfigBottomSheet :
|
||||
ActivityResultCallback<Uri?>,
|
||||
View.OnClickListener,
|
||||
MaterialButtonToggleGroup.OnButtonCheckedListener,
|
||||
Slider.OnSliderTouchListener {
|
||||
Slider.OnChangeListener {
|
||||
|
||||
private val viewModel by activityViewModels<ReaderViewModel>()
|
||||
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
|
||||
@@ -62,11 +62,10 @@ class ReaderConfigBottomSheet :
|
||||
binding.buttonScreenRotate.setOnClickListener(this)
|
||||
binding.buttonSettings.setOnClickListener(this)
|
||||
binding.buttonColorFilter.setOnClickListener(this)
|
||||
binding.sliderTimer.addOnSliderTouchListener(this)
|
||||
binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources))
|
||||
binding.sliderTimer.addOnChangeListener(this)
|
||||
|
||||
findCallback()?.run {
|
||||
binding.sliderTimer.setValueRounded(pageSwitchDelay)
|
||||
binding.sliderTimer.value = autoScrollSpeed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +110,8 @@ class ReaderConfigBottomSheet :
|
||||
mode = newMode
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(slider: Slider) = Unit
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
findCallback()?.pageSwitchDelay = slider.value
|
||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
findCallback()?.autoScrollSpeed = value
|
||||
}
|
||||
|
||||
override fun onActivityResult(uri: Uri?) {
|
||||
@@ -138,7 +135,8 @@ class ReaderConfigBottomSheet :
|
||||
|
||||
interface Callback {
|
||||
|
||||
var pageSwitchDelay: Float
|
||||
@get:FloatRange(from = 0.0, to = 1.0)
|
||||
var autoScrollSpeed: Float
|
||||
|
||||
fun onReaderModeChanged(mode: ReaderMode)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
|
||||
|
||||
abstract fun switchPageTo(position: Int, smooth: Boolean)
|
||||
|
||||
open fun scrollBy(delta: Int): Boolean = false
|
||||
|
||||
abstract fun getCurrentState(): ReaderState?
|
||||
|
||||
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
|
||||
|
||||
@@ -114,6 +114,11 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||
binding.recyclerView.firstVisibleItemPosition = position
|
||||
}
|
||||
|
||||
override fun scrollBy(delta: Int): Boolean {
|
||||
binding.recyclerView.nestedScrollBy(0, delta)
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class PageScrollListener : WebtoonRecyclerView.OnPageScrollListener() {
|
||||
|
||||
override fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) {
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
android:contentDescription="@string/automatic_scroll"
|
||||
android:labelFor="@id/textView_timer"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="20"
|
||||
android:valueTo="1"
|
||||
app:labelBehavior="floating"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
Reference in New Issue
Block a user