Horis пре 2 година
родитељ
комит
0312975d4a
36 измењених фајлова са 683 додато и 463 уклоњено
  1. 0 1
      app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt
  2. 2 2
      app/src/main/java/io/legado/app/model/ReadBook.kt
  3. 22 37
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  4. 1 1
      app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt
  5. 30 9
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  6. 14 4
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  7. 14 14
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  8. 13 13
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  9. 20 15
      app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt
  10. 5 4
      app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt
  11. 31 19
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  12. 6 1
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  13. 3 1
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  14. 15 56
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt
  15. 11 16
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  16. 10 1
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  17. 11 1
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  18. 42 2
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  19. 10 63
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt
  20. 7 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  21. 11 18
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  22. 7 7
      app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
  23. 2 35
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  24. 4 7
      app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt
  25. 37 25
      app/src/main/java/io/legado/app/ui/widget/BatteryView.kt
  26. 10 17
      app/src/main/java/io/legado/app/utils/ChineseUtils.kt
  27. 0 88
      app/src/main/java/io/legado/app/utils/PictureMirror.kt
  28. 12 0
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  29. 36 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt
  30. 27 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt
  31. 39 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt
  32. 62 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt
  33. 35 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt
  34. 23 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt
  35. 59 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt
  36. 52 0
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt

+ 0 - 1
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -567,7 +567,6 @@ object ReadBookConfig {
                     textColorInt = color
                 }
             }
-            ChapterProvider.upStyle()
         }
 
         fun curTextColor(): Int {

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

@@ -274,10 +274,10 @@ object ReadBook : CoroutineScope by MainScope() {
         if (textChapter != null) {
             val pageIndex = durPageIndex
             if (index > pageIndex) {
-                textChapter.getPage(index - 2)?.recyclePictures()
+                textChapter.getPage(index - 2)?.recycleRecorders()
             }
             if (index < pageIndex) {
-                textChapter.getPage(index + 3)?.recyclePictures()
+                textChapter.getPage(index + 3)?.recycleRecorders()
             }
         }
         durChapterPos = curTextChapter?.getReadLength(index) ?: index

+ 22 - 37
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -6,6 +6,7 @@ import android.content.Intent
 import android.content.res.Configuration
 import android.net.Uri
 import android.os.Bundle
+import android.os.Looper
 import android.view.Gravity
 import android.view.InputDevice
 import android.view.KeyEvent
@@ -76,6 +77,7 @@ import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_C
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.ReadView
 import io.legado.app.ui.book.read.page.entities.PageDirection
+import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.searchContent.SearchContentActivity
 import io.legado.app.ui.book.searchContent.SearchResult
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
@@ -116,7 +118,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.asExecutor
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -217,7 +218,6 @@ class ReadBookActivity : BaseReadBookActivity(),
     private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } }
     private var bookChanged = false
     private var pageChanged = false
-    private var reloadContent = false
     private val handler by lazy { buildMainHandler() }
     private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
     private val executor = ReadBook.executor
@@ -264,23 +264,9 @@ class ReadBookActivity : BaseReadBookActivity(),
 
     override fun onPostCreate(savedInstanceState: Bundle?) {
         super.onPostCreate(savedInstanceState)
-        viewModel.initData(intent) {
-            initDataSuccess()
-        }
-    }
-
-    override fun onNewIntent(intent: Intent?) {
-        super.onNewIntent(intent)
-        viewModel.initData(intent ?: return) {
-            initDataSuccess()
-        }
-    }
-
-    private fun initDataSuccess() {
-        upMenu()
-        if (reloadContent) {
-            reloadContent = false
-            ReadBook.loadContent(resetPageOffset = false)
+        Looper.myQueue().addIdleHandler {
+            viewModel.initData(intent) { upMenu() }
+            false
         }
     }
 
@@ -1312,24 +1298,24 @@ class ReadBookActivity : BaseReadBookActivity(),
         when (dialogId) {
             TEXT_COLOR -> {
                 setCurTextColor(color)
-                postEvent(EventBus.UP_CONFIG, false)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
             }
 
             BG_COLOR -> {
                 setCurBg(0, "#${color.hexString}")
-                postEvent(EventBus.UP_CONFIG, false)
+                postEvent(EventBus.UP_CONFIG, arrayOf(1))
             }
 
             TIP_COLOR -> {
                 ReadTipConfig.tipColor = color
                 postEvent(EventBus.TIP_COLOR, "")
-                postEvent(EventBus.UP_CONFIG, false)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
 
             TIP_DIVIDER_COLOR -> {
                 ReadTipConfig.tipDividerColor = color
                 postEvent(EventBus.TIP_COLOR, "")
-                postEvent(EventBus.UP_CONFIG, false)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
     }
@@ -1493,22 +1479,21 @@ class ReadBookActivity : BaseReadBookActivity(),
                 ReadBook.readAloud(!BaseReadAloudService.pause)
             }
         }
-        observeEvent<Boolean>(EventBus.UP_CONFIG) {
-            upSystemUiVisibility()
-            readView.upPageSlopSquare()
-            readView.upBg()
-            readView.upStyle()
-            readView.upBgAlpha()
-            if (it) { // 更新内容排版布局
-                if (isInitFinish) {
-                    ReadBook.loadContent(resetPageOffset = false)
-                } else {
-                    reloadContent = true
+        observeEvent<Array<Int>>(EventBus.UP_CONFIG) {
+            it.forEach { value ->
+                when (value) {
+                    0 -> upSystemUiVisibility()
+                    1 -> readView.upBg()
+                    2 -> readView.upStyle()
+                    3 -> readView.upBgAlpha()
+                    4 -> readView.upPageSlopSquare()
+                    5 -> if (isInitFinish) ReadBook.loadContent(resetPageOffset = false)
+                    6 -> readView.upContent(resetPageOffset = false)
+                    8 -> ChapterProvider.upStyle()
+                    9 -> binding.readView.invalidateTextPage()
+                    10 -> ChapterProvider.upLayout()
                 }
-            } else {
-                readView.upContent(resetPageOffset = false)
             }
-            binding.readMenu.reset()
         }
         observeEvent<Int>(EventBus.ALOUD_STATE) {
             if (it == Status.STOP || it == Status.PAUSE) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt

@@ -41,7 +41,7 @@ class BgAdapter(context: Context, val textColor: Int) :
             this.setOnClickListener {
                 getItemByLayoutPosition(holder.layoutPosition)?.let {
                     ReadBookConfig.durConfig.setCurBg(1, it)
-                    postEvent(EventBus.UP_CONFIG, false)
+                    postEvent(EventBus.UP_CONFIG, arrayOf(1))
                 }
             }
         }

+ 30 - 9
app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt

@@ -33,11 +33,32 @@ import io.legado.app.lib.theme.getSecondaryTextColor
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.file.HandleFileContract
 import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
-import io.legado.app.utils.*
+import io.legado.app.utils.ColorUtils
+import io.legado.app.utils.FileUtils
+import io.legado.app.utils.GSON
+import io.legado.app.utils.MD5Utils
+import io.legado.app.utils.SelectImageContract
 import io.legado.app.utils.compress.ZipUtils
+import io.legado.app.utils.createFileReplace
+import io.legado.app.utils.createFolderReplace
+import io.legado.app.utils.externalCache
+import io.legado.app.utils.externalFiles
+import io.legado.app.utils.getFile
+import io.legado.app.utils.inputStream
+import io.legado.app.utils.isContentScheme
+import io.legado.app.utils.launch
+import io.legado.app.utils.longToast
+import io.legado.app.utils.openOutputStream
+import io.legado.app.utils.outputStream
+import io.legado.app.utils.parseToUri
+import io.legado.app.utils.postEvent
+import io.legado.app.utils.printOnDebug
+import io.legado.app.utils.readBytes
+import io.legado.app.utils.readUri
+import io.legado.app.utils.stackTraceStr
+import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import splitties.init.appCtx
-
 import java.io.File
 import java.io.FileOutputStream
 
@@ -168,7 +189,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
                 if (i >= 0) {
                     ReadBookConfig.durConfig = defaultConfigs[i].copy()
                     initData()
-                    postEvent(EventBus.UP_CONFIG, true)
+                    postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
                 }
             }
         }
@@ -178,7 +199,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
         }
         binding.swUnderline.setOnCheckedChangeListener { _, isChecked ->
             underline = isChecked
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(9))
         }
         binding.tvTextColor.setOnClickListener {
             ColorPickerDialog.newBuilder()
@@ -214,7 +235,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
         }
         binding.ivDelete.setOnClickListener {
             if (ReadBookConfig.deleteDur()) {
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
                 dismissAllowingStateLoss()
             } else {
                 toastOnUi("数量已是最少,不能删除.")
@@ -223,11 +244,11 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
         binding.sbBgAlpha.setOnSeekBarChangeListener(object : SeekBarChangeListener {
             override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
                 ReadBookConfig.bgAlpha = progress
-                postEvent(EventBus.UP_CONFIG, false)
+                postEvent(EventBus.UP_CONFIG, arrayOf(3))
             }
 
             override fun onStopTrackingTouch(seekBar: SeekBar) {
-                postEvent(EventBus.UP_CONFIG, false)
+                postEvent(EventBus.UP_CONFIG, arrayOf(3))
             }
         })
     }
