[新增] 新增了可选的文本强调方式
[新增] 新增了Html文本渲染(@Luoyacheng) [修复] 修复了某些情况下下划线错误渲染的问题 #551
This commit is contained in:
16 files changed
+499
-135
No files matched your search
@@ -11,6 +11,12 @@ object AppPattern {
|
|||||||
//匹配格式化后的图片格式
|
//匹配格式化后的图片格式
|
||||||
val imgPattern: Pattern = Pattern.compile("<img[^>]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
|
val imgPattern: Pattern = Pattern.compile("<img[^>]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
|
||||||
|
|
||||||
|
//匹配自定义html格式字符串
|
||||||
|
val useHtmlRegex = Regex("<usehtml>.*?</usehtml>", RegexOption.DOT_MATCHES_ALL) //.包含换行
|
||||||
|
|
||||||
|
//匹配html字符串中的head
|
||||||
|
val htmlHeadRegex = Regex("<head[^>]*>", RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
//dataURL图片类型
|
//dataURL图片类型
|
||||||
val dataUriRegex = Regex("^data:.*?;base64,(.*)")
|
val dataUriRegex = Regex("^data:.*?;base64,(.*)")
|
||||||
//提取标题中的段评
|
//提取标题中的段评
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import androidx.annotation.IntDef
|
|||||||
*/
|
*/
|
||||||
@Suppress("ConstPropertyName")
|
@Suppress("ConstPropertyName")
|
||||||
object BookType {
|
object BookType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4 视频
|
||||||
|
*/
|
||||||
|
const val video = 0b100
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 8 文本
|
* 8 文本
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package io.legado.app.help.book
|
package io.legado.app.help.book
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.script.buildScriptBindings
|
import com.script.buildScriptBindings
|
||||||
import com.script.rhino.RhinoScriptEngine
|
import com.script.rhino.RhinoScriptEngine
|
||||||
import io.legado.app.constant.AppLog
|
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.find
|
||||||
import io.legado.app.utils.inputStream
|
import io.legado.app.utils.inputStream
|
||||||
import io.legado.app.utils.isUri
|
import io.legado.app.utils.isUri
|
||||||
|
import io.legado.app.utils.normalizeFileName
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -32,8 +34,6 @@ import java.time.temporal.ChronoUnit
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import androidx.core.net.toUri
|
|
||||||
import io.legado.app.utils.normalizeFileName
|
|
||||||
|
|
||||||
|
|
||||||
val Book.isAudio: Boolean
|
val Book.isAudio: Boolean
|
||||||
@@ -42,6 +42,9 @@ val Book.isAudio: Boolean
|
|||||||
val Book.isImage: Boolean
|
val Book.isImage: Boolean
|
||||||
get() = isType(BookType.image)
|
get() = isType(BookType.image)
|
||||||
|
|
||||||
|
val Book.isVideo: Boolean
|
||||||
|
get() = isType(BookType.video)
|
||||||
|
|
||||||
val Book.isLocal: Boolean
|
val Book.isLocal: Boolean
|
||||||
get() {
|
get() {
|
||||||
if (type == 0) {
|
if (type == 0) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.legado.app.help.book
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.constant.AppPattern.spaceRegex
|
import io.legado.app.constant.AppPattern.spaceRegex
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
@@ -143,6 +144,14 @@ class ContentProcessor private constructor(
|
|||||||
appCtx.toastOnUi("简繁转换出错")
|
appCtx.toastOnUi("简繁转换出错")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val useHtmlMap = mutableMapOf<String, String>()
|
||||||
|
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()) {
|
if (useReplace && book.getUseReplaceRule()) {
|
||||||
//替换
|
//替换
|
||||||
effectiveReplaceRules = arrayListOf()
|
effectiveReplaceRules = arrayListOf()
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import io.legado.app.data.entities.rule.ContentRule
|
|||||||
import io.legado.app.exception.ContentEmptyException
|
import io.legado.app.exception.ContentEmptyException
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.book.BookHelp
|
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.help.config.AppConfig
|
||||||
import io.legado.app.model.Debug
|
import io.legado.app.model.Debug
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeRule
|
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.HtmlFormatter
|
||||||
import io.legado.app.utils.NetworkUtils
|
import io.legado.app.utils.NetworkUtils
|
||||||
import io.legado.app.utils.mapAsync
|
import io.legado.app.utils.mapAsync
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import org.apache.commons.text.StringEscapeUtils
|
import org.apache.commons.text.StringEscapeUtils
|
||||||
@@ -181,16 +184,29 @@ object BookContent {
|
|||||||
): Pair<String, List<String>> {
|
): Pair<String, List<String>> {
|
||||||
val analyzeRule = AnalyzeRule(book, bookSource)
|
val analyzeRule = AnalyzeRule(book, bookSource)
|
||||||
analyzeRule.setContent(body, baseUrl)
|
analyzeRule.setContent(body, baseUrl)
|
||||||
analyzeRule.setCoroutineContext(coroutineContext)
|
analyzeRule.setCoroutineContext(currentCoroutineContext())
|
||||||
val rUrl = analyzeRule.setRedirectUrl(redirectUrl)
|
val rUrl = analyzeRule.setRedirectUrl(redirectUrl)
|
||||||
analyzeRule.setNextChapterUrl(nextChapterUrl)
|
analyzeRule.setNextChapterUrl(nextChapterUrl)
|
||||||
val nextUrlList = arrayListOf<String>()
|
val nextUrlList = arrayListOf<String>()
|
||||||
analyzeRule.setChapter(chapter)
|
analyzeRule.setChapter(chapter)
|
||||||
//获取正文
|
//获取正文
|
||||||
var content = analyzeRule.getString(contentRule.content, unescape = false)
|
var content = analyzeRule.getString(contentRule.content, unescape = false)
|
||||||
content = HtmlFormatter.formatKeepImg(content, rUrl)
|
if (!book.isAudio && !book.isVideo) { //音频和视频获取的是链接,不需要html格式化 && !book.isVideo
|
||||||
if (content.indexOf('&') > -1) {
|
val useHtmlMap = mutableMapOf<String, String>()
|
||||||
content = StringEscapeUtils.unescapeHtml4(content)
|
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) {
|
if (getNextPageUrl) {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class MoreConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_more_conf
|
|||||||
|
|
||||||
PreferKey.textFullJustify,
|
PreferKey.textFullJustify,
|
||||||
PreferKey.textBottomJustify,
|
PreferKey.textBottomJustify,
|
||||||
PreferKey.useZhLayout -> {
|
PreferKey.useZhLayout, PreferKey.adaptSpecialStyle, PreferKey.useUnderline -> {
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ 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 isHtml: Boolean = false,
|
||||||
var startX: Float = 0f,
|
var startX: Float = 0f,
|
||||||
var indentSize: Int = 0,
|
var indentSize: Int = 0,
|
||||||
var extraLetterSpacing: Float = 0f,
|
var extraLetterSpacing: Float = 0f,
|
||||||
@@ -66,7 +67,8 @@ data class TextLine(
|
|||||||
}
|
}
|
||||||
var textPage: TextPage = emptyTextPage
|
var textPage: TextPage = emptyTextPage
|
||||||
var isLeftLine = true
|
var isLeftLine = true
|
||||||
|
val useUnderline: Boolean
|
||||||
|
get() = AppConfig.useUnderline
|
||||||
fun addColumn(column: BaseColumn) {
|
fun addColumn(column: BaseColumn) {
|
||||||
if (column !is TextColumn) {
|
if (column !is TextColumn) {
|
||||||
onlyTextColumn = false
|
onlyTextColumn = false
|
||||||
@@ -75,6 +77,14 @@ data class TextLine(
|
|||||||
textColumns.add(column)
|
textColumns.add(column)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addColumns(columns: Collection<BaseColumn>) {
|
||||||
|
onlyTextColumn = false
|
||||||
|
columns.forEach { column ->
|
||||||
|
column.textLine = this
|
||||||
|
}
|
||||||
|
textColumns.addAll(columns)
|
||||||
|
}
|
||||||
|
|
||||||
fun getColumn(index: Int): BaseColumn {
|
fun getColumn(index: Int): BaseColumn {
|
||||||
return textColumns.getOrElse(index) {
|
return textColumns.getOrElse(index) {
|
||||||
textColumns.last()
|
textColumns.last()
|
||||||
@@ -158,7 +168,7 @@ data class TextLine(
|
|||||||
for (i in columns.indices) columns[i].draw(view, canvas)
|
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 linePaint = ChapterProvider.linePaint
|
||||||
val lineY = height - 1.dpToPx()
|
val lineY = height - 1.dpToPx()
|
||||||
canvas.drawLine(lineStart + indentWidth, lineY, lineEnd, lineY, linePaint)
|
canvas.drawLine(lineStart + indentWidth, lineY, lineEnd, lineY, linePaint)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ data class TextColumn(
|
|||||||
} else {
|
} else {
|
||||||
ChapterProvider.contentPaint
|
ChapterProvider.contentPaint
|
||||||
}
|
}
|
||||||
val textColor = if (textLine.isReadAloud || isSearchResult) {
|
val textColor = if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
|
||||||
ReadBookConfig.textAccentColor
|
ReadBookConfig.textAccentColor
|
||||||
} else {
|
} else {
|
||||||
ReadBookConfig.textColor
|
ReadBookConfig.textColor
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ import java.util.Locale
|
|||||||
@Suppress("DEPRECATION", "ConstPropertyName")
|
@Suppress("DEPRECATION", "ConstPropertyName")
|
||||||
object ChapterProvider {
|
object ChapterProvider {
|
||||||
//用于图片字的替换
|
//用于图片字的替换
|
||||||
const val srcReplaceChar = "⼁"//▩ 丨
|
const val srcReplaceChar = "袮" //▩▣ 丨⼁ //换成袮,这是不应该存在的汉字,替换为祢
|
||||||
|
const val srcReplaceCharC = '袮' //可能有略微的提升
|
||||||
|
const val srcReplaceCharD = '祢'
|
||||||
|
|
||||||
//用于评论按钮的替换
|
//用于评论按钮的替换
|
||||||
const val reviewChar = "꧁"
|
const val reviewChar = "꧁"
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
package io.legado.app.ui.book.read.page.provider
|
package io.legado.app.ui.book.read.page.provider
|
||||||
|
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
|
import android.os.Build
|
||||||
import android.text.Layout
|
import android.text.Layout
|
||||||
|
import android.text.Spanned
|
||||||
import android.text.StaticLayout
|
import android.text.StaticLayout
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.RelativeSizeSpan
|
||||||
|
import android.text.style.URLSpan
|
||||||
import android.util.Size
|
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.AppLog
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.constant.AppPattern.noWordCountRegex
|
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.ImageProvider
|
||||||
import io.legado.app.model.ReadBook
|
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.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.TextLine
|
||||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
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.ImageColumn
|
||||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
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.StringUtils
|
||||||
import io.legado.app.utils.dpToPx
|
import io.legado.app.utils.dpToPx
|
||||||
import io.legado.app.utils.fastSum
|
import io.legado.app.utils.fastSum
|
||||||
@@ -39,7 +51,6 @@ import kotlinx.coroutines.currentCoroutineContext
|
|||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class TextChapterLayout(
|
class TextChapterLayout(
|
||||||
@@ -67,8 +78,6 @@ class TextChapterLayout(
|
|||||||
private val titleTopSpacing = ChapterProvider.titleTopSpacing
|
private val titleTopSpacing = ChapterProvider.titleTopSpacing
|
||||||
private val titleBottomSpacing = ChapterProvider.titleBottomSpacing
|
private val titleBottomSpacing = ChapterProvider.titleBottomSpacing
|
||||||
private val lineSpacingExtra = ChapterProvider.lineSpacingExtra
|
private val lineSpacingExtra = ChapterProvider.lineSpacingExtra
|
||||||
private val titleLineSpacingExtra = ChapterProvider.titleLineSpacingExtra
|
|
||||||
|
|
||||||
private val paragraphSpacing = ChapterProvider.paragraphSpacing
|
private val paragraphSpacing = ChapterProvider.paragraphSpacing
|
||||||
|
|
||||||
private val visibleHeight = ChapterProvider.visibleHeight
|
private val visibleHeight = ChapterProvider.visibleHeight
|
||||||
@@ -84,12 +93,8 @@ class TextChapterLayout(
|
|||||||
private val useZhLayout = ReadBookConfig.useZhLayout
|
private val useZhLayout = ReadBookConfig.useZhLayout
|
||||||
private val isMiddleTitle = ReadBookConfig.isMiddleTitle
|
private val isMiddleTitle = ReadBookConfig.isMiddleTitle
|
||||||
private val textFullJustify = ReadBookConfig.textFullJustify
|
private val textFullJustify = ReadBookConfig.textFullJustify
|
||||||
|
private val adaptSpecialStyle = AppConfig.adaptSpecialStyle
|
||||||
private val pageAnim = book.getPageAnim()
|
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()
|
private var pendingTextPage = TextPage()
|
||||||
|
|
||||||
@@ -201,48 +206,6 @@ class TextChapterLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTitleSegments(
|
|
||||||
rawTitle: String,
|
|
||||||
segType: Int,
|
|
||||||
segDistance: Int,
|
|
||||||
segFlag: String
|
|
||||||
): List<String> {
|
|
||||||
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)
|
val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true)
|
||||||
|
|
||||||
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
|
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
|
||||||
val baseTitles = displayTitle.splitNotBlank("\n")
|
//标题非隐藏
|
||||||
baseTitles.forEach { rawTitle ->
|
displayTitle.splitNotBlank("\n").forEach { text ->
|
||||||
val titleSegments = parseTitleSegments(
|
val srcList = LinkedList<String>()
|
||||||
rawTitle,
|
val reviewImg = bookChapter.reviewImg
|
||||||
titleSegType,
|
var reviewTxt = ""
|
||||||
titleSegDistance,
|
if (!reviewImg.isNullOrEmpty()) {
|
||||||
titleSegFlag
|
srcList.add(reviewImg)
|
||||||
)
|
reviewTxt = if (reviewImg.contains("TEXT")) {
|
||||||
titleSegments.forEachIndexed { index, segment ->
|
reviewChar
|
||||||
val srcList = LinkedList<String>()
|
} else {
|
||||||
val reviewImg = bookChapter.reviewImg
|
srcReplaceChar
|
||||||
var reviewTxt = ""
|
|
||||||
if (reviewImg != null) {
|
|
||||||
srcList.add(reviewImg)
|
|
||||||
reviewTxt = if (reviewImg.contains("TEXT")) {
|
|
||||||
ChapterProvider.reviewChar
|
|
||||||
} else {
|
|
||||||
ChapterProvider.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
|
durY += titleBottomSpacing
|
||||||
|
|
||||||
|
// 如果是单图模式且当前页有内容,强制分页
|
||||||
if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty() && contents.isNotEmpty()) {
|
if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty() && contents.isNotEmpty()) {
|
||||||
prepareNextPageIfNeed()
|
prepareNextPageIfNeed()
|
||||||
}
|
}
|
||||||
@@ -310,16 +262,26 @@ class TextChapterLayout(
|
|||||||
var wordCount = 0
|
var wordCount = 0
|
||||||
contents.forEach { content ->
|
contents.forEach { content ->
|
||||||
currentCoroutineContext().ensureActive()
|
currentCoroutineContext().ensureActive()
|
||||||
|
if (adaptSpecialStyle) {
|
||||||
|
val text = content.trim()
|
||||||
|
if (text == "[newpage]") {
|
||||||
|
prepareNextPageIfNeed()
|
||||||
|
return@forEach
|
||||||
|
} else if (text.startsWith("<usehtml>")) {
|
||||||
|
setTypeHtml(book, text.substring(9, text.lastIndexOf("<")))
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var text = content.replace(srcReplaceCharC, srcReplaceCharD)
|
||||||
if (isTextImageStyle) {
|
if (isTextImageStyle) {
|
||||||
//图片样式为文字嵌入类型
|
//图片样式为文字嵌入类型
|
||||||
var text = content.replace(ChapterProvider.srcReplaceChar, "丨") //▣
|
|
||||||
val srcList = LinkedList<String>()
|
val srcList = LinkedList<String>()
|
||||||
sb.setLength(0)
|
sb.setLength(0)
|
||||||
val matcher = AppPattern.imgPattern.matcher(text)
|
val matcher = AppPattern.imgPattern.matcher(text)
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
matcher.group(1)?.let { src ->
|
matcher.group(1)?.let { src ->
|
||||||
srcList.add(src)
|
srcList.add(src)
|
||||||
matcher.appendReplacement(sb, ChapterProvider.srcReplaceChar)
|
matcher.appendReplacement(sb, srcReplaceChar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
matcher.appendTail(sb)
|
matcher.appendTail(sb)
|
||||||
@@ -344,7 +306,6 @@ class TextChapterLayout(
|
|||||||
sb.setLength(0)
|
sb.setLength(0)
|
||||||
var isFirstLine = true
|
var isFirstLine = true
|
||||||
if (content.contains("<img")) {
|
if (content.contains("<img")) {
|
||||||
val text = content.replace(ChapterProvider.srcReplaceChar, "丨")
|
|
||||||
val matcher = AppPattern.imgPattern.matcher(text)
|
val matcher = AppPattern.imgPattern.matcher(text)
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
currentCoroutineContext().ensureActive()
|
currentCoroutineContext().ensureActive()
|
||||||
@@ -355,25 +316,18 @@ class TextChapterLayout(
|
|||||||
var imgSize: Size? = null
|
var imgSize: Size? = null
|
||||||
if (matchResult != null) {
|
if (matchResult != null) {
|
||||||
val styleValue = matchResult.groupValues[1].trim()
|
val styleValue = matchResult.groupValues[1].trim()
|
||||||
if (styleValue == "TEXT" && matcher.end() == text.length) {
|
if (styleValue.equals("text", true)) { //忽略大小写
|
||||||
iStyle = "TEXT"
|
iStyle = styleValue
|
||||||
wordCount--
|
} else {
|
||||||
}
|
|
||||||
else if (styleValue.equals("text", true)) {
|
|
||||||
iStyle = "text"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
|
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
|
||||||
iStyle = styleValue
|
iStyle = styleValue
|
||||||
isSmallImage = false
|
isSmallImage = false
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
|
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
|
||||||
if (imgSize.width < 80 && imgSize.height < 80) {
|
if (imgSize.width < 80 && imgSize.height < 80) {
|
||||||
iStyle = "text"
|
iStyle = "text"
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
isSmallImage = false
|
isSmallImage = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,9 +337,9 @@ class TextChapterLayout(
|
|||||||
if (isSmallImage) {
|
if (isSmallImage) {
|
||||||
sb.append(
|
sb.append(
|
||||||
if (iStyle == "TEXT")
|
if (iStyle == "TEXT")
|
||||||
ChapterProvider.reviewChar
|
reviewChar
|
||||||
else
|
else
|
||||||
ChapterProvider.srcReplaceChar
|
srcReplaceChar
|
||||||
)
|
)
|
||||||
srcList.add(imgSrc)
|
srcList.add(imgSrc)
|
||||||
} else {
|
} else {
|
||||||
@@ -403,7 +357,7 @@ class TextChapterLayout(
|
|||||||
srcList = srcList
|
srcList = srcList
|
||||||
)
|
)
|
||||||
sb.setLength(0)
|
sb.setLength(0)
|
||||||
isFirstLine=false
|
isFirstLine = false
|
||||||
}
|
}
|
||||||
setTypeImage(
|
setTypeImage(
|
||||||
book,
|
book,
|
||||||
@@ -425,17 +379,17 @@ class TextChapterLayout(
|
|||||||
val textAfter = content.substring(start, content.length)
|
val textAfter = content.substring(start, content.length)
|
||||||
sb.append(textAfter)
|
sb.append(textAfter)
|
||||||
}
|
}
|
||||||
val text = sb.toString()
|
text = sb.toString()
|
||||||
if (text.isNotBlank()) {
|
if (text.isNotBlank()) {
|
||||||
wordCount += text.replace(noWordCountRegex,"").length
|
wordCount += text.replace(noWordCountRegex,"").length
|
||||||
setTypeText(
|
setTypeText(
|
||||||
book,
|
book,
|
||||||
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text,
|
if (AppConfig.enableReview) text + reviewChar else text,
|
||||||
contentPaint,
|
contentPaint,
|
||||||
contentPaintTextHeight,
|
contentPaintTextHeight,
|
||||||
contentPaintFontMetrics,
|
contentPaintFontMetrics,
|
||||||
"TEXT",
|
"TEXT",
|
||||||
isFirstLine = start == 0,
|
isFirstLine = isFirstLine,
|
||||||
srcList = srcList.ifEmpty { null }
|
srcList = srcList.ifEmpty { null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -544,6 +498,232 @@ class TextChapterLayout(
|
|||||||
durY += textHeight * paragraphSpacing / 10f
|
durY += textHeight * paragraphSpacing / 10f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排版html样式
|
||||||
|
*/
|
||||||
|
private suspend fun setTypeHtml(
|
||||||
|
book: Book,
|
||||||
|
htmlContent: String,
|
||||||
|
) {
|
||||||
|
val spanned = htmlContent.parseAsHtml(HtmlCompat.FROM_HTML_MODE_COMPACT)
|
||||||
|
val width = visibleWidth
|
||||||
|
val textPaint = contentPaint
|
||||||
|
val textColor = ReadBookConfig.textColor
|
||||||
|
if (textPaint.color != textColor) {
|
||||||
|
textPaint.color = textColor
|
||||||
|
}
|
||||||
|
val staticLayout = if (Build.VERSION.SDK_INT >= 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<TextHtmlColumn>()
|
||||||
|
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<TextHtmlColumn>,
|
||||||
|
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 (words, widths) = measureTextSplit(lineText, widthsArray, lineStart)
|
||||||
val desiredWidth = widths.fastSum()
|
val desiredWidth = widths.fastSum()
|
||||||
textLine.text = lineText
|
textLine.text = lineText
|
||||||
when {
|
when (lineIndex) {
|
||||||
lineIndex == 0 && layout.lineCount > 1 && !isTitle && isFirstLine -> {
|
0 if layout.lineCount > 1 && !isTitle && isFirstLine -> {
|
||||||
//多行的第一行 非标题
|
//多行的第一行 非标题
|
||||||
addCharsToLineFirst(
|
addCharsToLineFirst(
|
||||||
book, absStartX, textLine, words, textPaint,
|
book, absStartX, textLine, words, textPaint,
|
||||||
@@ -623,7 +803,7 @@ class TextChapterLayout(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
lineIndex == layout.lineCount - 1 -> {
|
layout.lineCount - 1 -> {
|
||||||
//最后一行、单行
|
//最后一行、单行
|
||||||
//标题x轴居中
|
//标题x轴居中
|
||||||
val startX = if (
|
val startX = if (
|
||||||
@@ -640,7 +820,6 @@ class TextChapterLayout(
|
|||||||
startX, !isTitle && lineIndex == 0, widths, srcList
|
startX, !isTitle && lineIndex == 0, widths, srcList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (
|
if (
|
||||||
isTitle &&
|
isTitle &&
|
||||||
@@ -670,13 +849,12 @@ class TextChapterLayout(
|
|||||||
textLine.upTopBottom(durY, textHeight, fontMetrics)
|
textLine.upTopBottom(durY, textHeight, fontMetrics)
|
||||||
val textPage = pendingTextPage
|
val textPage = pendingTextPage
|
||||||
textPage.addLine(textLine)
|
textPage.addLine(textLine)
|
||||||
durY += textHeight * if (isTitle) titleLineSpacingExtra else lineSpacingExtra
|
durY += textHeight * lineSpacingExtra
|
||||||
if (textPage.height < durY) {
|
if (textPage.height < durY) {
|
||||||
textPage.height = durY
|
textPage.height = durY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!isTitle)
|
durY += textHeight * paragraphSpacing / 10f
|
||||||
durY += textHeight * paragraphSpacing / 10f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calcTextLinePosition(
|
private fun calcTextLinePosition(
|
||||||
@@ -852,7 +1030,7 @@ class TextChapterLayout(
|
|||||||
srcList: LinkedList<String>?
|
srcList: LinkedList<String>?
|
||||||
) {
|
) {
|
||||||
val column = when {
|
val column = when {
|
||||||
!srcList.isNullOrEmpty() && (char == ChapterProvider.srcReplaceChar || char == ChapterProvider.reviewChar && isLineEnd) -> {
|
!srcList.isNullOrEmpty() && (char == srcReplaceChar || char == reviewChar) -> {
|
||||||
val src = srcList.removeFirst()
|
val src = srcList.removeFirst()
|
||||||
ImageProvider.cacheImage(book, src, ReadBook.bookSource)
|
ImageProvider.cacheImage(book, src, ReadBook.bookSource)
|
||||||
ImageColumn(
|
ImageColumn(
|
||||||
@@ -861,13 +1039,13 @@ class TextChapterLayout(
|
|||||||
src = src
|
src = src
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/*isLineEnd && char == ChapterProvider.reviewChar -> {
|
// isLineEnd && char == ChapterProvider.reviewChar -> {
|
||||||
ReviewColumn(
|
// ReviewColumn(
|
||||||
start = absStartX + xStart,
|
// start = absStartX + xStart,
|
||||||
end = absStartX + xEnd,
|
// end = absStartX + xEnd,
|
||||||
count = 10
|
// count = 10
|
||||||
)
|
// )
|
||||||
}*/
|
// }
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
TextColumn(
|
TextColumn(
|
||||||
|
|||||||
@@ -168,6 +168,12 @@
|
|||||||
<item>内容取色</item>
|
<item>内容取色</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="composeEngine">
|
||||||
|
<item>Material Design</item>
|
||||||
|
<item>SaltUI</item>
|
||||||
|
<item>Miuix</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="customMode">
|
<string-array name="customMode">
|
||||||
<item>全局</item>
|
<item>全局</item>
|
||||||
<item>仅强调色</item>
|
<item>仅强调色</item>
|
||||||
|
|||||||
@@ -90,6 +90,12 @@
|
|||||||
<item>content</item>
|
<item>content</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="composeEngine_value">
|
||||||
|
<item>material</item>
|
||||||
|
<item>salt</item>
|
||||||
|
<item>miuix</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="customMode_value">
|
<string-array name="customMode_value">
|
||||||
<item>global</item>
|
<item>global</item>
|
||||||
<item>accent</item>
|
<item>accent</item>
|
||||||
|
|||||||
@@ -205,6 +205,12 @@
|
|||||||
<item>Content</item>
|
<item>Content</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="composeEngine">
|
||||||
|
<item>Material Design</item>
|
||||||
|
<item>SaltUI</item>
|
||||||
|
<item>Miuix</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="customMode">
|
<string-array name="customMode">
|
||||||
<item>Global</item>
|
<item>Global</item>
|
||||||
<item>Accent</item>
|
<item>Accent</item>
|
||||||
|
|||||||
@@ -80,6 +80,13 @@
|
|||||||
android:title="@string/text_bottom_justify"
|
android:title="@string/text_bottom_justify"
|
||||||
app:isBottomBackground="true" />
|
app:isBottomBackground="true" />
|
||||||
|
|
||||||
|
<io.legado.app.lib.prefs.SwitchPreference
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:key="adaptSpecialStyle"
|
||||||
|
android:title="@string/adapt_special_style"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
app:isBottomBackground="true" />
|
||||||
|
|
||||||
<io.legado.app.lib.prefs.SwitchPreference
|
<io.legado.app.lib.prefs.SwitchPreference
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:key="useZhLayout"
|
android:key="useZhLayout"
|
||||||
@@ -92,6 +99,12 @@
|
|||||||
android:title="@string/show_brightness_view"
|
android:title="@string/show_brightness_view"
|
||||||
app:isBottomBackground="true" />
|
app:isBottomBackground="true" />
|
||||||
|
|
||||||
|
<io.legado.app.lib.prefs.SwitchPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="useUnderline"
|
||||||
|
android:title="@string/use_underline"
|
||||||
|
app:isBottomBackground="true" />
|
||||||
|
|
||||||
</io.legado.app.lib.prefs.PreferenceCategory>
|
</io.legado.app.lib.prefs.PreferenceCategory>
|
||||||
|
|
||||||
<io.legado.app.lib.prefs.PreferenceCategory
|
<io.legado.app.lib.prefs.PreferenceCategory
|
||||||
|
|||||||
Reference in new issue
Block a user