Improve keyboard control in reader
This commit is contained in:
@@ -132,7 +132,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.4.0'
|
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.4.0'
|
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:9b1d20be67'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:169806d928'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class ReaderActivity :
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
touchHelper = GridTouchHelper(this, this)
|
touchHelper = GridTouchHelper(this, this)
|
||||||
scrollTimer = scrollTimerFactory.create(this, this)
|
scrollTimer = scrollTimerFactory.create(this, this)
|
||||||
controlDelegate = ReaderControlDelegate(settings, this, this)
|
controlDelegate = ReaderControlDelegate(resources, settings, this, this)
|
||||||
viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
|
viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
|
||||||
viewBinding.slider.setLabelFormatter(PageLabelFormatter())
|
viewBinding.slider.setLabelFormatter(PageLabelFormatter())
|
||||||
ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider)
|
ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider)
|
||||||
@@ -347,8 +347,8 @@ class ReaderActivity :
|
|||||||
readerManager.currentReader?.switchPageBy(delta)
|
readerManager.currentReader?.switchPageBy(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scrollBy(delta: Int): Boolean {
|
override fun scrollBy(delta: Int, smooth: Boolean): Boolean {
|
||||||
return readerManager.currentReader?.scrollBy(delta) ?: false
|
return readerManager.currentReader?.scrollBy(delta, smooth) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toggleUiVisibility() {
|
override fun toggleUiVisibility() {
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.content.res.Resources
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.SoundEffectConstants
|
import android.view.SoundEffectConstants
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.core.util.GridTouchHelper
|
import org.koitharu.kotatsu.core.util.GridTouchHelper
|
||||||
|
|
||||||
class ReaderControlDelegate(
|
class ReaderControlDelegate(
|
||||||
|
resources: Resources,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val listener: OnInteractionListener,
|
private val listener: OnInteractionListener,
|
||||||
owner: LifecycleOwner,
|
owner: LifecycleOwner,
|
||||||
@@ -19,6 +22,7 @@ class ReaderControlDelegate(
|
|||||||
private var isTapSwitchEnabled: Boolean = true
|
private var isTapSwitchEnabled: Boolean = true
|
||||||
private var isVolumeKeysSwitchEnabled: Boolean = false
|
private var isVolumeKeysSwitchEnabled: Boolean = false
|
||||||
private var isReaderTapsAdaptive: Boolean = true
|
private var isReaderTapsAdaptive: Boolean = true
|
||||||
|
private var minScrollDelta = resources.getDimensionPixelSize(R.dimen.reader_scroll_delta_min)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
owner.lifecycle.addObserver(this)
|
owner.lifecycle.addObserver(this)
|
||||||
@@ -82,8 +86,6 @@ class ReaderControlDelegate(
|
|||||||
|
|
||||||
KeyEvent.KEYCODE_SPACE,
|
KeyEvent.KEYCODE_SPACE,
|
||||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||||
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
|
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
|
||||||
-> {
|
-> {
|
||||||
listener.switchPageBy(1)
|
listener.switchPageBy(1)
|
||||||
true
|
true
|
||||||
@@ -95,8 +97,6 @@ class ReaderControlDelegate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_PAGE_UP,
|
KeyEvent.KEYCODE_PAGE_UP,
|
||||||
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
|
|
||||||
KeyEvent.KEYCODE_DPAD_UP,
|
|
||||||
-> {
|
-> {
|
||||||
listener.switchPageBy(-1)
|
listener.switchPageBy(-1)
|
||||||
true
|
true
|
||||||
@@ -112,6 +112,22 @@ class ReaderControlDelegate(
|
|||||||
true
|
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
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +155,7 @@ class ReaderControlDelegate(
|
|||||||
|
|
||||||
fun switchPageBy(delta: Int)
|
fun switchPageBy(delta: Int)
|
||||||
|
|
||||||
fun scrollBy(delta: Int): Boolean
|
fun scrollBy(delta: Int, smooth: Boolean): Boolean
|
||||||
|
|
||||||
fun toggleUiVisibility()
|
fun toggleUiVisibility()
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class ScrollTimer @AssistedInject constructor(
|
|||||||
if (!listener.isReaderResumed()) {
|
if (!listener.isReaderResumed()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!listener.scrollBy(1)) {
|
if (!listener.scrollBy(1, false)) {
|
||||||
accumulator += delayMs
|
accumulator += delayMs
|
||||||
}
|
}
|
||||||
if (accumulator >= pageSwitchDelay) {
|
if (accumulator >= pageSwitchDelay) {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ abstract class BaseReaderFragment<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
|
open fun scrollBy(delta: Int, smooth: Boolean): Boolean = false
|
||||||
|
|
||||||
abstract fun getCurrentState(): ReaderState?
|
abstract fun getCurrentState(): ReaderState?
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.InputDevice
|
import android.view.InputDevice
|
||||||
import android.view.KeyEvent
|
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.BaseReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
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.NoAnimPageTransformer
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerEventSupplier
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
@@ -54,6 +56,10 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
|
|||||||
offscreenPageLimit = 2
|
offscreenPageLimit = 2
|
||||||
doOnPageChanged(::notifyPageChanged)
|
doOnPageChanged(::notifyPageChanged)
|
||||||
setOnGenericMotionListener(this@ReversedReaderFragment)
|
setOnGenericMotionListener(this@ReversedReaderFragment)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
recyclerView?.defaultFocusHighlightEnabled = false
|
||||||
|
}
|
||||||
|
PagerEventSupplier(this).attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.pageAnimation.observe(viewLifecycleOwner) {
|
viewModel.pageAnimation.observe(viewLifecycleOwner) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ open class PageHolder(
|
|||||||
binding.ssiv.bindToLifecycle(owner)
|
binding.ssiv.bindToLifecycle(owner)
|
||||||
binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice()
|
binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice()
|
||||||
binding.ssiv.addOnImageEventListener(delegate)
|
binding.ssiv.addOnImageEventListener(delegate)
|
||||||
binding.ssiv.setOnGenericMotionListener(SsivZoomListener())
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.InputDevice
|
import android.view.InputDevice
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
@@ -55,6 +56,10 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
|
|||||||
offscreenPageLimit = 2
|
offscreenPageLimit = 2
|
||||||
doOnPageChanged(::notifyPageChanged)
|
doOnPageChanged(::notifyPageChanged)
|
||||||
setOnGenericMotionListener(this@PagerReaderFragment)
|
setOnGenericMotionListener(this@PagerReaderFragment)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
recyclerView?.defaultFocusHighlightEnabled = false
|
||||||
|
}
|
||||||
|
PagerEventSupplier(this).attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.pageAnimation.observe(viewLifecycleOwner) {
|
viewModel.pageAnimation.observe(viewLifecycleOwner) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,8 @@ class WebtoonFrameLayout @JvmOverloads constructor(
|
|||||||
@AttrRes defStyleAttr: Int = 0,
|
@AttrRes defStyleAttr: Int = 0,
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
val target by lazy(LazyThreadSafetyMode.NONE) {
|
val target: WebtoonImageView by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
findViewById<WebtoonImageView>(R.id.ssiv)
|
findViewById(R.id.ssiv)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dispatchVerticalScroll(dy: Int): Int {
|
fun dispatchVerticalScroll(dy: Int): Int {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -31,7 +31,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var pageLoader: PageLoader
|
lateinit var pageLoader: PageLoader
|
||||||
|
|
||||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
private val scrollInterpolator = DecelerateInterpolator()
|
||||||
|
|
||||||
override fun onCreateViewBinding(
|
override fun onCreateViewBinding(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -122,8 +122,12 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
requireViewBinding().recyclerView.firstVisibleItemPosition = position
|
requireViewBinding().recyclerView.firstVisibleItemPosition = position
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scrollBy(delta: Int): Boolean {
|
override fun scrollBy(delta: Int, smooth: Boolean): Boolean {
|
||||||
requireViewBinding().recyclerView.nestedScrollBy(0, delta)
|
if (smooth && isAnimationEnabled()) {
|
||||||
|
requireViewBinding().recyclerView.smoothScrollBy(0, delta, scrollInterpolator)
|
||||||
|
} else {
|
||||||
|
requireViewBinding().recyclerView.nestedScrollBy(0, delta)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ import android.view.InputDevice
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.ScaleGestureDetector
|
import android.view.ScaleGestureDetector
|
||||||
|
import android.view.ViewConfiguration
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.OverScroller
|
import android.widget.OverScroller
|
||||||
import androidx.core.view.GestureDetectorCompat
|
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 MAX_SCALE = 2.5f
|
||||||
private const val MIN_SCALE = 0.5f
|
private const val MIN_SCALE = 0.5f
|
||||||
private const val WHEEL_SCALE_FACTOR = 0.2f
|
|
||||||
|
|
||||||
class WebtoonScalingFrame @JvmOverloads constructor(
|
class WebtoonScalingFrame @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -43,6 +46,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
|||||||
private var halfHeight = 0f
|
private var halfHeight = 0f
|
||||||
private val translateBounds = RectF()
|
private val translateBounds = RectF()
|
||||||
private val targetHitRect = Rect()
|
private val targetHitRect = Rect()
|
||||||
|
private var animator: ValueAnimator? = null
|
||||||
|
|
||||||
var isZoomEnable = true
|
var isZoomEnable = true
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -81,13 +85,15 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
|
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) {
|
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
|
||||||
val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0
|
val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0
|
||||||
if (withCtrl) {
|
if (withCtrl) {
|
||||||
val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
|
val axisValue =
|
||||||
val newScale =
|
event.getAxisValue(MotionEvent.AXIS_VSCROLL) * ViewConfigurationCompat.getScaledVerticalScrollFactor(
|
||||||
(scale + axisValue * WHEEL_SCALE_FACTOR).coerceIn(MIN_SCALE, MAX_SCALE)
|
ViewConfiguration.get(context), context,
|
||||||
|
)
|
||||||
|
val newScale = (scale + axisValue).coerceIn(MIN_SCALE, MAX_SCALE)
|
||||||
scaleChild(newScale, event.x, event.y)
|
scaleChild(newScale, event.x, event.y)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -96,6 +102,49 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
|||||||
return super.onGenericMotionEvent(event)
|
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) {
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
super.onSizeChanged(w, h, oldw, oldh)
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
halfWidth = w / 2f
|
halfWidth = w / 2f
|
||||||
@@ -173,10 +222,24 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
|||||||
return true
|
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
|
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 {
|
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
|
||||||
|
|
||||||
@@ -231,7 +294,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
|||||||
if (overScroller.computeScrollOffset()) {
|
if (overScroller.computeScrollOffset()) {
|
||||||
transformMatrix.postTranslate(
|
transformMatrix.postTranslate(
|
||||||
overScroller.currX - transX,
|
overScroller.currX - transX,
|
||||||
overScroller.currY - transY
|
overScroller.currY - transY,
|
||||||
)
|
)
|
||||||
invalidateTarget()
|
invalidateTarget()
|
||||||
postOnAnimation(this)
|
postOnAnimation(this)
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/pager"
|
android:id="@+id/pager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent"
|
||||||
|
android:defaultFocusHighlightEnabled="false" />
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/frame"
|
android:id="@+id/frame"
|
||||||
android:layout_width="match_parent"
|
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
|
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonRecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:defaultFocusHighlightEnabled="false"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
|
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
|
||||||
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>
|
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
android:id="@+id/ssiv"
|
android:id="@+id/ssiv"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:defaultFocusHighlightEnabled="false"
|
||||||
|
android:focusable="true"
|
||||||
app:restoreStrategy="deferred" />
|
app:restoreStrategy="deferred" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
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
|
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonImageView
|
||||||
android:id="@+id/ssiv"
|
android:id="@+id/ssiv"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:defaultFocusHighlightEnabled="false"
|
||||||
android:minHeight="1dp"
|
android:minHeight="1dp"
|
||||||
app:panEnabled="false"
|
app:panEnabled="false"
|
||||||
app:quickScaleEnabled="false"
|
app:quickScaleEnabled="false"
|
||||||
|
|||||||
@@ -80,4 +80,6 @@
|
|||||||
<dimen name="fastscroll_scrollbar_padding_end">6dp</dimen>
|
<dimen name="fastscroll_scrollbar_padding_end">6dp</dimen>
|
||||||
|
|
||||||
<dimen name="m3_side_sheet_width">400dp</dimen>
|
<dimen name="m3_side_sheet_width">400dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="reader_scroll_delta_min">200dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user