Преглед на файлове

Merge branch 'gedoor:master' into master

huolibao преди 2 години
родител
ревизия
6cbe293a97
променени са 27 файла, в които са добавени 273 реда и са изтрити 65 реда
  1. 3 1
      app/src/main/java/io/legado/app/data/entities/BaseSource.kt
  2. 1 1
      app/src/main/java/io/legado/app/help/JsEncodeUtils.kt
  3. 17 0
      app/src/main/java/io/legado/app/help/PaintPool.kt
  4. 33 0
      app/src/main/java/io/legado/app/help/SymmetricCryptoAndroid.kt
  5. 4 1
      app/src/main/java/io/legado/app/help/config/ThemeConfig.kt
  6. 2 2
      app/src/main/java/io/legado/app/service/DownloadService.kt
  7. 1 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  8. 3 0
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  9. 73 2
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  10. 19 1
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  11. 16 7
      app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
  12. 2 0
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  13. 15 5
      app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt
  14. 2 1
      app/src/main/java/io/legado/app/utils/ContextExtensions.kt
  15. 5 0
      app/src/main/java/io/legado/app/utils/EncoderUtils.kt
  16. 1 1
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt
  17. 1 1
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt
  18. 4 5
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt
  19. 0 24
      app/src/main/java/io/legado/app/utils/canvasrecorder/objectpool/BaseObjectPool.kt
  20. 36 5
      app/src/main/java/io/legado/app/utils/canvasrecorder/pools/BitmapPool.kt
  21. 2 2
      app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt
  22. 2 2
      app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt
  23. 19 0
      app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt
  24. 9 0
      app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt
  25. 1 1
      app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt
  26. 1 1
      app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt
  27. 1 1
      app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt

+ 3 - 1
app/src/main/java/io/legado/app/data/entities/BaseSource.kt

@@ -8,6 +8,7 @@ import io.legado.app.constant.AppLog
 import io.legado.app.data.entities.rule.RowUi
 import io.legado.app.data.entities.rule.RowUi
 import io.legado.app.help.CacheManager
 import io.legado.app.help.CacheManager
 import io.legado.app.help.JsExtensions
 import io.legado.app.help.JsExtensions
+import io.legado.app.help.SymmetricCryptoAndroid
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.CookieStore
 import io.legado.app.help.http.CookieStore
 import io.legado.app.model.SharedJsScope
 import io.legado.app.model.SharedJsScope
@@ -106,6 +107,7 @@ interface BaseSource : JsExtensions {
                             it.lastIndexOf("<")
                             it.lastIndexOf("<")
                         )
                         )
                     ).toString()
                     ).toString()
