标题分段 #181

This commit is contained in:
HapeLee
2025-11-01 13:29:51 +08:00
parent 030033e79b
commit 42c7449a8c
6 changed files with 264 additions and 32 deletions
@@ -273,6 +273,12 @@ object ReadBookConfig {
config.lineSpacingExtra = value
}
var titleLineSpacingExtra: Int
get() = config.titleLineSpacingExtra
set(value) {
config.titleLineSpacingExtra = value
}
var paragraphSpacing: Int
get() = config.paragraphSpacing
set(value) {
@@ -293,6 +299,30 @@ object ReadBookConfig {
config.titleSize = value
}
var titleSegType: Int
get() = config.titleSegType
set(value) {
config.titleSegType = value
}
var titleSegScaling: Float
get() = config.titleSegScaling
set(value) {
config.titleSegScaling = value
}
var titleSegDistance: Int
get() = config.titleSegDistance
set(value) {
config.titleSegDistance = value
}
var titleSegFlag: String
get() = config.titleSegFlag
set(value) {
config.titleSegFlag = value
}
/**
* 是否标题居中
*/
@@ -497,6 +527,11 @@ object ReadBookConfig {
var titleTopSpacing: Int = 0,
var titleBottomSpacing: Int = 0,
var titleBold: Int = 500,
var titleLineSpacingExtra: Int = 4,
var titleSegType: Int = 0,//分段模式
var titleSegScaling: Float = 1.2f,//分段缩放,第二段与第一段的字体大小比例
var titleSegDistance: Int = 4,//分段判断,第几个字符开始分段
var titleSegFlag: String = "",//分段判断,碰到指定值时分段
var paragraphIndent: String = "  ",//段落缩进
var underline: Boolean = false, //下划线
var paddingBottom: Int = 6,
@@ -1,12 +1,16 @@
package io.legado.app.ui.book.read.config
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.google.android.material.chip.Chip
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import io.legado.app.R
import io.legado.app.base.BaseBottomSheetDialogFragment
import io.legado.app.constant.EventBus
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogTipConfigBinding
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ReadTipConfig
@@ -15,6 +19,8 @@ import io.legado.app.lib.dialogs.selector
import io.legado.app.utils.hexString
import io.legado.app.utils.observeEvent
import io.legado.app.utils.postEvent
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -74,10 +80,82 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
addOnChangeListener { _, newValue, _ ->
binding.textFontWeightConverter.text = "自定义"
ReadBookConfig.titleBold = newValue.toInt()
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 9, 6))
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
}
}
binding.btnTitleSegType.setOnClickListener {
val types = arrayOf("不分段", "按字符数分段", "按标志字符串分段")
val current = ReadBookConfig.titleSegType
alert(title = "选择标题分段模式") {
singleChoiceItems(types, current) { _, which ->
ReadBookConfig.titleSegType = which
}
positiveButton("确定") {
toastOnUi("分段模式已设置为:${types[ReadBookConfig.titleSegType]}")
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
}
negativeButton("取消")
}.show()
}
binding.btnTitleSegConfig.setOnClickListener {
when (ReadBookConfig.titleSegType) {
1 -> { // 按字符数分段
alert(title = "设置分段字符数") {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.inputType = InputType.TYPE_CLASS_NUMBER
editView.setText(ReadBookConfig.titleSegDistance.toString())
editView.hint = "输入分段字符数"
}
customView { alertBinding.root }
okButton {
val value = alertBinding.editView.text?.toString()?.toIntOrNull()
if (value != null && value > 0) {
ReadBookConfig.titleSegDistance = value
toastOnUi("分段字符数设置为 $value")
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
} else {
toastOnUi("请输入有效数字")
}
}
cancelButton()
}.requestInputMethod()
}
2 -> { // 按标志字符串分段
alert(title = "设置分段标志") {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.inputType = InputType.TYPE_CLASS_TEXT
editView.setText(ReadBookConfig.titleSegFlag)
editLayout.hint = "输入多个标志,用英文逗号分隔,例如:章,回,篇"
}
customView { alertBinding.root }
okButton {
val value = alertBinding.editView.text?.toString()?.trim()
if (!value.isNullOrEmpty()) {
ReadBookConfig.titleSegFlag = value
toastOnUi("分段标志设置为 \"$value\"")
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
} else {
toastOnUi("标志不能为空")
}
}
cancelButton()
}.requestInputMethod()
}
else -> {
toastOnUi("当前分段模式无需配置参数")
}
}
}
binding.dsbTitleSegScaling.progress = ReadBookConfig.titleSegScaling.toInt() * 10
binding.dsbTitleLineSpacingExtra.progress = ReadBookConfig.titleLineSpacingExtra
binding.dsbTitleSize.progress = ReadBookConfig.titleSize
binding.dsbTitleTop.progress = ReadBookConfig.titleTopSpacing
binding.dsbTitleBottom.progress = ReadBookConfig.titleBottomSpacing
@@ -166,6 +244,14 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
}
}
binding.dsbTitleSegScaling.onChanged = {
ReadBookConfig.titleSegScaling = it / 10f
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
}
binding.dsbTitleLineSpacingExtra.onChanged = {
ReadBookConfig.titleLineSpacingExtra = it
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
}
dsbTitleSize.onChanged = {
ReadBookConfig.titleSize = it
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
@@ -97,6 +97,9 @@ object ChapterProvider {
var lineSpacingExtra = 0f
private set
var titleLineSpacingExtra = 0f
private set
@JvmStatic
var paragraphSpacing = 0
private set
@@ -861,6 +864,7 @@ object ChapterProvider {
}
//间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f
titleLineSpacingExtra = ReadBookConfig.titleLineSpacingExtra / 10f
paragraphSpacing = ReadBookConfig.paragraphSpacing
titleTopSpacing = ReadBookConfig.titleTopSpacing.dpToPx()
titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dpToPx()
@@ -65,6 +65,8 @@ 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
@@ -80,6 +82,11 @@ class TextChapterLayout(
private val useZhLayout = ReadBookConfig.useZhLayout
private val isMiddleTitle = ReadBookConfig.isMiddleTitle
private val textFullJustify = ReadBookConfig.textFullJustify
private val titleSegType = ReadBookConfig.titleSegType
private val titleSegDistance = ReadBookConfig.titleSegDistance
private val titleSegFlag = ReadBookConfig.titleSegFlag
private val titleSegScaling = ReadBookConfig.titleSegScaling
private var pendingTextPage = TextPage()
@@ -189,6 +196,48 @@ 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)
}
}
/**
* 获取拆分完的章节数据
*/
@@ -204,37 +253,48 @@ class TextChapterLayout(
val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true)
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
//标题非隐藏
displayTitle.splitNotBlank("\n").forEach { text ->
val srcList = LinkedList<String>()
val reviewImg = bookChapter.reviewImg
var reviewTxt = ""
if (reviewImg != null) {
srcList.add(reviewImg)
reviewTxt = if (reviewImg.contains("TEXT")) {
ChapterProvider.reviewChar
} else {
ChapterProvider.srcReplaceChar
}
}
setTypeText(
book,
text + reviewTxt,
titlePaint,
titlePaintTextHeight,
titlePaintFontMetrics,
imageStyle,
srcList = srcList.ifEmpty { null },
isTitle = true,
emptyContent = contents.isEmpty(),
isVolumeTitle = bookChapter.isVolume
val baseTitles = displayTitle.splitNotBlank("\n")
baseTitles.forEach { rawTitle ->
val titleSegments = parseTitleSegments(
rawTitle,
titleSegType,
titleSegDistance,
titleSegFlag
)
pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n")
titleSegments.forEachIndexed { index, segment ->
val srcList = LinkedList<String>()
val reviewImg = bookChapter.reviewImg
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")
}
}
durY += titleBottomSpacing
// 如果是单图模式且当前页有内容,强制分页
if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty() && contents.isNotEmpty()) {
prepareNextPageIfNeed()
}
@@ -598,7 +658,7 @@ class TextChapterLayout(
textLine.upTopBottom(durY, textHeight, fontMetrics)
val textPage = pendingTextPage
textPage.addLine(textLine)
durY += textHeight * lineSpacingExtra
durY += textHeight * if (isTitle) titleLineSpacingExtra else lineSpacingExtra
if (textPage.height < durY) {
textPage.height = durY
}