mirror of
https://github.com/gedoor/legado.git
synced 2024-08-30 09:23:26 +08:00
Merge branch 'gedoor:master' into master
This commit is contained in:
commit
ce13ee9d1f
@ -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) {
|
||||||
|
@ -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
app/src/main/java/io/legado/app/help/PaintPool.kt
Normal file
17
app/src/main/java/io/legado/app/help/PaintPool.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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) {
|
||||||
|
if (checkFastDraw()) {
|
||||||
|
fastDrawTextLine(view, canvas)
|
||||||
|
} else {
|
||||||
for (i in columns.indices) {
|
for (i in columns.indices) {
|
||||||
columns[i].draw(view, canvas)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
@ -38,6 +38,11 @@ object EncoderUtils {
|
|||||||
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 {
|
||||||
return Base64.decode(str, flags)
|
return Base64.decode(str, flags)
|
||||||
|
@ -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() {
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)) {
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,4 +1,4 @@
|
|||||||
package io.legado.app.utils.canvasrecorder.objectpool
|
package io.legado.app.utils.objectpool
|
||||||
|
|
||||||
interface ObjectPool<T> {
|
interface ObjectPool<T> {
|
||||||
|
|
@ -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,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 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user