添加自定义中文分行

pull/883/head
gedoor 4 years ago
parent 76e28828e6
commit 256e2bb59d
  1. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  2. 2
      app/src/main/java/io/legado/app/help/AppConfig.kt
  3. 40
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  4. 4
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  5. 357
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt
  6. 1
      app/src/main/res/values-zh-rHK/strings.xml
  7. 1
      app/src/main/res/values-zh-rTW/strings.xml
  8. 1
      app/src/main/res/values-zh/strings.xml
  9. 1
      app/src/main/res/values/strings.xml
  10. 7
      app/src/main/res/xml/pref_config_read.xml

@ -69,6 +69,7 @@ object PreferKey {
const val defaultToRead = "defaultToRead" const val defaultToRead = "defaultToRead"
const val exportCharset = "exportCharset" const val exportCharset = "exportCharset"
const val exportUseReplace = "exportUseReplace" const val exportUseReplace = "exportUseReplace"
const val useZhLayout = "useZhLayout"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" const val cAccent = "colorAccent"

@ -46,6 +46,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.getPrefInt(PreferKey.clickActionBR, 2) appCtx.getPrefInt(PreferKey.clickActionBR, 2)
PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh = PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh =
appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
PreferKey.useZhLayout -> ReadBookConfig.useZhLayout =
appCtx.getPrefBoolean(PreferKey.useZhLayout)
PreferKey.userAgent -> userAgent = getPrefUserAgent() PreferKey.userAgent -> userAgent = getPrefUserAgent()
} }
} }

