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( fun toSearchBook() = SearchBook(
name = name, name = name,
author = author, author = author,

View File

@ -124,7 +124,7 @@ object BookHelp {
) { ) {
try { try {
saveText(book, bookChapter, content) saveText(book, bookChapter, content)
saveImages(bookSource, book, bookChapter, content) //saveImages(bookSource, book, bookChapter, content)
postEvent(EventBus.SAVE_CONTENT, Pair(book, bookChapter)) postEvent(EventBus.SAVE_CONTENT, Pair(book, bookChapter))
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()

View File

@ -145,6 +145,9 @@ object ReadBook : CoroutineScope by MainScope() {
} }
fun clearTextChapter() { fun clearTextChapter() {
prevTextChapter?.cancelLayout()
curTextChapter?.cancelLayout()
nextTextChapter?.cancelLayout()
prevTextChapter = null prevTextChapter = null
curTextChapter = null curTextChapter = null
nextTextChapter = null nextTextChapter = null
@ -210,6 +213,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex < chapterSize - 1) { if (durChapterIndex < chapterSize - 1) {
durChapterPos = 0 durChapterPos = 0
durChapterIndex++ durChapterIndex++
prevTextChapter?.cancelLayout()
prevTextChapter = curTextChapter prevTextChapter = curTextChapter
curTextChapter = nextTextChapter curTextChapter = nextTextChapter
nextTextChapter = null nextTextChapter = null
@ -226,6 +230,7 @@ object ReadBook : CoroutineScope by MainScope() {
callBack?.upMenuView() callBack?.upMenuView()
AppLog.putDebug("moveToNextChapter-curPageChanged()") AppLog.putDebug("moveToNextChapter-curPageChanged()")
curPageChanged() curPageChanged()
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it) }
return true return true
} else { } else {
AppLog.putDebug("跳转下一章失败,没有下一章") AppLog.putDebug("跳转下一章失败,没有下一章")
@ -241,6 +246,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex > 0) { if (durChapterIndex > 0) {
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0 durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
durChapterIndex-- durChapterIndex--
nextTextChapter?.cancelLayout()
nextTextChapter = curTextChapter nextTextChapter = curTextChapter
curTextChapter = prevTextChapter curTextChapter = prevTextChapter
prevTextChapter = null prevTextChapter = null
@ -254,6 +260,7 @@ object ReadBook : CoroutineScope by MainScope() {
saveRead() saveRead()
callBack?.upMenuView() callBack?.upMenuView()
curPageChanged() curPageChanged()
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it) }
return true return true
} else { } else {
return false return false
@ -477,23 +484,33 @@ object ReadBook : CoroutineScope by MainScope() {
) )
val contents = contentProcessor val contents = contentProcessor
.getContent(book, chapter, content, includeTitle = false) .getContent(book, chapter, content, includeTitle = false)
val textChapter = ChapterProvider val textChapter = ChapterProvider.getTextChapterAsync(
.getTextChapter(book, chapter, displayTitle, contents, chapterSize) book,
chapter,
displayTitle,
contents,
chapterSize,
this@ReadBook
)
when (val offset = chapter.index - durChapterIndex) { when (val offset = chapter.index - durChapterIndex) {
0 -> { 0 -> {
curTextChapter?.cancelLayout()
curTextChapter = textChapter curTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
callBack?.upMenuView() callBack?.upMenuView()
curPageChanged() curPageChanged()
callBack?.contentLoadFinish() callBack?.contentLoadFinish()
callBack?.onCurrentTextChapterChanged(textChapter)
} }
-1 -> { -1 -> {
prevTextChapter?.cancelLayout()
prevTextChapter = textChapter prevTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
1 -> { 1 -> {
nextTextChapter?.cancelLayout()
nextTextChapter = textChapter nextTextChapter = textChapter
if (upContent) callBack?.upContent(offset, resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
@ -632,6 +649,8 @@ object ReadBook : CoroutineScope by MainScope() {
fun upPageAnim() fun upPageAnim()
fun notifyBookChanged() 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.ContentTextView
import io.legado.app.ui.book.read.page.ReadView 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.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.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.SearchContentActivity
import io.legado.app.ui.book.searchContent.SearchResult import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.ui.book.source.edit.BookSourceEditActivity 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.observeEventSticky
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.stackTraceStr
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import io.legado.app.utils.sysScreenOffTime import io.legado.app.utils.sysScreenOffTime
import io.legado.app.utils.throttle
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -139,7 +144,8 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadBook.CallBack, ReadBook.CallBack,
AutoReadDialog.CallBack, AutoReadDialog.CallBack,
TxtTocRuleDialog.CallBack, TxtTocRuleDialog.CallBack,
ColorPickerDialogListener { ColorPickerDialogListener,
LayoutProgressListener {
private val tocActivity = private val tocActivity =
registerForActivityResult(TocActivityResult()) { registerForActivityResult(TocActivityResult()) {
@ -221,6 +227,12 @@ class ReadBookActivity : BaseReadBookActivity(),
private val handler by lazy { buildMainHandler() } private val handler by lazy { buildMainHandler() }
private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } } private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
private val executor = ReadBook.executor private val executor = ReadBook.executor
private val upSeekBarThrotle = throttle(200) {
runOnUiThread {
upSeekBarProgress()
binding.readMenu.upSeekBar()
}
}
//恢复跳转前进度对话框的交互结果 //恢复跳转前进度对话框的交互结果
private var confirmRestoreProcess: Boolean? = null private var confirmRestoreProcess: Boolean? = null
@ -1357,6 +1369,25 @@ class ReadBookActivity : BaseReadBookActivity(),
binding.readView.autoPager.resume() 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) { private fun skipToSearch(searchResult: SearchResult) {
val previousResult = binding.searchMenu.previousSearchResult val previousResult = binding.searchMenu.previousSearchResult

View File

@ -655,7 +655,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
fun createBookmark(): Bookmark? { fun createBookmark(): Bookmark? {
val page = relativePage(selectStart.relativePagePos) val page = relativePage(selectStart.relativePagePos)
page.getTextChapter()?.let { chapter -> page.getTextChapter().let { chapter ->
ReadBook.book?.let { book -> ReadBook.book?.let { book ->
return book.createBookMark().apply { return book.createBookMark().apply {
chapterIndex = page.chapterIndex chapterIndex = page.chapterIndex

View File

@ -309,11 +309,18 @@ class PageView(context: Context) : FrameLayout(context) {
fun setProgress(textPage: TextPage) = textPage.apply { fun setProgress(textPage: TextPage) = textPage.apply {
tvBookName?.setTextIfNotEqual(ReadBook.book?.name) tvBookName?.setTextIfNotEqual(ReadBook.book?.name)
tvTitle?.setTextIfNotEqual(textPage.title) tvTitle?.setTextIfNotEqual(textPage.title)
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
val readProgress = readProgress val readProgress = readProgress
tvTotalProgress?.setTextIfNotEqual(readProgress) tvTotalProgress?.setTextIfNotEqual(readProgress)
tvTotalProgress1?.setTextIfNotEqual("${chapterIndex.plus(1)}/${chapterSize}") tvTotalProgress1?.setTextIfNotEqual("${chapterIndex.plus(1)}/${chapterSize}")
if (textChapter.isCompleted) {
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") 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?) { 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.TextPage
import io.legado.app.ui.book.read.page.entities.TextPos 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.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.ui.book.read.page.provider.TextPageFactory
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.invisible import io.legado.app.utils.invisible
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.throttle
import java.text.BreakIterator import java.text.BreakIterator
import java.util.Locale import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
@ -41,7 +43,7 @@ import kotlin.math.abs
*/ */
class ReadView(context: Context, attrs: AttributeSet) : class ReadView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs), FrameLayout(context, attrs),
DataSource { DataSource, LayoutProgressListener {
val callBack: CallBack get() = activity as CallBack val callBack: CallBack get() = activity as CallBack
var pageFactory: TextPageFactory = TextPageFactory(this) var pageFactory: TextPageFactory = TextPageFactory(this)
@ -99,6 +101,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
private val bcRect = RectF() private val bcRect = RectF()
private val brRect = RectF() private val brRect = RectF()
private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) } private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) }
private val upProgressThrottle = throttle(200) { post { upProgress() } }
val autoPager = AutoPager(this) val autoPager = AutoPager(this)
val isAutoPage get() = autoPager.isRunning val isAutoPage get() = autoPager.isRunning
@ -539,6 +542,10 @@ class ReadView(context: Context, attrs: AttributeSet) :
callBack.screenOffTimerStart() callBack.screenOffTimerStart()
} }
private fun upProgress() {
curPage.setProgress(pageFactory.curPage)
}
/** /**
* 更新滑动距离 * 更新滑动距离
*/ */
@ -654,6 +661,37 @@ class ReadView(context: Context, attrs: AttributeSet) :
curPage.submitRenderTask() 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? override val currentChapter: TextChapter?
get() { get() {
return if (callBack.isInitFinish) ReadBook.textChapter(0) else null 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 androidx.annotation.Keep
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule 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 io.legado.app.utils.fastBinarySearchBy
import kotlinx.coroutines.CoroutineScope
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@ -17,14 +22,18 @@ data class TextChapter(
val chapter: BookChapter, val chapter: BookChapter,
val position: Int, val position: Int,
val title: String, val title: String,
val pages: List<TextPage>,
val chaptersSize: Int, val chaptersSize: Int,
val sameTitleRemoved: Boolean, val sameTitleRemoved: Boolean,
val isVip: Boolean, val isVip: Boolean,
val isPay: Boolean, val isPay: Boolean,
//起效的替换规则 //起效的替换规则
val effectiveReplaceRules: List<ReplaceRule>? 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? { fun getPage(index: Int): TextPage? {
return pages.getOrNull(index) return pages.getOrNull(index)
@ -42,6 +51,10 @@ data class TextChapter(
val pageSize: Int get() = pages.size val pageSize: Int get() = pages.size
var listener: LayoutProgressListener? = null
var isCompleted = false
val paragraphs by lazy { val paragraphs by lazy {
paragraphsInternal paragraphsInternal
} }
@ -79,7 +92,12 @@ data class TextChapter(
* @return 是否是最后一页 * @return 是否是最后一页
*/ */
fun isLastIndex(index: Int): Boolean { 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 根据索引位置获取所在页 * @return 根据索引位置获取所在页
*/ */
fun getPageIndexByCharIndex(charIndex: Int): Int { 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 it.lines.first().chapterPosition
} }
return if (index >= 0) { val index = abs(bIndex + 1) - 1
index if (index == -1) {
} else { return -1
abs(index + 1) - 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 var length = 0
for (i in pages.indices) { for (i in pages.indices) {
val page = pages[i] 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 androidx.core.graphics.withTranslation
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.config.ReadBookConfig 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.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.entities.column.TextColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
@ -27,7 +27,6 @@ data class TextPage(
var text: String = appCtx.getString(R.string.data_loading), var text: String = appCtx.getString(R.string.data_loading),
var title: String = appCtx.getString(R.string.data_loading), var title: String = appCtx.getString(R.string.data_loading),
private val textLines: ArrayList<TextLine> = arrayListOf(), private val textLines: ArrayList<TextLine> = arrayListOf(),
var pageSize: Int = 0,
var chapterSize: Int = 0, var chapterSize: Int = 0,
var chapterIndex: Int = 0, var chapterIndex: Int = 0,
var height: Float = 0f, var height: Float = 0f,
@ -47,6 +46,10 @@ data class TextPage(
var canvasRecorder = CanvasRecorderFactory.create(true) var canvasRecorder = CanvasRecorderFactory.create(true)
var doublePage = false var doublePage = false
var paddingTop = 0 var paddingTop = 0
var isCompleted = false
@JvmField
var textChapter = emptyTextChapter
val pageSize get() = textChapter.pageSize
val paragraphs by lazy { val paragraphs by lazy {
paragraphsInternal paragraphsInternal
@ -162,6 +165,7 @@ data class TextPage(
} }
height = ChapterProvider.visibleHeight.toFloat() height = ChapterProvider.visibleHeight.toFloat()
invalidate() invalidate()
isCompleted = true
} }
return this return this
} }
@ -248,23 +252,8 @@ data class TextPage(
/** /**
* @return 页面所在章节 * @return 页面所在章节
*/ */
fun getTextChapter(): TextChapter? { fun getTextChapter(): TextChapter {
ReadBook.curTextChapter?.let { return textChapter
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 draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) { fun draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) {
@ -284,6 +273,7 @@ data class TextPage(
} }
fun render(view: ContentTextView): Boolean { fun render(view: ContentTextView): Boolean {
if (!isCompleted) return false
return canvasRecorder.recordIfNeeded(view.width, height.toInt()) { return canvasRecorder.recordIfNeeded(view.width, height.toInt()) {
drawPage(view, this) 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.spToPx
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.textHeight import io.legado.app.utils.textHeight
import kotlinx.coroutines.CoroutineScope
import splitties.init.appCtx import splitties.init.appCtx
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
@ -42,12 +43,12 @@ import java.util.Locale
@Suppress("DEPRECATION", "ConstPropertyName") @Suppress("DEPRECATION", "ConstPropertyName")
object ChapterProvider { object ChapterProvider {
//用于图片字的替换 //用于图片字的替换
private const val srcReplaceChar = "" const val srcReplaceChar = ""
//用于评论按钮的替换 //用于评论按钮的替换
private const val reviewChar = "" const val reviewChar = ""
private const val indentChar = " " const val indentChar = " "
@JvmStatic @JvmStatic
var viewWidth = 0 var viewWidth = 0
@ -94,25 +95,31 @@ object ChapterProvider {
private set private set
@JvmStatic @JvmStatic
private var paragraphSpacing = 0 var paragraphSpacing = 0
private set
@JvmStatic @JvmStatic
private var titleTopSpacing = 0 var titleTopSpacing = 0
private set
@JvmStatic @JvmStatic
private var titleBottomSpacing = 0 var titleBottomSpacing = 0
private set
@JvmStatic @JvmStatic
private var indentCharWidth = 0f var indentCharWidth = 0f
private set
@JvmStatic @JvmStatic
var titlePaintTextHeight = 0f var titlePaintTextHeight = 0f
private set
@JvmStatic @JvmStatic
var contentPaintTextHeight = 0f var contentPaintTextHeight = 0f
private set
@JvmStatic @JvmStatic
private var titlePaintFontMetrics = FontMetrics() var titlePaintFontMetrics = FontMetrics()
@JvmStatic @JvmStatic
var contentPaintFontMetrics = FontMetrics() var contentPaintFontMetrics = FontMetrics()
@ -278,7 +285,7 @@ object ChapterProvider {
textPage.text = stringBuilder.toString() textPage.text = stringBuilder.toString()
textPages.forEachIndexed { index, item -> textPages.forEachIndexed { index, item ->
item.index = index item.index = index
item.pageSize = textPages.size //item.pageSize = textPages.size
item.chapterIndex = bookChapter.index item.chapterIndex = bookChapter.index
item.chapterSize = chapterSize item.chapterSize = chapterSize
item.title = displayTitle item.title = displayTitle
@ -290,7 +297,8 @@ object ChapterProvider {
return TextChapter( return TextChapter(
bookChapter, bookChapter,
bookChapter.index, displayTitle, bookChapter.index, displayTitle,
textPages, chapterSize, //textPages,
chapterSize,
bookContent.sameTitleRemoved, bookContent.sameTitleRemoved,
bookChapter.isVip, bookChapter.isVip,
bookChapter.isPay, 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) { override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) {
return if (hasNext()) { return if (hasNext()) {
val pageIndex = pageIndex
if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) { if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) {
if ((currentChapter == null || isScroll) && nextChapter == null) { if ((currentChapter == null || isScroll) && nextChapter == null) {
return@with false return@with false
} }
ReadBook.moveToNextChapter(upContent, false) ReadBook.moveToNextChapter(upContent, false)
} else { } else {
if (pageIndex < 0 || currentChapter?.isLastIndexCurrent(pageIndex) == true) {
return@with false
}
ReadBook.setPageIndex(pageIndex.plus(1)) ReadBook.setPageIndex(pageIndex.plus(1))
} }
if (upContent) upContent(resetPageOffset = false) if (upContent) upContent(resetPageOffset = false)
@ -55,6 +59,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
if (currentChapter == null && prevChapter == null) { if (currentChapter == null && prevChapter == null) {
return@with false return@with false
} }
if (prevChapter != null && prevChapter?.isCompleted == false) {
return@with false
}
ReadBook.moveToPrevChapter(upContent, upContentInPlace = false) ReadBook.moveToPrevChapter(upContent, upContentInPlace = false)
} else { } else {
if (currentChapter == null) { if (currentChapter == null) {
@ -74,7 +81,8 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
return@with TextPage(text = it).format() return@with TextPage(text = it).format()
} }
currentChapter?.let { 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() return TextPage().format()
} }
@ -90,6 +98,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
return@with it.getPage(pageIndex + 1)?.removePageAloudSpan() return@with it.getPage(pageIndex + 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
} }
if (!it.isCompleted) {
return@with TextPage(title = it.title).format()
}
} }
nextChapter?.let { nextChapter?.let {
return@with it.getPage(0)?.removePageAloudSpan() return@with it.getPage(0)?.removePageAloudSpan()
@ -109,6 +120,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
return@with it.getPage(pageIndex - 1)?.removePageAloudSpan() return@with it.getPage(pageIndex - 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
} }
if (!it.isCompleted) {
return@with TextPage(title = it.title).format()
}
} }
prevChapter?.let { prevChapter?.let {
return@with it.lastPage?.removePageAloudSpan() return@with it.lastPage?.removePageAloudSpan()
@ -125,6 +139,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
return@with it.getPage(pageIndex + 2)?.removePageAloudSpan() return@with it.getPage(pageIndex + 2)?.removePageAloudSpan()
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
} }
if (!it.isCompleted) {
return@with TextPage(title = it.title).format()
}
nextChapter?.let { nc -> nextChapter?.let { nc ->
if (pageIndex < it.pageSize - 1) { if (pageIndex < it.pageSize - 1) {
return@with nc.getPage(0)?.removePageAloudSpan() return@with nc.getPage(0)?.removePageAloudSpan()

View File

@ -13,6 +13,12 @@ inline fun <T> List<T>.fastBinarySearch(
toIndex: Int = size, toIndex: Int = size,
comparison: (T) -> Int comparison: (T) -> Int
): 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 low = fromIndex
var high = toIndex - 1 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, trailing: Boolean = true,
func: () -> T func: () -> T
) : Debounce<T>(wait, wait, leading, trailing, func) ) : 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)