@@ -357,7 +378,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
             ReadBookConfig.import(byteArray).getOrThrow()
         }.onSuccess {
             ReadBookConfig.durConfig = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
             toastOnUi("导入成功")
         }.onError {
             it.printOnDebug()
@@ -378,7 +399,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
                     inputStream.copyTo(outputStream)
                 }
                 ReadBookConfig.durConfig.setCurBg(2, fileName)
-                postEvent(EventBus.UP_CONFIG, false)
+                postEvent(EventBus.UP_CONFIG, arrayOf(1))
             }.onFailure {
                 appCtx.toastOnUi(it.localizedMessage)
             }

+ 14 - 4
app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt

@@ -107,39 +107,48 @@ class MoreConfigDialog : DialogFragment() {
                 PreferKey.readBodyToLh -> activity?.recreate()
                 PreferKey.hideStatusBar -> {
                     ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
-                    postEvent(EventBus.UP_CONFIG, true)
+                    postEvent(EventBus.UP_CONFIG, arrayOf(0))
                 }
+
                 PreferKey.hideNavigationBar -> {
                     ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar)
-                    postEvent(EventBus.UP_CONFIG, true)
+                    postEvent(EventBus.UP_CONFIG, arrayOf(0))
                 }
+
                 PreferKey.keepLight -> postEvent(key, true)
                 PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key))
                 PreferKey.screenOrientation -> {
                     (activity as? ReadBookActivity)?.setOrientation()
                 }
+
                 PreferKey.textFullJustify,
                 PreferKey.textBottomJustify,
                 PreferKey.useZhLayout -> {
-                    postEvent(EventBus.UP_CONFIG, true)
+                    postEvent(EventBus.UP_CONFIG, arrayOf(5))
                 }
+
                 PreferKey.showBrightnessView -> {
                     postEvent(PreferKey.showBrightnessView, "")
                 }
+
                 PreferKey.expandTextMenu -> {
                     (activity as? ReadBookActivity)?.textActionMenu?.upMenu()
                 }
+
                 PreferKey.doublePageHorizontal -> {
                     ChapterProvider.upLayout()
                     ReadBook.loadContent(false)
                 }
+
                 PreferKey.showReadTitleAddition,
                 PreferKey.readBarStyleFollowPage -> {
                     postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
                 }
+
                 PreferKey.progressBarBehavior -> {
                     postEvent(EventBus.UP_SEEK_BAR, true)
                 }
+
                 PreferKey.noAnimScrollPage -> {
                     ReadBook.callBack?.upPageAnim()
                 }
