From de04a1f69d3971d26b1fb6a2f5bb9ad917b4ca67 Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Sat, 17 Jan 2026 05:46:23 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=96=B0=E5=A2=9E]=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BA=86=E5=8F=AF=E9=80=89=E7=9A=84=E6=96=87=E6=9C=AC=E5=BC=BA?= =?UTF-8?q?=E8=B0=83=E6=96=B9=E5=BC=8F=20[=E6=96=B0=E5=A2=9E]=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=BA=86Html=E6=96=87=E6=9C=AC=E6=B8=B2=E6=9F=93(@Luo?= =?UTF-8?q?yacheng)=20[=E4=BF=AE=E5=A4=8D]=20=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E6=9F=90=E4=BA=9B=E6=83=85=E5=86=B5=E4=B8=8B=E4=B8=8B=E5=88=92?= =?UTF-8?q?=E7=BA=BF=E9=94=99=E8=AF=AF=E6=B8=B2=E6=9F=93=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20#551?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/legado/app/constant/AppPattern.kt | 8 +- .../java/io/legado/app/constant/BookType.kt | 6 + .../io/legado/app/help/book/BookExtensions.kt | 7 +- .../legado/app/help/book/ContentProcessor.kt | 9 + .../legado/app/model/webBook/BookContent.kt | 24 +- .../ui/book/read/config/MoreConfigDialog.kt | 2 +- .../app/ui/book/read/page/TextBaseColumn.kt | 14 + .../book/read/page/entities/TextHtmlColumn.kt | 89 ++++ .../ui/book/read/page/entities/TextLine.kt | 14 +- .../read/page/entities/column/TextColumn.kt | 2 +- .../read/page/provider/ChapterProvider.kt | 4 +- .../read/page/provider/TextChapterLayout.kt | 428 +++++++++++++----- app/src/main/res/values-zh-rCN/arrays.xml | 6 + app/src/main/res/values/array_values.xml | 6 + app/src/main/res/values/arrays.xml | 6 + app/src/main/res/xml/pref_config_read.xml | 13 + 16 files changed, 501 insertions(+), 137 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/book/read/page/TextBaseColumn.kt create mode 100644 app/src/main/java/io/legado/app/ui/book/read/page/entities/TextHtmlColumn.kt 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" /> + + + +