+
                     else -> it
                     else -> it
                 }
                 }
             ).getOrNull()?.let { map ->
             ).getOrNull()?.let { map ->
@@ -176,7 +178,7 @@ interface BaseSource : JsExtensions {
     fun putLoginInfo(info: String): Boolean {
     fun putLoginInfo(info: String): Boolean {
         return try {
         return try {
             val key = (AppConst.androidId).encodeToByteArray(0, 16)
             val key = (AppConst.androidId).encodeToByteArray(0, 16)
-            val encodeStr = AES(key).encryptBase64(info)
+            val encodeStr = SymmetricCryptoAndroid("AES", key).encryptBase64(info)
             CacheManager.put("userInfo_${getKey()}", encodeStr)
             CacheManager.put("userInfo_${getKey()}", encodeStr)
             true
             true
         } catch (e: Exception) {
         } catch (e: Exception) {

+ 1 - 1
app/src/main/java/io/legado/app/help/JsEncodeUtils.kt

@@ -42,7 +42,7 @@ interface JsEncodeUtils {
         key: ByteArray?,
         key: ByteArray?,
         iv: ByteArray?
         iv: ByteArray?
     ): SymmetricCrypto {
     ): SymmetricCrypto {
-        val symmetricCrypto = SymmetricCrypto(transformation, key)
+        val symmetricCrypto = SymmetricCryptoAndroid(transformation, key)
         return if (iv != null && iv.isNotEmpty()) symmetricCrypto.setIv(iv) else symmetricCrypto
         return if (iv != null && iv.isNotEmpty()) symmetricCrypto.setIv(iv) else symmetricCrypto
     }
     }
 
 

+ 17 - 0
app/src/main/java/io/legado/app/help/PaintPool.kt

@@ -0,0 +1,17 @@
+package io.legado.app.help
+
+import android.graphics.Paint
+import io.legado.app.utils.objectpool.BaseSafeObjectPool
+
+object PaintPool : BaseSafeObjectPool<Paint>(8) {
+
+    private val emptyPaint = Paint()
+
+    override fun create(): Paint = Paint()
+
+    override fun recycle(target: Paint) {
+        target.set(emptyPaint)
+        super.recycle(target)
+    }
+
+}

+ 33 - 0
app/src/main/java/io/legado/app/help/SymmetricCryptoAndroid.kt

@@ -0,0 +1,33 @@
+package io.legado.app.help
+
+import cn.hutool.crypto.symmetric.SymmetricCrypto
+import io.legado.app.utils.EncoderUtils
+import java.io.InputStream
+import java.nio.charset.Charset
+
+class SymmetricCryptoAndroid(
+    algorithm: String,
+    key: ByteArray?,
+) : SymmetricCrypto(algorithm, key) {
+
+    override fun encryptBase64(data: ByteArray): String {
+        return EncoderUtils.base64Encode(encrypt(data))
+    }
+
+    override fun encryptBase64(data: String, charset: String?): String {
+        return EncoderUtils.base64Encode(encrypt(data, charset))
+    }
+
+    override fun encryptBase64(data: String, charset: Charset?): String {
+        return EncoderUtils.base64Encode(encrypt(data, charset))
+    }
+
+    override fun encryptBase64(data: String): String {
+        return EncoderUtils.base64Encode(encrypt(data))
+    }
+
+    override fun encryptBase64(data: InputStream): String {
+        return EncoderUtils.base64Encode(encrypt(data))
+    }
+
+}

+ 4 - 1
app/src/main/java/io/legado/app/help/config/ThemeConfig.kt

@@ -55,7 +55,6 @@ object ThemeConfig {
         initNightMode()
         initNightMode()
         BookCover.upDefaultCover()
         BookCover.upDefaultCover()
         postEvent(EventBus.RECREATE, "")
         postEvent(EventBus.RECREATE, "")
-        postEvent(EventBus.UP_CONFIG, arrayOf(2, 9))
     }
     }
 
 
     private fun initNightMode() {
     private fun initNightMode() {
@@ -74,10 +73,12 @@ object ThemeConfig {
                 context.getPrefString(PreferKey.bgImage),
                 context.getPrefString(PreferKey.bgImage),
                 context.getPrefInt(PreferKey.bgImageBlurring, 0)
                 context.getPrefInt(PreferKey.bgImageBlurring, 0)
             )
             )
+
             Theme.Dark -> Pair(
             Theme.Dark -> Pair(
                 context.getPrefString(PreferKey.bgImageN),
                 context.getPrefString(PreferKey.bgImageN),
                 context.getPrefInt(PreferKey.bgImageNBlurring, 0)
                 context.getPrefInt(PreferKey.bgImageNBlurring, 0)
             )
             )
+
             else -> null
             else -> null
         } ?: return null
         } ?: return null
         if (bgCfg.first.isNullOrBlank()) return null
         if (bgCfg.first.isNullOrBlank()) return null
@@ -218,6 +219,7 @@ object ThemeConfig {
                     .bottomBackground(Color.WHITE)
                     .bottomBackground(Color.WHITE)
                     .apply()
                     .apply()
             }
             }
+
             AppConfig.isNightTheme -> {
             AppConfig.isNightTheme -> {
                 val primary =
                 val primary =
                     getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_blue_grey_600))
                     getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_blue_grey_600))
@@ -238,6 +240,7 @@ object ThemeConfig {
                     .bottomBackground(ColorUtils.withAlpha(bBackground, 1f))
                     .bottomBackground(ColorUtils.withAlpha(bBackground, 1f))
                     .apply()
                     .apply()
             }
             }
+
             else -> {
             else -> {
                 val primary =
                 val primary =
                     getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_brown_500))
                     getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_brown_500))

+ 2 - 2
app/src/main/java/io/legado/app/service/DownloadService.kt