@@ -152,6 +161,7 @@ class MoreConfigDialog : DialogFragment() {
                 "clickRegionalConfig" -> {
                     (activity as? ReadBookActivity)?.showClickRegionalConfig()
                 }
+
                 PreferKey.pageTouchSlop -> {
                     NumberPickerDialog(requireContext())
                         .setTitle(getString(R.string.page_touch_slop_dialog_title))
@@ -160,7 +170,7 @@ class MoreConfigDialog : DialogFragment() {
                         .setValue(AppConfig.pageTouchSlop)
                         .show {
                             AppConfig.pageTouchSlop = it
-                            postEvent(EventBus.UP_CONFIG, false)
+                            postEvent(EventBus.UP_CONFIG, arrayOf(4))
                         }
                 }
             }

+ 14 - 14
app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt

@@ -63,61 +63,61 @@ class PaddingConfigDialog : BaseDialogFragment(R.layout.dialog_read_padding) {
         //正文
         dsbPaddingTop.onChanged = {
             ReadBookConfig.paddingTop = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
         }
         dsbPaddingBottom.onChanged = {
             ReadBookConfig.paddingBottom = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
         }
         dsbPaddingLeft.onChanged = {
             ReadBookConfig.paddingLeft = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
         }
         dsbPaddingRight.onChanged = {
             ReadBookConfig.paddingRight = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
         }
         //页眉
         dsbHeaderPaddingTop.onChanged = {
             ReadBookConfig.headerPaddingTop = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         dsbHeaderPaddingBottom.onChanged = {
             ReadBookConfig.headerPaddingBottom = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         dsbHeaderPaddingLeft.onChanged = {
             ReadBookConfig.headerPaddingLeft = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         dsbHeaderPaddingRight.onChanged = {
             ReadBookConfig.headerPaddingRight = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         //页脚
         dsbFooterPaddingTop.onChanged = {
             ReadBookConfig.footerPaddingTop = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         dsbFooterPaddingBottom.onChanged = {
             ReadBookConfig.footerPaddingBottom = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         dsbFooterPaddingLeft.onChanged = {
             ReadBookConfig.footerPaddingLeft = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         dsbFooterPaddingRight.onChanged = {
             ReadBookConfig.footerPaddingRight = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         cbShowTopLine.onCheckedChangeListener = { _, isChecked ->
             ReadBookConfig.showHeaderLine = isChecked
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
         cbShowBottomLine.onCheckedChangeListener = { _, isChecked ->
             ReadBookConfig.showFooterLine = isChecked
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
     }
 

+ 13 - 13
app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt

@@ -108,10 +108,10 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
     private fun initViewEvent() = binding.run {
         chineseConverter.onChanged {
             ChineseUtils.unLoad(*TransType.entries.toTypedArray())
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(5))
         }
         textFontWeightConverter.onChanged {
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8))
         }
         tvTextFont.setOnClickListener {
             showDialogFragment<FontSelectDialog>()
@@ -122,7 +122,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
                 items = resources.getStringArray(R.array.indent).toList()
             ) { _, index ->
                 ReadBookConfig.paragraphIndent = " ".repeat(index)
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(5))
             }
         }
         tvPadding.setOnClickListener {
@@ -141,40 +141,40 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
         cbShareLayout.onCheckedChangeListener = { _, isChecked ->
             ReadBookConfig.shareLayout = isChecked
             upView()
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
         }
         dsbTextSize.onChanged = {
             ReadBookConfig.textSize = it + 5
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
         dsbTextLetterSpacing.onChanged = {
             ReadBookConfig.letterSpacing = (it - 50) / 100f
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
         dsbLineSize.onChanged = {
             ReadBookConfig.lineSpacingExtra = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
         dsbParagraphSpacing.onChanged = {
             ReadBookConfig.paragraphSpacing = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
     }
 
-    private fun changeBg(index: Int) {
+    private fun changeBgTextConfig(index: Int) {
         val oldIndex = ReadBookConfig.styleSelect
         if (index != oldIndex) {
             ReadBookConfig.styleSelect = index
             upView()
             styleAdapter.notifyItemChanged(oldIndex)
             styleAdapter.notifyItemChanged(index)
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
         }
     }
 
     private fun showBgTextConfig(index: Int): Boolean {
         dismissAllowingStateLoss()
-        changeBg(index)
+        changeBgTextConfig(index)
         callBack?.showBgTextConfig()
         return true
     }
@@ -200,7 +200,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
     override fun selectFont(path: String) {
         if (path != ReadBookConfig.textFont) {
             ReadBookConfig.textFont = path
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
     }
 
@@ -235,7 +235,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
             binding.apply {
                 ivStyle.setOnClickListener {
                     if (ivStyle.isInView) {
-                        changeBg(holder.layoutPosition)
+                        changeBgTextConfig(holder.layoutPosition)
                     }
                 }
                 ivStyle.onLongClick(ivStyle.isInView) {

+ 20 - 15
app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt

@@ -11,7 +11,12 @@ import io.legado.app.databinding.DialogTipConfigBinding
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadTipConfig
 import io.legado.app.lib.dialogs.selector
-import io.legado.app.utils.*
+import io.legado.app.utils.checkByIndex
+import io.legado.app.utils.getIndexById
+import io.legado.app.utils.hexString
+import io.legado.app.utils.observeEvent
+import io.legado.app.utils.postEvent
+import io.legado.app.utils.setLayout
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 
 
@@ -89,26 +94,26 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
     private fun initEvent() = binding.run {
         rgTitleMode.setOnCheckedChangeListener { _, checkedId ->
             ReadBookConfig.titleMode = rgTitleMode.getIndexById(checkedId)
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(5))
         }
         dsbTitleSize.onChanged = {
             ReadBookConfig.titleSize = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
         dsbTitleTop.onChanged = {
             ReadBookConfig.titleTopSpacing = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
         dsbTitleBottom.onChanged = {
             ReadBookConfig.titleBottomSpacing = it
-            postEvent(EventBus.UP_CONFIG, true)
+            postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
         }
         llHeaderShow.setOnClickListener {
             val headerModes = ReadTipConfig.getHeaderModes(requireContext())
             context?.selector(items = headerModes.values.toList()) { _, i ->
                 ReadTipConfig.headerMode = headerModes.keys.toList()[i]
                 tvHeaderShow.text = headerModes[ReadTipConfig.headerMode]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llFooterShow.setOnClickListener {
@@ -116,7 +121,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
             context?.selector(items = footerModes.values.toList()) { _, i ->
                 ReadTipConfig.footerMode = footerModes.keys.toList()[i]
                 tvFooterShow.text = footerModes[ReadTipConfig.footerMode]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llHeaderLeft.setOnClickListener {
@@ -125,7 +130,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                 clearRepeat(tipValue)
                 ReadTipConfig.tipHeaderLeft = tipValue
                 tvHeaderLeft.text = ReadTipConfig.tipNames[i]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llHeaderMiddle.setOnClickListener {
@@ -134,7 +139,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                 clearRepeat(tipValue)
                 ReadTipConfig.tipHeaderMiddle = tipValue
                 tvHeaderMiddle.text = ReadTipConfig.tipNames[i]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llHeaderRight.setOnClickListener {
@@ -143,7 +148,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                 clearRepeat(tipValue)
                 ReadTipConfig.tipHeaderRight = tipValue
                 tvHeaderRight.text = ReadTipConfig.tipNames[i]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llFooterLeft.setOnClickListener {
@@ -152,7 +157,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                 clearRepeat(tipValue)
                 ReadTipConfig.tipFooterLeft = tipValue
                 tvFooterLeft.text = ReadTipConfig.tipNames[i]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llFooterMiddle.setOnClickListener {
@@ -161,7 +166,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                 clearRepeat(tipValue)
                 ReadTipConfig.tipFooterMiddle = tipValue
                 tvFooterMiddle.text = ReadTipConfig.tipNames[i]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llFooterRight.setOnClickListener {
@@ -170,7 +175,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                 clearRepeat(tipValue)
                 ReadTipConfig.tipFooterRight = tipValue
                 tvFooterRight.text = ReadTipConfig.tipNames[i]
-                postEvent(EventBus.UP_CONFIG, true)
+                postEvent(EventBus.UP_CONFIG, arrayOf(2))
             }
         }
         llTipColor.setOnClickListener {
@@ -179,7 +184,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                     0 -> {
                         ReadTipConfig.tipColor = 0
                         upTvTipColor()
-                        postEvent(EventBus.UP_CONFIG, true)
+                        postEvent(EventBus.UP_CONFIG, arrayOf(2))
                     }
                     1 -> ColorPickerDialog.newBuilder()
                         .setShowAlphaSlider(false)
@@ -195,7 +200,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
                     0, 1 -> {
                         ReadTipConfig.tipDividerColor = i - 1
                         upTvTipDividerColor()
-                        postEvent(EventBus.UP_CONFIG, true)
+                        postEvent(EventBus.UP_CONFIG, arrayOf(2))
                     }
                     2 -> ColorPickerDialog.newBuilder()
                         .setShowAlphaSlider(false)

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

@@ -82,7 +82,6 @@ class AutoPager(private val readView: ReadView) {
         }
 
         if (readView.isScroll) {
-            computeOffset()
             if (!isPausing) readView.curPage.scroll(-scrollOffset)
         } else {
             val bottom = progress
@@ -112,13 +111,15 @@ class AutoPager(private val readView: ReadView) {
                 bottom.toFloat(),
                 paint
             )
-            if (!isPausing) readView.invalidate()
-            computeOffset()
+            if (!isPausing) readView.postInvalidate()
         }
 
     }
 
-    private fun computeOffset() {
+    fun computeOffset() {
+        if (!isRunning) {
+            return
+        }
 
         val currentTime = SystemClock.uptimeMillis()
         val elapsedTime = currentTime - lastTimeMillis

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

@@ -22,7 +22,6 @@ import io.legado.app.ui.book.read.page.entities.column.TextColumn
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
 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.showDialogFragment
@@ -57,6 +56,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
     private val pageDelegate get() = callBack.pageDelegate
     private var pageOffset = 0
     private var autoPager: AutoPager? = null
+    private var isScroll = false
     private val renderRunnable by lazy { Runnable { preRenderPage() } }
 
     //绘制图片的paint
@@ -75,14 +75,17 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
      */
     fun setContent(textPage: TextPage) {
         this.textPage = textPage
-        imagePaint.isAntiAlias = AppConfig.useAntiAlias
-        invalidate()
+        // 非滑动翻页动画需要同步重绘,不然翻页可能会出现闪烁
+        if (isScroll) {
+            postInvalidate()
+        } else {
+            invalidate()
+        }
     }
 
     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
         super.onSizeChanged(w, h, oldw, oldh)
-        if (!isMainView) return
-        ChapterProvider.upViewSize(w, h)
+        ChapterProvider.upViewSize(w, h, isMainView)
         textPage.format()
     }
 
@@ -107,16 +110,21 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         //滚动翻页
         if (!pageFactory.hasNext()) return
         val textPage1 = relativePage(1)
-        relativeOffset = relativeOffset(1)
+        relativeOffset += textPage.height
         textPage1.draw(this, canvas, relativeOffset)
         if (!pageFactory.hasNextPlus()) return
-        relativeOffset = relativeOffset(2)
+        relativeOffset += textPage1.height
         if (relativeOffset < ChapterProvider.visibleHeight) {
             val textPage2 = relativePage(2)
             textPage2.draw(this, canvas, relativeOffset)
         }
     }
 
+    override fun computeScroll() {
+        pageDelegate?.computeScroll()
+        autoPager?.computeOffset()
+    }
+
     /**
      * 滚动事件
      * pageOffset 向上滚动 减小 向下滚动 增大
@@ -125,7 +133,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
      * pageOffset + textPage.height 为 textPage 下方的高度
      */
     fun scroll(mOffset: Int) {
-        if (mOffset == 0) return
         pageOffset += mOffset
         if (longScreenshot) {
             scrollY += -mOffset
@@ -156,33 +163,34 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
                 pageDelegate?.abortAnim()
             }
         }
-        invalidate()
+        postInvalidate()
     }
 
-    fun submitPreRenderTask() {
-        if (PictureMirror.atLeastApi23) {
-            renderThread.submit(renderRunnable)
-        }
+    fun submitRenderTask() {
+        renderThread.submit(renderRunnable)
     }
 
     private fun preRenderPage() {
         val view = this
         var invalidate = false
         pageFactory.run {
-            hasPrev() && prevPage.preRender(view)
-            if (curPage.preRender(view)) {
+            if (hasPrev() && prevPage.render(view)) {
                 invalidate = true
             }
-            if (hasNext() && nextPage.preRender(view) && callBack.isScroll) {
+            if (curPage.render(view)) {
                 invalidate = true
             }
-            if (hasNextPlus() && nextPlusPage.preRender(view) && callBack.isScroll
+            if (hasNext() && nextPage.render(view) && callBack.isScroll) {
+                invalidate = true
+            }
+            if (hasNextPlus() && nextPlusPage.render(view) && callBack.isScroll
                 && relativeOffset(2) < ChapterProvider.visibleHeight
             ) {
                 invalidate = true
             }
             if (invalidate) {
                 postInvalidate()
+                pageDelegate?.postInvalidate()
             }
         }
     }
@@ -254,7 +262,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         touchRough(x, y) { _, textPos, _, _, column ->
             if (column is TextColumn) {
                 column.selected = true
-                invalidate()
                 select(textPos)
             }
         }
@@ -557,7 +564,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
                 }
             }
         }
-        invalidate()
+        // 由后台线程完成渲染后通知视图重绘
+        submitRenderTask()
     }
 
     private fun upSelectedStart(x: Float, y: Float, top: Float) {
@@ -681,6 +689,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         this.autoPager = autoPager
     }
 
+    fun setIsScroll(value: Boolean) {
+        isScroll = value
+    }
+
     override fun canScrollVertically(direction: Int): Boolean {
         return callBack.isScroll && pageFactory.hasNext()
     }

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

@@ -321,7 +321,12 @@ class PageView(context: Context) : FrameLayout(context) {
     }
 
     fun submitPreRenderTask() {
-        binding.contentTextView.submitPreRenderTask()
+        binding.contentTextView.submitRenderTask()
+    }
+
+    fun setIsScroll(value: Boolean) {
+        isScroll = value
+        binding.contentTextView.setIsScroll(value)
     }
 
     /**

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

@@ -148,6 +148,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
 
     override fun computeScroll() {
         pageDelegate?.computeScroll()
+        autoPager.computeOffset()
     }
 
     override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
@@ -510,7 +511,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
         } else {
             curPage.setAutoPager(null)
         }
-        curPage.isScroll = isScroll
+        curPage.setIsScroll(isScroll)
     }
 
     /**
@@ -634,6 +635,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
             nextPage.invalidateAll()
             nextPlusPage.invalidateAll()
         }
+        upContent()
     }
 
     fun onScrollAnimStart() {

+ 15 - 56
app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt

@@ -1,10 +1,7 @@
 package io.legado.app.ui.book.read.page.delegate
 
 import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Picture
 import android.graphics.drawable.GradientDrawable
-import android.os.Build
 import androidx.core.graphics.withClip
 import androidx.core.graphics.withTranslation
 import io.legado.app.ui.book.read.page.ReadView
@@ -12,26 +9,14 @@ import io.legado.app.ui.book.read.page.entities.PageDirection
 import io.legado.app.utils.screenshot
 
 class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
-    private val bitmapMatrix = Matrix()
     private val shadowDrawableR: GradientDrawable
 
-    private lateinit var curPicture: Picture
-    private lateinit var prevPicture: Picture
-    private lateinit var nextPicture: Picture
-
-    private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-
     init {
         val shadowColors = intArrayOf(0x66111111, 0x00000000)
         shadowDrawableR = GradientDrawable(
             GradientDrawable.Orientation.LEFT_RIGHT, shadowColors
         )
         shadowDrawableR.gradientType = GradientDrawable.LINEAR_GRADIENT
-        if (atLeastApi23) {
-            curPicture = Picture()
-            prevPicture = Picture()
-            nextPicture = Picture()
-        }
     }
 
     override fun onDraw(canvas: Canvas) {
@@ -47,42 +32,21 @@ class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
         val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
         if (mDirection == PageDirection.PREV) {
             if (offsetX <= viewWidth) {
-                if (!atLeastApi23) {
-                    bitmapMatrix.setTranslate(distanceX, 0.toFloat())
-                    prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
-                } else {
-                    canvas.withTranslation(distanceX) {
-                        drawPicture(prevPicture)
-                    }
+                canvas.withTranslation(distanceX) {
+                    prevRecorder.draw(canvas)
                 }
                 addShadow(distanceX, canvas)
             } else {
-                if (!atLeastApi23) {
-                    prevBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
-                } else {
-                    canvas.drawPicture(prevPicture)
-                }
+                prevRecorder.draw(canvas)
             }
         } else if (mDirection == PageDirection.NEXT) {
-            if (!atLeastApi23) {
-                bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat())
-                nextBitmap?.let {
-                    val width = it.width.toFloat()
-                    val height = it.height.toFloat()
-                    canvas.withClip(width + offsetX, 0f, width, height) {
-                        drawBitmap(it, 0f, 0f, null)
-                    }
-                }
-                curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
-            } else {
-                val width = nextPicture.width.toFloat()
-                val height = nextPicture.height.toFloat()
-                canvas.withClip(width + offsetX, 0f, width, height) {
-                    drawPicture(nextPicture)
-                }
-                canvas.withTranslation(distanceX - viewWidth) {
-                    drawPicture(curPicture)
-                }
+            val width = nextRecorder.width.toFloat()
+            val height = nextRecorder.height.toFloat()
+            canvas.withClip(width + offsetX, 0f, width, height) {
+                nextRecorder.draw(this)
+            }
+            canvas.withTranslation(distanceX - viewWidth) {
+                curRecorder.draw(this)
             }
             addShadow(distanceX, canvas)
         }
@@ -90,18 +54,13 @@ class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
 
     override fun setBitmap() {
         when (mDirection) {
-            PageDirection.PREV -> if (!atLeastApi23) {
-                prevBitmap = prevPage.screenshot(prevBitmap, canvas)
-            } else {
-                prevPage.screenshot(prevPicture)
+            PageDirection.PREV -> {
+                prevPage.screenshot(prevRecorder)
             }
 
-            PageDirection.NEXT -> if (!atLeastApi23) {
-                nextBitmap = nextPage.screenshot(nextBitmap, canvas)
-                curBitmap = curPage.screenshot(curBitmap, canvas)
-            } else {
-                nextPage.screenshot(nextPicture)
-                curPage.screenshot(curPicture)
+            PageDirection.NEXT -> {
+                nextPage.screenshot(nextRecorder)
+                curPage.screenshot(curRecorder)
             }
 
             else -> Unit

+ 11 - 16
app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt

@@ -1,18 +1,16 @@
 package io.legado.app.ui.book.read.page.delegate
 
-import android.graphics.Bitmap
-import android.graphics.Canvas
 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
 
 abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readView) {
 
-    protected var curBitmap: Bitmap? = null
-    protected var prevBitmap: Bitmap? = null
-    protected var nextBitmap: Bitmap? = null
-    protected var canvas: Canvas = Canvas()
+    protected val curRecorder = CanvasRecorderFactory.create()
+    protected val prevRecorder = CanvasRecorderFactory.create()
+    protected val nextRecorder = CanvasRecorderFactory.create()
     private val slopSquare get() = readView.pageSlopSquare2
 
     override fun setDirection(direction: PageDirection) {
@@ -23,13 +21,13 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
     open fun setBitmap() {
         when (mDirection) {
             PageDirection.PREV -> {
-                prevBitmap = prevPage.screenshot(prevBitmap, canvas)
-                curBitmap = curPage.screenshot(curBitmap, canvas)
+                prevPage.screenshot(prevRecorder)
+                curPage.screenshot(curRecorder)
             }
 
             PageDirection.NEXT -> {
-                nextBitmap = nextPage.screenshot(nextBitmap, canvas)
-                curBitmap = curPage.screenshot(curBitmap, canvas)
+                nextPage.screenshot(nextRecorder)
+                curPage.screenshot(curRecorder)
             }
 
             else -> Unit
@@ -140,12 +138,9 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
 
     override fun onDestroy() {
         super.onDestroy()
-        prevBitmap?.recycle()
-        prevBitmap = null
-        curBitmap?.recycle()
-        curBitmap = null
-        nextBitmap?.recycle()
-        nextBitmap = null
+        prevRecorder.recycle()
+        curRecorder.recycle()
+        nextRecorder.recycle()
     }
 
 }

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

@@ -96,7 +96,7 @@ abstract class PageDelegate(protected val readView: ReadView) {
         viewHeight = height
     }
 
-    fun computeScroll() {
+    open fun computeScroll() {
         if (scroller.computeScrollOffset()) {
             readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
         } else if (isStarted) {
@@ -190,6 +190,15 @@ abstract class PageDelegate(protected val readView: ReadView) {
         }
     }
 
+    fun postInvalidate() {
+        if (isRunning && this is HorizontalPageDelegate) {
+            readView.post {
+                setBitmap()
+                readView.invalidate()
+            }
+        }
+    }
+
     open fun onDestroy() {
         // run on destroy
     }

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

@@ -79,7 +79,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
         val pointX = event.getX(event.pointerCount - 1)
         val pointY = event.getY(event.pointerCount - 1)
         if (isMoved) {
-            readView.setTouchPoint(pointX, pointY)
+            readView.setTouchPoint(pointX, pointY, false)
         }
         if (!isMoved) {
             val deltaX = (pointX - startX).toInt()
@@ -95,12 +95,22 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
         }
     }
 
+    override fun computeScroll() {
+        if (scroller.computeScrollOffset()) {
+            readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat(), false)
+        } else if (isStarted) {
+            onAnimStop()
+            stopScroll()
+        }
+    }
+
     override fun onDestroy() {
         super.onDestroy()
         mVelocity.recycle()
     }
 
     override fun abortAnim() {
+        readView.onScrollAnimStop()
         isStarted = false
         isMoved = false
         isRunning = false

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

@@ -1,13 +1,27 @@
 package io.legado.app.ui.book.read.page.delegate
 
-import android.graphics.*
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PointF
+import android.graphics.Region
 import android.graphics.drawable.GradientDrawable
 import android.os.Build
 import android.view.MotionEvent
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.book.read.page.ReadView
 import io.legado.app.ui.book.read.page.entities.PageDirection
-import kotlin.math.*
+import io.legado.app.utils.screenshot
+import kotlin.math.abs
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.hypot
+import kotlin.math.min
+import kotlin.math.sin
 
 @Suppress("DEPRECATION")
 class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
@@ -86,6 +100,11 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
 
     private val mPaint: Paint = Paint().apply { style = Paint.Style.FILL }
 
+    private var curBitmap: Bitmap? = null
+    private var prevBitmap: Bitmap? = null
+    private var nextBitmap: Bitmap? = null
+    private var canvas: Canvas = Canvas()
+
     init {
         //设置颜色数组
         val color = intArrayOf(0x333333, -0x4fcccccd)
@@ -122,6 +141,22 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
         mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT
     }
 
+    override fun setBitmap() {
+        when (mDirection) {
+            PageDirection.PREV -> {
+                prevBitmap = prevPage.screenshot(prevBitmap, canvas)
+                curBitmap = curPage.screenshot(curBitmap, canvas)
+            }
+
+            PageDirection.NEXT -> {
+                nextBitmap = nextPage.screenshot(nextBitmap, canvas)
+                curBitmap = curPage.screenshot(curBitmap, canvas)
+            }
+
+            else -> Unit
+        }
+    }
+
     override fun setViewSize(width: Int, height: Int) {
         super.setViewSize(width, height)
         mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
@@ -133,6 +168,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
             MotionEvent.ACTION_DOWN -> {
                 calcCornerXY(event.x, event.y)
             }
+
             MotionEvent.ACTION_MOVE -> {
                 if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3)
                     || mDirection == PageDirection.PREV
@@ -159,10 +195,12 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
                 } else {
                     calcCornerXY(viewWidth - startX, viewHeight.toFloat())
                 }
+
             PageDirection.NEXT ->
                 if (viewWidth / 2 > startX) {
                     calcCornerXY(viewWidth - startX, startY)
                 }
+
             else -> Unit
         }
     }
@@ -216,6 +254,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
                 drawCurrentPageShadow(canvas)
                 drawCurrentBackArea(canvas, curBitmap)
             }
+
             PageDirection.PREV -> {
                 calcPoints()
                 drawCurrentPageArea(canvas, prevBitmap)
@@ -223,6 +262,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
                 drawCurrentPageShadow(canvas)
                 drawCurrentBackArea(canvas, prevBitmap)
             }
+
             else -> return
         }
     }

+ 10 - 63
app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt

@@ -1,51 +1,12 @@
 package io.legado.app.ui.book.read.page.delegate
 
 import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Picture
-import android.os.Build
 import androidx.core.graphics.withTranslation
 import io.legado.app.ui.book.read.page.ReadView
 import io.legado.app.ui.book.read.page.entities.PageDirection
-import io.legado.app.utils.screenshot
 
 class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
 
-    private val bitmapMatrix = Matrix()
-
-    private lateinit var curPicture: Picture
-    private lateinit var prevPicture: Picture
-    private lateinit var nextPicture: Picture
-
-    private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-
-    init {
-        if (atLeastApi23) {
-            curPicture = Picture()
-            prevPicture = Picture()
-            nextPicture = Picture()
-        }
-    }
-
-    override fun setBitmap() {
-        if (!atLeastApi23) {
-            return super.setBitmap()
-        }
-        when (mDirection) {
-            PageDirection.PREV -> {
-                prevPage.screenshot(prevPicture)
-                curPage.screenshot(curPicture)
-            }
-
-            PageDirection.NEXT -> {
-                nextPage.screenshot(nextPicture)
-                curPage.screenshot(curPicture)
-            }
-
-            else -> Unit
-        }
-    }
-
     override fun onAnimStart(animationSpeed: Int) {
         val distanceX: Float
         when (mDirection) {
@@ -79,32 +40,18 @@ class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
         val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
         if (!isRunning) return
         if (mDirection == PageDirection.PREV) {
-            if (!atLeastApi23) {
-                bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat())
-                curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
-                bitmapMatrix.setTranslate(distanceX, 0.toFloat())
-                prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
-            } else {
-                canvas.withTranslation(distanceX + viewWidth) {
-                    drawPicture(curPicture)
-                }
-                canvas.withTranslation(distanceX) {
-                    drawPicture(prevPicture)
-                }
+            canvas.withTranslation(distanceX + viewWidth) {
+                curRecorder.draw(this)
+            }
+            canvas.withTranslation(distanceX) {
+                prevRecorder.draw(this)
             }
         } else if (mDirection == PageDirection.NEXT) {
-            if (!atLeastApi23) {
-                bitmapMatrix.setTranslate(distanceX, 0.toFloat())
-                nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
-                bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat())
-                curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
-            } else {
-                canvas.withTranslation(distanceX) {
-                    drawPicture(nextPicture)
-                }
-                canvas.withTranslation(distanceX - viewWidth) {
-                    drawPicture(curPicture)
-                }
+            canvas.withTranslation(distanceX) {
+                nextRecorder.draw(this)
+            }
+            canvas.withTranslation(distanceX - viewWidth) {
+                curRecorder.draw(this)
             }
         }
     }

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

@@ -10,7 +10,8 @@ import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.entities.TextPage.Companion.emptyTextPage
 import io.legado.app.ui.book.read.page.entities.column.BaseColumn
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
-import io.legado.app.utils.PictureMirror
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
+import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
 import io.legado.app.utils.dpToPx
 
 /**
@@ -39,7 +40,7 @@ data class TextLine(
     val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
     val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
     val height: Float inline get() = lineBottom - lineTop
-    val pictureMirror: PictureMirror = PictureMirror()
+    val canvasRecorder = CanvasRecorderFactory.create()
     var isReadAloud: Boolean = false
         set(value) {
             if (field != value) {
@@ -122,7 +123,7 @@ data class TextLine(
     }
 
     fun draw(view: ContentTextView, canvas: Canvas) {
-        pictureMirror.draw(canvas, view.width, height.toInt()) {
+        canvasRecorder.recordIfNeededThenDraw(canvas, view.width, height.toInt()) {
             drawTextLine(view, this)
         }
     }
@@ -156,11 +157,11 @@ data class TextLine(
     }
 
     fun invalidateSelf() {
-        pictureMirror.invalidate()
+        canvasRecorder.invalidate()
     }
 
-    fun recyclePicture() {
-        pictureMirror.recycle()
+    fun recycleRecorder() {
+        canvasRecorder.recycle()
     }
 
     companion object {

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

@@ -11,7 +11,8 @@ import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.entities.column.TextColumn
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
-import io.legado.app.utils.PictureMirror
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
+import io.legado.app.utils.canvasrecorder.recordIfNeeded
 import splitties.init.appCtx
 import java.text.DecimalFormat
 import kotlin.math.min
@@ -43,7 +44,7 @@ data class TextPage(
     val charSize: Int get() = text.length.coerceAtLeast(1)
     val searchResult = hashSetOf<TextColumn>()
     var isMsgPage: Boolean = false
-    var pictureMirror: PictureMirror = PictureMirror()
+    var canvasRecorder = CanvasRecorderFactory.create(true)
     var doublePage = false
     var paddingTop = 0
 
@@ -267,11 +268,9 @@ data class TextPage(
     }
 
     fun draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) {
-        val height = height.toInt()
+        render(view)
         canvas.withTranslation(0f, relativeOffset + paddingTop) {
-            pictureMirror.drawLocked(canvas, view.width, height) {
-                drawPage(view, this)
-            }
+            canvasRecorder.draw(this)
         }
     }
 
@@ -284,20 +283,14 @@ data class TextPage(
         }
     }
 
-    fun preRender(view: ContentTextView): Boolean {
-        if (!pictureMirror.isDirty) return false
-        pictureMirror.drawLocked(null, view.width, height.toInt()) {
+    fun render(view: ContentTextView): Boolean {
+        return canvasRecorder.recordIfNeeded(view.width, height.toInt()) {
             drawPage(view, this)
         }
-        return true
-    }
-
-    fun isDirty(): Boolean {
-        return pictureMirror.isDirty
     }
 
     fun invalidate() {
-        pictureMirror.invalidate()
+        canvasRecorder.invalidate()
     }
 
     fun invalidateAll() {
@@ -307,10 +300,10 @@ data class TextPage(
         invalidate()
     }
 
-    fun recyclePictures() {
-        pictureMirror.recycle()
+    fun recycleRecorders() {
+        canvasRecorder.recycle()
         for (i in lines.indices) {
-            lines[i].recyclePicture()
+            lines[i].recycleRecorder()
         }
     }
 

+ 7 - 7
app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt

@@ -42,15 +42,15 @@ data class TextColumn(
         } else {
             ChapterProvider.contentPaint
         }
-        val textColor = if (textLine.isReadAloud || isSearchResult) {
-            ThemeStore.accentColor
+        if (textLine.isReadAloud || isSearchResult) {
+            synchronized(textPaint) {
+                textPaint.color = ThemeStore.accentColor
+                canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
+                textPaint.color = ReadBookConfig.textColor
+            }
         } else {
-            ReadBookConfig.textColor
-        }
-        if (textPaint.color != textColor) {
-            textPaint.color = textColor
+            canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
         }
-        canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
         if (selected) {
             canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
         }

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

@@ -690,39 +690,6 @@ object ChapterProvider {
         exceed(absStartX, textLine, words)
     }
 
-    fun getStringArrayAndTextWidths(
-        text: String,
-        textWidths: List<Float>,
-        textPaint: TextPaint
-    ): Pair<List<String>, List<Float>> {
-        val charArray = text.toCharArray()
-        val strList = ArrayList<String>(text.length)
-        val textWidthList = ArrayList<Float>(text.length)
-        val lastIndex = charArray.lastIndex
-        var ca: CharArray? = null
-        for (i in textWidths.indices) {
-            if (charArray[i].isLowSurrogate()) {
-                continue
-            }
-            val char = if (i + 1 <= lastIndex && charArray[i + 1].isLowSurrogate()) {
-                if (ca == null) ca = CharArray(2)
-                System.arraycopy(charArray, i, ca, 0, 2)
-                String(ca)
-            } else {
-                charArray[i].toString()
-            }
-            val w = textWidths[i]
-            if (w == 0f && textWidthList.size > 0) {
-                textWidthList[textWidthList.lastIndex] = textPaint.measureText(strList.last())
-                textWidthList.add(textPaint.measureText(char))
-            } else {
-                textWidthList.add(w)
-            }
-            strList.add(char)
-        }
-        return strList to textWidthList
-    }
-
     /**
      * 添加字符
      */
@@ -883,12 +850,12 @@ object ChapterProvider {
     /**
      * 更新View尺寸
      */
-    fun upViewSize(width: Int, height: Int) {
+    fun upViewSize(width: Int, height: Int, postEvent: Boolean) {
         if (width > 0 && height > 0 && (width != viewWidth || height != viewHeight)) {
             viewWidth = width
             viewHeight = height
             upLayout()
-            postEvent(EventBus.UP_CONFIG, true)
+            if (postEvent) postEvent(EventBus.UP_CONFIG, arrayOf(2))
         }
     }
 

+ 4 - 7
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt

@@ -3,13 +3,11 @@ package io.legado.app.ui.book.read.page.provider
 import android.text.TextPaint
 import android.util.SparseArray
 import androidx.core.util.getOrDefault
-import java.util.BitSet
 import kotlin.math.ceil
 
 class TextMeasure(private var paint: TextPaint) {
 
     private var chineseCommonWidth = paint.measureText("一")
-    private val chineseCommonWidthBitSet = BitSet()
     private val asciiWidths = FloatArray(128) { -1f }
     private val codePointWidths = SparseArray<Float>()
 
@@ -17,7 +15,8 @@ class TextMeasure(private var paint: TextPaint) {
         if (codePoint < 128) {
             return asciiWidths[codePoint]
         }
-        if (chineseCommonWidthBitSet[codePoint]) {
+        // 中文 Unicode 范围 U+4E00 - U+9FA5
+        if (codePoint in 19968 .. 40869) {
             return chineseCommonWidth
         }
         return codePointWidths.getOrDefault(codePoint, -1f)
@@ -33,7 +32,8 @@ class TextMeasure(private var paint: TextPaint) {
             if (charArray[i].isLowSurrogate()) continue
             val width = ceil(widths[i])
             widthsList.add(width)
-            if (width == 0f && widthsList.size > 0) {
+            // 可能需要检查是否不可见字符
+            if (width == 0f && widthsList.size > 1) {
                 val lastIndex = widthsList.lastIndex
                 buf[0] = codePoints[lastIndex - 1]
                 widthsList[lastIndex - 1] = paint.measureText(String(buf, 0, 1))
@@ -46,8 +46,6 @@ class TextMeasure(private var paint: TextPaint) {
             val width = widthsList[i]
             if (codePoint < 128) {
                 asciiWidths[codePoint] = width
-            } else if (width == chineseCommonWidth) {
-                chineseCommonWidthBitSet.set(codePoint)
             } else {
                 codePointWidths[codePoint] = width
             }
@@ -138,7 +136,6 @@ class TextMeasure(private var paint: TextPaint) {
 
     private fun invalidate() {
         chineseCommonWidth = paint.measureText("一")
-        chineseCommonWidthBitSet.clear()
         codePointWidths.clear()
         asciiWidths.fill(-1f)
     }

+ 37 - 25
app/src/main/java/io/legado/app/ui/widget/BatteryView.kt

@@ -10,6 +10,8 @@ import android.text.StaticLayout
 import android.util.AttributeSet
 import androidx.annotation.ColorInt
 import androidx.appcompat.widget.AppCompatTextView
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
+import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
 import io.legado.app.utils.dpToPx
 
 class BatteryView @JvmOverloads constructor(
@@ -22,6 +24,7 @@ class BatteryView @JvmOverloads constructor(
     private val batteryPaint = Paint()
     private val outFrame = Rect()
     private val polar = Rect()
+    private val canvasRecorder = CanvasRecorderFactory.create()
     var isBattery = false
         set(value) {
             field = value
@@ -62,31 +65,40 @@ class BatteryView @JvmOverloads constructor(
     }
 
     override fun onDraw(canvas: Canvas) {
-        super.onDraw(canvas)
-        if (!isBattery) return
-        layout.getLineBounds(0, outFrame)
-        val batteryStart = layout
-            .getPrimaryHorizontal(text.length - battery.toString().length)
-            .toInt() + 2.dpToPx()
-        val batteryEnd = batteryStart +
-                StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx()
-        outFrame.set(
-            batteryStart,
-            2.dpToPx(),
-            batteryEnd,
-            height - 2.dpToPx()
-        )
-        val dj = (outFrame.bottom - outFrame.top) / 3
-        polar.set(
-            batteryEnd,
-            outFrame.top + dj,
-            batteryEnd + 2.dpToPx(),
-            outFrame.bottom - dj
-        )
-        batteryPaint.style = Paint.Style.STROKE
-        canvas.drawRect(outFrame, batteryPaint)
-        batteryPaint.style = Paint.Style.FILL
-        canvas.drawRect(polar, batteryPaint)
+        canvasRecorder.recordIfNeededThenDraw(canvas, width, height) {
+            super.onDraw(this)
+            if (!isBattery) return@recordIfNeededThenDraw
+            layout.getLineBounds(0, outFrame)
+            val batteryStart = layout
+                .getPrimaryHorizontal(text.length - battery.toString().length)
+                .toInt() + 2.dpToPx()
+            val batteryEnd = batteryStart +
+                    StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx()
+            outFrame.set(
+                batteryStart,
+                2.dpToPx(),
+                batteryEnd,
+                height - 2.dpToPx()
+            )
+            val dj = (outFrame.bottom - outFrame.top) / 3
+            polar.set(
+                batteryEnd,
+                outFrame.top + dj,
+                batteryEnd + 2.dpToPx(),
+                outFrame.bottom - dj
+            )
+            batteryPaint.style = Paint.Style.STROKE
+            drawRect(outFrame, batteryPaint)
+            batteryPaint.style = Paint.Style.FILL
+            drawRect(polar, batteryPaint)
+        }
+    }
+
+    override fun invalidate() {
+        super.invalidate()
+        kotlin.runCatching {
+            canvasRecorder.invalidate()
+        }
     }
 
 }

+ 10 - 17
app/src/main/java/io/legado/app/utils/ChineseUtils.kt

@@ -32,26 +32,19 @@ object ChineseUtils {
     fun fixT2sDict() {
         val dict = DictionaryContainer.getInstance().getDictionary(TransType.TRADITIONAL_TO_SIMPLE)
         dict.run {
-            remove("劈")
-            remove("脊")
-            remove("支援")
-            remove("沈默")
-            remove("類比")
-            remove("模擬")
-            remove("划槳")
-            remove("列根")
-            remove("路易斯")
-            remove("非同步")
-            remove("出租车")
-            remove("周杰倫")
+            remove("劈", "脊")
+            remove("支援", "沈默", "類比", "模擬", "划槳", "列根", "先進")
+            remove("路易斯", "非同步", "出租车", "周杰倫")
         }
     }
 
-    fun BasicDictionary.remove(key: String) {
-        if (key.length == 1) {
-            chars.remove(key[0])
-        } else {
-            dict.remove(key)
+    fun BasicDictionary.remove(vararg keys: String) {
+        for (key in keys) {
+            if (key.length == 1) {
+                chars.remove(key[0])
+            } else {
+                dict.remove(key)
+            }
         }
     }
 

+ 0 - 88
app/src/main/java/io/legado/app/utils/PictureMirror.kt

@@ -1,88 +0,0 @@
-package io.legado.app.utils
-
-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 {
-    @Volatile
-    var picture: Picture? = null
-
-    @Volatile
-    var lock: ReentrantLock? = null
-
-    @Volatile
-    var isDirty = true
-
-    inline fun drawLocked(
-        canvas: Canvas?,
-        width: Int,
-        height: Int,
-        block: Canvas.() -> Unit
-    ) {
-        if (atLeastApi23) {
-            if (picture == null || lock == null) {
-                synchronized(this) {
-                    if (picture == null) {
-                        picture = Picture()
-                    }
-                    if (lock == null) {
-                        lock = ReentrantLock()
-                    }
-                }
-            }
-            val picture = picture!!
-            val lock = lock!!
-            if (isDirty) {
-                if (!lock.tryLock()) return
-                try {
-                    picture.record(width, height, block)
-                    isDirty = false
-                } finally {
-                    lock.unlock()
-                }
-            }
-            canvas?.drawPicture(picture)
-        } else {
-            canvas?.block()
-        }
-    }
-
-    /**
-     * 非线程安全,多线程调用可能会崩溃
-     */
-    inline fun draw(
-        canvas: Canvas?,
-        width: Int,
-        height: Int,
-        block: Canvas.() -> Unit
-    ) {
-        if (atLeastApi23) {
-            if (picture == null) picture = Picture()
-            val picture = picture!!
-            if (isDirty) {
-                picture.record(width, height, block)
-                isDirty = false
-            }
-            canvas?.drawPicture(picture)
-        } else {
-            canvas?.block()
-        }
-    }
-
-    fun invalidate() {
-        isDirty = true
-    }
-
-    fun recycle() {
-        picture = null
-        isDirty = true
-    }
-
-    companion object {
-        val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-    }
-
-}

+ 12 - 0
app/src/main/java/io/legado/app/utils/ViewExtensions.kt

@@ -37,6 +37,8 @@ import androidx.viewpager.widget.ViewPager
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.globalExecutor
 import io.legado.app.lib.theme.TintHelper
+import io.legado.app.utils.canvasrecorder.CanvasRecorder
+import io.legado.app.utils.canvasrecorder.record
 import splitties.systemservices.inputMethodManager
 import java.lang.reflect.Field
 
@@ -180,6 +182,16 @@ fun View.screenshot(picture: Picture) {
     }
 }
 
+fun View.screenshot(canvasRecorder: CanvasRecorder) {
+    if (width > 0 && height > 0) {
+        canvasRecorder.record(width, height) {
+            withTranslation(-scrollX.toFloat(), -scrollY.toFloat()) {
+                draw(this)
+            }
+        }
+    }
+}
+
 fun View.setPaddingBottom(bottom: Int) {
     setPadding(paddingLeft, paddingTop, paddingRight, bottom)
 }

+ 36 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt

@@ -0,0 +1,36 @@
+package io.legado.app.utils.canvasrecorder
+
+import androidx.annotation.CallSuper
+
+abstract class BaseCanvasRecorder : CanvasRecorder {
+
+    @JvmField
+    protected var isDirty = true
+
+    override fun invalidate() {
+        isDirty = true
+    }
+
+    @CallSuper
+    override fun recycle() {
+        isDirty = true
+    }
+
+    @CallSuper
+    override fun endRecording() {
+        isDirty = false
+    }
+
+    override fun isDirty(): Boolean {
+        return isDirty
+    }
+
+    override fun isLocked(): Boolean {
+        return false
+    }
+
+    override fun needRecord(): Boolean {
+        return isDirty() && !isLocked()
+    }
+
+}

+ 27 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt

@@ -0,0 +1,27 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+
+interface CanvasRecorder {
+
+    val width: Int
+
+    val height: Int
+
+    fun beginRecording(width: Int, height: Int): Canvas
+
+    fun endRecording()
+
+    fun draw(canvas: Canvas)
+
+    fun invalidate()
+
+    fun recycle()
+
+    fun isDirty(): Boolean
+
+    fun isLocked(): Boolean
+
+    fun needRecord(): Boolean
+
+}

+ 39 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt

@@ -0,0 +1,39 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import android.graphics.Picture
+
+class CanvasRecorderApi23Impl : BaseCanvasRecorder() {
+
+    private var picture: Picture? = null
+
+    override val width get() = picture?.width ?: -1
+    override val height get() = picture?.height ?: -1
+
+    private fun initPicture() {
+        if (picture == null) {
+            picture = Picture()
+        }
+    }
+
+    override fun beginRecording(width: Int, height: Int): Canvas {
+        initPicture()
+        return picture!!.beginRecording(width, height)
+    }
+
+    override fun endRecording() {
+        picture!!.endRecording()
+        super.endRecording()
+    }
+
+    override fun draw(canvas: Canvas) {
+        if (picture == null) return
+        canvas.drawPicture(picture!!)
+    }
+
+    override fun recycle() {
+        super.recycle()
+        picture = null
+    }
+
+}

+ 62 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt

@@ -0,0 +1,62 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import android.graphics.Picture
+import android.graphics.RenderNode
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.Q)
+class CanvasRecorderApi29Impl : BaseCanvasRecorder() {
+
+    private var renderNode: RenderNode? = null
+    private var picture: Picture? = null
+
+    override val width get() = renderNode?.width ?: -1
+    override val height get() = renderNode?.height ?: -1
+
+    private fun init() {
+        if (renderNode == null) {
+            renderNode = RenderNode("CanvasRecorder")
+        }
+        if (picture == null) {
+            picture = Picture()
+        }
+    }
+
+    override fun beginRecording(width: Int, height: Int): Canvas {
+        init()
+        renderNode!!.setPosition(0, 0, width, height)
+        return picture!!.beginRecording(width, height)
+    }
+
+    override fun endRecording() {
+        picture!!.endRecording()
+        val rc = renderNode!!.beginRecording()
+        rc.drawPicture(picture!!)
+        renderNode!!.endRecording()
+        super.endRecording()
+    }
+
+    override fun draw(canvas: Canvas) {
+        if (renderNode == null || picture == null) return
+        if (canvas.isHardwareAccelerated) {
+            if (!renderNode!!.hasDisplayList()) {
+                val rc = renderNode!!.beginRecording()
+                rc.drawPicture(picture!!)
+                renderNode!!.endRecording()
+            }
+            canvas.drawRenderNode(renderNode!!)
+        } else {
+            canvas.drawPicture(picture!!)
+        }
+    }
+
+    override fun recycle() {
+        super.recycle()
+        renderNode?.discardDisplayList()
+        renderNode = null
+        picture = null
+    }
+
+}

+ 35 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt

@@ -0,0 +1,35 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import androidx.core.graphics.withSave
+
+inline fun CanvasRecorder.recordIfNeeded(
+    width: Int,
+    height: Int,
+    block: Canvas.() -> Unit
+): Boolean {
+    if (!needRecord()) return false
+    record(width, height, block)
+    return true
+}
+
+inline fun CanvasRecorder.record(width: Int, height: Int, block: Canvas.() -> Unit) {
+    val canvas = beginRecording(width, height)
+    try {
+        canvas.withSave {
+            block()
+        }
+    } finally {
+        endRecording()
+    }
+}
+
+inline fun CanvasRecorder.recordIfNeededThenDraw(
+    canvas: Canvas,
+    width: Int,
+    height: Int,
+    block: Canvas.() -> Unit
+) {
+    recordIfNeeded(width, height, block)
+    draw(canvas)
+}

+ 23 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt

@@ -0,0 +1,23 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.os.Build
+
+object CanvasRecorderFactory {
+
+    private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+    private val atLeastApi29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+
+    fun create(locked: Boolean = false): CanvasRecorder {
+        val impl = when {
+            atLeastApi29 -> CanvasRecorderApi29Impl()
+            atLeastApi23 -> CanvasRecorderApi23Impl()
+            else -> CanvasRecorderImpl()
+        }
+        return if (locked) {
+            CanvasRecorderLocked(impl)
+        } else {
+            impl
+        }
+    }
+
+}

+ 59 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt

@@ -0,0 +1,59 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+
+class CanvasRecorderImpl : BaseCanvasRecorder() {
+
+    var bitmap: Bitmap? = null
+    var canvas: Canvas? = null
+
+    override val width get() = bitmap?.width ?: -1
+    override val height get() = bitmap?.height ?: -1
+
+    private fun init(width: Int, height: Int) {
+        if (bitmap == null) {
+            bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+        }
+        if (canvas == null) {
+            canvas = Canvas(bitmap!!)
+        }
+        if (bitmap!!.width != width || bitmap!!.height != height) {
+            if (canReconfigure(width, height)) {
+                bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888)
+            } else {
+                bitmap!!.recycle()
+                bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+            }
+            canvas!!.setBitmap(bitmap!!)
+        }
+    }
+
+    private fun canReconfigure(width: Int, height: Int): Boolean {
+        return bitmap!!.allocationByteCount >= width * height * 4
+    }
+
+    override fun beginRecording(width: Int, height: Int): Canvas {
+        init(width, height)
+        bitmap!!.eraseColor(Color.TRANSPARENT)
+        return canvas!!
+    }
+
+    override fun endRecording() {
+        bitmap!!.prepareToDraw()
+        super.endRecording()
+    }
+
+    override fun draw(canvas: Canvas) {
+        if (bitmap == null) return
+        canvas.drawBitmap(bitmap!!, 0f, 0f, null)
+    }
+
+    override fun recycle() {
+        super.recycle()
+        bitmap?.recycle()
+        bitmap = null
+    }
+
+}

+ 52 - 0
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt

@@ -0,0 +1,52 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import java.util.concurrent.locks.ReentrantLock
+
+class CanvasRecorderLocked(private val delegate: CanvasRecorder) :
+    CanvasRecorder by delegate {
+
+    var lock: ReentrantLock? = ReentrantLock()
+
+    private fun initLock() {
+        if (lock == null) {
+            synchronized(this) {
+                if (lock == null) {
+                    lock = ReentrantLock()
+                }
+            }
+        }
+    }
+
+    override fun beginRecording(width: Int, height: Int): Canvas {
+        initLock()
+        lock!!.lock()
+        return delegate.beginRecording(width, height)
+    }
+
+    override fun endRecording() {
+        delegate.endRecording()
+        lock!!.unlock()
+    }
+
+    override fun draw(canvas: Canvas) {
+        if (lock == null) return
+        if (!lock!!.tryLock()) return
+        try {
+            delegate.draw(canvas)
+        } finally {
+            lock!!.unlock()
+        }
+    }
+
+    override fun isLocked(): Boolean {
+        if (lock == null) return false
+        return lock!!.isLocked
+    }
+
+    override fun recycle() {
+        delegate.recycle()
+        lock = null
+    }
+
+}