@ -20,9 +20,8 @@ import java.io.File
object ReadBookConfig { object ReadBookConfig {
const val configFileName = "readConfig.json" const val configFileName = "readConfig.json"
const val shareConfigFileName = "shareReadConfig.json" const val shareConfigFileName = "shareReadConfig.json"
val context get() = appCtx val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
val configFilePath = FileUtils.getPath(context.filesDir, configFileName) val shareConfigFilePath = FileUtils.getPath(appCtx.filesDir, shareConfigFileName)
val shareConfigFilePath = FileUtils.getPath(context.filesDir, shareConfigFileName)
val configList: ArrayList<Config> = arrayListOf() val configList: ArrayList<Config> = arrayListOf()
lateinit var shareConfig: Config lateinit var shareConfig: Config
var durConfig var durConfig
@ -84,7 +83,7 @@ object ReadBookConfig {
} }
fun upBg() { fun upBg() {
val resources = context.resources val resources = appCtx.resources
val dm = resources.displayMetrics val dm = resources.displayMetrics
val width = dm.widthPixels val width = dm.widthPixels
val height = dm.heightPixels val height = dm.heightPixels
@ -133,30 +132,31 @@ object ReadBookConfig {
} }
//配置写入读取 //配置写入读取
var readBodyToLh = context.getPrefBoolean(PreferKey.readBodyToLh, true) var readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
var autoReadSpeed = context.getPrefInt(PreferKey.autoReadSpeed, 46) var autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46)
set(value) { set(value) {
field = 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) { set(value) {
field = value field = value
if (context.getPrefInt(PreferKey.readStyleSelect) != value) { if (appCtx.getPrefInt(PreferKey.readStyleSelect) != value) {
context.putPrefInt(PreferKey.readStyleSelect, value) appCtx.putPrefInt(PreferKey.readStyleSelect, value)
} }
} }
var shareLayout = context.getPrefBoolean(PreferKey.shareLayout) var shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout)
set(value) { set(value) {
field = value field = value
if (context.getPrefBoolean(PreferKey.shareLayout) != value) { if (appCtx.getPrefBoolean(PreferKey.shareLayout) != value) {
context.putPrefBoolean(PreferKey.shareLayout, value) appCtx.putPrefBoolean(PreferKey.shareLayout, value)
} }
} }
val textFullJustify get() = context.getPrefBoolean(PreferKey.textFullJustify, true) val textFullJustify get() = appCtx.getPrefBoolean(PreferKey.textFullJustify, true)
val textBottomJustify get() = context.getPrefBoolean(PreferKey.textBottomJustify, true) val textBottomJustify get() = appCtx.getPrefBoolean(PreferKey.textBottomJustify, true)
var hideStatusBar = context.getPrefBoolean(PreferKey.hideStatusBar) var hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar)
var hideNavigationBar = context.getPrefBoolean(PreferKey.hideNavigationBar) var hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar)
var useZhLayout = appCtx.getPrefBoolean(PreferKey.useZhLayout)
val config get() = if (shareLayout) shareConfig else durConfig val config get() = if (shareLayout) shareConfig else durConfig
@ -490,7 +490,7 @@ object ReadBookConfig {
fun curBgDrawable(width: Int, height: Int): Drawable { fun curBgDrawable(width: Int, height: Int): Drawable {
var bgDrawable: Drawable? = null var bgDrawable: Drawable? = null
val resources = context.resources val resources = appCtx.resources
try { try {
bgDrawable = when (curBgType()) { bgDrawable = when (curBgType()) {
0 -> ColorDrawable(Color.parseColor(curBgStr())) 0 -> ColorDrawable(Color.parseColor(curBgStr()))
@ -498,7 +498,7 @@ object ReadBookConfig {
BitmapDrawable( BitmapDrawable(
resources, resources,
BitmapUtils.decodeAssetsBitmap( BitmapUtils.decodeAssetsBitmap(
context, appCtx,
"bg" + File.separator + curBgStr(), "bg" + File.separator + curBgStr(),
width, width,
height height
@ -513,7 +513,7 @@ object ReadBookConfig {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
return bgDrawable ?: ColorDrawable(context.getCompatColor(R.color.background)) return bgDrawable ?: ColorDrawable(appCtx.getCompatColor(R.color.background))
} }
} }
} }

@ -200,7 +200,9 @@ object ChapterProvider {
textPaint: TextPaint textPaint: TextPaint
): Float { ): Float {
var durY = if (isTitle) y + titleTopSpacing else y 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 text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true
) )
for (lineIndex in 0 until layout.lineCount) { for (lineIndex in 0 until layout.lineCount) {

@ -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<String>, 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)
}
}
}
}

@ -807,5 +807,6 @@
<string name="reverse_content">反转内容</string> <string name="reverse_content">反转内容</string>
<string name="debug">调试</string> <string name="debug">调试</string>
<string name="crash_log">崩溃日志</string> <string name="crash_log">崩溃日志</string>
<string name="use_zh_layout">使用自定义中文分行</string>
</resources> </resources>

@ -811,5 +811,6 @@
<string name="reverse_content">反转内容</string> <string name="reverse_content">反转内容</string>
<string name="debug">调试</string> <string name="debug">调试</string>
<string name="crash_log">崩溃日志</string> <string name="crash_log">崩溃日志</string>
<string name="use_zh_layout">使用自定义中文分行</string>
</resources> </resources>

@ -811,5 +811,6 @@
<string name="reverse_content">反转内容</string> <string name="reverse_content">反转内容</string>
<string name="debug">调试</string> <string name="debug">调试</string>
<string name="crash_log">崩溃日志</string> <string name="crash_log">崩溃日志</string>
<string name="use_zh_layout">使用自定义中文分行</string>
</resources> </resources>

@ -814,5 +814,6 @@
<string name="reverse_content">反转内容</string> <string name="reverse_content">反转内容</string>
<string name="debug">调试</string> <string name="debug">调试</string>
<string name="crash_log">崩溃日志</string> <string name="crash_log">崩溃日志</string>
<string name="use_zh_layout">使用自定义中文分行</string>
</resources> </resources>

@ -41,6 +41,13 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:isBottomBackground="true" /> app:isBottomBackground="true" />
<io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="false"
android:title="@string/use_zh_layout"
android:key="useZhLayout"
app:iconSpaceReserved="false"
app:isBottomBackground="true" />
<io.legado.app.ui.widget.prefs.SwitchPreference <io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:title="@string/text_full_justify" android:title="@string/text_full_justify"

Loading…
Cancel
Save