@@ -250,12 +250,12 @@ class DownloadService : BaseService() {
             .setSubText(getString(R.string.action_download))
             .setSubText(getString(R.string.action_download))
             .setContentTitle(content)
             .setContentTitle(content)
             .setContentIntent(
             .setContentIntent(
-                servicePendingIntent<DownloadService>(IntentAction.play) {
+                servicePendingIntent<DownloadService>(IntentAction.play, downloadId.toInt()) {
                     putExtra("downloadId", downloadId)
                     putExtra("downloadId", downloadId)
                 }
                 }
             )
             )
             .setDeleteIntent(
             .setDeleteIntent(
-                servicePendingIntent<DownloadService>(IntentAction.stop) {
+                servicePendingIntent<DownloadService>(IntentAction.stop, downloadId.toInt()) {
                     putExtra("downloadId", downloadId)
                     putExtra("downloadId", downloadId)
                 }
                 }
             )
             )

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

@@ -422,7 +422,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
         var curLine = curTextLines[lineIndex]
         var curLine = curTextLines[lineIndex]
         length = length - currentPage.text.length + curLine.text.length
         length = length - currentPage.text.length + curLine.text.length
         if (curLine.isParagraphEnd) length++
         if (curLine.isParagraphEnd) length++
-        while (length < contentPosition && lineIndex + 1 < curTextLines.size) {
+        while (length <= contentPosition && lineIndex + 1 < curTextLines.size) {
             lineIndex += 1
             lineIndex += 1
             curLine = curTextLines[lineIndex]
             curLine = curTextLines[lineIndex]
             length += curLine.text.length
             length += curLine.text.length

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

@@ -31,6 +31,7 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
 import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
 import io.legado.app.utils.activity
 import io.legado.app.utils.activity
+import io.legado.app.utils.canvasrecorder.pools.BitmapPool
 import io.legado.app.utils.invisible
 import io.legado.app.utils.invisible
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.throttle
 import io.legado.app.utils.throttle
@@ -460,6 +461,8 @@ class ReadView(context: Context, attrs: AttributeSet) :
     fun onDestroy() {
     fun onDestroy() {
         pageDelegate?.onDestroy()
         pageDelegate?.onDestroy()
         curPage.cancelSelect()
         curPage.cancelSelect()
+        invalidateTextPage()
+        BitmapPool.clear()
     }
     }
 
 
     /**
     /**

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

@@ -1,14 +1,20 @@
 package io.legado.app.ui.book.read.page.entities
 package io.legado.app.ui.book.read.page.entities
 
 
+import android.annotation.SuppressLint
 import android.graphics.Canvas
 import android.graphics.Canvas
 import android.graphics.Paint.FontMetrics
 import android.graphics.Paint.FontMetrics
+import android.os.Build
 import androidx.annotation.Keep
 import androidx.annotation.Keep
+import androidx.core.graphics.withTranslation
+import io.legado.app.help.PaintPool
 import io.legado.app.help.book.isImage
 import io.legado.app.help.book.isImage
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.lib.theme.ThemeStore
 import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.page.ContentTextView
 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.TextPage.Companion.emptyTextPage
 import io.legado.app.ui.book.read.page.entities.column.BaseColumn
 import io.legado.app.ui.book.read.page.entities.column.BaseColumn
+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.ChapterProvider
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
 import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
@@ -32,6 +38,13 @@ data class TextLine(
     val isTitle: Boolean = false,
     val isTitle: Boolean = false,
     var isParagraphEnd: Boolean = false,
     var isParagraphEnd: Boolean = false,
     var isImage: Boolean = false,
     var isImage: Boolean = false,
+    var startX: Float = 0f,
+    var indentSize: Int = 0,
+    var extraLetterSpacing: Float = 0f,
+    var extraLetterSpacingOffsetX: Float = 0f,
+    var wordSpacing: Float = 0f,
+    var exceed: Boolean = false,
+    var onlyTextColumn: Boolean = true,
 ) {
 ) {
 
 
     val columns: List<BaseColumn> get() = textColumns
     val columns: List<BaseColumn> get() = textColumns
@@ -41,6 +54,7 @@ data class TextLine(
     val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
     val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
     val height: Float inline get() = lineBottom - lineTop
     val height: Float inline get() = lineBottom - lineTop
     val canvasRecorder = CanvasRecorderFactory.create()
     val canvasRecorder = CanvasRecorderFactory.create()
+    var searchResultColumnCount = 0
     var isReadAloud: Boolean = false
     var isReadAloud: Boolean = false
         set(value) {
         set(value) {
             if (field != value) {
             if (field != value) {
@@ -52,6 +66,9 @@ data class TextLine(
     var isLeftLine = true
     var isLeftLine = true
 
 
     fun addColumn(column: BaseColumn) {
     fun addColumn(column: BaseColumn) {
+        if (column !is TextColumn) {
+            onlyTextColumn = false
+        }
         column.textLine = this
         column.textLine = this
         textColumns.add(column)
         textColumns.add(column)
     }
     }
@@ -129,14 +146,57 @@ data class TextLine(
     }
     }
 
 
     private fun drawTextLine(view: ContentTextView, canvas: Canvas) {
     private fun drawTextLine(view: ContentTextView, canvas: Canvas) {
-        for (i in columns.indices) {
-            columns[i].draw(view, canvas)
+        if (checkFastDraw()) {
+            fastDrawTextLine(view, canvas)
+        } else {
+            for (i in columns.indices) {
+                columns[i].draw(view, canvas)
+            }
         }
         }
         if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) {
         if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) {
             drawUnderline(canvas)
             drawUnderline(canvas)
         }
         }
     }
     }
 
 
+    @SuppressLint("NewApi")
+    private fun fastDrawTextLine(view: ContentTextView, canvas: Canvas) {
+        val textPaint = if (isTitle) {
+            ChapterProvider.titlePaint
+        } else {
+            ChapterProvider.contentPaint
+        }
+        val textColor = if (isReadAloud) {
+            ThemeStore.accentColor
+        } else {
+            ReadBookConfig.textColor
+        }
+        val paint = PaintPool.obtain()
+        paint.set(textPaint)
+        if (extraLetterSpacing != 0f) {
+            paint.letterSpacing += extraLetterSpacing
+        }
+        if (wordSpacing != 0f) {
+            paint.wordSpacing = wordSpacing
+        }
+        if (paint.color != textColor) {
+            paint.color = textColor
+        }
+        if (extraLetterSpacingOffsetX != 0f) {
+            canvas.withTranslation(extraLetterSpacingOffsetX) {
+                canvas.drawText(text, indentSize, text.length, startX, lineBase - lineTop, paint)
+            }
+        } else {
+            canvas.drawText(text, indentSize, text.length, startX, lineBase - lineTop, paint)
+        }
+        PaintPool.recycle(paint)
+        for (i in columns.indices) {
+            val column = columns[i] as TextColumn
+            if (column.selected) {
+                canvas.drawRect(column.start, 0f, column.end, height, view.selectedPaint)
+            }
+        }
+    }
+
     /**
     /**
      * 绘制下划线
      * 绘制下划线
      */
      */
@@ -151,6 +211,16 @@ data class TextLine(
         )
         )
     }
     }
 
 
