This commit is contained in:
Horis 2024-02-21 23:34:36 +08:00
parent abdc23967c
commit 946cd1ab94
16 changed files with 1059 additions and 47 deletions

View File

@ -282,6 +282,10 @@ data class Book(
}
}
fun getBookSource(): BookSource? {
return appDb.bookSourceDao.getBookSource(origin)
}
fun toSearchBook() = SearchBook(
name = name,
author = author,

View File

@ -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()

View File

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

View File

@ -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

View File

@ -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

View File

@ -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?) {

View File

@ -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

View File

@ -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 }
}
}

View File

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

View File

@ -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
}
/**
* 排版图片
*/

View File

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

View File

@ -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
}
}

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)