From ce4e1b39d2be59411ed18efe5e03a30f21e6061d Mon Sep 17 00:00:00 2001
From: HapeLee <1321903405@qq.com>
Date: Sun, 16 Nov 2025 17:48:26 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B8=90=E5=8F=98=E5=8A=A8=E7=94=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../java/io/legado/app/constant/PageAnim.kt | 3 +-
.../ui/book/read/config/ReadStyleDialog.kt | 3 +-
.../legado/app/ui/book/read/page/ReadView.kt | 5 +
.../read/page/delegate/FadePageDelegate.kt | 216 ++++++++++++++++++
.../book/read/page/delegate/PageDelegate.kt | 2 +-
.../ui/replace/edit/ReplaceEditActivity.kt | 1 -
.../res/layout/dialog_read_book_style.xml | 7 +
app/src/main/res/values-zh/strings.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
9 files changed, 235 insertions(+), 4 deletions(-)
create mode 100644 app/src/main/java/io/legado/app/ui/book/read/page/delegate/FadePageDelegate.kt
diff --git a/app/src/main/java/io/legado/app/constant/PageAnim.kt b/app/src/main/java/io/legado/app/constant/PageAnim.kt
index ec505b455..68cec4376 100644
--- a/app/src/main/java/io/legado/app/constant/PageAnim.kt
+++ b/app/src/main/java/io/legado/app/constant/PageAnim.kt
@@ -13,7 +13,8 @@ object PageAnim {
const val scrollPageAnim = 3
- const val noAnim = 4
+ const val fadePageAnim = 4
+ const val noAnim = 5
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
index 947722522..16551635c 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
@@ -133,7 +133,8 @@ class ReadStyleDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_book_
R.id.rb_anim1 -> 1 // 滑动动画
R.id.rb_simulation_anim -> 2 // 仿真翻页
R.id.rb_scroll_anim -> 3 // 滚动动画
- R.id.rb_no_anim -> 4 // 无动画
+ R.id.rb_fade_anim -> 4
+ R.id.rb_no_anim -> 5 // 无动画
else -> 0
}
callBack?.upPageAnim()
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
index f3ccb1845..e6f204640 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
@@ -21,6 +21,7 @@ import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.book.read.ContentEditDialog
import io.legado.app.ui.book.read.page.api.DataSource
import io.legado.app.ui.book.read.page.delegate.CoverPageDelegate
+import io.legado.app.ui.book.read.page.delegate.FadePageDelegate
import io.legado.app.ui.book.read.page.delegate.HorizontalPageDelegate
import io.legado.app.ui.book.read.page.delegate.NoAnimPageDelegate
import io.legado.app.ui.book.read.page.delegate.PageDelegate
@@ -537,6 +538,10 @@ class ReadView(context: Context, attrs: AttributeSet) :
pageDelegate = ScrollPageDelegate(this)
}
+ PageAnim.fadePageAnim -> if (pageDelegate !is FadePageDelegate) {
+ pageDelegate = FadePageDelegate(this)
+ }
+
else -> if (pageDelegate !is NoAnimPageDelegate) {
pageDelegate = NoAnimPageDelegate(this)
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/FadePageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/FadePageDelegate.kt
new file mode 100644
index 000000000..f2935db83
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/FadePageDelegate.kt
@@ -0,0 +1,216 @@
+package io.legado.app.ui.book.read.page.delegate
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.view.MotionEvent
+import io.legado.app.ui.book.read.page.ReadView
+import io.legado.app.ui.book.read.page.entities.PageDirection
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
+import io.legado.app.utils.screenshot
+
+class FadePageDelegate(readView: ReadView) : PageDelegate(readView) {
+
+ private var curRecorder = CanvasRecorderFactory.create()
+ private var prevRecorder = CanvasRecorderFactory.create()
+ private var nextRecorder = CanvasRecorderFactory.create()
+ private val slopSquare get() = readView.pageSlopSquare2
+
+ private var fadeProgress = 0f
+ private val flipThreshold = 0.2f
+
+ override fun setDirection(direction: PageDirection) {
+ super.setDirection(direction)
+ fadeProgress = 0f
+ setBitmap()
+ }
+
+ private fun setBitmap() {
+ when (mDirection) {
+ PageDirection.PREV -> {
+ prevPage.screenshot(prevRecorder)
+ curPage.screenshot(curRecorder)
+ }
+ PageDirection.NEXT -> {
+ nextPage.screenshot(nextRecorder)
+ curPage.screenshot(curRecorder)
+ }
+ else -> {
+ curPage.screenshot(curRecorder)
+ }
+ }
+ }
+
+ private fun upRecorder() {
+ curRecorder.recycle()
+ prevRecorder.recycle()
+ nextRecorder.recycle()
+ curRecorder = CanvasRecorderFactory.create()
+ prevRecorder = CanvasRecorderFactory.create()
+ nextRecorder = CanvasRecorderFactory.create()
+ }
+
+ override fun onTouch(event: MotionEvent) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ abortAnim()
+ readView.setStartPoint(event.x, event.y, false)
+ }
+ MotionEvent.ACTION_MOVE -> onScroll(event)
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ if (!isMoved || mDirection == PageDirection.NONE) return
+ // 超过阈值自动翻页,否则回弹
+ val shouldFlip = fadeProgress >= flipThreshold
+ if (shouldFlip) 1f else 0f
+ isCancel = !shouldFlip
+ onAnimStart(readView.defaultAnimationSpeed)
+ }
+ }
+ }
+
+ private fun onScroll(event: MotionEvent) {
+ val focusX = event.x
+ val deltaX = focusX - startX
+ if (!isMoved) {
+ val distance = deltaX * deltaX
+ isMoved = distance > slopSquare
+ if (isMoved) {
+ animDirectionByDelta(deltaX)
+ readView.setStartPoint(event.x, event.y, false)
+ }
+ }
+
+ if (isMoved && mDirection != PageDirection.NONE) {
+ fadeProgress = (kotlin.math.abs(deltaX) / viewWidth).coerceIn(0f, 1f)
+ isRunning = true
+ readView.setTouchPoint(focusX, startY)
+ readView.invalidate()
+ }
+ }
+
+ private fun animDirectionByDelta(deltaX: Float) {
+ mDirection = if (deltaX > 0) {
+ if (!hasPrev()) PageDirection.NONE else PageDirection.PREV
+ } else {
+ if (!hasNext()) PageDirection.NONE else PageDirection.NEXT
+ }
+ setBitmap()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ curRecorder.draw(canvas)
+ if (mDirection == PageDirection.NONE) {
+ // 直接绘制 curPage,不使用 curRecorder
+ curPage.draw(canvas)
+ return
+ }
+
+ val alpha = (fadeProgress * 255).toInt().coerceIn(0, 255)
+ val paint = Paint().apply { this.alpha = alpha }
+
+ val save = canvas.saveLayer(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat(), paint)
+ when (mDirection) {
+ PageDirection.PREV -> prevRecorder.draw(canvas)
+ PageDirection.NEXT -> nextRecorder.draw(canvas)
+ else -> {}
+ }
+ canvas.restoreToCount(save)
+ }
+
+ override fun onAnimStart(animationSpeed: Int) {
+ if (mDirection == PageDirection.NONE) return
+ // 使用 scroller 做松手后的自动动画
+ val start = (fadeProgress * 1000).toInt()
+ val end = if (isCancel) 0 else 1000
+ scroller.startScroll(start, 0, end - start, 0, animationSpeed)
+ isStarted = true
+ isRunning = true
+ readView.invalidate()
+ }
+
+ override fun computeScroll() {
+ if (scroller.computeScrollOffset()) {
+ fadeProgress = scroller.currX / 1000f
+ readView.invalidate()
+ } else if (isStarted) {
+ onAnimStop()
+ stopScroll()
+ }
+ }
+
+ override fun onAnimStop() {
+ if (!isCancel) {
+ // 更新当前页显示
+ readView.fillPage(mDirection)
+ // 延迟更新 curRecorder,确保 curPage 已经刷新
+ readView.post {
+ try {
+ curPage.screenshot(curRecorder)
+ readView.invalidate()
+ } catch (_: Exception) {
+ // 捕获可能的异常,避免闪退
+ }
+ }
+ }
+
+ // 重置状态
+ fadeProgress = 0f
+ isRunning = false
+ isMoved = false
+
+ mDirection = PageDirection.NONE
+ readView.setStartPoint(0f, 0f, false)
+ readView.setTouchPoint(0f, 0f)
+ }
+
+
+ override fun abortAnim() {
+ isStarted = false
+ isMoved = false
+ isRunning = false
+ if (!scroller.isFinished) {
+ scroller.abortAnimation()
+ if (!isCancel) {
+ readView.fillPage(mDirection)
+ curPage.screenshot(curRecorder) // 同步 curRecorder
+ readView.invalidate()
+ }
+ }
+ }
+
+ override fun nextPageByAnim(animationSpeed: Int) {
+ if (!hasNext()) return
+ setDirection(PageDirection.NEXT)
+ fadeProgress = 0f
+ isCancel = false
+ onAnimStart(animationSpeed)
+ }
+
+ override fun prevPageByAnim(animationSpeed: Int) {
+ if (!hasPrev()) return
+ setDirection(PageDirection.PREV)
+ fadeProgress = 0f
+ isCancel = false
+ onAnimStart(animationSpeed)
+ }
+
+ override fun onDown() {
+ super.onDown()
+ fadeProgress = 0f
+ mDirection = PageDirection.NONE
+ isMoved = false
+ isCancel = false
+ readView.setStartPoint(0f, 0f, false)
+ }
+
+ override fun setViewSize(width: Int, height: Int) {
+ super.setViewSize(width, height)
+ upRecorder()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ curRecorder.recycle()
+ prevRecorder.recycle()
+ nextRecorder.recycle()
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
index 374622a1b..fbfc9097f 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
@@ -141,7 +141,7 @@ abstract class PageDelegate(protected val readView: ReadView) {
/**
* 按下
*/
- fun onDown() {
+ open fun onDown() {
//是否移动
isMoved = false
//是否存在下一章
diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt
index a0836b403..22daf1e7d 100644
--- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt
@@ -21,7 +21,6 @@ import io.legado.app.utils.sendToClip
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
import io.legado.app.utils.showHelp
import io.legado.app.utils.viewbindingdelegate.viewBinding
-import androidx.core.content.edit
import io.legado.app.data.appDb
import io.legado.app.lib.dialogs.alert
import io.legado.app.utils.toastOnUi
diff --git a/app/src/main/res/layout/dialog_read_book_style.xml b/app/src/main/res/layout/dialog_read_book_style.xml
index 374d79a54..4db140d18 100644
--- a/app/src/main/res/layout/dialog_read_book_style.xml
+++ b/app/src/main/res/layout/dialog_read_book_style.xml
@@ -216,6 +216,13 @@
android:checkable="true"
android:text="@string/page_anim_scroll" />
+
+
菜单标题显示方式
是否确认开始缓存?
计数替换净化
+ 渐变
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8842e1b7b..5ff981746 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1348,4 +1348,5 @@
菜单标题显示方式
是否确认开始缓存?
计数替换净化
+ 渐变