+    fun checkFastDraw(): Boolean {
+        if (exceed || !onlyTextColumn || textPage.isMsgPage) {
+            return false
+        }
+        if (!atLeastApi29 && wordSpacing != 0f) {
+            return false
+        }
+        return searchResultColumnCount == 0
+    }
+
     fun invalidate() {
     fun invalidate() {
         invalidateSelf()
         invalidateSelf()
         textPage.invalidate()
         textPage.invalidate()
@@ -166,6 +236,7 @@ data class TextLine(
 
 
     companion object {
     companion object {
         val emptyTextLine = TextLine()
         val emptyTextLine = TextLine()
+        private val atLeastApi29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
     }
     }
 
 
 }
 }

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

@@ -1,11 +1,13 @@
 package io.legado.app.ui.book.read.page.entities
 package io.legado.app.ui.book.read.page.entities
 
 
 import android.graphics.Canvas
 import android.graphics.Canvas
+import android.graphics.Paint
 import android.text.Layout
 import android.text.Layout
 import android.text.StaticLayout
 import android.text.StaticLayout
 import androidx.annotation.Keep
 import androidx.annotation.Keep
 import androidx.core.graphics.withTranslation
 import androidx.core.graphics.withTranslation
 import io.legado.app.R
 import io.legado.app.R
+import io.legado.app.help.PaintPool
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.entities.TextChapter.Companion.emptyTextChapter
 import io.legado.app.ui.book.read.page.entities.TextChapter.Companion.emptyTextChapter
@@ -13,6 +15,7 @@ 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.ChapterProvider
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.recordIfNeeded
 import io.legado.app.utils.canvasrecorder.recordIfNeeded
+import io.legado.app.utils.dpToPx
 import splitties.init.appCtx
 import splitties.init.appCtx
 import java.text.DecimalFormat
 import java.text.DecimalFormat
 import kotlin.math.min
 import kotlin.math.min
@@ -45,7 +48,7 @@ data class TextPage(
     var isMsgPage: Boolean = false
     var isMsgPage: Boolean = false
     var canvasRecorder = CanvasRecorderFactory.create(true)
     var canvasRecorder = CanvasRecorderFactory.create(true)
     var doublePage = false
     var doublePage = false
-    var paddingTop = 0
+    var paddingTop = ChapterProvider.paddingTop
     var isCompleted = false
     var isCompleted = false
 
 
     @JvmField
     @JvmField
@@ -277,6 +280,21 @@ data class TextPage(
         }
         }
     }
     }
 
 
