Smooth auto scrolling #319

This commit is contained in:
Koitharu
2023-03-21 21:00:57 +02:00
parent b45147563a
commit bc4dd1c507
8 changed files with 112 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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