Merge branch 'gedoor:master' into master

This commit is contained in:
huolibao 2024-02-26 12:31:28 +08:00 committed by GitHub
commit ce13ee9d1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 273 additions and 65 deletions

View File

@ -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) {

View File

@ -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
} }

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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))

View File

@ -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)
} }
) )

View File

@ -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

View File

@ -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()
} }
/** /**

View File

@ -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) { if (checkFastDraw()) {
columns[i].draw(view, canvas) 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
} }
} }

View File

@ -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]

View File

@ -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) { val textColor = if (textLine.isReadAloud || isSearchResult) {
synchronized(textPaint) { ThemeStore.accentColor
textPaint.color = ThemeStore.accentColor
canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
textPaint.color = ReadBookConfig.textColor
}
} 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)
} }

View File

@ -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)
} }

View File

@ -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 {

View File

@ -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")

View File

@ -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 {

View File

@ -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() {

View File

@ -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

View File

@ -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!!) BitmapPool.recycle(bitmap!!)
bitmap = bitmapPool.obtain(width, height) 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()
} }
} }

View File

@ -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))
}
}

View File

@ -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)) {

View File

@ -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()

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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)
}

View File

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

View File

@ -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)

View File

@ -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 {