Smooth auto scrolling #319

pull/311/head
Koitharu 3 years ago
parent b45147563a
commit bc4dd1c507
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -40,7 +40,6 @@ import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage 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.config.ReaderConfigBottomSheet
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
@ -70,16 +69,16 @@ class ReaderActivity :
private val viewModel: ReaderViewModel by viewModels() private val viewModel: ReaderViewModel by viewModels()
override var pageSwitchDelay: Float override var autoScrollSpeed: Float
get() = pageSwitchTimer.delaySec get() = scrollTimer.speed
set(value) { set(value) {
pageSwitchTimer.delaySec = value scrollTimer.speed = value
} }
override val readerMode: ReaderMode? override val readerMode: ReaderMode?
get() = readerManager.currentMode get() = readerManager.currentMode
private lateinit var pageSwitchTimer: PageSwitchTimer private lateinit var scrollTimer: ScrollTimer
private lateinit var touchHelper: GridTouchHelper private lateinit var touchHelper: GridTouchHelper
private lateinit var controlDelegate: ReaderControlDelegate private lateinit var controlDelegate: ReaderControlDelegate
private var gestureInsets: Insets = Insets.NONE private var gestureInsets: Insets = Insets.NONE
@ -92,7 +91,7 @@ class ReaderActivity :
readerManager = ReaderManager(supportFragmentManager, R.id.container) readerManager = ReaderManager(supportFragmentManager, R.id.container)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this) touchHelper = GridTouchHelper(this, this)
pageSwitchTimer = PageSwitchTimer(this, this) scrollTimer = ScrollTimer(this, this)
controlDelegate = ReaderControlDelegate(settings, this, this) controlDelegate = ReaderControlDelegate(settings, this, this)
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
binding.slider.setLabelFormatter(PageLabelFormatter()) binding.slider.setLabelFormatter(PageLabelFormatter())
@ -134,7 +133,6 @@ class ReaderActivity :
override fun onUserInteraction() { override fun onUserInteraction() {
super.onUserInteraction() super.onUserInteraction()
pageSwitchTimer.onUserInteraction()
idlingDetector.onUserInteraction() idlingDetector.onUserInteraction()
} }
@ -337,6 +335,10 @@ class ReaderActivity :
readerManager.currentReader?.switchPageBy(delta) readerManager.currentReader?.switchPageBy(delta)
} }
override fun scrollBy(delta: Int): Boolean {
return readerManager.currentReader?.scrollBy(delta) ?: false
}
override fun toggleUiVisibility() { override fun toggleUiVisibility() {
setUiIsVisible(!binding.appbarTop.isVisible) setUiIsVisible(!binding.appbarTop.isVisible)
} }

