diff --git a/app/src/main/java/io/legado/app/constant/AppPattern.kt b/app/src/main/java/io/legado/app/constant/AppPattern.kt
index c028fe350..5c0e96347 100644
--- a/app/src/main/java/io/legado/app/constant/AppPattern.kt
+++ b/app/src/main/java/io/legado/app/constant/AppPattern.kt
@@ -11,6 +11,12 @@ object AppPattern {
//匹配格式化后的图片格式
val imgPattern: Pattern = Pattern.compile("
]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
+ //匹配自定义html格式字符串
+ val useHtmlRegex = Regex(".*?", RegexOption.DOT_MATCHES_ALL) //.包含换行
+
+ //匹配html字符串中的head
+ val htmlHeadRegex = Regex("
]*>", RegexOption.IGNORE_CASE)
+
//dataURL图片类型
val dataUriRegex = Regex("^data:.*?;base64,(.*)")
//提取标题中的段评
@@ -67,4 +73,4 @@ object AppPattern {
val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex()
val LFRegex = "\n".toRegex()
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/constant/BookType.kt b/app/src/main/java/io/legado/app/constant/BookType.kt
index fcdd0bd8e..e26f1f91a 100644
--- a/app/src/main/java/io/legado/app/constant/BookType.kt
+++ b/app/src/main/java/io/legado/app/constant/BookType.kt
@@ -8,6 +8,12 @@ import androidx.annotation.IntDef
*/
@Suppress("ConstPropertyName")
object BookType {
+
+ /**
+ * 4 视频
+ */
+ const val video = 0b100
+
/**
* 8 文本
*/
diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt
index 3acb1e342..4523d1340 100644
--- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt
+++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt
@@ -3,6 +3,7 @@
package io.legado.app.help.book
import android.net.Uri
+import androidx.core.net.toUri
import com.script.buildScriptBindings
import com.script.rhino.RhinoScriptEngine
import io.legado.app.constant.AppLog
@@ -24,6 +25,7 @@ import io.legado.app.utils.exists
import io.legado.app.utils.find
import io.legado.app.utils.inputStream
import io.legado.app.utils.isUri
+import io.legado.app.utils.normalizeFileName
import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
import java.io.File
@@ -32,8 +34,6 @@ import java.time.temporal.ChronoUnit
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max
import kotlin.math.min
-import androidx.core.net.toUri
-import io.legado.app.utils.normalizeFileName
val Book.isAudio: Boolean
@@ -42,6 +42,9 @@ val Book.isAudio: Boolean
val Book.isImage: Boolean
get() = isType(BookType.image)
+val Book.isVideo: Boolean
+ get() = isType(BookType.video)
+
val Book.isLocal: Boolean
get() {
if (type == 0) {
diff --git a/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt b/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt
index 1b2ee5219..35f3e9b6f 100644
--- a/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt
+++ b/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt
@@ -2,6 +2,7 @@ package io.legado.app.help.book
import android.os.Build
import io.legado.app.constant.AppLog
+import io.legado.app.constant.AppPattern
import io.legado.app.constant.AppPattern.spaceRegex
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@@ -143,6 +144,14 @@ class ContentProcessor private constructor(
appCtx.toastOnUi("简繁转换出错")
}
}
+ val useHtmlMap = mutableMapOf()
+ if (AppConfig.adaptSpecialStyle) { //html处理
+ mContent = AppPattern.useHtmlRegex.replace(mContent) { matchResult ->
+ val placeholder = "特殊格式的占位不应该被看见${useHtmlMap.size}。"
+ useHtmlMap[placeholder] = "\n${matchResult.value.replace("\n", "")}\n"
+ placeholder
+ }
+ }
if (useReplace && book.getUseReplaceRule()) {
//替换
effectiveReplaceRules = arrayListOf()
diff --git a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt
index f1796fb92..0ae753654 100644
--- a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt
+++ b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt
@@ -10,6 +10,8 @@ import io.legado.app.data.entities.rule.ContentRule
import io.legado.app.exception.ContentEmptyException
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp
+import io.legado.app.help.book.isAudio
+import io.legado.app.help.book.isVideo
import io.legado.app.help.config.AppConfig
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeRule
@@ -20,6 +22,7 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.mapAsync
+import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.flow
import org.apache.commons.text.StringEscapeUtils
@@ -181,16 +184,29 @@ object BookContent {
): Pair> {
val analyzeRule = AnalyzeRule(book, bookSource)
analyzeRule.setContent(body, baseUrl)
- analyzeRule.setCoroutineContext(coroutineContext)
+ analyzeRule.setCoroutineContext(currentCoroutineContext())
val rUrl = analyzeRule.setRedirectUrl(redirectUrl)
analyzeRule.setNextChapterUrl(nextChapterUrl)
val nextUrlList = arrayListOf()
analyzeRule.setChapter(chapter)
//获取正文
var content = analyzeRule.getString(contentRule.content, unescape = false)
- content = HtmlFormatter.formatKeepImg(content, rUrl)
- if (content.indexOf('&') > -1) {
- content = StringEscapeUtils.unescapeHtml4(content)
+ if (!book.isAudio && !book.isVideo) { //音频和视频获取的是链接,不需要html格式化 && !book.isVideo
+ val useHtmlMap = mutableMapOf()
+ if (AppConfig.adaptSpecialStyle) {
+ content = AppPattern.useHtmlRegex.replace(content) { matchResult ->
+ val placeholder = "{usehtml_${useHtmlMap.size}}"
+ useHtmlMap[placeholder] = matchResult.value
+ placeholder
+ }
+ }
+ content = HtmlFormatter.formatKeepImg(content, rUrl) //内置净化格式化
+ if (content.indexOf('&') > -1) {
+ content = StringEscapeUtils.unescapeHtml4(content)
+ }
+ useHtmlMap.forEach { (placeholder, originalContent) ->
+ content = content.replace(placeholder, originalContent)
+ }
}
//获取下一页链接
if (getNextPageUrl) {
diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
index 263d93fd5..cfd3caafa 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
@@ -97,7 +97,7 @@ class MoreConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_more_conf
PreferKey.textFullJustify,
PreferKey.textBottomJustify,
- PreferKey.useZhLayout -> {
+ PreferKey.useZhLayout, PreferKey.adaptSpecialStyle, PreferKey.useUnderline -> {
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/TextBaseColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/TextBaseColumn.kt
new file mode 100644
index 000000000..ba91c4fce
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/TextBaseColumn.kt
@@ -0,0 +1,14 @@
+package io.legado.app.ui.book.read.page
+
+import io.legado.app.ui.book.read.page.entities.column.BaseColumn
+
+/**
+ * 文字基列
+ */
+interface TextBaseColumn : BaseColumn {
+ override var start: Float
+ override var end: Float
+ val charData: String
+ var selected: Boolean
+ var isSearchResult: Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextHtmlColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextHtmlColumn.kt
new file mode 100644
index 000000000..046ef2ef9
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextHtmlColumn.kt
@@ -0,0 +1,89 @@
+package io.legado.app.ui.book.read.page.entities
+
+import android.graphics.Canvas
+import android.os.Build
+import android.text.TextPaint
+import androidx.annotation.Keep
+import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.ui.book.read.page.ContentTextView
+import io.legado.app.ui.book.read.page.TextBaseColumn
+import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
+import io.legado.app.ui.book.read.page.provider.ChapterProvider
+
+/**
+ * 带html样式的文字列
+ */
+@Keep
+data class TextHtmlColumn(
+ override var start: Float,
+ override var end: Float,
+ override val charData: String,
+ val mTextSize: Float,
+ val mTextColor: Int,
+ val linkUrl: String?
+) : TextBaseColumn {
+
+ override var textLine: TextLine = emptyTextLine
+
+ private val textPaint: TextPaint by lazy {
+ TextPaint(ChapterProvider.contentPaint).apply {
+ textSize = mTextSize
+ }
+ }
+
+ override var selected: Boolean = false
+ set(value) {
+ if (field != value) {
+ textLine.invalidate()
+ }
+ field = value
+ }
+
+ override var isSearchResult: Boolean = false
+ set(value) {
+ if (field != value) {
+ textLine.invalidate()
+ if (value) {
+ textLine.searchResultColumnCount++
+ } else {
+ textLine.searchResultColumnCount--
+ }
+ }
+ field = value
+ }
+
+ override fun draw(view: ContentTextView, canvas: Canvas) {
+ val y = textLine.lineBase - textLine.lineTop
+ if (linkUrl != null) {
+ textPaint.run {
+ color = ReadBookConfig.textAccentColor
+ isUnderlineText = true
+ }
+ drawText(view, canvas, y, textPaint)
+ return
+ }
+ textPaint.run {
+ color = if (textLine.isReadAloud || isSearchResult) {
+ ReadBookConfig.textAccentColor
+ } else {
+ mTextColor
+ }
+ isUnderlineText = false
+ }
+ drawText(view, canvas, y, textPaint)
+ }
+
+ private fun drawText(view: ContentTextView, canvas: Canvas, y: Float, textPaint: TextPaint) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ val letterSpacing = textPaint.letterSpacing * textPaint.textSize
+ val letterSpacingHalf = letterSpacing * 0.5f
+ canvas.drawText(charData, start + letterSpacingHalf, y, textPaint)
+ } else {
+ canvas.drawText(charData, start, y, textPaint)
+ }
+ if (selected) {
+ canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
+ }
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
index 46f7e74d7..8e722738b 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
@@ -37,6 +37,7 @@ data class TextLine(
val isTitle: Boolean = false,
var isParagraphEnd: Boolean = false,
var isImage: Boolean = false,
+ var isHtml: Boolean = false,
var startX: Float = 0f,
var indentSize: Int = 0,
var extraLetterSpacing: Float = 0f,
@@ -66,7 +67,8 @@ data class TextLine(
}
var textPage: TextPage = emptyTextPage
var isLeftLine = true
-
+ val useUnderline: Boolean
+ get() = AppConfig.useUnderline
fun addColumn(column: BaseColumn) {
if (column !is TextColumn) {
onlyTextColumn = false
@@ -75,6 +77,14 @@ data class TextLine(
textColumns.add(column)
}
+ fun addColumns(columns: Collection) {
+ onlyTextColumn = false
+ columns.forEach { column ->
+ column.textLine = this
+ }
+ textColumns.addAll(columns)
+ }
+
fun getColumn(index: Int): BaseColumn {
return textColumns.getOrElse(index) {
textColumns.last()
@@ -158,7 +168,7 @@ data class TextLine(
for (i in columns.indices) columns[i].draw(view, canvas)
}
- if (AppConfig.isEInkMode && (isReadAloud || searchResultColumnCount > 0)) {
+ if (useUnderline && (isReadAloud || searchResultColumnCount > 0)) {
val linePaint = ChapterProvider.linePaint
val lineY = height - 1.dpToPx()
canvas.drawLine(lineStart + indentWidth, lineY, lineEnd, lineY, linePaint)
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
index 5f23764bd..8e39aee26 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
@@ -47,7 +47,7 @@ data class TextColumn(
} else {
ChapterProvider.contentPaint
}
- val textColor = if (textLine.isReadAloud || isSearchResult) {
+ val textColor = if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
ReadBookConfig.textAccentColor
} else {
ReadBookConfig.textColor
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
index 26ee2983a..217558a0d 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
@@ -50,7 +50,9 @@ import java.util.Locale
@Suppress("DEPRECATION", "ConstPropertyName")
object ChapterProvider {
//用于图片字的替换
- const val srcReplaceChar = "⼁"//▩ 丨
+ const val srcReplaceChar = "袮" //▩▣ 丨⼁ //换成袮,这是不应该存在的汉字,替换为祢
+ const val srcReplaceCharC = '袮' //可能有略微的提升
+ const val srcReplaceCharD = '祢'
//用于评论按钮的替换
const val reviewChar = "꧁"
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt
index 018a59c7f..3380e1b56 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt
@@ -1,10 +1,17 @@
package io.legado.app.ui.book.read.page.provider
import android.graphics.Paint
+import android.os.Build
import android.text.Layout
+import android.text.Spanned
import android.text.StaticLayout
import android.text.TextPaint
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.URLSpan
import android.util.Size
+import androidx.core.text.HtmlCompat
+import androidx.core.text.parseAsHtml
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern
import io.legado.app.constant.AppPattern.noWordCountRegex
@@ -21,10 +28,15 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.ImageProvider
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChapter
+import io.legado.app.ui.book.read.page.entities.TextHtmlColumn
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.entities.column.ImageColumn
import io.legado.app.ui.book.read.page.entities.column.TextColumn
+import io.legado.app.ui.book.read.page.provider.ChapterProvider.reviewChar
+import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceChar
+import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharC
+import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharD
import io.legado.app.utils.StringUtils
import io.legado.app.utils.dpToPx
import io.legado.app.utils.fastSum
@@ -39,7 +51,6 @@ import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import java.util.LinkedList
-import kotlin.coroutines.coroutineContext
import kotlin.math.roundToInt
class TextChapterLayout(
@@ -67,8 +78,6 @@ class TextChapterLayout(
private val titleTopSpacing = ChapterProvider.titleTopSpacing
private val titleBottomSpacing = ChapterProvider.titleBottomSpacing
private val lineSpacingExtra = ChapterProvider.lineSpacingExtra
- private val titleLineSpacingExtra = ChapterProvider.titleLineSpacingExtra
-
private val paragraphSpacing = ChapterProvider.paragraphSpacing
private val visibleHeight = ChapterProvider.visibleHeight
@@ -84,12 +93,8 @@ class TextChapterLayout(
private val useZhLayout = ReadBookConfig.useZhLayout
private val isMiddleTitle = ReadBookConfig.isMiddleTitle
private val textFullJustify = ReadBookConfig.textFullJustify
+ private val adaptSpecialStyle = AppConfig.adaptSpecialStyle
private val pageAnim = book.getPageAnim()
- private val titleSegType = ReadBookConfig.titleSegType
- private val titleSegDistance = ReadBookConfig.titleSegDistance
- private val titleSegFlag = ReadBookConfig.titleSegFlag
- private val titleSegScaling = ReadBookConfig.titleSegScaling
-
private var pendingTextPage = TextPage()
@@ -201,48 +206,6 @@ class TextChapterLayout(
}
}
- private fun parseTitleSegments(
- rawTitle: String,
- segType: Int,
- segDistance: Int,
- segFlag: String
- ): List {
- if (rawTitle.isBlank()) return emptyList()
-
- return when (segType) {
- // 0:不分段
- 0 -> listOf(rawTitle)
-
- // 1:按字符数分段
- 1 -> {
- if (segDistance <= 0 || segDistance >= rawTitle.length)
- listOf(rawTitle)
- else
- listOf(
- rawTitle.substring(0, segDistance),
- rawTitle.substring(segDistance)
- )
- }
-
- // 2:按标志字符串分段
- 2 -> {
- val flags = segFlag.split(",").map { it.trim() }.filter { it.isNotEmpty() }
- if (flags.isEmpty()) return listOf(rawTitle)
-
- val pattern = flags.joinToString("|") { Regex.escape(it) }
- val regex = Regex("(?<=$pattern)") // 在分隔符后断开
- val segments = rawTitle.split(regex)
- .map { it.trim() }
- .filter { it.isNotEmpty() }
-
- segments.ifEmpty { listOf(rawTitle) }
- }
-
- else -> listOf(rawTitle)
- }
- }
-
-
/**
* 获取拆分完的章节数据
*/
@@ -258,48 +221,37 @@ class TextChapterLayout(
val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true)
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
- val baseTitles = displayTitle.splitNotBlank("\n")
- baseTitles.forEach { rawTitle ->
- val titleSegments = parseTitleSegments(
- rawTitle,
- titleSegType,
- titleSegDistance,
- titleSegFlag
- )
- titleSegments.forEachIndexed { index, segment ->
- val srcList = LinkedList()
- val reviewImg = bookChapter.reviewImg
- var reviewTxt = ""
- if (reviewImg != null) {
- srcList.add(reviewImg)
- reviewTxt = if (reviewImg.contains("TEXT")) {
- ChapterProvider.reviewChar
- } else {
- ChapterProvider.srcReplaceChar
- }
+ //标题非隐藏
+ displayTitle.splitNotBlank("\n").forEach { text ->
+ val srcList = LinkedList()
+ val reviewImg = bookChapter.reviewImg
+ var reviewTxt = ""
+ if (!reviewImg.isNullOrEmpty()) {
+ srcList.add(reviewImg)
+ reviewTxt = if (reviewImg.contains("TEXT")) {
+ reviewChar
+ } else {
+ srcReplaceChar
}
- val paint = if (index == 0) titlePaint else {
- TextPaint(titlePaint).apply {
- textSize = titlePaint.textSize * titleSegScaling
- }
- }
- setTypeText(
- book,
- segment + reviewTxt,
- paint,
- titlePaintTextHeight,
- titlePaintFontMetrics,
- imageStyle,
- srcList = srcList.ifEmpty { null },
- isTitle = true,
- emptyContent = contents.isEmpty(),
- isVolumeTitle = bookChapter.isVolume
- )
- pendingTextPage.lines.last().isParagraphEnd = true
- stringBuilder.append("\n")
}
+ setTypeText(
+ book,
+ text + reviewTxt,
+ titlePaint,
+ titlePaintTextHeight,
+ titlePaintFontMetrics,
+ imageStyle,
+ srcList = srcList.ifEmpty { null },
+ isTitle = true,
+ emptyContent = contents.isEmpty(),
+ isVolumeTitle = bookChapter.isVolume
+ )
+ pendingTextPage.lines.last().isParagraphEnd = true
+ stringBuilder.append("\n")
}
durY += titleBottomSpacing
+
+ // 如果是单图模式且当前页有内容,强制分页
if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty() && contents.isNotEmpty()) {
prepareNextPageIfNeed()
}
@@ -310,16 +262,26 @@ class TextChapterLayout(
var wordCount = 0
contents.forEach { content ->
currentCoroutineContext().ensureActive()
+ if (adaptSpecialStyle) {
+ val text = content.trim()
+ if (text == "[newpage]") {
+ prepareNextPageIfNeed()
+ return@forEach
+ } else if (text.startsWith("")) {
+ setTypeHtml(book, text.substring(9, text.lastIndexOf("<")))
+ return@forEach
+ }
+ }
+ var text = content.replace(srcReplaceCharC, srcReplaceCharD)
if (isTextImageStyle) {
//图片样式为文字嵌入类型
- var text = content.replace(ChapterProvider.srcReplaceChar, "丨") //▣
val srcList = LinkedList()
sb.setLength(0)
val matcher = AppPattern.imgPattern.matcher(text)
while (matcher.find()) {
matcher.group(1)?.let { src ->
srcList.add(src)
- matcher.appendReplacement(sb, ChapterProvider.srcReplaceChar)
+ matcher.appendReplacement(sb, srcReplaceChar)
}
}
matcher.appendTail(sb)
@@ -344,7 +306,6 @@ class TextChapterLayout(
sb.setLength(0)
var isFirstLine = true
if (content.contains("
= Build.VERSION_CODES.P) {
+ StaticLayout.Builder.obtain(spanned, 0, spanned.length, textPaint, width)
+ .setLineSpacing(paragraphSpacing.toFloat(), lineSpacingExtra)
+ .setIncludePad(true)
+ .setUseLineSpacingFromFallbacks(true)
+ .build()
+ } else {
+ @Suppress("DEPRECATION")
+ StaticLayout(
+ spanned,
+ textPaint,
+ width,
+ Layout.Alignment.ALIGN_NORMAL,
+ lineSpacingExtra,
+ paragraphSpacing.toFloat(),
+ true
+ )
+ }
+ val tempPaint = TextPaint(textPaint)
+ for (lineIndex in 0 until staticLayout.lineCount) {
+ val lineStart = staticLayout.getLineStart(lineIndex)
+ val lineEnd = staticLayout.getLineEnd(lineIndex)
+ if (lineStart == lineEnd) { //这一行没有内容,跳过
+ continue
+ }
+ val textLine = TextLine(isHtml = true)
+ val lineText = spanned.subSequence(lineStart, lineEnd).toString()
+ textLine.text = lineText //文本
+ val lineLeft = staticLayout.getLineLeft(lineIndex)
+ textLine.startX = absStartX + lineLeft //x坐标
+ val mLineTop = staticLayout.getLineTop(lineIndex).toFloat()
+ val mLineBottom = staticLayout.getLineBottom(lineIndex).toFloat()
+ val lineHeight = mLineBottom - mLineTop
+ prepareNextPageIfNeed(durY + lineHeight)
+ textLine.upTopBottom(durY, lineHeight, textPaint.fontMetrics) //y坐标
+
+ val columns = mutableListOf()
+ var charIndex = lineStart
+ while (charIndex < lineEnd) {
+ val char = spanned[charIndex].toString()
+ if (char == "\n") {
+ textLine.isParagraphEnd = true
+ durY += lineHeight * paragraphSpacing / 10f //段距
+ charIndex++
+ continue
+ }
+ val charX = staticLayout.getPrimaryHorizontal(charIndex) + lineLeft
+ val textSize = extractTextSize(spanned, charIndex, textPaint.textSize)
+ val textColor = extractTextColor(spanned, charIndex, textPaint.color)
+ val linkUrl = extractLinkUrl(spanned, charIndex)
+
+ val charRight = if (charIndex + 1 < lineEnd) {
+ staticLayout.getPrimaryHorizontal(charIndex + 1) + lineLeft
+ } else {
+ tempPaint.textSize = textSize
+ val charWidth = tempPaint.measureText(char)
+ charX + charWidth
+ }
+
+ columns.add(
+ TextHtmlColumn(
+ absStartX + charX,
+ absStartX + charRight,
+ char,
+ textSize,
+ textColor,
+ linkUrl
+ )
+ )
+ charIndex++
+ if (charIndex == lineEnd && lineIndex == staticLayout.lineCount - 1) {
+ textLine.isParagraphEnd = true
+ durY += lineHeight * paragraphSpacing / 10f //段距
+ }
+ }
+ if (textFullJustify && !textLine.isParagraphEnd) {
+ justifyHtmlLine(columns, textLine, visibleWidth)
+ } else {
+ textLine.addColumns(columns)
+ }
+ calcTextLinePosition(textPages, textLine, stringBuilder.length)
+ stringBuilder.append(lineText)
+ val textPage = pendingTextPage
+ textPage.addLine(textLine)
+ durY += lineHeight * lineSpacingExtra //行距
+ if (textPage.height < durY) {
+ textPage.height = durY
+ }
+ }
+ }
+
+ /**
+ * 对HTML行进行两端对齐
+ */
+ private fun justifyHtmlLine(
+ columns: MutableList,
+ textLine: TextLine,
+ lineWidth: Int
+ ) {
+ if (columns.isEmpty()) return
+ // 计算当前行的总宽度
+ val firstCol = columns.first()
+ val lastCol = columns.last()
+ val currentWidth = lastCol.end - firstCol.start
+ // 计算剩余空间
+ val residualWidth = lineWidth - currentWidth
+
+ if (residualWidth <= 0) {
+ textLine.addColumns(columns)
+ return
+ }
+
+ // 统计空格数量
+ val spaceCount = columns.count { it.charData == " " }
+
+ if (spaceCount > 1) {
+ // 多个空格:调整单词间距
+ val spaceIncrement = residualWidth / spaceCount
+ textLine.wordSpacing = spaceIncrement
+
+ // 重新计算字符位置
+ var currentX = columns[0].start
+ for (i in columns.indices) {
+ val col = columns[i]
+ val width = col.end - col.start
+
+ if (col.charData == " " && i != columns.lastIndex) {
+ // 空格,增加额外的间距
+ col.start = currentX
+ col.end = currentX + width + spaceIncrement
+ currentX = col.start
+ } else {
+ // 非空格或最后一个字符
+ col.start = currentX
+ col.end = currentX + width
+ currentX = col.start
+ }
+
+ textLine.addColumn(col)
+ }
+ } else {
+ // 没有或只有一个空格:调整字符间距
+ val gapCount = columns.lastIndex
+ if (gapCount > 0) {
+ val charIncrement = residualWidth / gapCount
+ var currentX = columns[0].start
+ for (i in columns.indices) {
+ val col = columns[i]
+ val width = col.end - col.start
+
+ if (i != columns.lastIndex) {
+ // 非最后一个字符,增加额外的间距
+ col.start = currentX
+ col.end = currentX + width + charIncrement
+ currentX = col.end
+ } else {
+ // 最后一个字符,不增加额外间距
+ col.start = currentX
+ col.end = currentX + width
+ }
+
+ textLine.addColumn(col)
+ }
+ } else {
+ // 只有一个字符,不需要调整
+ textLine.addColumns(columns)
+ }
+ }
+ }
+
+ private fun extractTextSize(spanned: Spanned, index: Int, defaultSize: Float): Float {
+ val relativeSpans = spanned.getSpans(index, index + 1, RelativeSizeSpan::class.java)
+ // 如果有 RelativeSizeSpan,基于基准大小计算
+ relativeSpans.firstOrNull()?.let { span ->
+ return defaultSize * span.sizeChange
+ }
+// val sizeSpans = spanned.getSpans(index, index + 1, AbsoluteSizeSpan::class.java)
+// sizeSpans.firstOrNull()?.let { span ->
+// return span.size.toFloat()
+// }
+ return defaultSize
+ }
+
+ private fun extractTextColor(spanned: Spanned, index: Int, defaultColor: Int): Int {
+ // 检查 ForegroundColorSpan(前景色)
+ val foregroundSpans = spanned.getSpans(index, index + 1, ForegroundColorSpan::class.java)
+ foregroundSpans.firstOrNull()?.let { span ->
+ return span.foregroundColor
+ }
+
+ // 2. 检查自定义的彩色 Span
+// val customColorSpans = spanned.getSpans(index, index + 1, CharacterStyle::class.java)
+// customColorSpans.firstOrNull()?.let { span ->
+// if (span is ForegroundColorSpan) {
+// return span.foregroundColor
+// }
+// }
+
+ // 默认返回 Paint 的颜色
+ return defaultColor
+ }
+
+ private fun extractLinkUrl(spanned: Spanned, index: Int): String? {
+ // 检查URLSpan(超链接)
+ val urlSpans = spanned.getSpans(index, index + 1, URLSpan::class.java)
+ urlSpans.firstOrNull()?.let { span ->
+ return span.url
+ }
+ return null
+ }
+
+
/**
* 排版文字
*/
@@ -614,8 +794,8 @@ class TextChapterLayout(
val (words, widths) = measureTextSplit(lineText, widthsArray, lineStart)
val desiredWidth = widths.fastSum()
textLine.text = lineText
- when {
- lineIndex == 0 && layout.lineCount > 1 && !isTitle && isFirstLine -> {
+ when (lineIndex) {
+ 0 if layout.lineCount > 1 && !isTitle && isFirstLine -> {
//多行的第一行 非标题
addCharsToLineFirst(
book, absStartX, textLine, words, textPaint,
@@ -623,7 +803,7 @@ class TextChapterLayout(
)
}
- lineIndex == layout.lineCount - 1 -> {
+ layout.lineCount - 1 -> {
//最后一行、单行
//标题x轴居中
val startX = if (
@@ -640,7 +820,6 @@ class TextChapterLayout(
startX, !isTitle && lineIndex == 0, widths, srcList
)
}
-
else -> {
if (
isTitle &&
@@ -670,13 +849,12 @@ class TextChapterLayout(
textLine.upTopBottom(durY, textHeight, fontMetrics)
val textPage = pendingTextPage
textPage.addLine(textLine)
- durY += textHeight * if (isTitle) titleLineSpacingExtra else lineSpacingExtra
+ durY += textHeight * lineSpacingExtra
if (textPage.height < durY) {
textPage.height = durY
}
}
- if(!isTitle)
- durY += textHeight * paragraphSpacing / 10f
+ durY += textHeight * paragraphSpacing / 10f
}
private fun calcTextLinePosition(
@@ -852,7 +1030,7 @@ class TextChapterLayout(
srcList: LinkedList?
) {
val column = when {
- !srcList.isNullOrEmpty() && (char == ChapterProvider.srcReplaceChar || char == ChapterProvider.reviewChar && isLineEnd) -> {
+ !srcList.isNullOrEmpty() && (char == srcReplaceChar || char == reviewChar) -> {
val src = srcList.removeFirst()
ImageProvider.cacheImage(book, src, ReadBook.bookSource)
ImageColumn(
@@ -861,13 +1039,13 @@ class TextChapterLayout(
src = src
)
}
- /*isLineEnd && char == ChapterProvider.reviewChar -> {
- ReviewColumn(
- start = absStartX + xStart,
- end = absStartX + xEnd,
- count = 10
- )
- }*/
+// isLineEnd && char == ChapterProvider.reviewChar -> {
+// ReviewColumn(
+// start = absStartX + xStart,
+// end = absStartX + xEnd,
+// count = 10
+// )
+// }
else -> {
TextColumn(
@@ -974,4 +1152,4 @@ class TextChapterLayout(
return code == 8203 || code == 8204 || code == 8205 || code == 8288
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/values-zh-rCN/arrays.xml b/app/src/main/res/values-zh-rCN/arrays.xml
index 4338ea683..56e98b34b 100644
--- a/app/src/main/res/values-zh-rCN/arrays.xml
+++ b/app/src/main/res/values-zh-rCN/arrays.xml
@@ -168,6 +168,12 @@
- 内容取色
+
+ - Material Design
+ - SaltUI
+ - Miuix
+
+
- 全局
- 仅强调色
diff --git a/app/src/main/res/values/array_values.xml b/app/src/main/res/values/array_values.xml
index f38ca5b96..b5a1a0485 100644
--- a/app/src/main/res/values/array_values.xml
+++ b/app/src/main/res/values/array_values.xml
@@ -90,6 +90,12 @@
- content
+
+ - material
+ - salt
+ - miuix
+
+
- global
- accent
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 8c5249d58..04715765b 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -205,6 +205,12 @@
- Content
+
+ - Material Design
+ - SaltUI
+ - Miuix
+
+
- Global
- Accent
diff --git a/app/src/main/res/xml/pref_config_read.xml b/app/src/main/res/xml/pref_config_read.xml
index 10e6888cf..7200fdae8 100644
--- a/app/src/main/res/xml/pref_config_read.xml
+++ b/app/src/main/res/xml/pref_config_read.xml
@@ -80,6 +80,13 @@
android:title="@string/text_bottom_justify"
app:isBottomBackground="true" />
+
+
+
+