+    private fun drawDebugInfo(canvas: Canvas) {
+        ChapterProvider.run {
+            val paint = PaintPool.obtain()
+            paint.style = Paint.Style.STROKE
+            canvas.drawRect(
+                paddingLeft.toFloat(),
+                0f,
+                (paddingLeft + visibleWidth).toFloat(),
+                height - 1.dpToPx(),
+                paint
+            )
+            PaintPool.recycle(paint)
+        }
+    }
+
     private fun drawPage(view: ContentTextView, canvas: Canvas) {
     private fun drawPage(view: ContentTextView, canvas: Canvas) {
         for (i in lines.indices) {
         for (i in lines.indices) {
             val line = lines[i]
             val line = lines[i]

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

@@ -2,6 +2,7 @@ package io.legado.app.ui.book.read.page.entities.column
 
 
 import android.graphics.Canvas
 import android.graphics.Canvas
 import androidx.annotation.Keep
 import androidx.annotation.Keep
+import io.legado.app.help.PaintPool
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.lib.theme.ThemeStore
 import io.legado.app.lib.theme.ThemeStore
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.ContentTextView
@@ -32,6 +33,11 @@ data class TextColumn(
         set(value) {
         set(value) {
             if (field != value) {
             if (field != value) {
                 textLine.invalidate()
                 textLine.invalidate()
+                if (value) {
+                    textLine.searchResultColumnCount++
+                } else {
+                    textLine.searchResultColumnCount--
+                }
             }
             }
             field = value
             field = value
         }
         }
@@ -42,15 +48,18 @@ data class TextColumn(
         } else {
         } else {
             ChapterProvider.contentPaint
             ChapterProvider.contentPaint
         }
         }
-        if (textLine.isReadAloud || isSearchResult) {
-            synchronized(textPaint) {
-                textPaint.color = ThemeStore.accentColor
-                canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
-                textPaint.color = ReadBookConfig.textColor
-            }
+        val textColor = if (textLine.isReadAloud || isSearchResult) {
+            ThemeStore.accentColor
         } else {
         } else {
-            canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
+            ReadBookConfig.textColor
+        }
+        val paint = PaintPool.obtain()
+        paint.set(textPaint)
+        if (paint.color != textColor) {
+            paint.color = textColor
         }
         }
+        canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, paint)
+        PaintPool.recycle(paint)
         if (selected) {
         if (selected) {
             canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
             canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
         }
         }

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

@@ -879,6 +879,7 @@ object ChapterProvider {
         tPaint.typeface = titleFont
         tPaint.typeface = titleFont
         tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.toFloat().spToPx()
         tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.toFloat().spToPx()
         tPaint.isAntiAlias = true
         tPaint.isAntiAlias = true
+        tPaint.isLinearText = true
         //正文
         //正文
         val cPaint = TextPaint()
         val cPaint = TextPaint()
         cPaint.color = ReadBookConfig.textColor
         cPaint.color = ReadBookConfig.textColor
@@ -886,6 +887,7 @@ object ChapterProvider {
         cPaint.typeface = textFont
         cPaint.typeface = textFont
         cPaint.textSize = ReadBookConfig.textSize.toFloat().spToPx()
         cPaint.textSize = ReadBookConfig.textSize.toFloat().spToPx()
         cPaint.isAntiAlias = true
         cPaint.isAntiAlias = true
+        cPaint.isLinearText = true
         return Pair(tPaint, cPaint)
         return Pair(tPaint, cPaint)
     }
     }
 
 

+ 15 - 5
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt

@@ -31,6 +31,7 @@ import kotlinx.coroutines.launch
 import java.util.LinkedList
 import java.util.LinkedList
 import java.util.Locale
 import java.util.Locale
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.coroutineContext
+import kotlin.math.roundToInt
 
 
 class TextChapterLayout(
 class TextChapterLayout(
     scope: CoroutineScope,
     scope: CoroutineScope,
@@ -467,7 +468,7 @@ class TextChapterLayout(
                     //第一行 非标题
                     //第一行 非标题
                     textLine.text = lineText
                     textLine.text = lineText
                     addCharsToLineFirst(
                     addCharsToLineFirst(
-                        book, absStartX, textLine, words,
+                        book, absStartX, textLine, words, textPaint,
                         desiredWidth, widths, srcList
                         desiredWidth, widths, srcList
                     )
                     )
                 }
                 }
@@ -505,7 +506,7 @@ class TextChapterLayout(
                         //中间行
                         //中间行
                         textLine.text = lineText
                         textLine.text = lineText
                         addCharsToLineMiddle(
                         addCharsToLineMiddle(
-                            book, absStartX, textLine, words,
+                            book, absStartX, textLine, words, textPaint,
                             desiredWidth, 0f, widths, srcList
                             desiredWidth, 0f, widths, srcList
                         )
                         )
                     }
                     }
@@ -556,6 +557,7 @@ class TextChapterLayout(
         absStartX: Int,
         absStartX: Int,
         textLine: TextLine,
         textLine: TextLine,
         words: List<String>,
         words: List<String>,
+        textPaint: TextPaint,
         /**自然排版长度**/
         /**自然排版长度**/
         desiredWidth: Float,
         desiredWidth: Float,
         textWidths: List<Float>,
         textWidths: List<Float>,
@@ -582,11 +584,12 @@ class TextChapterLayout(
             x = x1
             x = x1
             textLine.indentWidth = x
             textLine.indentWidth = x
         }
         }
+        textLine.indentSize = bodyIndent.length
         if (words.size > bodyIndent.length) {
         if (words.size > bodyIndent.length) {
             val text1 = words.subList(bodyIndent.length, words.size)
             val text1 = words.subList(bodyIndent.length, words.size)
             val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
             val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
             addCharsToLineMiddle(
             addCharsToLineMiddle(
-                book, absStartX, textLine, text1,
+                book, absStartX, textLine, text1, textPaint,
                 desiredWidth, x, textWidths1, srcList
                 desiredWidth, x, textWidths1, srcList
             )
             )
         }
         }
@@ -600,6 +603,7 @@ class TextChapterLayout(
         absStartX: Int,
         absStartX: Int,
         textLine: TextLine,
         textLine: TextLine,
         words: List<String>,
         words: List<String>,
+        textPaint: TextPaint,
         /**自然排版长度**/
         /**自然排版长度**/
         desiredWidth: Float,
         desiredWidth: Float,
         /**起始x坐标**/
         /**起始x坐标**/
@@ -616,8 +620,10 @@ class TextChapterLayout(
         }
         }
         val residualWidth = visibleWidth - desiredWidth
         val residualWidth = visibleWidth - desiredWidth
         val spaceSize = words.count { it == " " }
         val spaceSize = words.count { it == " " }
+        textLine.startX = absStartX + startX
         if (spaceSize > 1) {
         if (spaceSize > 1) {
-            val d = residualWidth / spaceSize
+            val d = residualWidth / (spaceSize - 1)
+            textLine.wordSpacing = d
             var x = startX
             var x = startX
             for (index in words.indices) {
             for (index in words.indices) {
                 val char = words[index]
                 val char = words[index]
@@ -636,6 +642,8 @@ class TextChapterLayout(
         } else {
         } else {
             val gapCount: Int = words.lastIndex
             val gapCount: Int = words.lastIndex
             val d = residualWidth / gapCount
             val d = residualWidth / gapCount
+            textLine.extraLetterSpacingOffsetX = -d / 2
+            textLine.extraLetterSpacing = d / textPaint.textSize
             var x = startX
             var x = startX
             for (index in words.indices) {
             for (index in words.indices) {
                 val char = words[index]
                 val char = words[index]
@@ -666,6 +674,7 @@ class TextChapterLayout(
     ) {
     ) {
         val indentLength = ReadBookConfig.paragraphIndent.length
         val indentLength = ReadBookConfig.paragraphIndent.length
         var x = startX
         var x = startX
+        textLine.startX = absStartX + startX
         for (index in words.indices) {
         for (index in words.indices) {
             val char = words[index]
             val char = words[index]
             val cw = textWidths[index]
             val cw = textWidths[index]
@@ -727,8 +736,9 @@ class TextChapterLayout(
      */
      */
     private fun exceed(absStartX: Int, textLine: TextLine, words: List<String>) {
     private fun exceed(absStartX: Int, textLine: TextLine, words: List<String>) {
         val visibleEnd = absStartX + visibleWidth
         val visibleEnd = absStartX + visibleWidth
-        val endX = textLine.columns.lastOrNull()?.end ?: return
+        val endX = textLine.columns.lastOrNull()?.end?.roundToInt() ?: return
         if (endX > visibleEnd) {
         if (endX > visibleEnd) {
+            textLine.exceed = true
             val cc = (endX - visibleEnd) / words.size
             val cc = (endX - visibleEnd) / words.size
             for (i in 0..words.lastIndex) {
             for (i in 0..words.lastIndex) {
                 textLine.getColumnReverseAt(i).let {
                 textLine.getColumnReverseAt(i).let {

+ 2 - 1
app/src/main/java/io/legado/app/utils/ContextExtensions.kt

@@ -63,6 +63,7 @@ inline fun <reified T : Service> Context.stopService() {
 @SuppressLint("UnspecifiedImmutableFlag")
 @SuppressLint("UnspecifiedImmutableFlag")
 inline fun <reified T : Service> Context.servicePendingIntent(
 inline fun <reified T : Service> Context.servicePendingIntent(
     action: String,
     action: String,
+    requestCode: Int = 0,
     configIntent: Intent.() -> Unit = {}
     configIntent: Intent.() -> Unit = {}
 ): PendingIntent? {
 ): PendingIntent? {
     val intent = Intent(this, T::class.java)
     val intent = Intent(this, T::class.java)
@@ -73,7 +74,7 @@ inline fun <reified T : Service> Context.servicePendingIntent(
     } else {
     } else {
         FLAG_UPDATE_CURRENT
         FLAG_UPDATE_CURRENT
     }
     }
-    return getService(this, 0, intent, flags)
+    return getService(this, requestCode, intent, flags)
 }
 }
 
 
 @SuppressLint("UnspecifiedImmutableFlag")
 @SuppressLint("UnspecifiedImmutableFlag")

+ 5 - 0
app/src/main/java/io/legado/app/utils/EncoderUtils.kt

@@ -37,6 +37,11 @@ object EncoderUtils {
     fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? {
     fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? {
         return Base64.encodeToString(str.toByteArray(), flags)
         return Base64.encodeToString(str.toByteArray(), flags)
     }
     }
+
+    @JvmOverloads
+    fun base64Encode(bytes: ByteArray, flags: Int = Base64.NO_WRAP): String {
+        return Base64.encodeToString(bytes, flags)
+    }
     
     
     @JvmOverloads
     @JvmOverloads
     fun base64DecodeToByteArray(str: String, flags: Int = Base64.DEFAULT): ByteArray {
     fun base64DecodeToByteArray(str: String, flags: Int = Base64.DEFAULT): ByteArray {

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

@@ -2,8 +2,8 @@ package io.legado.app.utils.canvasrecorder
 
 
 import android.graphics.Canvas
 import android.graphics.Canvas
 import android.graphics.Picture
 import android.graphics.Picture
-import io.legado.app.utils.canvasrecorder.objectpool.synchronized
 import io.legado.app.utils.canvasrecorder.pools.PicturePool
 import io.legado.app.utils.canvasrecorder.pools.PicturePool
+import io.legado.app.utils.objectpool.synchronized
 
 
 class CanvasRecorderApi23Impl : BaseCanvasRecorder() {
 class CanvasRecorderApi23Impl : BaseCanvasRecorder() {
 
 

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

@@ -5,7 +5,7 @@ import android.graphics.Picture
 import android.graphics.RenderNode
 import android.graphics.RenderNode
 import android.os.Build
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.annotation.RequiresApi
-import io.legado.app.utils.canvasrecorder.objectpool.synchronized
+import io.legado.app.utils.objectpool.synchronized
 import io.legado.app.utils.canvasrecorder.pools.PicturePool
 import io.legado.app.utils.canvasrecorder.pools.PicturePool
 import io.legado.app.utils.canvasrecorder.pools.RenderNodePool
 import io.legado.app.utils.canvasrecorder.pools.RenderNodePool
 
 

+ 4 - 5
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt

@@ -16,14 +16,14 @@ class CanvasRecorderImpl : BaseCanvasRecorder() {
 
 
     private fun init(width: Int, height: Int) {
     private fun init(width: Int, height: Int) {
         if (bitmap == null) {
         if (bitmap == null) {
-            bitmap = bitmapPool.obtain(width, height)
+            bitmap = BitmapPool.obtain(width, height)
         }
         }
         if (bitmap!!.width != width || bitmap!!.height != height) {
         if (bitmap!!.width != width || bitmap!!.height != height) {
             if (canReconfigure(width, height)) {
             if (canReconfigure(width, height)) {
                 bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888)
                 bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888)
             } else {
             } else {
-                bitmapPool.recycle(bitmap!!)
-                bitmap = bitmapPool.obtain(width, height)
+                BitmapPool.recycle(bitmap!!)
+                bitmap = BitmapPool.obtain(width, height)
             }
             }
         }
         }
     }
     }
@@ -54,13 +54,12 @@ class CanvasRecorderImpl : BaseCanvasRecorder() {
     override fun recycle() {
     override fun recycle() {
         super.recycle()
         super.recycle()
         val bitmap = bitmap ?: return
         val bitmap = bitmap ?: return
-        bitmapPool.recycle(bitmap)
+        BitmapPool.recycle(bitmap)
         this.bitmap = null
         this.bitmap = null
     }
     }
 
 
     companion object {
     companion object {
         private val canvasPool = CanvasPool(2)
         private val canvasPool = CanvasPool(2)
-        private val bitmapPool = BitmapPool()
     }
     }
 
 
 }
 }

+ 0 - 24
app/src/main/java/io/legado/app/utils/canvasrecorder/objectpool/BaseObjectPool.kt

@@ -1,24 +0,0 @@
-package io.legado.app.utils.canvasrecorder.objectpool
-
-import androidx.annotation.CallSuper
-import java.lang.ref.SoftReference
-import java.util.LinkedList
-
-abstract class BaseObjectPool<T> : ObjectPool<T> {
-
-    private val pool = LinkedList<SoftReference<T>>()
-
-    override fun obtain(): T {
-        while (true) {
-            if (pool.isEmpty()) break
-            return pool.poll()?.get() ?: continue
-        }
-        return create()
-    }
-
-    @CallSuper
-    override fun recycle(target: T) {
-        pool.add(SoftReference(target))
-    }
-
-}

+ 36 - 5
app/src/main/java/io/legado/app/utils/canvasrecorder/pools/BitmapPool.kt

@@ -1,15 +1,46 @@
 package io.legado.app.utils.canvasrecorder.pools
 package io.legado.app.utils.canvasrecorder.pools
 
 
 import android.graphics.Bitmap
 import android.graphics.Bitmap
-import java.lang.ref.SoftReference
+import io.legado.app.help.globalExecutor
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.ConcurrentHashMap
 
 
-class BitmapPool {
+object BitmapPool {
 
 
-    private val reusableBitmaps: MutableSet<SoftReference<Bitmap>> = ConcurrentHashMap.newKeySet()
+    private val reusableBitmaps: MutableSet<Bitmap> = ConcurrentHashMap.newKeySet()
 
 
     fun recycle(bitmap: Bitmap) {
     fun recycle(bitmap: Bitmap) {
-        reusableBitmaps.add(SoftReference(bitmap))
+        reusableBitmaps.add(bitmap)
+        trimSize()
+    }
+
+    fun clear() {
+        if (reusableBitmaps.isEmpty()) {
+            return
+        }
+        globalExecutor.execute {
+            val iterator = reusableBitmaps.iterator()
+            while (iterator.hasNext()) {
+                val item = iterator.next()
+                iterator.remove()
+                item.recycle()
+            }
+        }
+    }
+
+    private fun trimSize() {
+        globalExecutor.execute {
+            var byteCount = 0
+            val iterator = reusableBitmaps.iterator()
+            while (iterator.hasNext()) {
+                val item = iterator.next()
+                if (byteCount > 64 * 1024 * 1024) {
+                    iterator.remove()
+                    item.recycle()
+                } else {
+                    byteCount += item.byteCount
+                }
+            }
+        }
     }
     }
 
 
     fun obtain(width: Int, height: Int): Bitmap {
     fun obtain(width: Int, height: Int): Bitmap {
@@ -18,7 +49,7 @@ class BitmapPool {
         }
         }
         val iterator = reusableBitmaps.iterator()
         val iterator = reusableBitmaps.iterator()
         while (iterator.hasNext()) {
         while (iterator.hasNext()) {
-            val item = iterator.next().get() ?: continue
+            val item = iterator.next()
             if (item.isMutable) {
             if (item.isMutable) {
                 // Check to see it the item can be used for inBitmap.
                 // Check to see it the item can be used for inBitmap.
                 if (canReconfigure(item, width, height)) {
                 if (canReconfigure(item, width, height)) {

+ 2 - 2
app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt

@@ -1,9 +1,9 @@
 package io.legado.app.utils.canvasrecorder.pools
 package io.legado.app.utils.canvasrecorder.pools
 
 
 import android.graphics.Picture
 import android.graphics.Picture
-import io.legado.app.utils.canvasrecorder.objectpool.BaseObjectPool
+import io.legado.app.utils.objectpool.BaseObjectPool
 
 
-class PicturePool : BaseObjectPool<Picture>() {
+class PicturePool : BaseObjectPool<Picture>(64) {
 
 
     override fun create(): Picture = Picture()
     override fun create(): Picture = Picture()
 
 

+ 2 - 2
app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt

@@ -3,10 +3,10 @@ package io.legado.app.utils.canvasrecorder.pools
 import android.graphics.RenderNode
 import android.graphics.RenderNode
 import android.os.Build
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.annotation.RequiresApi
-import io.legado.app.utils.canvasrecorder.objectpool.BaseObjectPool
+import io.legado.app.utils.objectpool.BaseObjectPool
 
 
 @RequiresApi(Build.VERSION_CODES.Q)
 @RequiresApi(Build.VERSION_CODES.Q)
-class RenderNodePool : BaseObjectPool<RenderNode>() {
+class RenderNodePool : BaseObjectPool<RenderNode>(64) {
 
 
     override fun recycle(target: RenderNode) {
     override fun recycle(target: RenderNode) {
         target.discardDisplayList()
         target.discardDisplayList()

+ 19 - 0
app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt

@@ -0,0 +1,19 @@
+package io.legado.app.utils.objectpool
+
+import androidx.annotation.CallSuper
+import androidx.core.util.Pools
+
+abstract class BaseObjectPool<T : Any>(size: Int) : ObjectPool<T> {
+
+    open val pool = Pools.SimplePool<T>(size)
+
+    override fun obtain(): T {
+        return pool.acquire() ?: create()
+    }
+
+    @CallSuper
+    override fun recycle(target: T) {
+        pool.release(target)
+    }
+
+}

+ 9 - 0
app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt

@@ -0,0 +1,9 @@
+package io.legado.app.utils.objectpool
+
+import androidx.core.util.Pools
+
+abstract class BaseSafeObjectPool<T : Any>(size: Int): BaseObjectPool<T>(size) {
+
+    override val pool = Pools.SynchronizedPool<T>(size)
+
+}

+ 1 - 1
app/src/main/java/io/legado/app/utils/canvasrecorder/objectpool/ObjectPool.kt → app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt

@@ -1,4 +1,4 @@
-package io.legado.app.utils.canvasrecorder.objectpool
+package io.legado.app.utils.objectpool
 
 
 interface ObjectPool<T> {
 interface ObjectPool<T> {
 
 

+ 1 - 1
app/src/main/java/io/legado/app/utils/canvasrecorder/objectpool/ObjectPoolExtensions.kt → app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt

@@ -1,3 +1,3 @@
-package io.legado.app.utils.canvasrecorder.objectpool
+package io.legado.app.utils.objectpool
 
 
 fun <T> ObjectPool<T>.synchronized(): ObjectPool<T> = ObjectPoolLocked(this)
 fun <T> ObjectPool<T>.synchronized(): ObjectPool<T> = ObjectPoolLocked(this)

+ 1 - 1
app/src/main/java/io/legado/app/utils/canvasrecorder/objectpool/ObjectPoolLocked.kt → app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt

@@ -1,4 +1,4 @@
-package io.legado.app.utils.canvasrecorder.objectpool
+package io.legado.app.utils.objectpool
 
 
 class ObjectPoolLocked<T>(private val delegate: ObjectPool<T>) : ObjectPool<T> by delegate {
 class ObjectPoolLocked<T>(private val delegate: ObjectPool<T>) : ObjectPool<T> by delegate {