Improve mouse interaction

This commit is contained in:
Koitharu
2023-09-14 11:20:22 +03:00
parent f4313525c2
commit d872044252
22 changed files with 307 additions and 64 deletions

View File

@@ -1,7 +1,11 @@
package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
@@ -26,9 +30,11 @@ import org.koitharu.kotatsu.reader.ui.pager.standard.NoAnimPageTransformer
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import javax.inject.Inject
import kotlin.math.absoluteValue
import kotlin.math.sign
@AndroidEntryPoint
class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() {
class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
View.OnGenericMotionListener {
@Inject
lateinit var networkState: NetworkState
@@ -47,6 +53,7 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
adapter = readerAdapter
offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@ReversedReaderFragment)
}
viewModel.pageAnimation.observe(viewLifecycleOwner) {
@@ -69,6 +76,20 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
super.onDestroyView()
}
override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
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) {
switchPageBy(-axisValue.sign.toInt())
return true
}
}
}
return false
}
override fun onCreateAdapter() = ReversedPagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,

View File

@@ -35,6 +35,7 @@ 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

@@ -1,7 +1,11 @@
package org.koitharu.kotatsu.reader.ui.pager.standard
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
@@ -24,9 +28,11 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject
import kotlin.math.absoluteValue
import kotlin.math.sign
@AndroidEntryPoint
class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() {
class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
View.OnGenericMotionListener {
@Inject
lateinit var networkState: NetworkState
@@ -39,12 +45,16 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
container: ViewGroup?,
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) {
override fun onViewBindingCreated(
binding: FragmentReaderStandardBinding,
savedInstanceState: Bundle?,
) {
super.onViewBindingCreated(binding, savedInstanceState)
with(binding.pager) {
adapter = readerAdapter
offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@PagerReaderFragment)
}
viewModel.pageAnimation.observe(viewLifecycleOwner) {
@@ -67,28 +77,43 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
super.onDestroyView()
}
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
val items = async {
requireAdapter().setItems(pages)
yield()
}
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
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) {
switchPageBy(-axisValue.sign.toInt())
return true
}
}
items.await()
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
}
} else {
items.await()
}
return false
}
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) =
coroutineScope {
val items = async {
requireAdapter().setItems(pages)
yield()
}
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
}
items.await()
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
}
} else {
items.await()
}
}
override fun onCreateAdapter() = PagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,

View File

@@ -0,0 +1,32 @@
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

@@ -7,6 +7,8 @@ import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.animation.AccelerateDecelerateInterpolator
@@ -16,6 +18,7 @@ import androidx.core.view.GestureDetectorCompat
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,
@@ -77,10 +80,26 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return super.dispatchTouchEvent(ev)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
halfWidth = measuredWidth / 2f
halfHeight = measuredHeight / 2f
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
if (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)
scaleChild(newScale, event.x, event.y)
return true
}
}
}
return super.onGenericMotionEvent(event)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
halfWidth = w / 2f
halfHeight = h / 2f
}
private fun invalidateTarget() {
@@ -161,7 +180,12 @@ class WebtoonScalingFrame @JvmOverloads constructor(
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float,
): Boolean {
if (scale <= 1f) return false
transformMatrix.postTranslate(-distanceX, -distanceY)
invalidateTarget()
@@ -181,7 +205,12 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
if (scale <= 1) return false
overScroller.fling(
@@ -200,7 +229,10 @@ class WebtoonScalingFrame @JvmOverloads constructor(
override fun run() {
if (overScroller.computeScrollOffset()) {
transformMatrix.postTranslate(overScroller.currX - transX, overScroller.currY - transY)
transformMatrix.postTranslate(
overScroller.currX - transX,
overScroller.currY - transY
)
invalidateTarget()
postOnAnimation(this)
}