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.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"
|
||||||
|
|||||||
Reference in New Issue
Block a user