Horis 2 лет назад
Родитель
Сommit
f708a31d70

+ 3 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -137,6 +137,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
             }
         }
 
+    val textSelectAble: Boolean
+        get() = appCtx.getPrefBoolean(PreferKey.textSelectAble, true)
+
     val isTransparentStatusBar: Boolean
         get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true)
 

+ 13 - 2
app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt

@@ -1,5 +1,6 @@
 package io.legado.app.help.coroutine
 
+import android.os.Looper
 import io.legado.app.utils.printOnDebug
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CompletionHandler
@@ -15,6 +16,7 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.plus
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
+import java.util.concurrent.Executors
 import kotlin.coroutines.CoroutineContext
 
 /**
@@ -33,6 +35,9 @@ class Coroutine<T>(
     companion object {
 
         private val DEFAULT = MainScope()
+        private val launchExecutor = Executors.newSingleThreadExecutor()
+        private val mainThread = Looper.getMainLooper().thread
+        private val isMainThread inline get() = mainThread === Thread.currentThread()
 
         fun <T> async(
             scope: CoroutineScope = DEFAULT,
@@ -46,7 +51,7 @@ class Coroutine<T>(
 
     }
 
-    private val job: Job
+    private val job: Job by lazy { executeInternal(context, block) }
 
     private var start: VoidCallback? = null
     private var success: Callback<T>? = null
@@ -67,7 +72,13 @@ class Coroutine<T>(
         get() = job.isCompleted
 
     init {
-        this.job = executeInternal(context, block)
+        if (context == Dispatchers.Main.immediate && isMainThread) {
+            job
+        } else {
+            launchExecutor.execute {
+                job
+            }
+        }
     }
 
     fun timeout(timeMillis: () -> Long): Coroutine<T> {

+ 1 - 1
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -275,7 +275,7 @@ object ReadBook : CoroutineScope by MainScope() {
                 textChapter.getPage(index - 2)?.recyclePictures()
             }
             if (index < pageIndex) {
-                textChapter.getPage(index + 2)?.recyclePictures()
+                textChapter.getPage(index + 3)?.recyclePictures()
             }
         }
         durChapterPos = curTextChapter?.getReadLength(index) ?: index

+ 21 - 0
app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt

@@ -58,6 +58,12 @@ abstract class BaseReadBookActivity :
     override val binding by viewBinding(ActivityBookReadBinding::inflate)
     override val viewModel by viewModels<ReadBookViewModel>()
     var bottomDialog = 0
+        set(value) {
+            if (field != value) {
+                field = value
+                onBottomDialogChange()
+            }
+        }
     private val selectBookFolderResult = registerForActivityResult(HandleFileContract()) {
         it.uri?.let { uri ->
             ReadBook.book?.let { book ->
@@ -94,6 +100,21 @@ abstract class BaseReadBookActivity :
         }
     }
 
+    private fun onBottomDialogChange() {
+        when (bottomDialog) {
+            0 -> onMenuHide()
+            1 -> onMenuShow()
+        }
+    }
+
+    open fun onMenuShow() {
+
+    }
+
+    open fun onMenuHide() {
+
+    }
+
     fun showPaddingConfig() {
         showDialogFragment<PaddingConfigDialog>()
     }

+ 13 - 54
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -94,7 +94,6 @@ 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
@@ -201,8 +200,7 @@ class ReadBookActivity : BaseReadBookActivity(),
     }
     override val isInitFinish: Boolean get() = viewModel.isInitFinish
     override val isScroll: Boolean get() = binding.readView.isScroll
-    override var autoPageProgress = 0
-    override var isAutoPage = false
+    private val isAutoPage get() = binding.readView.isAutoPage
     override var isShowingSearchResult = false
     override var isSelectingSearchResult = false
         set(value) {
@@ -220,8 +218,6 @@ class ReadBookActivity : BaseReadBookActivity(),
     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 val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
 
@@ -934,6 +930,7 @@ class ReadBookActivity : BaseReadBookActivity(),
             ReadAloud.upTtsProgress(this)
         }
         loadStates = true
+        binding.readView.onContentLoadFinish()
     }
 
     /**
@@ -945,9 +942,6 @@ class ReadBookActivity : BaseReadBookActivity(),
         success: (() -> Unit)?
     ) {
         lifecycleScope.launch {
-            if (relativePosition == 0) {
-                autoPageProgress = 0
-            }
             binding.readView.upContent(relativePosition, resetPageOffset)
             upSeekBarProgress()
             loadStates = false
@@ -970,8 +964,7 @@ class ReadBookActivity : BaseReadBookActivity(),
      */
     override fun pageChanged() {
         pageChanged = true
-        lifecycleScope.launch {
-            autoPageProgress = 0
+        handler.post {
             upSeekBarProgress()
             startBackupJob()
         }
@@ -1053,8 +1046,7 @@ class ReadBookActivity : BaseReadBookActivity(),
         if (isAutoPage) {
             autoPageStop()
         } else {
-            isAutoPage = true
-            autoPagePlus()
+            binding.readView.autoPager.start()
             binding.readMenu.setAutoPage(true)
             screenTimeOut = -1L
             screenOffTimerStart()
@@ -1063,53 +1055,12 @@ class ReadBookActivity : BaseReadBookActivity(),
 
     override fun autoPageStop() {
         if (isAutoPage) {
-            isAutoPage = false
-            autoPageRenderer.stop()
-            binding.readView.invalidate()
-            binding.readView.clearNextPageBitmap()
+            binding.readView.autoPager.stop()
             binding.readMenu.setAutoPage(false)
             upScreenTimeOut()
         }
     }
 
-    private fun autoPagePlus() {
-        autoPageProgress = 0
-        autoPageScrollOffset = 0.0
-        autoPageRenderer.start()
-    }
-
-    private fun doAutoPage(frameTime: Double) {
-        if (menuLayoutIsVisible) {
-            return
-        }
-        if (binding.readView.run { isScroll && pageDelegate?.isRunning == true }) {
-            return
-        }
-        val readTime = ReadBookConfig.autoReadSpeed * 1000.0
-        val height = binding.readView.height
-        autoPageScrollOffset += height / readTime * frameTime
-        if (autoPageScrollOffset < 1) {
-            return
-        }
-        val scrollOffset = autoPageScrollOffset.toInt()
-        autoPageScrollOffset -= scrollOffset
-        if (binding.readView.isScroll) {
-            binding.readView.curPage.scroll(-scrollOffset)
-        } else {
-            autoPageProgress += scrollOffset
-            if (autoPageProgress >= height) {
-                autoPageProgress = 0
-                if (!binding.readView.fillPage(PageDirection.NEXT)) {
-                    autoPageStop()
-                } else {
-                    binding.readView.clearNextPageBitmap()
-                }
-            } else {
-                binding.readView.invalidate()
-            }
-        }
-    }
-
     override fun openSourceEditActivity() {
         ReadBook.bookSource?.let {
             sourceEditActivity.launch {
@@ -1414,6 +1365,14 @@ class ReadBookActivity : BaseReadBookActivity(),
         skipToSearch(searchResult)
     }
 
+    override fun onMenuShow() {
+        binding.readView.autoPager.pause()
+    }
+
+    override fun onMenuHide() {
+        binding.readView.autoPager.resume()
+    }
+
     /* 全文搜索跳转 */
     private fun skipToSearch(searchResult: SearchResult) {
         val previousResult = binding.searchMenu.previousSearchResult

+ 32 - 3
app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt

@@ -27,13 +27,38 @@ import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ThemeConfig
 import io.legado.app.lib.dialogs.alert
-import io.legado.app.lib.theme.*
+import io.legado.app.lib.theme.Selector
+import io.legado.app.lib.theme.accentColor
+import io.legado.app.lib.theme.bottomBackground
+import io.legado.app.lib.theme.buttonDisabledColor
+import io.legado.app.lib.theme.getPrimaryTextColor
+import io.legado.app.lib.theme.primaryColor
+import io.legado.app.lib.theme.primaryTextColor
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.info.BookInfoActivity
 import io.legado.app.ui.browser.WebViewActivity
 import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
-import io.legado.app.utils.*
-import splitties.views.*
+import io.legado.app.utils.ColorUtils
+import io.legado.app.utils.ConstraintModify
+import io.legado.app.utils.activity
+import io.legado.app.utils.dpToPx
+import io.legado.app.utils.getPrefBoolean
+import io.legado.app.utils.gone
+import io.legado.app.utils.invisible
+import io.legado.app.utils.loadAnimation
+import io.legado.app.utils.modifyBegin
+import io.legado.app.utils.navigationBarGravity
+import io.legado.app.utils.navigationBarHeight
+import io.legado.app.utils.openUrl
+import io.legado.app.utils.putPrefBoolean
+import io.legado.app.utils.startActivity
+import io.legado.app.utils.visible
+import splitties.views.bottomPadding
+import splitties.views.leftPadding
+import splitties.views.onClick
+import splitties.views.onLongClick
+import splitties.views.padding
+import splitties.views.rightPadding
 
 /**
  * 阅读界面菜单
@@ -273,6 +298,7 @@ class ReadMenu @JvmOverloads constructor(
     }
 
     fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
+        callBack.onMenuShow()
         this.visible()
         binding.titleBar.visible()
         binding.bottomMenu.visible()
@@ -286,6 +312,7 @@ class ReadMenu @JvmOverloads constructor(
     }
 
     fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) {
+        callBack.onMenuHide()
         this.onMenuOutEnd = onMenuOutEnd
         if (this.isVisible) {
             if (anim) {
@@ -558,6 +585,8 @@ class ReadMenu @JvmOverloads constructor(
         fun payAction()
         fun disableSource()
         fun skipToChapter(index: Int)
+        fun onMenuShow()
+        fun onMenuHide()
     }
 
 }

+ 9 - 1
app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt

@@ -18,7 +18,13 @@ import io.legado.app.lib.theme.bottomBackground
 import io.legado.app.lib.theme.getPrimaryTextColor
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.searchContent.SearchResult
-import io.legado.app.utils.*
+import io.legado.app.utils.ColorUtils
+import io.legado.app.utils.activity
+import io.legado.app.utils.invisible
+import io.legado.app.utils.loadAnimation
+import io.legado.app.utils.navigationBarGravity
+import io.legado.app.utils.navigationBarHeight
+import io.legado.app.utils.visible
 import splitties.views.bottomPadding
 import splitties.views.leftPadding
 import splitties.views.padding
@@ -235,6 +241,8 @@ class SearchMenu @JvmOverloads constructor(
         fun exitSearchMenu()
         fun showMenuBar()
         fun navigateToSearch(searchResult: SearchResult, index: Int)
+        fun onMenuShow()
+        fun onMenuHide()
     }
 
 }

+ 146 - 0
app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt

@@ -0,0 +1,146 @@
+package io.legado.app.ui.book.read.page
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Picture
+import android.graphics.Rect
+import android.os.Build
+import android.os.SystemClock
+import androidx.core.graphics.withClip
+import io.legado.app.help.config.AppConfig
+import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.lib.theme.ThemeStore
+import io.legado.app.ui.book.read.page.entities.PageDirection
+import io.legado.app.utils.screenshot
+
+/**
+ * 自动翻页
+ */
+class AutoPager(private val readView: ReadView) {
+    private var progress = 0
+    var isRunning = false
+    private var isPausing = false
+    private var scrollOffsetRemain = 0.0
+    private var scrollOffset = 0
+    private var lastTimeMillis = 0L
+    private var bitmap: Bitmap? = null
+    private var picture: Picture? = null
+    private var pictureIsDirty = true
+    private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+    private val rect = Rect()
+    private val paint by lazy { Paint() }
+
+    fun start() {
+        isRunning = true
+        paint.color = ThemeStore.accentColor
+        lastTimeMillis = SystemClock.uptimeMillis()
+        readView.curPage.upSelectAble(false)
+        readView.invalidate()
+    }
+
+    fun stop() {
+        if (!isRunning) {
+            return
+        }
+        isRunning = false
+        isPausing = false
+        readView.curPage.upSelectAble(AppConfig.textSelectAble)
+        readView.invalidate()
+        reset()
+    }
+
+    fun pause() {
+        if (!isRunning) {
+            return
+        }
+        isPausing = true
+    }
+
+    fun resume() {
+        if (!isRunning) {
+            return
+        }
+        isPausing = false
+        lastTimeMillis = SystemClock.uptimeMillis()
+        readView.invalidate()
+    }
+
+    fun reset() {
+        progress = 0
+        scrollOffsetRemain = 0.0
+        scrollOffset = 0
+        bitmap?.recycle()
+        bitmap = null
+        pictureIsDirty = true
+    }
+
+    fun onDraw(canvas: Canvas) {
+        if (!isRunning) {
+            return
+        }
+
+        if (readView.isScroll) {
+            computeOffset()
+            if (!isPausing) readView.curPage.scroll(-scrollOffset)
+        } else {
+            val bottom = progress
+            val width = readView.width
+            if (atLeastApi23) {
+                if (picture == null) {
+                    picture = Picture()
+                }
+                if (pictureIsDirty) {
+                    pictureIsDirty = false
+                    readView.nextPage.screenshot(picture!!)
+                }
+                canvas.withClip(0, 0, width, bottom) {
+                    drawPicture(picture!!)
+                }
+            } else {
+                if (bitmap == null) {
+                    bitmap = readView.nextPage.screenshot()
+                }
+                rect.set(0, 0, width, bottom)
+                canvas.drawBitmap(bitmap!!, rect, rect, null)
+            }
+            canvas.drawRect(
+                0f,
+                bottom.toFloat() - 1,
+                width.toFloat(),
+                bottom.toFloat(),
+                paint
+            )
+            if (!isPausing) readView.invalidate()
+            computeOffset()
+        }
+
+    }
+
+    private fun computeOffset() {
+
+        val currentTime = SystemClock.uptimeMillis()
+        val elapsedTime = currentTime - lastTimeMillis
+        lastTimeMillis = currentTime
+
+        val readTime = ReadBookConfig.autoReadSpeed * 1000.0
+        val height = readView.height
+        scrollOffsetRemain += height / readTime * elapsedTime
+        if (scrollOffsetRemain < 1) {
+            return
+        }
+        scrollOffset = scrollOffsetRemain.toInt()
+        this.scrollOffsetRemain -= scrollOffset
+        if (!readView.isScroll) {
+            progress += scrollOffset
+            if (progress >= height) {
+                if (!readView.fillPage(PageDirection.NEXT)) {
+                    stop()
+                } else {
+                    reset()
+                }
+            }
+        }
+    }
+
+}

+ 24 - 3
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -9,7 +9,6 @@ import android.view.View
 import androidx.core.graphics.withTranslation
 import io.legado.app.R
 import io.legado.app.constant.PageAnim
-import io.legado.app.constant.PreferKey
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.ReadBook
@@ -28,16 +27,16 @@ import io.legado.app.ui.widget.dialog.PhotoDialog
 import io.legado.app.utils.PictureMirror
 import io.legado.app.utils.activity
 import io.legado.app.utils.getCompatColor
-import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.toastOnUi
+import java.util.concurrent.Executors
 import kotlin.math.min
 
 /**
  * 阅读内容视图
  */
 class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
-    var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true)
+    var selectAble = AppConfig.textSelectAble
     val selectedPaint by lazy {
         Paint().apply {
             color = context.getCompatColor(R.color.btn_bg_press_2)
@@ -61,6 +60,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
     private var pageOffset = 0
     private val pictureMirror = PictureMirror()
     private val isNoAnim get() = ReadBook.pageAnim() == PageAnim.noAnim
+    private var autoPager: AutoPager? = null
+    private val renderThread by lazy { Executors.newSingleThreadExecutor() }
+    private val renderRunnable by lazy { Runnable { preRenderPage() } }
 
     //绘制图片的paint
     val imagePaint by lazy {
@@ -91,6 +93,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
 
     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)
+        autoPager?.onDraw(canvas)
         if (longScreenshot) {
             canvas.translate(0f, scrollY.toFloat())
         }
@@ -179,6 +182,20 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         pictureMirror.invalidate()
     }
 
+    fun submitPreRenderTask() {
+        renderThread.submit(renderRunnable)
+    }
+
+    private fun preRenderPage() {
+        val view = this
+        pageFactory.run {
+            prevPage.preRender(view)
+            prevPage.preRender(view)
+            nextPage.preRender(view)
+            nextPlusPage.preRender(view)
+        }
+    }
+
     /**
      * 重置滚动位置
      */
@@ -669,6 +686,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         }
     }
 
+    fun setAutoPager(autoPager: AutoPager?) {
+        this.autoPager = autoPager
+    }
+
     override fun canScrollVertically(direction: Int): Boolean {
         return callBack.isScroll && pageFactory.hasNext()
     }

+ 18 - 1
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -45,6 +45,8 @@ class PageView(context: Context) : FrameLayout(context) {
     private var tvBookName: BatteryView? = null
     private var tvTimeBattery: BatteryView? = null
     private var tvTimeBatteryP: BatteryView? = null
+    private var isMainView = false
+    var isScroll = false
 
     val headerHeight: Int
         get() {
@@ -272,7 +274,13 @@ class PageView(context: Context) : FrameLayout(context) {
      * 设置内容
      */
     fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
-        setProgress(textPage)
+        if (isMainView && !isScroll) {
+            setProgress(textPage)
+        } else {
+            post {
+                setProgress(textPage)
+            }
+        }
         if (resetPageOffset) {
             resetPageOffset()
         }
@@ -324,6 +332,14 @@ class PageView(context: Context) : FrameLayout(context) {
         tvPageAndTotal?.text = "${index.plus(1)}/$pageSize  $readProgress"
     }
 
+    fun setAutoPager(autoPager: AutoPager?) {
+        binding.contentTextView.setAutoPager(autoPager)
+    }
+
+    fun submitPreRenderTask() {
+        binding.contentTextView.submitPreRenderTask()
+    }
+
     /**
      * 滚动事件
      */
@@ -375,6 +391,7 @@ class PageView(context: Context) : FrameLayout(context) {
     }
 
     fun markAsMainView() {
+        isMainView = true
         binding.contentTextView.isMainView = true
     }
 

+ 33 - 23
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -35,7 +35,6 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
 import io.legado.app.utils.activity
 import io.legado.app.utils.invisible
-import io.legado.app.utils.screenshot
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.visible
 import java.text.BreakIterator
@@ -108,6 +107,8 @@ class ReadView(context: Context, attrs: AttributeSet) :
     private val autoPagePint by lazy { Paint().apply { color = context.accentColor } }
     private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) }
     private var nextPageBitmap: Bitmap? = null
+    val autoPager = AutoPager(this)
+    val isAutoPage get() = autoPager.isRunning
 
     init {
         addView(nextPage)
@@ -149,22 +150,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
     override fun dispatchDraw(canvas: Canvas) {
         super.dispatchDraw(canvas)
         pageDelegate?.onDraw(canvas)
-        if (!isInEditMode && callBack.isAutoPage && !isScroll) {
-            // 自动翻页
-            val bitmap = nextPageBitmap ?: nextPage.screenshot()?.also { nextPageBitmap = it }
-            bitmap?.let {
-                val bottom = callBack.autoPageProgress
-                autoPageRect.set(0, 0, width, bottom)
-                canvas.drawBitmap(it, autoPageRect, autoPageRect, null)
-                canvas.drawRect(
-                    0f,
-                    bottom.toFloat() - 1,
-                    width.toFloat(),
-                    bottom.toFloat(),
-                    autoPagePint
-                )
-            }
-        }
+        autoPager.onDraw(canvas)
     }
 
     override fun computeScroll() {
@@ -262,6 +248,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
                     pageDelegate?.onTouch(event)
                 }
                 pressOnTextSelected = false
+                autoPager.resume()
             }
         }
         return true
@@ -530,6 +517,12 @@ class ReadView(context: Context, attrs: AttributeSet) :
         } else {
             nextPage.visible()
         }
+        if (isScroll) {
+            curPage.setAutoPager(autoPager)
+        } else {
+            curPage.setAutoPager(null)
+        }
+        curPage.isScroll = isScroll
     }
 
     /**
@@ -541,12 +534,13 @@ class ReadView(context: Context, attrs: AttributeSet) :
         post {
             curPage.setContentDescription(pageFactory.curPage.text)
         }
-        if (isScroll && !callBack.isAutoPage) {
+        if (isScroll && !isAutoPage) {
             curPage.setContent(pageFactory.curPage, resetPageOffset)
         } else {
-            if (callBack.isAutoPage && relativePosition >= 0) {
-                clearNextPageBitmap()
-            }
+//            if (isAutoPage && relativePosition >= 0) {
+////                clearNextPageBitmap()
+//                autoPager.clear()
+//            }
             when (relativePosition) {
                 -1 -> prevPage.setContent(pageFactory.prevPage)
                 1 -> nextPage.setContent(pageFactory.nextPage)
@@ -663,6 +657,24 @@ class ReadView(context: Context, attrs: AttributeSet) :
         }
     }
 
+    fun onScrollAnimStart() {
+        autoPager.pause()
+    }
+
+    fun onScrollAnimStop() {
+        autoPager.resume()
+    }
+
+    override fun onPageChange() {
+        autoPager.reset()
+        curPage.submitPreRenderTask()
+    }
+
+    fun onContentLoadFinish() {
+        autoPager.reset()
+        curPage.submitPreRenderTask()
+    }
+
     override val currentChapter: TextChapter?
         get() {
             return if (callBack.isInitFinish) ReadBook.textChapter(0) else null
@@ -688,8 +700,6 @@ class ReadView(context: Context, attrs: AttributeSet) :
 
     interface CallBack {
         val isInitFinish: Boolean
-        val isAutoPage: Boolean
-        val autoPageProgress: Int
         fun showActionMenu()
         fun screenOffTimerStart()
         fun showTextActionMenu()

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt

@@ -20,4 +20,6 @@ interface DataSource {
     fun hasPrevChapter(): Boolean
 
     fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
+
+    fun onPageChange()
 }

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt

@@ -21,6 +21,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
     var noAnim: Boolean = false
 
     override fun onAnimStart(animationSpeed: Int) {
+        readView.onScrollAnimStart()
         //惯性滚动
         fling(
             0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(),
@@ -29,7 +30,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
     }
 
     override fun onAnimStop() {
-        // nothing
+        readView.onScrollAnimStop()
     }
 
     override fun onTouch(event: MotionEvent) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt

@@ -121,7 +121,7 @@ data class TextLine(
         return visible
     }
 
-    fun draw(view: ContentTextView, canvas: Canvas) {
+    fun draw(view: ContentTextView, canvas: Canvas?) {
         pictureMirror.draw(canvas, view.width, height.toInt()) {
             drawTextLine(view, this)
         }

+ 10 - 1
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt

@@ -263,7 +263,7 @@ data class TextPage(
         return null
     }
 
-    fun draw(view: ContentTextView, canvas: Canvas) {
+    fun draw(view: ContentTextView, canvas: Canvas?) {
         pictureMirror.draw(canvas, view.width, height.toInt()) {
             drawPage(view, this)
         }
@@ -278,6 +278,15 @@ data class TextPage(
         }
     }
 
+    fun preRender(view: ContentTextView) {
+        if (!pictureMirror.isDirty) return
+        draw(view, null)
+    }
+
+    fun isDirty(): Boolean {
+        return pictureMirror.isDirty
+    }
+
     fun invalidate() {
         pictureMirror.invalidate()
     }

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt

@@ -44,6 +44,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
                 ReadBook.setPageIndex(pageIndex.plus(1))
             }
             if (upContent) upContent(resetPageOffset = false)
+            dataSource.onPageChange()
             true
         } else
             false
@@ -63,6 +64,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
                 ReadBook.setPageIndex(pageIndex.minus(1))
             }
             if (upContent) upContent(resetPageOffset = false)
+            dataSource.onPageChange()
             true
         } else
             false

+ 13 - 5
app/src/main/java/io/legado/app/utils/PictureMirror.kt

@@ -4,23 +4,31 @@ import android.graphics.Canvas
 import android.graphics.Picture
 import android.os.Build
 import androidx.core.graphics.record
+import java.util.concurrent.locks.ReentrantLock
 
 class PictureMirror {
 
     var picture: Picture? = null
+    @Volatile
     var isDirty = true
+    val lock = ReentrantLock()
 
-    inline fun draw(canvas: Canvas, width: Int, height: Int, block: Canvas.() -> Unit) {
+    inline fun draw(canvas: Canvas?, width: Int, height: Int, block: Canvas.() -> Unit) {
         if (atLeastApi23) {
             if (picture == null) picture = Picture()
             val picture = picture!!
             if (isDirty) {
-                isDirty = false
-                picture.record(width, height, block)
+                if (!lock.tryLock()) return
+                try {
+                    picture.record(width, height, block)
+                    isDirty = false
+                } finally {
+                    lock.unlock()
+                }
             }
-            canvas.drawPicture(picture)
+            canvas?.drawPicture(picture)
         } else {
-            canvas.block()
+            canvas?.block()
         }
     }