@ -42,18 +42,22 @@ class ReaderControlDelegate(
listener.toggleUiVisibility() listener.toggleUiVisibility()
view.playSoundEffect(SoundEffectConstants.CLICK) view.playSoundEffect(SoundEffectConstants.CLICK)
} }
GridTouchHelper.AREA_TOP -> if (isTapSwitchEnabled) { GridTouchHelper.AREA_TOP -> if (isTapSwitchEnabled) {
listener.switchPageBy(-1) listener.switchPageBy(-1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP) view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP)
} }
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) { GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1) listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT) view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT)
} }
GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) { GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) {
listener.switchPageBy(1) listener.switchPageBy(1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN) view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN)
} }
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) { GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1) listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT) view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT)
@ -68,12 +72,14 @@ class ReaderControlDelegate(
} else { } else {
false false
} }
KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) { KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) {
listener.switchPageBy(1) listener.switchPageBy(1)
true true
} else { } else {
false false
} }
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
@ -82,10 +88,12 @@ class ReaderControlDelegate(
listener.switchPageBy(1) listener.switchPageBy(1)
true true
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1) listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
true true
} }
KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_UP,
@ -93,14 +101,17 @@ class ReaderControlDelegate(
listener.switchPageBy(-1) listener.switchPageBy(-1)
true true
} }
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1) listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
true true
} }
KeyEvent.KEYCODE_DPAD_CENTER -> { KeyEvent.KEYCODE_DPAD_CENTER -> {
listener.toggleUiVisibility() listener.toggleUiVisibility()
true true
} }
else -> false else -> false
} }
@ -128,6 +139,8 @@ class ReaderControlDelegate(
fun switchPageBy(delta: Int) fun switchPageBy(delta: Int)
fun scrollBy(delta: Int): Boolean
fun toggleUiVisibility() 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.coroutineScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.LabelFormatter
import kotlin.math.roundToLong
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -14,7 +13,9 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.ui.ReaderControlDelegate import org.koitharu.kotatsu.reader.ui.ReaderControlDelegate
import kotlin.math.roundToLong
@Deprecated("")
class PageSwitchTimer( class PageSwitchTimer(
private val listener: ReaderControlDelegate.OnInteractionListener, private val listener: ReaderControlDelegate.OnInteractionListener,
private val lifecycleOwner: LifecycleOwner, private val lifecycleOwner: LifecycleOwner,

@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.annotation.FloatRange
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels 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.reader.ui.colorfilter.ColorFilterConfigActivity
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ScreenOrientationHelper 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.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@ -32,7 +32,7 @@ class ReaderConfigBottomSheet :
ActivityResultCallback<Uri?>, ActivityResultCallback<Uri?>,
View.OnClickListener, View.OnClickListener,
MaterialButtonToggleGroup.OnButtonCheckedListener, MaterialButtonToggleGroup.OnButtonCheckedListener,
Slider.OnSliderTouchListener { Slider.OnChangeListener {
private val viewModel by activityViewModels<ReaderViewModel>() private val viewModel by activityViewModels<ReaderViewModel>()
private val savePageRequest = registerForActivityResult(PageSaveContract(), this) private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
@ -62,11 +62,10 @@ class ReaderConfigBottomSheet :
binding.buttonScreenRotate.setOnClickListener(this) binding.buttonScreenRotate.setOnClickListener(this)
binding.buttonSettings.setOnClickListener(this) binding.buttonSettings.setOnClickListener(this)
binding.buttonColorFilter.setOnClickListener(this) binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnSliderTouchListener(this) binding.sliderTimer.addOnChangeListener(this)
binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources))
findCallback()?.run { findCallback()?.run {
binding.sliderTimer.setValueRounded(pageSwitchDelay) binding.sliderTimer.value = autoScrollSpeed
} }
} }
@ -111,10 +110,8 @@ class ReaderConfigBottomSheet :
mode = newMode mode = newMode
} }
override fun onStartTrackingTouch(slider: Slider) = Unit override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
findCallback()?.autoScrollSpeed = value
override fun onStopTrackingTouch(slider: Slider) {
findCallback()?.pageSwitchDelay = slider.value
} }
override fun onActivityResult(uri: Uri?) { override fun onActivityResult(uri: Uri?) {
@ -138,7 +135,8 @@ class ReaderConfigBottomSheet :
interface Callback { interface Callback {
var pageSwitchDelay: Float @get:FloatRange(from = 0.0, to = 1.0)
var autoScrollSpeed: Float
fun onReaderModeChanged(mode: ReaderMode) fun onReaderModeChanged(mode: ReaderMode)
} }

@ -51,6 +51,8 @@ abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
abstract fun switchPageTo(position: Int, smooth: Boolean) abstract fun switchPageTo(position: Int, smooth: Boolean)
open fun scrollBy(delta: Int): Boolean = false
abstract fun getCurrentState(): ReaderState? abstract fun getCurrentState(): ReaderState?
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)

@ -114,6 +114,11 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
binding.recyclerView.firstVisibleItemPosition = position binding.recyclerView.firstVisibleItemPosition = position
} }
override fun scrollBy(delta: Int): Boolean {
binding.recyclerView.nestedScrollBy(0, delta)
return true
}
private inner class PageScrollListener : WebtoonRecyclerView.OnPageScrollListener() { private inner class PageScrollListener : WebtoonRecyclerView.OnPageScrollListener() {
override fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) { override fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) {

@ -139,7 +139,7 @@
android:contentDescription="@string/automatic_scroll" android:contentDescription="@string/automatic_scroll"
android:labelFor="@id/textView_timer" android:labelFor="@id/textView_timer"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="20" android:valueTo="1"
app:labelBehavior="floating" app:labelBehavior="floating"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

Loading…
Cancel
Save