diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 31fa6fc05..e9801a743 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -69,6 +69,7 @@ object PreferKey { const val defaultToRead = "defaultToRead" const val exportCharset = "exportCharset" const val exportUseReplace = "exportUseReplace" + const val useZhLayout = "useZhLayout" const val cPrimary = "colorPrimary" const val cAccent = "colorAccent" diff --git a/app/src/main/java/io/legado/app/help/AppConfig.kt b/app/src/main/java/io/legado/app/help/AppConfig.kt index ec4890e66..3b110e589 100644 --- a/app/src/main/java/io/legado/app/help/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/AppConfig.kt @@ -46,6 +46,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { appCtx.getPrefInt(PreferKey.clickActionBR, 2) PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) + PreferKey.useZhLayout -> ReadBookConfig.useZhLayout = + appCtx.getPrefBoolean(PreferKey.useZhLayout) PreferKey.userAgent -> userAgent = getPrefUserAgent() } } diff --git a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt index 8ebc339fc..b024838f3 100644 --- a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt @@ -20,9 +20,8 @@ import java.io.File object ReadBookConfig { const val configFileName = "readConfig.json" const val shareConfigFileName = "shareReadConfig.json" - val context get() = appCtx - val configFilePath = FileUtils.getPath(context.filesDir, configFileName) - val shareConfigFilePath = FileUtils.getPath(context.filesDir, shareConfigFileName) + val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName) + val shareConfigFilePath = FileUtils.getPath(appCtx.filesDir, shareConfigFileName) val configList: ArrayList = arrayListOf() lateinit var shareConfig: Config var durConfig @@ -84,7 +83,7 @@ object ReadBookConfig { } fun upBg() { - val resources = context.resources + val resources = appCtx.resources val dm = resources.displayMetrics val width = dm.widthPixels val height = dm.heightPixels @@ -133,30 +132,31 @@ object ReadBookConfig { } //配置写入读取 - var readBodyToLh = context.getPrefBoolean(PreferKey.readBodyToLh, true) - var autoReadSpeed = context.getPrefInt(PreferKey.autoReadSpeed, 46) + var readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) + var autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46) set(value) { field = value - context.putPrefInt(PreferKey.autoReadSpeed, value) + appCtx.putPrefInt(PreferKey.autoReadSpeed, value) } - var styleSelect = context.getPrefInt(PreferKey.readStyleSelect) + var styleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect) set(value) { field = value - if (context.getPrefInt(PreferKey.readStyleSelect) != value) { - context.putPrefInt(PreferKey.readStyleSelect, value) + if (appCtx.getPrefInt(PreferKey.readStyleSelect) != value) { + appCtx.putPrefInt(PreferKey.readStyleSelect, value) } } - var shareLayout = context.getPrefBoolean(PreferKey.shareLayout) + var shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout) set(value) { field = value - if (context.getPrefBoolean(PreferKey.shareLayout) != value) { - context.putPrefBoolean(PreferKey.shareLayout, value) + if (appCtx.getPrefBoolean(PreferKey.shareLayout) != value) { + appCtx.putPrefBoolean(PreferKey.shareLayout, value) } } - val textFullJustify get() = context.getPrefBoolean(PreferKey.textFullJustify, true) - val textBottomJustify get() = context.getPrefBoolean(PreferKey.textBottomJustify, true) - var hideStatusBar = context.getPrefBoolean(PreferKey.hideStatusBar) - var hideNavigationBar = context.getPrefBoolean(PreferKey.hideNavigationBar) + val textFullJustify get() = appCtx.getPrefBoolean(PreferKey.textFullJustify, true) + val textBottomJustify get() = appCtx.getPrefBoolean(PreferKey.textBottomJustify, true) + var hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar) + var hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar) + var useZhLayout = appCtx.getPrefBoolean(PreferKey.useZhLayout) val config get() = if (shareLayout) shareConfig else durConfig @@ -490,7 +490,7 @@ object ReadBookConfig { fun curBgDrawable(width: Int, height: Int): Drawable { var bgDrawable: Drawable? = null - val resources = context.resources + val resources = appCtx.resources try { bgDrawable = when (curBgType()) { 0 -> ColorDrawable(Color.parseColor(curBgStr())) @@ -498,7 +498,7 @@ object ReadBookConfig { BitmapDrawable( resources, BitmapUtils.decodeAssetsBitmap( - context, + appCtx, "bg" + File.separator + curBgStr(), width, height @@ -513,7 +513,7 @@ object ReadBookConfig { } catch (e: Exception) { e.printStackTrace() } - return bgDrawable ?: ColorDrawable(context.getCompatColor(R.color.background)) + return bgDrawable ?: ColorDrawable(appCtx.getCompatColor(R.color.background)) } } } \ No newline at end of file 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 7c94b5a2b..a31833111 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 @@ -200,7 +200,9 @@ object ChapterProvider { textPaint: TextPaint ): Float { var durY = if (isTitle) y + titleTopSpacing else y - val layout = StaticLayout( + val layout = if (ReadBookConfig.useZhLayout) { + ZhLayout(text, textPaint, visibleWidth) + } else StaticLayout( text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true ) for (lineIndex in 0 until layout.lineCount) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt new file mode 100644 index 000000000..4728c50e1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt @@ -0,0 +1,357 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Rect +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import io.legado.app.utils.toStringArray +import kotlin.math.max + +/* +* 针对中文的断行排版处理-by hoodie13 +* 因为StaticLayout对标点处理不符合国人习惯,继承Layout +* 接口封的不抽象,数组用的也琐碎,因目前语法不熟悉,后面完善。 +* */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ZhLayout( + text: String, + textPaint: TextPaint, + width: Int +) : Layout(text, textPaint, width, Alignment.ALIGN_NORMAL, 0f, 0f) { + var lineStart = IntArray(1000) + var lineEnd = IntArray(1000) + var lineWidth = FloatArray(1000) + var lineCompressMod = IntArray(1000) + private var lineCount = 0 + private val curPaint = textPaint + private val cnCharWitch = getDesiredWidth("我", textPaint) + + enum class BreakMod { NORMAL, BREAK_ONE_CHAR, BREAK_MORE_CHAR, CPS_1, CPS_2, CPS_3, } + class Locate { + var start: Float = 0f + var end: Float = 0f + } + + class Interval { + var total: Float = 0f + var single: Float = 0f + } + + init { + var line = 0 + val words = text.toStringArray() + var lineW = 0f + var cwPre = 0f + + words.forEachIndexed { index, s -> + val cw = getDesiredWidth(s, curPaint) + var breakMod: BreakMod + var breakLine = false + lineW += cw + var offset = 0f + var breakCharCnt = 0 + + if (lineW > width) { + /*禁止在行尾的标点处理*/ + breakMod = if (index >= 1 && isPrePanc(words[index - 1])) { + if (index >= 2 && isPrePanc(words[index - 2])) BreakMod.CPS_2//如果后面还有一个禁首标点则异常 + else BreakMod.BREAK_ONE_CHAR //无异常场景 + } + /*禁止在行首的标点处理*/ + else if (isPostPanc(words[index])) { + if (index >= 1 && isPostPanc(words[index - 1])) BreakMod.CPS_1//如果后面还有一个禁首标点则异常,不过三个连续行尾标点的用法不通用 + else if (index >= 2 && isPrePanc(words[index - 2])) BreakMod.CPS_3//如果后面还有一个禁首标点则异常 + else BreakMod.BREAK_ONE_CHAR //无异常场景 + } else { + BreakMod.NORMAL //无异常场景 + } + + /*判断上述逻辑解决不了的特殊情况*/ + var reCheck = false + var breakIndex = 0 + if (breakMod == BreakMod.CPS_1 && + (inCompressible(words[index]) || inCompressible(words[index - 1])) + ) reCheck = true + if (breakMod == BreakMod.CPS_2 && + (inCompressible(words[index - 1]) || inCompressible(words[index - 2])) + ) reCheck = true + if (breakMod == BreakMod.CPS_3 && + (inCompressible(words[index]) || inCompressible(words[index - 2])) + ) reCheck = true + if (breakMod > BreakMod.BREAK_MORE_CHAR + && index < words.lastIndex && isPostPanc(words[index + 1]) + ) reCheck = true + + /*特殊标点使用难保证显示效果,所以不考虑间隔,直接查找到能满足条件的分割字*/ + if (reCheck && index > 2) { + breakMod = BreakMod.NORMAL + for (i in (index) downTo 1) { + if (i == index) { + breakIndex = 0 + cwPre = 0f + } else { + breakIndex++ + cwPre += StaticLayout.getDesiredWidth(words[i], textPaint) + } + if (!isPostPanc(words[i]) && !isPrePanc(words[i - 1])) { + breakMod = BreakMod.BREAK_MORE_CHAR + break + } + } + } + + when (breakMod) { + BreakMod.NORMAL -> {//模式0 正常断行 + offset = cw + lineEnd[line] = index + lineCompressMod[line] = 0 + breakCharCnt = 1 + } + BreakMod.BREAK_ONE_CHAR -> {//模式1 当前行下移一个字 + offset = cw + cwPre + lineEnd[line] = index - 1 + lineCompressMod[line] = 0 + breakCharCnt = 2 + } + BreakMod.BREAK_MORE_CHAR -> {//模式2 当前行下移多个字 + offset = cw + cwPre + lineEnd[line] = index - breakIndex + lineCompressMod[line] = 0 + breakCharCnt = breakIndex + 1 + } + BreakMod.CPS_1 -> {//模式3 两个后置标点压缩 + offset = 0f + lineEnd[line] = index + 1 + lineCompressMod[line] = 1 + breakCharCnt = 0 + } + BreakMod.CPS_2 -> { //模式4 前置标点压缩+前置标点压缩+字 + offset = 0f + lineEnd[line] = index + 1 + lineCompressMod[line] = 2 + breakCharCnt = 0 + } + BreakMod.CPS_3 -> {//模式5 前置标点压缩+字+后置标点压缩 + offset = 0f + lineEnd[line] = index + 1 + lineCompressMod[line] = 3 + breakCharCnt = 0 + } + } + breakLine = true + } + + /*当前行写满情况下的断行*/ + if (breakLine) { + lineWidth[line] = lineW - offset + lineStart[line + 1] = lineEnd[line] + lineW = offset + line++ + } + /*已到最后一个字符*/ + if ((words.lastIndex) == index) { + if (!breakLine) { + offset = 0f + lineEnd[line] = index + 1 + lineWidth[line] = lineW - offset + lineW = offset + line++ + } + /*写满断行、段落末尾、且需要下移字符,这种特殊情况下要额外多一行*/ + else if (breakCharCnt > 0) { + lineEnd[line] = lineStart[line] + breakCharCnt + lineWidth[line] = lineW + line++ + } + } + cwPre = cw + if (line >= 999) { + return@forEachIndexed + } + } + + lineCount = line + + } + + private fun isPostPanc(string: String): Boolean { + val panc = arrayOf( + ",", "。", ":", "?", "!", "、", "”", "’", ")", "》", "}", + "】", ")", ">", "]", "}", ",", ".", "?", "!", ":", "」", ";", ";" + ) + panc.forEach { + if (it == string) return true + } + return false + } + + private fun isPrePanc(string: String): Boolean { + val panc = arrayOf("“", "(", "《", "【", "‘", "‘", "(", "<", "[", "{", "「") + panc.forEach { + if (it == string) return true + } + return false + } + + private fun inCompressible(string: String): Boolean { + return getDesiredWidth(string, curPaint) < cnCharWitch + } + + private val gap = (cnCharWitch / 12.75).toFloat() + private fun getPostPancOffset(string: String): Float { + val textRect = Rect() + curPaint.getTextBounds(string, 0, 1, textRect) + return max(textRect.left.toFloat() - gap, 0f) + } + + private fun getPrePancOffset(string: String): Float { + val textRect = Rect() + curPaint.getTextBounds(string, 0, 1, textRect) + val d = max(cnCharWitch - textRect.right.toFloat() - gap, 0f) + return cnCharWitch / 2 - d + } + + fun getDesiredWidth(sting: String, paint: TextPaint) = paint.measureText(sting) + + override fun getLineCount(): Int { + return lineCount + } + + override fun getLineTop(line: Int): Int { + return 0 + } + + override fun getLineDescent(line: Int): Int { + return 0 + } + + override fun getLineStart(line: Int): Int = lineStart[line] + + override fun getParagraphDirection(line: Int): Int { + return 0 + } + + override fun getLineContainsTab(line: Int): Boolean { + return true + } + + override fun getLineDirections(line: Int): Directions? { + return null + } + + override fun getTopPadding(): Int { + return 0 + } + + override fun getBottomPadding(): Int { + return 0 + } + + override fun getLineWidth(line: Int): Float = lineWidth[line] + + override fun getEllipsisStart(line: Int): Int { + return 0 + } + + override fun getEllipsisCount(line: Int): Int { + return 0 + } + + fun getDefaultWidth(): Float = cnCharWitch + + /* + * @fun:获取当前行的平均间隔:用于两端对齐,获取左对齐时的右边间隔:用于间隔过大时不再两端对齐 + * @in:行,当前字符串,最大显示宽度 + * @out:单个字符的平均间隔,左对齐的最大间隔 + */ + fun getInterval(line: Int, words: Array, visibleWidth: Int): Interval { + val interval = Interval() + val total: Float + val d: Float + val lastIndex = words.lastIndex + val desiredWidth = getLineWidth(line) + if (lineCompressMod[line] > 0) { + val gapCount: Int = lastIndex - 1 + val lastWordsWith = getDesiredWidth(words[lastIndex], curPaint) + total = visibleWidth - desiredWidth + lastWordsWith + d = total / gapCount + } else { + val gapCount: Int = lastIndex + total = visibleWidth - desiredWidth + d = total / gapCount + } + interval.total = total + interval.single = d + return interval + } + + /* + * @fun:获取当前行不同字符的位置 + * @in:行,当前字符对于最后一个字符的偏移值,字符,间隔,定位参数 + * @out:定位参数 + */ + fun getLocate(line: Int, idx: Int, string: String, interval: Float, locate: Locate) { + val cw = getDesiredWidth(string, curPaint) + when (lineCompressMod[line]) { + 1 -> { + when (idx) { + 1 -> { + val offset = getPostPancOffset(string) + locate.start -= offset + locate.end = locate.start + cw / 2 + offset + } + 0 -> { + locate.start -= getPostPancOffset(string) + locate.end = locate.start + cw + } + else -> { + locate.end = locate.start + cw + interval + } + } + } + 2 -> { + when (idx) { + 2 -> { + val offset = getPostPancOffset(string) + locate.start -= offset + locate.end = locate.start + cw / 2 + offset + } + 1 -> { + val offset = getPostPancOffset(string) + locate.start -= offset + locate.end = locate.start + cw / 2 + offset + } + 0 -> { + locate.end = locate.start + cw + } + else -> { + locate.end = locate.start + cw + interval + } + } + } + 3 -> { + when (idx) { + 2 -> { + val offset = getPrePancOffset(string) + locate.start -= offset + locate.end = locate.start + cw / 2 + offset + } + 1 -> { + locate.end = locate.start + cw + interval + } + 0 -> { + locate.start -= getPostPancOffset(string) + locate.end = locate.start + cw + } + else -> { + locate.end = locate.start + cw + interval + } + } + } + else -> { + locate.end = if (idx != 0) (locate.start + cw + interval) else (locate.start + cw) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 2ec010260..85352658e 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -807,5 +807,6 @@ 反转内容 调试 崩溃日志 + 使用自定义中文分行 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 75121734a..12f7bdcae 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -811,5 +811,6 @@ 反转内容 调试 崩溃日志 + 使用自定义中文分行 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index bc57ca217..9365bc626 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -811,5 +811,6 @@ 反转内容 调试 崩溃日志 + 使用自定义中文分行 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d11c1a87..2c820a9a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -814,5 +814,6 @@ 反转内容 调试 崩溃日志 + 使用自定义中文分行 diff --git a/app/src/main/res/xml/pref_config_read.xml b/app/src/main/res/xml/pref_config_read.xml index 0135feff8..9f240fc63 100644 --- a/app/src/main/res/xml/pref_config_read.xml +++ b/app/src/main/res/xml/pref_config_read.xml @@ -41,6 +41,13 @@ app:iconSpaceReserved="false" app:isBottomBackground="true" /> + +