Improve keyboard control in reader

This commit is contained in:
Koitharu
2023-09-18 12:12:18 +03:00
parent 835c49ae79
commit 8398c01929
18 changed files with 155 additions and 60 deletions

View File

@@ -107,7 +107,7 @@ class ReaderActivity :
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
scrollTimer = scrollTimerFactory.create(this, this)
controlDelegate = ReaderControlDelegate(settings, this, this)
controlDelegate = ReaderControlDelegate(resources, settings, this, this)
viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
viewBinding.slider.setLabelFormatter(PageLabelFormatter())
ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider)
@@ -347,8 +347,8 @@ class ReaderActivity :
readerManager.currentReader?.switchPageBy(delta)
}
override fun scrollBy(delta: Int): Boolean {
return readerManager.currentReader?.scrollBy(delta) ?: false
override fun scrollBy(delta: Int, smooth: Boolean): Boolean {
return readerManager.currentReader?.scrollBy(delta, smooth) ?: false
}
override fun toggleUiVisibility() {

View File

@@ -1,16 +1,19 @@
package org.koitharu.kotatsu.reader.ui
import android.content.SharedPreferences
import android.content.res.Resources
import android.view.KeyEvent
import android.view.SoundEffectConstants
import android.view.View
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.GridTouchHelper
class ReaderControlDelegate(
resources: Resources,
private val settings: AppSettings,
private val listener: OnInteractionListener,
owner: LifecycleOwner,
@@ -19,6 +22,7 @@ class ReaderControlDelegate(
private var isTapSwitchEnabled: Boolean = true
private var isVolumeKeysSwitchEnabled: Boolean = false
private var isReaderTapsAdaptive: Boolean = true
private var minScrollDelta = resources.getDimensionPixelSize(R.dimen.reader_scroll_delta_min)
init {
owner.lifecycle.addObserver(this)
@@ -82,8 +86,6 @@ class ReaderControlDelegate(
KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN,
-> {
listener.switchPageBy(1)
true
@@ -95,8 +97,6 @@ class ReaderControlDelegate(
}
KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP,
-> {
listener.switchPageBy(-1)
true
@@ -112,6 +112,22 @@ class ReaderControlDelegate(
true
}
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP -> {
if (!listener.scrollBy(-minScrollDelta, smooth = true)) {
listener.switchPageBy(-1)
}
true
}
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (!listener.scrollBy(minScrollDelta, smooth = true)) {
listener.switchPageBy(1)
}
true
}
else -> false
}
@@ -139,7 +155,7 @@ class ReaderControlDelegate(
fun switchPageBy(delta: Int)
fun scrollBy(delta: Int): Boolean
fun scrollBy(delta: Int, smooth: Boolean): Boolean
fun toggleUiVisibility()

View File

@@ -96,7 +96,7 @@ class ScrollTimer @AssistedInject constructor(
if (!listener.isReaderResumed()) {
continue
}
if (!listener.scrollBy(1)) {
if (!listener.scrollBy(1, false)) {
accumulator += delayMs
}
if (accumulator >= pageSwitchDelay) {

View File

@@ -66,7 +66,7 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
abstract fun switchPageTo(position: Int, smooth: Boolean)
open fun scrollBy(delta: Int): Boolean = false
open fun scrollBy(delta: Int, smooth: Boolean): Boolean = false
abstract fun getCurrentState(): ReaderState?

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.os.Build
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
@@ -27,6 +28,7 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.standard.NoAnimPageTransformer
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerEventSupplier
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import javax.inject.Inject
import kotlin.math.absoluteValue
@@ -54,6 +56,10 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@ReversedReaderFragment)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
recyclerView?.defaultFocusHighlightEnabled = false
}
PagerEventSupplier(this).attach()
}
viewModel.pageAnimation.observe(viewLifecycleOwner) {

View File

@@ -35,7 +35,6 @@ open class PageHolder(
binding.ssiv.bindToLifecycle(owner)
binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice()
binding.ssiv.addOnImageEventListener(delegate)
binding.ssiv.setOnGenericMotionListener(SsivZoomListener())
@Suppress("LeakingThis")
bindingInfo.buttonRetry.setOnClickListener(this)
@Suppress("LeakingThis")

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.viewpager2.widget.ViewPager2
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.util.ext.recyclerView
class PagerEventSupplier(private val pager: ViewPager2) : View.OnKeyListener {
fun attach() {
pager.recyclerView?.setOnKeyListener(this)
}
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
val rootView = pager.recyclerView?.findViewHolderForAdapterPosition(pager.currentItem)?.itemView as? ViewGroup
?: return false
return rootView.children.firstNotNullOfOrNull { x ->
x as? SubsamplingScaleImageView
}?.dispatchKeyEvent(event) == true
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager.standard
import android.os.Build
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
@@ -55,6 +56,10 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@PagerReaderFragment)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
recyclerView?.defaultFocusHighlightEnabled = false
}
PagerEventSupplier(this).attach()
}
viewModel.pageAnimation.observe(viewLifecycleOwner) {

View File

@@ -1,32 +0,0 @@
package org.koitharu.kotatsu.reader.ui.pager.standard
import android.graphics.PointF
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.View.OnGenericMotionListener
import android.view.animation.DecelerateInterpolator
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
class SsivZoomListener : OnGenericMotionListener {
override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
val ssiv = v as? SubsamplingScaleImageView ?: return false
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0
if (withCtrl || ssiv.scale > ssiv.minScale) {
val center = PointF(event.x, event.y)
val scale = ssiv.scale + axisValue * 1.6f
(ssiv.animateScaleAndCenter(scale, center) ?: return false)
.withInterpolator(DecelerateInterpolator())
.start()
return true
}
}
}
return false
}
}

View File

@@ -12,8 +12,8 @@ class WebtoonFrameLayout @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) {
val target by lazy(LazyThreadSafetyMode.NONE) {
findViewById<WebtoonImageView>(R.id.ssiv)
val target: WebtoonImageView by lazy(LazyThreadSafetyMode.NONE) {
findViewById(R.id.ssiv)
}
fun dispatchVerticalScroll(dy: Int): Int {

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
@@ -31,7 +31,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
@Inject
lateinit var pageLoader: PageLoader
private val scrollInterpolator = AccelerateDecelerateInterpolator()
private val scrollInterpolator = DecelerateInterpolator()
override fun onCreateViewBinding(
inflater: LayoutInflater,
@@ -122,8 +122,12 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
requireViewBinding().recyclerView.firstVisibleItemPosition = position
}
override fun scrollBy(delta: Int): Boolean {
requireViewBinding().recyclerView.nestedScrollBy(0, delta)
override fun scrollBy(delta: Int, smooth: Boolean): Boolean {
if (smooth && isAnimationEnabled()) {
requireViewBinding().recyclerView.smoothScrollBy(0, delta, scrollInterpolator)
} else {
requireViewBinding().recyclerView.nestedScrollBy(0, delta)
}
return true
}

View File

@@ -11,14 +11,17 @@ import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.ViewConfiguration
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout
import android.widget.OverScroller
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewConfigurationCompat
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
private const val MAX_SCALE = 2.5f
private const val MIN_SCALE = 0.5f
private const val WHEEL_SCALE_FACTOR = 0.2f
class WebtoonScalingFrame @JvmOverloads constructor(
context: Context,
@@ -43,6 +46,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
private var halfHeight = 0f
private val translateBounds = RectF()
private val targetHitRect = Rect()
private var animator: ValueAnimator? = null
var isZoomEnable = true
set(value) {
@@ -81,13 +85,15 @@ class WebtoonScalingFrame @JvmOverloads constructor(
}
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
if (isZoomEnable && event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0
if (withCtrl) {
val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
val newScale =
(scale + axisValue * WHEEL_SCALE_FACTOR).coerceIn(MIN_SCALE, MAX_SCALE)
val axisValue =
event.getAxisValue(MotionEvent.AXIS_VSCROLL) * ViewConfigurationCompat.getScaledVerticalScrollFactor(
ViewConfiguration.get(context), context,
)
val newScale = (scale + axisValue).coerceIn(MIN_SCALE, MAX_SCALE)
scaleChild(newScale, event.x, event.y)
return true
}
@@ -96,6 +102,49 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return super.onGenericMotionEvent(event)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (!isZoomEnable) {
return super.onKeyDown(keyCode, event)
}
return when (keyCode) {
KeyEvent.KEYCODE_ZOOM_IN,
KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_PLUS -> {
smoothScaleTo(scale * 1.1f)
true
}
KeyEvent.KEYCODE_ZOOM_OUT,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_MINUS -> {
smoothScaleTo(scale * 0.9f)
true
}
KeyEvent.KEYCODE_ESCAPE -> {
smoothScaleTo(1f)
true
}
else -> super.onKeyDown(keyCode, event)
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return if (isZoomEnable) {
keyCode == KeyEvent.KEYCODE_NUMPAD_ADD
|| keyCode == KeyEvent.KEYCODE_PLUS
|| keyCode == KeyEvent.KEYCODE_NUMPAD_SUBTRACT
|| keyCode == KeyEvent.KEYCODE_MINUS
|| keyCode == KeyEvent.KEYCODE_ZOOM_IN
|| keyCode == KeyEvent.KEYCODE_ZOOM_OUT
|| keyCode == KeyEvent.KEYCODE_ESCAPE
|| super.onKeyUp(keyCode, event)
} else {
super.onKeyUp(keyCode, event)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
halfWidth = w / 2f
@@ -173,10 +222,24 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return true
}
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
animator?.cancel()
animator = null
return true
}
override fun onScaleEnd(p0: ScaleGestureDetector) = Unit
private fun smoothScaleTo(target: Float) {
val newScale = target.coerceIn(MIN_SCALE, MAX_SCALE)
animator?.cancel()
animator = ValueAnimator.ofFloat(scale, newScale).apply {
setDuration(context.getAnimationDuration(android.R.integer.config_shortAnimTime))
interpolator = DecelerateInterpolator()
addUpdateListener { scaleChild(it.animatedValue as Float, halfWidth, halfHeight) }
start()
}
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
@@ -231,7 +294,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
if (overScroller.computeScrollOffset()) {
transformMatrix.postTranslate(
overScroller.currX - transX,
overScroller.currY - transY
overScroller.currY - transY,
)
invalidateTarget()
postOnAnimation(this)

View File

@@ -3,4 +3,5 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false" />

View File

@@ -4,12 +4,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/frame"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:focusable="true"
android:defaultFocusHighlightEnabled="false">
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false"
android:orientation="vertical"
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>

View File

@@ -10,6 +10,8 @@
android:id="@+id/ssiv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
app:restoreStrategy="deferred" />
<TextView

View File

@@ -3,12 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:defaultFocusHighlightEnabled="false">
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonImageView
android:id="@+id/ssiv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:defaultFocusHighlightEnabled="false"
android:minHeight="1dp"
app:panEnabled="false"
app:quickScaleEnabled="false"

View File

@@ -80,4 +80,6 @@
<dimen name="fastscroll_scrollbar_padding_end">6dp</dimen>
<dimen name="m3_side_sheet_width">400dp</dimen>
<dimen name="reader_scroll_delta_min">200dp</dimen>
</resources>