diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index c9ae128c3..24d09eb3c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt index 3da6ed8d1..1e87ce21c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt @@ -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() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt new file mode 100644 index 000000000..894b33921 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt @@ -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 + } + } + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt index 2bc82623c..ac4134a25 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/PageSwitchTimer.kt @@ -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, diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt index 51b33d0b3..10bf4f6be 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt @@ -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, View.OnClickListener, MaterialButtonToggleGroup.OnButtonCheckedListener, - Slider.OnSliderTouchListener { + Slider.OnChangeListener { private val viewModel by activityViewModels() 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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt index 6bd234afd..eee0769bd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt @@ -51,6 +51,8 @@ abstract class BaseReader : BaseFragment() { abstract fun switchPageTo(position: Int, smooth: Boolean) + open fun scrollBy(delta: Int): Boolean = false + abstract fun getCurrentState(): ReaderState? protected abstract fun onPagesChanged(pages: List, pendingState: ReaderState?) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt index ff1e79e6e..cf61f2c9f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt @@ -114,6 +114,11 @@ class WebtoonReaderFragment : BaseReader() { 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) { diff --git a/app/src/main/res/layout/sheet_reader_config.xml b/app/src/main/res/layout/sheet_reader_config.xml index d99bf50ea..4521d89b9 100644 --- a/app/src/main/res/layout/sheet_reader_config.xml +++ b/app/src/main/res/layout/sheet_reader_config.xml @@ -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"