diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 5b4d3a073..dcc8e690d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.res.Configuration import android.net.Uri import android.os.Bundle -import android.os.SystemClock import android.view.Gravity import android.view.InputDevice import android.view.KeyEvent @@ -93,11 +92,11 @@ import io.legado.app.ui.widget.PopupAction import io.legado.app.ui.widget.dialog.PhotoDialog import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.ACache +import io.legado.app.utils.Debounce import io.legado.app.utils.LogUtils import io.legado.app.utils.StartActivityContract import io.legado.app.utils.SyncedRenderer import io.legado.app.utils.applyOpenTint -import io.legado.app.utils.buildMainHandler import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefString import io.legado.app.utils.hexString @@ -118,7 +117,6 @@ import io.legado.app.utils.toastOnUi import io.legado.app.utils.visible import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job -import kotlinx.coroutines.Runnable import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -217,15 +215,13 @@ class ReadBookActivity : BaseReadBookActivity(), override val pageFactory: TextPageFactory get() = binding.readView.pageFactory override val headerHeight: Int get() = binding.readView.curPage.headerHeight private val menuLayoutIsVisible get() = bottomDialog > 0 || binding.readMenu.isVisible - private val nextPageRunnable by lazy { Runnable { keyPage(PageDirection.NEXT) } } - private val prevPageRunnable by lazy { Runnable { keyPage(PageDirection.PREV) } } + private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } } + private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } } private var bookChanged = false private var pageChanged = false private var reloadContent = false private val autoPageRenderer by lazy { SyncedRenderer { doAutoPage(it) } } private var autoPageScrollOffset = 0.0 - private val handler by lazy { buildMainHandler() } - private var lastKeyPageTime = 0L //恢复跳转前进度对话框的交互结果 private var confirmRestoreProcess: Boolean? = null @@ -624,38 +620,39 @@ class ReadBookActivity : BaseReadBookActivity(), if (menuLayoutIsVisible) { return super.onKeyDown(keyCode, event) } + val longPress = event.repeatCount > 0 when { isPrevKey(keyCode) -> { - handleKeyPage(PageDirection.PREV) + handleKeyPage(PageDirection.PREV, longPress) return true } isNextKey(keyCode) -> { - handleKeyPage(PageDirection.NEXT) + handleKeyPage(PageDirection.NEXT, longPress) return true } } when (keyCode) { - KeyEvent.KEYCODE_VOLUME_UP -> if (volumeKeyPage(PageDirection.PREV)) { + KeyEvent.KEYCODE_VOLUME_UP -> if (volumeKeyPage(PageDirection.PREV, longPress)) { return true } - KeyEvent.KEYCODE_VOLUME_DOWN -> if (volumeKeyPage(PageDirection.NEXT)) { + KeyEvent.KEYCODE_VOLUME_DOWN -> if (volumeKeyPage(PageDirection.NEXT, longPress)) { return true } KeyEvent.KEYCODE_PAGE_UP -> { - handleKeyPage(PageDirection.PREV) + handleKeyPage(PageDirection.PREV, longPress) return true } KeyEvent.KEYCODE_PAGE_DOWN -> { - handleKeyPage(PageDirection.NEXT) + handleKeyPage(PageDirection.NEXT, longPress) return true } KeyEvent.KEYCODE_SPACE -> { - handleKeyPage(PageDirection.NEXT) + handleKeyPage(PageDirection.NEXT, longPress) return true } } @@ -669,7 +666,7 @@ class ReadBookActivity : BaseReadBookActivity(), override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (volumeKeyPage(PageDirection.NONE)) { + if (volumeKeyPage(PageDirection.NONE, false)) { return true } } @@ -852,49 +849,52 @@ class ReadBookActivity : BaseReadBookActivity(), if (menuLayoutIsVisible || !AppConfig.mouseWheelPage) { return } - keyPageDebounce(direction, 200L, true) + keyPageDebounce(direction, mouseWheel = true, longPress = false) } /** * 音量键翻页 */ - private fun volumeKeyPage(direction: PageDirection): Boolean { + private fun volumeKeyPage(direction: PageDirection, longPress: Boolean): Boolean { if (!AppConfig.volumeKeyPage) { return false } if (!AppConfig.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) { return false } - handleKeyPage(direction) + handleKeyPage(direction, longPress) return true } - private fun handleKeyPage(direction: PageDirection) { + private fun handleKeyPage(direction: PageDirection, longPress: Boolean) { if (AppConfig.keyPageOnLongPress || direction == PageDirection.NONE) { keyPage(direction) } else { - keyPageDebounce(direction) + keyPageDebounce(direction, longPress = longPress) } } private fun keyPageDebounce( direction: PageDirection, - delay: Long = 100L, - mouseWheel: Boolean = false + mouseWheel: Boolean = false, + longPress: Boolean ) { - handler.removeCallbacks(nextPageRunnable) - handler.removeCallbacks(prevPageRunnable) - if (!mouseWheel) { - val keyPageTime = SystemClock.uptimeMillis() - val elapsed = keyPageTime - lastKeyPageTime - lastKeyPageTime = keyPageTime - if (elapsed < 100) { - return - } + if (longPress) { + return + } + nextPageDebounce.apply { + wait = if (mouseWheel) 200L else 600L + leading = !mouseWheel + trailing = mouseWheel + } + prevPageDebounce.apply { + wait = if (mouseWheel) 200L else 600L + leading = !mouseWheel + trailing = mouseWheel } when (direction) { - PageDirection.NEXT -> handler.postDelayed(nextPageRunnable, delay) - PageDirection.PREV -> handler.postDelayed(prevPageRunnable, delay) + PageDirection.NEXT -> nextPageDebounce.invoke() + PageDirection.PREV -> prevPageDebounce.invoke() else -> {} } } diff --git a/app/src/main/java/io/legado/app/utils/Debounce.kt b/app/src/main/java/io/legado/app/utils/Debounce.kt new file mode 100644 index 000000000..0d5527746 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/Debounce.kt @@ -0,0 +1,121 @@ +package io.legado.app.utils + +import android.os.SystemClock +import kotlin.math.max + +@Suppress("MemberVisibilityCanBePrivate") +open class Debounce( + var wait: Long = 0L, + var maxWait: Long = -1L, + var leading: Boolean = false, + var trailing: Boolean = true, + private val func: () -> T +) { + companion object { + private val handler by lazy { buildMainHandler() } + } + + private var lastCallTime = -1L + private var lastInvokeTime = 0L + private val maxing get() = maxWait != -1L + private var result: T? = null + private var hasTimer = false + private val timerExpiredRunnable = Runnable { + timerExpired() + } + + init { + maxWait = if (maxing) max(maxWait, wait) else maxWait + } + + private fun invokeFunc(time: Long): T { + lastInvokeTime = time + return func.invoke().also { result = it } + } + + private fun startTimer(wait: Long) { + hasTimer = true + handler.postDelayed(timerExpiredRunnable, wait) + } + + private fun cancelTimer() { + handler.removeCallbacks(timerExpiredRunnable) + } + + private fun leadingEdge(time: Long): T? { + lastInvokeTime = time + startTimer(wait) + return if (leading) invokeFunc(time) else result + } + + private fun trailingEdge(time: Long): T? { + hasTimer = false + return if (trailing) invokeFunc(time) else result + } + + private fun remainingWait(time: Long): Long { + val timeSinceLastCall = time - lastCallTime + val timeSinceLastInvoke = time - lastInvokeTime + val timeWaiting = wait - timeSinceLastCall + + return if (maxing) timeWaiting.coerceAtMost(maxWait - timeSinceLastInvoke) else timeWaiting + } + + private fun shouldInvoke(time: Long): Boolean { + val timeSinceLastCall = time - lastCallTime + val timeSinceLastInvoke = time - lastInvokeTime + + return lastCallTime == -1L + || timeSinceLastCall >= wait + || timeSinceLastCall < 0 + || maxing && timeSinceLastInvoke >= maxWait + } + + private fun timerExpired() { + val time = SystemClock.uptimeMillis() + if (shouldInvoke(time)) { + trailingEdge(time) + } else { + startTimer(remainingWait(time)) + } + } + + fun cancel() { + if (hasTimer) { + cancelTimer() + } + lastInvokeTime = 0 + lastCallTime = -1L + hasTimer = false + } + + fun flush(): T? { + return if (hasTimer) trailingEdge(SystemClock.uptimeMillis()) else result + } + + fun pending(): Boolean = hasTimer + + operator fun invoke(): T? { + val time = SystemClock.uptimeMillis() + val isInvoking = shouldInvoke(time) + + lastCallTime = time + + if (isInvoking) { + if (!hasTimer) { + return leadingEdge(lastCallTime) + } + if (maxing) { + startTimer(wait) + return invokeFunc(lastCallTime) + } + } + + if (!hasTimer) { + startTimer(wait) + } + + return result + } + +} diff --git a/app/src/main/java/io/legado/app/utils/Throttle.kt b/app/src/main/java/io/legado/app/utils/Throttle.kt new file mode 100644 index 000000000..079c9d7d8 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/Throttle.kt @@ -0,0 +1,9 @@ +package io.legado.app.utils + +@Suppress("unused") +class Throttle( + wait: Long = 0L, + leading: Boolean = true, + trailing: Boolean = true, + func: () -> T +) : Debounce(wait, wait, leading, trailing, func)