mirror of
https://github.com/gedoor/legado.git
synced 2024-07-06 23:47:49 +08:00
优化
This commit is contained in:
parent
abdc23967c
commit
946cd1ab94
@ -282,6 +282,10 @@ data class Book(
|
||||
}
|
||||
}
|
||||
|
||||
fun getBookSource(): BookSource? {
|
||||
return appDb.bookSourceDao.getBookSource(origin)
|
||||
}
|
||||
|
||||
fun toSearchBook() = SearchBook(
|
||||
name = name,
|
||||
author = author,
|
||||
|
@ -124,7 +124,7 @@ object BookHelp {
|
||||
) {
|
||||
try {
|
||||
saveText(book, bookChapter, content)
|
||||
saveImages(bookSource, book, bookChapter, content)
|
||||
//saveImages(bookSource, book, bookChapter, content)
|
||||
postEvent(EventBus.SAVE_CONTENT, Pair(book, bookChapter))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
@ -145,6 +145,9 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
}
|
||||
|
||||
fun clearTextChapter() {
|
||||
prevTextChapter?.cancelLayout()
|
||||
curTextChapter?.cancelLayout()
|
||||
nextTextChapter?.cancelLayout()
|
||||
prevTextChapter = null
|
||||
curTextChapter = null
|
||||
nextTextChapter = null
|
||||
@ -210,6 +213,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
if (durChapterIndex < chapterSize - 1) {
|
||||
durChapterPos = 0
|
||||
durChapterIndex++
|
||||
prevTextChapter?.cancelLayout()
|
||||
prevTextChapter = curTextChapter
|
||||
curTextChapter = nextTextChapter
|
||||
nextTextChapter = null
|
||||
@ -226,6 +230,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
callBack?.upMenuView()
|
||||
AppLog.putDebug("moveToNextChapter-curPageChanged()")
|
||||
curPageChanged()
|
||||
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it) }
|
||||
return true
|
||||
} else {
|
||||
AppLog.putDebug("跳转下一章失败,没有下一章")
|
||||
@ -241,6 +246,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
if (durChapterIndex > 0) {
|
||||
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
|
||||
durChapterIndex--
|
||||
nextTextChapter?.cancelLayout()
|
||||
nextTextChapter = curTextChapter
|
||||
curTextChapter = prevTextChapter
|
||||
prevTextChapter = null
|
||||
@ -254,6 +260,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
saveRead()
|
||||
callBack?.upMenuView()
|
||||
curPageChanged()
|
||||
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it) }
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -477,23 +484,33 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
)
|
||||
val contents = contentProcessor
|
||||
.getContent(book, chapter, content, includeTitle = false)
|
||||
val textChapter = ChapterProvider
|
||||
.getTextChapter(book, chapter, displayTitle, contents, chapterSize)
|
||||
val textChapter = ChapterProvider.getTextChapterAsync(
|
||||
book,
|
||||
chapter,
|
||||
displayTitle,
|
||||
contents,
|
||||
chapterSize,
|
||||
this@ReadBook
|
||||
)
|
||||
when (val offset = chapter.index - durChapterIndex) {
|
||||
0 -> {
|
||||
curTextChapter?.cancelLayout()
|
||||
curTextChapter = textChapter
|
||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||
callBack?.upMenuView()
|
||||
curPageChanged()
|
||||
callBack?.contentLoadFinish()
|
||||
callBack?.onCurrentTextChapterChanged(textChapter)
|
||||
}
|
||||
|
||||
-1 -> {
|
||||
prevTextChapter?.cancelLayout()
|
||||
prevTextChapter = textChapter
|
||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
nextTextChapter?.cancelLayout()
|
||||
nextTextChapter = textChapter
|
||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||
}
|
||||
@ -632,6 +649,8 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
fun upPageAnim()
|
||||
|
||||
fun notifyBookChanged()
|
||||
|
||||
fun onCurrentTextChapterChanged(textChapter: TextChapter)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -77,7 +77,10 @@ import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_C
|
||||
import io.legado.app.ui.book.read.page.ContentTextView
|
||||
import io.legado.app.ui.book.read.page.ReadView
|
||||
import io.legado.app.ui.book.read.page.entities.PageDirection
|
||||
import io.legado.app.ui.book.read.page.entities.TextChapter
|
||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
||||
import io.legado.app.ui.book.searchContent.SearchContentActivity
|
||||
import io.legado.app.ui.book.searchContent.SearchResult
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
@ -112,8 +115,10 @@ import io.legado.app.utils.observeEvent
|
||||
import io.legado.app.utils.observeEventSticky
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.stackTraceStr
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.sysScreenOffTime
|
||||
import io.legado.app.utils.throttle
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.visible
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
@ -139,7 +144,8 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
ReadBook.CallBack,
|
||||
AutoReadDialog.CallBack,
|
||||
TxtTocRuleDialog.CallBack,
|
||||
ColorPickerDialogListener {
|
||||
ColorPickerDialogListener,
|
||||
LayoutProgressListener {
|
||||
|
||||
private val tocActivity =
|
||||
registerForActivityResult(TocActivityResult()) {
|
||||
@ -221,6 +227,12 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
private val handler by lazy { buildMainHandler() }
|
||||
private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
|
||||
private val executor = ReadBook.executor
|
||||
private val upSeekBarThrotle = throttle(200) {
|
||||
runOnUiThread {
|
||||
upSeekBarProgress()
|
||||
binding.readMenu.upSeekBar()
|
||||
}
|
||||
}
|
||||
|
||||
//恢复跳转前进度对话框的交互结果
|
||||
private var confirmRestoreProcess: Boolean? = null
|
||||
@ -1357,6 +1369,25 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
binding.readView.autoPager.resume()
|
||||
}
|
||||
|
||||
override fun onCurrentTextChapterChanged(textChapter: TextChapter) {
|
||||
textChapter.setProgressListener(this)
|
||||
}
|
||||
|
||||
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
|
||||
upSeekBarThrotle.invoke()
|
||||
binding.readView.onLayoutPageCompleted(index, page)
|
||||
}
|
||||
|
||||
override fun onLayoutCompleted() {
|
||||
binding.readView.onLayoutCompleted()
|
||||
}
|
||||
|
||||
override fun onLayoutException(e: Throwable) {
|
||||
AppLog.put("ChapterProvider ERROR", e)
|
||||
toastOnUi("ChapterProvider ERROR:\n${e.stackTraceStr}")
|
||||
binding.readView.onLayoutException(e)
|
||||
}
|
||||
|
||||
/* 全文搜索跳转 */
|
||||
private fun skipToSearch(searchResult: SearchResult) {
|
||||
val previousResult = binding.searchMenu.previousSearchResult
|
||||
|
@ -655,7 +655,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
||||
|
||||
fun createBookmark(): Bookmark? {
|
||||
val page = relativePage(selectStart.relativePagePos)
|
||||
page.getTextChapter()?.let { chapter ->
|
||||
page.getTextChapter().let { chapter ->
|
||||
ReadBook.book?.let { book ->
|
||||
return book.createBookMark().apply {
|
||||
chapterIndex = page.chapterIndex
|
||||
|
@ -309,11 +309,18 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
fun setProgress(textPage: TextPage) = textPage.apply {
|
||||
tvBookName?.setTextIfNotEqual(ReadBook.book?.name)
|
||||
tvTitle?.setTextIfNotEqual(textPage.title)
|
||||
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
|
||||
val readProgress = readProgress
|
||||
tvTotalProgress?.setTextIfNotEqual(readProgress)
|
||||
tvTotalProgress1?.setTextIfNotEqual("${chapterIndex.plus(1)}/${chapterSize}")
|
||||
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
|
||||
if (textChapter.isCompleted) {
|
||||
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
|
||||
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
|
||||
} else {
|
||||
val pageSizeInt = pageSize - 1
|
||||
val pageSize = if (pageSizeInt <= 0) "-" else "~$pageSizeInt"
|
||||
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
|
||||
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoPager(autoPager: AutoPager?) {
|
||||
|
@ -28,10 +28,12 @@ import io.legado.app.ui.book.read.page.entities.TextChapter
|
||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||
import io.legado.app.ui.book.read.page.entities.TextPos
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
||||
import io.legado.app.ui.book.read.page.provider.TextPageFactory
|
||||
import io.legado.app.utils.activity
|
||||
import io.legado.app.utils.invisible
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.throttle
|
||||
import java.text.BreakIterator
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
@ -41,7 +43,7 @@ import kotlin.math.abs
|
||||
*/
|
||||
class ReadView(context: Context, attrs: AttributeSet) :
|
||||
FrameLayout(context, attrs),
|
||||
DataSource {
|
||||
DataSource, LayoutProgressListener {
|
||||
|
||||
val callBack: CallBack get() = activity as CallBack
|
||||
var pageFactory: TextPageFactory = TextPageFactory(this)
|
||||
@ -99,6 +101,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
|
||||
private val bcRect = RectF()
|
||||
private val brRect = RectF()
|
||||
private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) }
|
||||
private val upProgressThrottle = throttle(200) { post { upProgress() } }
|
||||
val autoPager = AutoPager(this)
|
||||
val isAutoPage get() = autoPager.isRunning
|
||||
|
||||
@ -539,6 +542,10 @@ class ReadView(context: Context, attrs: AttributeSet) :
|
||||
callBack.screenOffTimerStart()
|
||||
}
|
||||
|
||||
private fun upProgress() {
|
||||
curPage.setProgress(pageFactory.curPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新滑动距离
|
||||
*/
|
||||
@ -654,6 +661,37 @@ class ReadView(context: Context, attrs: AttributeSet) :
|
||||
curPage.submitRenderTask()
|
||||
}
|
||||
|
||||
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
|
||||
val line = page.lines.first()
|
||||
val durChapterPos = ReadBook.durChapterPos
|
||||
val startPos = line.chapterPosition
|
||||
val endPos = line.chapterPosition + line.charSize
|
||||
if (durChapterPos in startPos..<endPos) {
|
||||
post {
|
||||
upContent(resetPageOffset = false)
|
||||
}
|
||||
}
|
||||
if (isScroll) {
|
||||
val pageIndex = ReadBook.durPageIndex
|
||||
if (index - 3 < pageIndex) {
|
||||
post {
|
||||
upContent(resetPageOffset = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
upProgressThrottle.invoke()
|
||||
}
|
||||
|
||||
override fun onLayoutCompleted() {
|
||||
post {
|
||||
upContent(resetPageOffset = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutException(e: Throwable) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override val currentChapter: TextChapter?
|
||||
get() {
|
||||
return if (callBack.isInitFinish) ReadBook.textChapter(0) else null
|
||||
|
@ -2,9 +2,14 @@ package io.legado.app.ui.book.read.page.entities
|
||||
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.ReplaceRule
|
||||
import io.legado.app.help.book.BookContent
|
||||
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
||||
import io.legado.app.ui.book.read.page.provider.TextChapterLayout
|
||||
import io.legado.app.utils.fastBinarySearchBy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
@ -17,14 +22,18 @@ data class TextChapter(
|
||||
val chapter: BookChapter,
|
||||
val position: Int,
|
||||
val title: String,
|
||||
val pages: List<TextPage>,
|
||||
val chaptersSize: Int,
|
||||
val sameTitleRemoved: Boolean,
|
||||
val isVip: Boolean,
|
||||
val isPay: Boolean,
|
||||
//起效的替换规则
|
||||
val effectiveReplaceRules: List<ReplaceRule>?
|
||||
) {
|
||||
) : LayoutProgressListener {
|
||||
|
||||
private val textPages = arrayListOf<TextPage>()
|
||||
val pages: List<TextPage> get() = textPages
|
||||
|
||||
var layout: TextChapterLayout? = null
|
||||
|
||||
fun getPage(index: Int): TextPage? {
|
||||
return pages.getOrNull(index)
|
||||
@ -42,6 +51,10 @@ data class TextChapter(
|
||||
|
||||
val pageSize: Int get() = pages.size
|
||||
|
||||
var listener: LayoutProgressListener? = null
|
||||
|
||||
var isCompleted = false
|
||||
|
||||
val paragraphs by lazy {
|
||||
paragraphsInternal
|
||||
}
|
||||
@ -79,7 +92,12 @@ data class TextChapter(
|
||||
* @return 是否是最后一页
|
||||
*/
|
||||
fun isLastIndex(index: Int): Boolean {
|
||||
return index >= pages.size - 1
|
||||
return isCompleted && index >= pages.size - 1
|
||||
}
|
||||
|
||||
fun isLastIndexCurrent(index: Int): Boolean {
|
||||
// 未完成排版时,最后一页是正在排版中的,需要去掉
|
||||
return index >= if (isCompleted) pages.size - 1 else pages.size - 2
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,15 +208,28 @@ data class TextChapter(
|
||||
* @return 根据索引位置获取所在页
|
||||
*/
|
||||
fun getPageIndexByCharIndex(charIndex: Int): Int {
|
||||
val index = pages.fastBinarySearchBy(charIndex) {
|
||||
val pageSize = pages.size
|
||||
if (pageSize == 0) {
|
||||
return -1
|
||||
}
|
||||
val size = if (isCompleted) pageSize else pageSize - 1
|
||||
val bIndex = pages.fastBinarySearchBy(charIndex, 0, size) {
|
||||
it.lines.first().chapterPosition
|
||||
}
|
||||
return if (index >= 0) {
|
||||
index
|
||||
} else {
|
||||
abs(index + 1) - 1
|
||||
val index = abs(bIndex + 1) - 1
|
||||
if (index == -1) {
|
||||
return -1
|
||||
}
|
||||
/* 相当于以下实现
|
||||
// 判断是否已经排版到 charIndex ,没有则返回 -1
|
||||
if (!isCompleted && index == size - 1) {
|
||||
val line = pages[index].lines.first()
|
||||
val pageEndPos = line.chapterPosition + line.charSize
|
||||
if (charIndex > pageEndPos) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return index
|
||||
/*
|
||||
var length = 0
|
||||
for (i in pages.indices) {
|
||||
val page = pages[i]
|
||||
@ -222,4 +253,52 @@ data class TextChapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun createLayout(scope: CoroutineScope, book: Book, bookContent: BookContent) {
|
||||
textPages.clear()
|
||||
layout = TextChapterLayout(
|
||||
scope,
|
||||
this,
|
||||
textPages,
|
||||
book,
|
||||
bookContent,
|
||||
)
|
||||
}
|
||||
|
||||
fun setProgressListener(l: LayoutProgressListener) {
|
||||
if (isCompleted) {
|
||||
l.onLayoutCompleted()
|
||||
} else {
|
||||
listener = l
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
|
||||
listener?.onLayoutPageCompleted(index, page)
|
||||
}
|
||||
|
||||
override fun onLayoutCompleted() {
|
||||
isCompleted = true
|
||||
listener?.onLayoutCompleted()
|
||||
listener = null
|
||||
}
|
||||
|
||||
override fun onLayoutException(e: Throwable) {
|
||||
listener?.onLayoutException(e)
|
||||
listener = null
|
||||
}
|
||||
|
||||
fun cancelLayout() {
|
||||
layout?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val emptyTextChapter = TextChapter(
|
||||
BookChapter(), -1, "emptyTextChapter", -1,
|
||||
sameTitleRemoved = false,
|
||||
isVip = false,
|
||||
isPay = false,
|
||||
null
|
||||
).apply { isCompleted = true }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,8 +7,8 @@ import androidx.annotation.Keep
|
||||
import androidx.core.graphics.withTranslation
|
||||
import io.legado.app.R
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.model.ReadBook
|
||||
import io.legado.app.ui.book.read.page.ContentTextView
|
||||
import io.legado.app.ui.book.read.page.entities.TextChapter.Companion.emptyTextChapter
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
|
||||
@ -27,7 +27,6 @@ data class TextPage(
|
||||
var text: String = appCtx.getString(R.string.data_loading),
|
||||
var title: String = appCtx.getString(R.string.data_loading),
|
||||
private val textLines: ArrayList<TextLine> = arrayListOf(),
|
||||
var pageSize: Int = 0,
|
||||
var chapterSize: Int = 0,
|
||||
var chapterIndex: Int = 0,
|
||||
var height: Float = 0f,
|
||||
@ -47,6 +46,10 @@ data class TextPage(
|
||||
var canvasRecorder = CanvasRecorderFactory.create(true)
|
||||
var doublePage = false
|
||||
var paddingTop = 0
|
||||
var isCompleted = false
|
||||
@JvmField
|
||||
var textChapter = emptyTextChapter
|
||||
val pageSize get() = textChapter.pageSize
|
||||
|
||||
val paragraphs by lazy {
|
||||
paragraphsInternal
|
||||
@ -162,6 +165,7 @@ data class TextPage(
|
||||
}
|
||||
height = ChapterProvider.visibleHeight.toFloat()
|
||||
invalidate()
|
||||
isCompleted = true
|
||||
}
|
||||
return this
|
||||
}
|
||||
@ -248,23 +252,8 @@ data class TextPage(
|
||||
/**
|
||||
* @return 页面所在章节
|
||||
*/
|
||||
fun getTextChapter(): TextChapter? {
|
||||
ReadBook.curTextChapter?.let {
|
||||
if (it.position == chapterIndex) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
ReadBook.nextTextChapter?.let {
|
||||
if (it.position == chapterIndex) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
ReadBook.prevTextChapter?.let {
|
||||
if (it.position == chapterIndex) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return null
|
||||
fun getTextChapter(): TextChapter {
|
||||
return textChapter
|
||||
}
|
||||
|
||||
fun draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) {
|
||||
@ -284,6 +273,7 @@ data class TextPage(
|
||||
}
|
||||
|
||||
fun render(view: ContentTextView): Boolean {
|
||||
if (!isCompleted) return false
|
||||
return canvasRecorder.recordIfNeeded(view.width, height.toInt()) {
|
||||
drawPage(view, this)
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.spToPx
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import io.legado.app.utils.textHeight
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import splitties.init.appCtx
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
@ -42,12 +43,12 @@ import java.util.Locale
|
||||
@Suppress("DEPRECATION", "ConstPropertyName")
|
||||
object ChapterProvider {
|
||||
//用于图片字的替换
|
||||
private const val srcReplaceChar = "▩"
|
||||
const val srcReplaceChar = "▩"
|
||||
|
||||
//用于评论按钮的替换
|
||||
private const val reviewChar = "▨"
|
||||
const val reviewChar = "▨"
|
||||
|
||||
private const val indentChar = " "
|
||||
const val indentChar = " "
|
||||
|
||||
@JvmStatic
|
||||
var viewWidth = 0
|
||||
@ -94,25 +95,31 @@ object ChapterProvider {
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
private var paragraphSpacing = 0
|
||||
var paragraphSpacing = 0
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
private var titleTopSpacing = 0
|
||||
var titleTopSpacing = 0
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
private var titleBottomSpacing = 0
|
||||
var titleBottomSpacing = 0
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
private var indentCharWidth = 0f
|
||||
var indentCharWidth = 0f
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
var titlePaintTextHeight = 0f
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
var contentPaintTextHeight = 0f
|
||||
private set
|
||||
|
||||
@JvmStatic
|
||||
private var titlePaintFontMetrics = FontMetrics()
|
||||
var titlePaintFontMetrics = FontMetrics()
|
||||
|
||||
@JvmStatic
|
||||
var contentPaintFontMetrics = FontMetrics()
|
||||
@ -278,7 +285,7 @@ object ChapterProvider {
|
||||
textPage.text = stringBuilder.toString()
|
||||
textPages.forEachIndexed { index, item ->
|
||||
item.index = index
|
||||
item.pageSize = textPages.size
|
||||
//item.pageSize = textPages.size
|
||||
item.chapterIndex = bookChapter.index
|
||||
item.chapterSize = chapterSize
|
||||
item.title = displayTitle
|
||||
@ -290,7 +297,8 @@ object ChapterProvider {
|
||||
return TextChapter(
|
||||
bookChapter,
|
||||
bookChapter.index, displayTitle,
|
||||
textPages, chapterSize,
|
||||
//textPages,
|
||||
chapterSize,
|
||||
bookContent.sameTitleRemoved,
|
||||
bookChapter.isVip,
|
||||
bookChapter.isPay,
|
||||
@ -298,6 +306,30 @@ object ChapterProvider {
|
||||
)
|
||||
}
|
||||
|
||||
fun getTextChapterAsync(
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
displayTitle: String,
|
||||
bookContent: BookContent,
|
||||
chapterSize: Int,
|
||||
scope: CoroutineScope
|
||||
): TextChapter {
|
||||
|
||||
val textChapter = TextChapter(
|
||||
bookChapter,
|
||||
bookChapter.index, displayTitle,
|
||||
chapterSize,
|
||||
bookContent.sameTitleRemoved,
|
||||
bookChapter.isVip,
|
||||
bookChapter.isPay,
|
||||
bookContent.effectiveReplaceRules
|
||||
).apply {
|
||||
createLayout(scope, book, bookContent)
|
||||
}
|
||||
|
||||
return textChapter
|
||||
}
|
||||
|
||||
/**
|
||||
* 排版图片
|
||||
*/
|
||||
|
@ -0,0 +1,22 @@
|
||||
package io.legado.app.ui.book.read.page.provider
|
||||
|
||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||
|
||||
interface LayoutProgressListener {
|
||||
|
||||
/**
|
||||
* 单页排版完成
|
||||
*/
|
||||
fun onLayoutPageCompleted(index: Int, page: TextPage)
|
||||
|
||||
/**
|
||||
* 全部排版完成
|
||||
*/
|
||||
fun onLayoutCompleted()
|
||||
|
||||
/**
|
||||
* 排版出现异常
|
||||
*/
|
||||
fun onLayoutException(e: Throwable)
|
||||
|
||||
}
|
@ -0,0 +1,752 @@
|
||||
package io.legado.app.ui.book.read.page.provider
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.help.book.BookContent
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
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.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.ReviewColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
||||
import io.legado.app.utils.dpToPx
|
||||
import io.legado.app.utils.fastSum
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
|
||||
class TextChapterLayout(
|
||||
scope: CoroutineScope,
|
||||
private val textChapter: TextChapter,
|
||||
private val textPages: ArrayList<TextPage>,
|
||||
private val book: Book,
|
||||
private val bookContent: BookContent,
|
||||
) {
|
||||
|
||||
@Volatile
|
||||
private var listener: LayoutProgressListener? = textChapter
|
||||
|
||||
private val paddingLeft = ChapterProvider.paddingLeft
|
||||
private val paddingTop = ChapterProvider.paddingTop
|
||||
|
||||
private val titlePaint = ChapterProvider.titlePaint
|
||||
private val titlePaintTextHeight = ChapterProvider.titlePaintTextHeight
|
||||
private val titlePaintFontMetrics = ChapterProvider.titlePaintFontMetrics
|
||||
|
||||
private val contentPaint = ChapterProvider.contentPaint
|
||||
private val contentPaintTextHeight = ChapterProvider.contentPaintTextHeight
|
||||
private val contentPaintFontMetrics = ChapterProvider.contentPaintFontMetrics
|
||||
|
||||
private val titleTopSpacing = ChapterProvider.titleTopSpacing
|
||||
private val titleBottomSpacing = ChapterProvider.titleBottomSpacing
|
||||
private val lineSpacingExtra = ChapterProvider.lineSpacingExtra
|
||||
private val paragraphSpacing = ChapterProvider.paragraphSpacing
|
||||
|
||||
private val visibleHeight = ChapterProvider.visibleHeight
|
||||
private val visibleWidth = ChapterProvider.visibleWidth
|
||||
|
||||
private val viewWidth = ChapterProvider.viewWidth
|
||||
private val doublePage = ChapterProvider.doublePage
|
||||
private val indentCharWidth = ChapterProvider.indentCharWidth
|
||||
private val stringBuilder = StringBuilder()
|
||||
|
||||
private var isCompleted = false
|
||||
private var exception: Throwable? = null
|
||||
private val job: Coroutine<*>
|
||||
private val bookChapter inline get() = textChapter.chapter
|
||||
private val displayTitle inline get() = textChapter.title
|
||||
private val chaptersSize inline get() = textChapter.chaptersSize
|
||||
|
||||
|
||||
init {
|
||||
job = Coroutine.async(scope) {
|
||||
launch {
|
||||
val bookSource = book.getBookSource() ?: return@launch
|
||||
BookHelp.saveImages(bookSource, book, bookChapter, bookContent.toString())
|
||||
}
|
||||
getTextChapter(book, bookChapter, displayTitle, bookContent)
|
||||
}.onError {
|
||||
exception = it
|
||||
onException(it)
|
||||
}.onFinally {
|
||||
isCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgressListener(l: LayoutProgressListener) {
|
||||
try {
|
||||
if (isCompleted) {
|
||||
l.onLayoutCompleted()
|
||||
} else if (exception != null) {
|
||||
l.onLayoutException(exception!!)
|
||||
} else {
|
||||
listener = l
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun onPageCompleted() {
|
||||
val textPage = textPages.last()
|
||||
textPage.index = textPages.lastIndex
|
||||
textPage.chapterIndex = bookChapter.index
|
||||
textPage.chapterSize = chaptersSize
|
||||
textPage.title = displayTitle
|
||||
textPage.doublePage = doublePage
|
||||
textPage.paddingTop = paddingTop
|
||||
textPage.isCompleted = true
|
||||
textPage.textChapter = textChapter
|
||||
textPage.upLinesPosition()
|
||||
try {
|
||||
listener?.onLayoutPageCompleted(textPages.lastIndex, textPage)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCompleted() {
|
||||
try {
|
||||
listener?.onLayoutCompleted()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e)
|
||||
} finally {
|
||||
listener = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun onException(e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
listener?.onLayoutException(e)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e)
|
||||
} finally {
|
||||
listener = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拆分完的章节数据
|
||||
*/
|
||||
private suspend fun getTextChapter(
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
displayTitle: String,
|
||||
bookContent: BookContent,
|
||||
) {
|
||||
val contents = bookContent.textList
|
||||
var absStartX = paddingLeft
|
||||
var durY = 0f
|
||||
textPages.add(TextPage())
|
||||
if (ReadBookConfig.titleMode != 2 || bookChapter.isVolume) {
|
||||
//标题非隐藏
|
||||
displayTitle.splitNotBlank("\n").forEach { text ->
|
||||
setTypeText(
|
||||
book, absStartX, durY,
|
||||
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text,
|
||||
titlePaint,
|
||||
titlePaintTextHeight,
|
||||
titlePaintFontMetrics,
|
||||
isTitle = true,
|
||||
emptyContent = contents.isEmpty(),
|
||||
isVolumeTitle = bookChapter.isVolume
|
||||
).let {
|
||||
absStartX = it.first
|
||||
durY = it.second
|
||||
}
|
||||
}
|
||||
textPages.last().lines.last().isParagraphEnd = true
|
||||
stringBuilder.append("\n")
|
||||
durY += titleBottomSpacing
|
||||
}
|
||||
val sb = StringBuffer()
|
||||
contents.forEach { content ->
|
||||
if (book.getImageStyle().equals(Book.imgStyleText, true)) {
|
||||
//图片样式为文字嵌入类型
|
||||
var text = content.replace(ChapterProvider.srcReplaceChar, "▣")
|
||||
val srcList = LinkedList<String>()
|
||||
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.appendTail(sb)
|
||||
text = sb.toString()
|
||||
setTypeText(
|
||||
book,
|
||||
absStartX,
|
||||
durY,
|
||||
text,
|
||||
contentPaint,
|
||||
contentPaintTextHeight,
|
||||
contentPaintFontMetrics,
|
||||
srcList = srcList
|
||||
).let {
|
||||
absStartX = it.first
|
||||
durY = it.second
|
||||
}
|
||||
} else {
|
||||
val matcher = AppPattern.imgPattern.matcher(content)
|
||||
var start = 0
|
||||
while (matcher.find()) {
|
||||
val text = content.substring(start, matcher.start())
|
||||
if (text.isNotBlank()) {
|
||||
setTypeText(
|
||||
book,
|
||||
absStartX,
|
||||
durY,
|
||||
text,
|
||||
contentPaint,
|
||||
contentPaintTextHeight,
|
||||
contentPaintFontMetrics
|
||||
).let {
|
||||
absStartX = it.first
|
||||
durY = it.second
|
||||
}
|
||||
}
|
||||
setTypeImage(
|
||||
book,
|
||||
matcher.group(1)!!,
|
||||
absStartX,
|
||||
durY,
|
||||
textPages,
|
||||
contentPaintTextHeight,
|
||||
stringBuilder,
|
||||
book.getImageStyle()
|
||||
).let {
|
||||
absStartX = it.first
|
||||
durY = it.second
|
||||
}
|
||||
start = matcher.end()
|
||||
}
|
||||
if (start < content.length) {
|
||||
val text = content.substring(start, content.length)
|
||||
if (text.isNotBlank()) {
|
||||
setTypeText(
|
||||
book, absStartX, durY,
|
||||
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text,
|
||||
contentPaint,
|
||||
contentPaintTextHeight,
|
||||
contentPaintFontMetrics
|
||||
).let {
|
||||
absStartX = it.first
|
||||
durY = it.second
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
textPages.last().lines.last().isParagraphEnd = true
|
||||
stringBuilder.append("\n")
|
||||
}
|
||||
val textPage = textPages.last()
|
||||
val endPadding = 20.dpToPx()
|
||||
val durYPadding = durY + endPadding
|
||||
if (textPage.height < durYPadding) {
|
||||
textPage.height = durYPadding
|
||||
} else {
|
||||
textPage.height += endPadding
|
||||
}
|
||||
textPage.text = stringBuilder.toString()
|
||||
onPageCompleted()
|
||||
onCompleted()
|
||||
}
|
||||
|
||||
/**
|
||||
* 排版图片
|
||||
*/
|
||||
private suspend fun setTypeImage(
|
||||
book: Book,
|
||||
src: String,
|
||||
x: Int,
|
||||
y: Float,
|
||||
textPages: ArrayList<TextPage>,
|
||||
textHeight: Float,
|
||||
stringBuilder: StringBuilder,
|
||||
imageStyle: String?,
|
||||
): Pair<Int, Float> {
|
||||
var absStartX = x
|
||||
var durY = y
|
||||
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
|
||||
if (size.width > 0 && size.height > 0) {
|
||||
if (durY > visibleHeight) {
|
||||
val textPage = textPages.last()
|
||||
if (textPage.height < durY) {
|
||||
textPage.height = durY
|
||||
}
|
||||
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
|
||||
stringBuilder.clear()
|
||||
onPageCompleted()
|
||||
textPages.add(TextPage())
|
||||
durY = 0f
|
||||
}
|
||||
var height = size.height
|
||||
var width = size.width
|
||||
when (imageStyle?.uppercase(Locale.ROOT)) {
|
||||
Book.imgStyleFull -> {
|
||||
width = visibleWidth
|
||||
height = size.height * visibleWidth / size.width
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (size.width > visibleWidth) {
|
||||
height = size.height * visibleWidth / size.width
|
||||
width = visibleWidth
|
||||
}
|
||||
if (height > visibleHeight) {
|
||||
width = width * visibleHeight / height
|
||||
height = visibleHeight
|
||||
}
|
||||
if (durY + height > visibleHeight) {
|
||||
val textPage = textPages.last()
|
||||
if (doublePage && absStartX < viewWidth / 2) {
|
||||
//当前页面左列结束
|
||||
textPage.leftLineSize = textPage.lineSize
|
||||
absStartX = viewWidth / 2 + paddingLeft
|
||||
} else {
|
||||
//当前页面结束
|
||||
if (textPage.leftLineSize == 0) {
|
||||
textPage.leftLineSize = textPage.lineSize
|
||||
}
|
||||
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
|
||||
stringBuilder.clear()
|
||||
onPageCompleted()
|
||||
textPages.add(TextPage())
|
||||
}
|
||||
// 双页的 durY 不正确,可能会小于实际高度
|
||||
if (textPage.height < durY) {
|
||||
textPage.height = durY
|
||||
}
|
||||
durY = 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
val textLine = TextLine(isImage = true)
|
||||
textLine.lineTop = durY + paddingTop
|
||||
durY += height
|
||||
textLine.lineBottom = durY + paddingTop
|
||||
val (start, end) = if (visibleWidth > width) {
|
||||
val adjustWidth = (visibleWidth - width) / 2f
|
||||
Pair(adjustWidth, adjustWidth + width)
|
||||
} else {
|
||||
Pair(0f, width.toFloat())
|
||||
}
|
||||
textLine.addColumn(
|
||||
ImageColumn(start = x + start, end = x + end, src = src)
|
||||
)
|
||||
calcTextLinePosition(textPages, textLine, stringBuilder.length)
|
||||
stringBuilder.append(" ") // 确保翻页时索引计算正确
|
||||
textPages.last().addLine(textLine)
|
||||
}
|
||||
return absStartX to durY + textHeight * paragraphSpacing / 10f
|
||||
}
|
||||
|
||||
/**
|
||||
* 排版文字
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private suspend fun setTypeText(
|
||||
book: Book,
|
||||
x: Int,
|
||||
y: Float,
|
||||
text: String,
|
||||
textPaint: TextPaint,
|
||||
textHeight: Float,
|
||||
fontMetrics: Paint.FontMetrics,
|
||||
isTitle: Boolean = false,
|
||||
emptyContent: Boolean = false,
|
||||
isVolumeTitle: Boolean = false,
|
||||
srcList: LinkedList<String>? = null
|
||||
): Pair<Int, Float> {
|
||||
var absStartX = x
|
||||
val layout = if (ReadBookConfig.useZhLayout) {
|
||||
ZhLayout(text, textPaint, visibleWidth)
|
||||
} else {
|
||||
StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
|
||||
}
|
||||
var durY = when {
|
||||
//标题y轴居中
|
||||
emptyContent && textPages.size == 1 -> {
|
||||
val textPage = textPages.last()
|
||||
if (textPage.lineSize == 0) {
|
||||
val ty = (visibleHeight - layout.lineCount * textHeight) / 2
|
||||
if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat()
|
||||
} else {
|
||||
var textLayoutHeight = layout.lineCount * textHeight
|
||||
val fistLine = textPage.getLine(0)
|
||||
if (fistLine.lineTop < textLayoutHeight + titleTopSpacing) {
|
||||
textLayoutHeight = fistLine.lineTop - titleTopSpacing
|
||||
}
|
||||
textPage.lines.forEach {
|
||||
it.lineTop -= textLayoutHeight
|
||||
it.lineBase -= textLayoutHeight
|
||||
it.lineBottom -= textLayoutHeight
|
||||
}
|
||||
y - textLayoutHeight
|
||||
}
|
||||
}
|
||||
|
||||
isTitle && textPages.size == 1 && textPages.last().lines.isEmpty() ->
|
||||
y + titleTopSpacing
|
||||
|
||||
else -> y
|
||||
}
|
||||
for (lineIndex in 0 until layout.lineCount) {
|
||||
val textLine = TextLine(isTitle = isTitle)
|
||||
if (durY + textHeight > visibleHeight) {
|
||||
val textPage = textPages.last()
|
||||
if (doublePage && absStartX < viewWidth / 2) {
|
||||
//当前页面左列结束
|
||||
textPage.leftLineSize = textPage.lineSize
|
||||
absStartX = viewWidth / 2 + paddingLeft
|
||||
} else {
|
||||
//当前页面结束,设置各种值
|
||||
if (textPage.leftLineSize == 0) {
|
||||
textPage.leftLineSize = textPage.lineSize
|
||||
}
|
||||
textPage.text = stringBuilder.toString()
|
||||
onPageCompleted()
|
||||
//新建页面
|
||||
textPages.add(TextPage())
|
||||
stringBuilder.clear()
|
||||
absStartX = paddingLeft
|
||||
}
|
||||
if (textPage.height < durY) {
|
||||
textPage.height = durY
|
||||
}
|
||||
durY = 0f
|
||||
}
|
||||
val lineStart = layout.getLineStart(lineIndex)
|
||||
val lineEnd = layout.getLineEnd(lineIndex)
|
||||
val lineText = text.substring(lineStart, lineEnd)
|
||||
val (words, widths) = measureTextSplit(lineText, textPaint)
|
||||
val desiredWidth = widths.fastSum()
|
||||
when {
|
||||
lineIndex == 0 && layout.lineCount > 1 && !isTitle -> {
|
||||
//第一行 非标题
|
||||
textLine.text = lineText
|
||||
addCharsToLineFirst(
|
||||
book, absStartX, textLine, words,
|
||||
desiredWidth, widths, srcList
|
||||
)
|
||||
}
|
||||
|
||||
lineIndex == layout.lineCount - 1 -> {
|
||||
//最后一行
|
||||
textLine.text = lineText
|
||||
//标题x轴居中
|
||||
val startX = if (
|
||||
isTitle &&
|
||||
(ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle)
|
||||
) {
|
||||
(visibleWidth - desiredWidth) / 2
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
startX, !isTitle && lineIndex == 0, widths, srcList
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (
|
||||
isTitle &&
|
||||
(ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle)
|
||||
) {
|
||||
//标题居中
|
||||
val startX = (visibleWidth - desiredWidth) / 2
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
startX, false, widths, srcList
|
||||
)
|
||||
} else {
|
||||
//中间行
|
||||
textLine.text = lineText
|
||||
addCharsToLineMiddle(
|
||||
book, absStartX, textLine, words,
|
||||
desiredWidth, 0f, widths, srcList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (doublePage) {
|
||||
textLine.isLeftLine = absStartX < viewWidth / 2
|
||||
}
|
||||
calcTextLinePosition(textPages, textLine, stringBuilder.length)
|
||||
stringBuilder.append(lineText)
|
||||
textLine.upTopBottom(durY, textHeight, fontMetrics)
|
||||
val textPage = textPages.last()
|
||||
textPage.addLine(textLine)
|
||||
durY += textHeight * lineSpacingExtra
|
||||
if (textPage.height < durY) {
|
||||
textPage.height = durY
|
||||
}
|
||||
}
|
||||
durY += textHeight * paragraphSpacing / 10f
|
||||
return Pair(absStartX, durY)
|
||||
}
|
||||
|
||||
private fun calcTextLinePosition(
|
||||
textPages: ArrayList<TextPage>,
|
||||
textLine: TextLine,
|
||||
sbLength: Int
|
||||
) {
|
||||
val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 }
|
||||
?: textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull { it.paragraphNum > 0 }
|
||||
val paragraphNum = when {
|
||||
lastLine == null -> 1
|
||||
lastLine.isParagraphEnd -> lastLine.paragraphNum + 1
|
||||
else -> lastLine.paragraphNum
|
||||
}
|
||||
textLine.paragraphNum = paragraphNum
|
||||
textLine.chapterPosition =
|
||||
(textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull()?.run {
|
||||
chapterPosition + charSize + if (isParagraphEnd) 1 else 0
|
||||
} ?: 0) + sbLength
|
||||
textLine.pagePosition = sbLength
|
||||
}
|
||||
|
||||
/**
|
||||
* 有缩进,两端对齐
|
||||
*/
|
||||
private suspend fun addCharsToLineFirst(
|
||||
book: Book,
|
||||
absStartX: Int,
|
||||
textLine: TextLine,
|
||||
words: List<String>,
|
||||
/**自然排版长度**/
|
||||
desiredWidth: Float,
|
||||
textWidths: List<Float>,
|
||||
srcList: LinkedList<String>?
|
||||
) {
|
||||
var x = 0f
|
||||
if (!ReadBookConfig.textFullJustify) {
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
x, true, textWidths, srcList
|
||||
)
|
||||
return
|
||||
}
|
||||
val bodyIndent = ReadBookConfig.paragraphIndent
|
||||
for (i in bodyIndent.indices) {
|
||||
val x1 = x + indentCharWidth
|
||||
textLine.addColumn(
|
||||
TextColumn(
|
||||
charData = ChapterProvider.indentChar,
|
||||
start = absStartX + x,
|
||||
end = absStartX + x1
|
||||
)
|
||||
)
|
||||
x = x1
|
||||
textLine.indentWidth = x
|
||||
}
|
||||
if (words.size > bodyIndent.length) {
|
||||
val text1 = words.subList(bodyIndent.length, words.size)
|
||||
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
|
||||
addCharsToLineMiddle(
|
||||
book, absStartX, textLine, text1,
|
||||
desiredWidth, x, textWidths1, srcList
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无缩进,两端对齐
|
||||
*/
|
||||
private suspend fun addCharsToLineMiddle(
|
||||
book: Book,
|
||||
absStartX: Int,
|
||||
textLine: TextLine,
|
||||
words: List<String>,
|
||||
/**自然排版长度**/
|
||||
desiredWidth: Float,
|
||||
/**起始x坐标**/
|
||||
startX: Float,
|
||||
textWidths: List<Float>,
|
||||
srcList: LinkedList<String>?
|
||||
) {
|
||||
if (!ReadBookConfig.textFullJustify) {
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
startX, false, textWidths, srcList
|
||||
)
|
||||
return
|
||||
}
|
||||
val residualWidth = visibleWidth - desiredWidth
|
||||
val spaceSize = words.count { it == " " }
|
||||
if (spaceSize > 1) {
|
||||
val d = residualWidth / spaceSize
|
||||
var x = startX
|
||||
for (index in words.indices) {
|
||||
val char = words[index]
|
||||
val cw = textWidths[index]
|
||||
val x1 = if (char == " ") {
|
||||
if (index != words.lastIndex) (x + cw + d) else (x + cw)
|
||||
} else {
|
||||
(x + cw)
|
||||
}
|
||||
addCharToLine(
|
||||
book, absStartX, textLine, char,
|
||||
x, x1, index + 1 == words.size, srcList
|
||||
)
|
||||
x = x1
|
||||
}
|
||||
} else {
|
||||
val gapCount: Int = words.lastIndex
|
||||
val d = residualWidth / gapCount
|
||||
var x = startX
|
||||
for (index in words.indices) {
|
||||
val char = words[index]
|
||||
val cw = textWidths[index]
|
||||
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
|
||||
addCharToLine(
|
||||
book, absStartX, textLine, char,
|
||||
x, x1, index + 1 == words.size, srcList
|
||||
)
|
||||
x = x1
|
||||
}
|
||||
}
|
||||
exceed(absStartX, textLine, words)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自然排列
|
||||
*/
|
||||
private suspend fun addCharsToLineNatural(
|
||||
book: Book,
|
||||
absStartX: Int,
|
||||
textLine: TextLine,
|
||||
words: List<String>,
|
||||
startX: Float,
|
||||
hasIndent: Boolean,
|
||||
textWidths: List<Float>,
|
||||
srcList: LinkedList<String>?
|
||||
) {
|
||||
val indentLength = ReadBookConfig.paragraphIndent.length
|
||||
var x = startX
|
||||
for (index in words.indices) {
|
||||
val char = words[index]
|
||||
val cw = textWidths[index]
|
||||
val x1 = x + cw
|
||||
addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
|
||||
x = x1
|
||||
if (hasIndent && index == indentLength - 1) {
|
||||
textLine.indentWidth = x
|
||||
}
|
||||
}
|
||||
exceed(absStartX, textLine, words)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加字符
|
||||
*/
|
||||
private suspend fun addCharToLine(
|
||||
book: Book,
|
||||
absStartX: Int,
|
||||
textLine: TextLine,
|
||||
char: String,
|
||||
xStart: Float,
|
||||
xEnd: Float,
|
||||
isLineEnd: Boolean,
|
||||
srcList: LinkedList<String>?
|
||||
) {
|
||||
val column = when {
|
||||
srcList != null && char == ChapterProvider.srcReplaceChar -> {
|
||||
val src = srcList.removeFirst()
|
||||
ImageProvider.cacheImage(book, src, ReadBook.bookSource)
|
||||
ImageColumn(
|
||||
start = absStartX + xStart,
|
||||
end = absStartX + xEnd,
|
||||
src = src
|
||||
)
|
||||
}
|
||||
|
||||
isLineEnd && char == ChapterProvider.reviewChar -> {
|
||||
ReviewColumn(
|
||||
start = absStartX + xStart,
|
||||
end = absStartX + xEnd,
|
||||
count = 100
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
TextColumn(
|
||||
start = absStartX + xStart,
|
||||
end = absStartX + xEnd,
|
||||
charData = char
|
||||
)
|
||||
}
|
||||
}
|
||||
textLine.addColumn(column)
|
||||
}
|
||||
|
||||
/**
|
||||
* 超出边界处理
|
||||
*/
|
||||
private fun exceed(absStartX: Int, textLine: TextLine, words: List<String>) {
|
||||
val visibleEnd = absStartX + visibleWidth
|
||||
val endX = textLine.columns.lastOrNull()?.end ?: return
|
||||
if (endX > visibleEnd) {
|
||||
val cc = (endX - visibleEnd) / words.size
|
||||
for (i in 0..words.lastIndex) {
|
||||
textLine.getColumnReverseAt(i).let {
|
||||
val py = cc * (words.size - i)
|
||||
it.start -= py
|
||||
it.end -= py
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun measureTextSplit(
|
||||
text: String,
|
||||
paint: TextPaint
|
||||
): Pair<ArrayList<String>, ArrayList<Float>> {
|
||||
val length = text.length
|
||||
val widthsArray = FloatArray(length)
|
||||
paint.getTextWidths(text, widthsArray)
|
||||
val clusterCount = widthsArray.count { it > 0f }
|
||||
val widths = ArrayList<Float>(clusterCount)
|
||||
val stringList = ArrayList<String>(clusterCount)
|
||||
var i = 0
|
||||
while (i < length) {
|
||||
val clusterBaseIndex = i++
|
||||
widths.add(widthsArray[clusterBaseIndex])
|
||||
while (i < length && widthsArray[i] == 0f) {
|
||||
i++
|
||||
}
|
||||
stringList.add(text.substring(clusterBaseIndex, i))
|
||||
}
|
||||
return stringList to widths
|
||||
}
|
||||
|
||||
}
|
@ -35,12 +35,16 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
|
||||
|
||||
override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) {
|
||||
return if (hasNext()) {
|
||||
val pageIndex = pageIndex
|
||||
if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) {
|
||||
if ((currentChapter == null || isScroll) && nextChapter == null) {
|
||||
return@with false
|
||||
}
|
||||
ReadBook.moveToNextChapter(upContent, false)
|
||||
} else {
|
||||
if (pageIndex < 0 || currentChapter?.isLastIndexCurrent(pageIndex) == true) {
|
||||
return@with false
|
||||
}
|
||||
ReadBook.setPageIndex(pageIndex.plus(1))
|
||||
}
|
||||
if (upContent) upContent(resetPageOffset = false)
|
||||
@ -55,6 +59,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
|
||||
if (currentChapter == null && prevChapter == null) {
|
||||
return@with false
|
||||
}
|
||||
if (prevChapter != null && prevChapter?.isCompleted == false) {
|
||||
return@with false
|
||||
}
|
||||
ReadBook.moveToPrevChapter(upContent, upContentInPlace = false)
|
||||
} else {
|
||||
if (currentChapter == null) {
|
||||
@ -74,7 +81,8 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
|
||||
return@with TextPage(text = it).format()
|
||||
}
|
||||
currentChapter?.let {
|
||||
return@with it.getPage(pageIndex) ?: TextPage(title = it.title).format()
|
||||
return@with it.getPage(pageIndex)
|
||||
?: TextPage(title = it.title).apply { textChapter = it }.format()
|
||||
}
|
||||
return TextPage().format()
|
||||
}
|
||||
@ -90,6 +98,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
|
||||
return@with it.getPage(pageIndex + 1)?.removePageAloudSpan()
|
||||
?: TextPage(title = it.title).format()
|
||||
}
|
||||
if (!it.isCompleted) {
|
||||
return@with TextPage(title = it.title).format()
|
||||
}
|
||||
}
|
||||
nextChapter?.let {
|
||||
return@with it.getPage(0)?.removePageAloudSpan()
|
||||
@ -109,6 +120,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
|
||||
return@with it.getPage(pageIndex - 1)?.removePageAloudSpan()
|
||||
?: TextPage(title = it.title).format()
|
||||
}
|
||||
if (!it.isCompleted) {
|
||||
return@with TextPage(title = it.title).format()
|
||||
}
|
||||
}
|
||||
prevChapter?.let {
|
||||
return@with it.lastPage?.removePageAloudSpan()
|
||||
@ -125,6 +139,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
|
||||
return@with it.getPage(pageIndex + 2)?.removePageAloudSpan()
|
||||
?: TextPage(title = it.title).format()
|
||||
}
|
||||
if (!it.isCompleted) {
|
||||
return@with TextPage(title = it.title).format()
|
||||
}
|
||||
nextChapter?.let { nc ->
|
||||
if (pageIndex < it.pageSize - 1) {
|
||||
return@with nc.getPage(0)?.removePageAloudSpan()
|
||||
|
@ -13,6 +13,12 @@ inline fun <T> List<T>.fastBinarySearch(
|
||||
toIndex: Int = size,
|
||||
comparison: (T) -> Int
|
||||
): Int {
|
||||
when {
|
||||
fromIndex > toIndex -> throw IllegalArgumentException("fromIndex ($fromIndex) is greater than toIndex ($toIndex).")
|
||||
fromIndex < 0 -> throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than zero.")
|
||||
toIndex > size -> throw IndexOutOfBoundsException("toIndex ($toIndex) is greater than size ($size).")
|
||||
}
|
||||
|
||||
var low = fromIndex
|
||||
var high = toIndex - 1
|
||||
|
||||
|
@ -119,3 +119,11 @@ open class Debounce<T>(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun <T> debounce(
|
||||
wait: Long = 0L,
|
||||
maxWait: Long = -1L,
|
||||
leading: Boolean = false,
|
||||
trailing: Boolean = true,
|
||||
func: () -> T
|
||||
) = Debounce(wait, maxWait, leading, trailing, func)
|
||||
|
@ -7,3 +7,10 @@ class Throttle<T>(
|
||||
trailing: Boolean = true,
|
||||
func: () -> T
|
||||
) : Debounce<T>(wait, wait, leading, trailing, func)
|
||||
|
||||
fun <T> throttle(
|
||||
wait: Long = 0L,
|
||||
leading: Boolean = true,
|
||||
trailing: Boolean = true,
|
||||
func: () -> T
|
||||
) = Throttle(wait, leading, trailing, func)
|
||||
|
Loading…
Reference in New Issue
Block a user