Merge branch 'gedoor:master' into master

This commit is contained in:
huolibao
2024-02-25 10:13:45 +08:00
committed by GitHub
15 changed files with 225 additions and 173 deletions
@@ -52,4 +52,6 @@ object AppPattern {
val spaceRegex = "\\s+".toRegex() val spaceRegex = "\\s+".toRegex()
val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex() val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex()
val LFRegex = "\n".toRegex()
} }
@@ -2,6 +2,7 @@ package io.legado.app.constant
import androidx.annotation.IntDef import androidx.annotation.IntDef
@Suppress("ConstPropertyName")
object PageAnim { object PageAnim {
const val coverPageAnim = 0 const val coverPageAnim = 0
@@ -16,10 +16,12 @@ import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isEpub import io.legado.app.help.book.isEpub
import io.legado.app.help.book.isImage import io.legado.app.help.book.isImage
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.isPdf import io.legado.app.help.book.isPdf
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.MD5Utils import io.legado.app.utils.MD5Utils
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
@@ -286,6 +288,10 @@ data class Book(
return appDb.bookSourceDao.getBookSource(origin) return appDb.bookSourceDao.getBookSource(origin)
} }
fun isLocalModified(): Boolean {
return isLocal && LocalBook.getLastModified(this).getOrDefault(0L) > latestChapterTime
}
fun toSearchBook() = SearchBook( fun toSearchBook() = SearchBook(
name = name, name = name,
author = author, author = author,
+104 -44
View File
@@ -1,6 +1,7 @@
package io.legado.app.model package io.legado.app.model
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.PageAnim.scrollPageAnim
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@@ -32,6 +33,7 @@ import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -88,7 +90,7 @@ object ReadBook : CoroutineScope by MainScope() {
readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0 readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
contentProcessor = ContentProcessor.get(book) contentProcessor = ContentProcessor.get(book)
durChapterIndex = book.durChapterIndex durChapterIndex = min(book.durChapterIndex, chapterSize - 1).coerceAtLeast(0)
durChapterPos = book.durChapterPos durChapterPos = book.durChapterPos
isLocalBook = book.isLocal isLocalBook = book.isLocal
clearTextChapter() clearTextChapter()
@@ -111,6 +113,15 @@ object ReadBook : CoroutineScope by MainScope() {
durChapterPos = book.durChapterPos durChapterPos = book.durChapterPos
clearTextChapter() clearTextChapter()
} }
if (curTextChapter?.isCompleted == false) {
curTextChapter = null
}
if (nextTextChapter?.isCompleted == false) {
nextTextChapter = null
}
if (prevTextChapter?.isCompleted == false) {
prevTextChapter = null
}
callBack?.upMenuView() callBack?.upMenuView()
upWebBook(book) upWebBook(book)
synchronized(this) { synchronized(this) {
@@ -221,18 +232,17 @@ object ReadBook : CoroutineScope by MainScope() {
nextTextChapter = null nextTextChapter = null
if (curTextChapter == null) { if (curTextChapter == null) {
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载") AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
if (upContentInPlace) callBack?.upContent(resetPageOffset = false) if (upContentInPlace) callBack?.upContent()
loadContent(durChapterIndex, upContent, resetPageOffset = false) loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent && upContentInPlace) { } else if (upContent && upContentInPlace) {
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图") AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
callBack?.upContent(resetPageOffset = false) callBack?.upContent()
} }
loadContent(durChapterIndex.plus(1), upContent, false) loadContent(durChapterIndex.plus(1), upContent, false)
saveRead() saveRead()
callBack?.upMenuView() callBack?.upMenuView()
AppLog.putDebug("moveToNextChapter-curPageChanged()") AppLog.putDebug("moveToNextChapter-curPageChanged()")
curPageChanged() curPageChanged()
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it, upContent) }
return true return true
} else { } else {
AppLog.putDebug("跳转下一章失败,没有下一章") AppLog.putDebug("跳转下一章失败,没有下一章")
@@ -253,16 +263,15 @@ object ReadBook : CoroutineScope by MainScope() {
curTextChapter = prevTextChapter curTextChapter = prevTextChapter
prevTextChapter = null prevTextChapter = null
if (curTextChapter == null) { if (curTextChapter == null) {
if (upContentInPlace) callBack?.upContent(resetPageOffset = false) if (upContentInPlace) callBack?.upContent()
loadContent(durChapterIndex, upContent, resetPageOffset = false) loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent && upContentInPlace) { } else if (upContent && upContentInPlace) {
callBack?.upContent(resetPageOffset = false) callBack?.upContent()
} }
loadContent(durChapterIndex.minus(1), upContent, false) loadContent(durChapterIndex.minus(1), upContent, false)
saveRead() saveRead()
callBack?.upMenuView() callBack?.upMenuView()
curPageChanged() curPageChanged()
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it, upContent) }
return true return true
} else { } else {
return false return false
@@ -312,7 +321,7 @@ object ReadBook : CoroutineScope by MainScope() {
*/ */
private fun curPageChanged(pageChanged: Boolean = false) { private fun curPageChanged(pageChanged: Boolean = false) {
callBack?.pageChanged() callBack?.pageChanged()
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun && isLayoutAvailable) {
val scrollPageAnim = pageAnim() == 3 val scrollPageAnim = pageAnim() == 3
if (scrollPageAnim && pageChanged) { if (scrollPageAnim && pageChanged) {
ReadAloud.pause(appCtx) ReadAloud.pause(appCtx)
@@ -341,6 +350,13 @@ object ReadBook : CoroutineScope by MainScope() {
return curTextChapter?.getPageIndexByCharIndex(durChapterPos) ?: durChapterPos return curTextChapter?.getPageIndexByCharIndex(durChapterPos) ?: durChapterPos
} }
/**
* 是否排版到了当前阅读位置
*/
val isLayoutAvailable inline get() = durPageIndex >= 0
val isScroll inline get() = pageAnim() == scrollPageAnim
/** /**
* chapterOnDur: 0为当前页,1为下一页,-1为上一页 * chapterOnDur: 0为当前页,1为下一页,-1为上一页
*/ */
@@ -369,6 +385,20 @@ object ReadBook : CoroutineScope by MainScope() {
loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset) loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset)
} }
fun loadOrUpContent() {
if (curTextChapter == null) {
loadContent(durChapterIndex)
} else {
callBack?.upContent()
}
if (nextTextChapter == null) {
loadContent(durChapterIndex + 1)
}
if (prevTextChapter == null) {
loadContent(durChapterIndex - 1)
}
}
/** /**
* 加载章节内容 * 加载章节内容
* @param index 章节序号 * @param index 章节序号
@@ -392,10 +422,9 @@ object ReadBook : CoroutineScope by MainScope() {
chapter, chapter,
it, it,
upContent, upContent,
resetPageOffset resetPageOffset,
) { success
success?.invoke() )
}
} ?: download( } ?: download(
downloadScope, downloadScope,
chapter, chapter,
@@ -456,9 +485,8 @@ object ReadBook : CoroutineScope by MainScope() {
chapter, chapter,
"加载正文失败\n$msg", "加载正文失败\n$msg",
resetPageOffset = resetPageOffset, resetPageOffset = resetPageOffset,
) { success = success
success?.invoke() )
}
} }
} }
@@ -505,48 +533,83 @@ object ReadBook : CoroutineScope by MainScope() {
0 -> { 0 -> {
curTextChapter?.cancelLayout() curTextChapter?.cancelLayout()
curTextChapter = textChapter curTextChapter = textChapter
if (resetPageOffset) {
callBack?.resetPageOffset()
}
callBack?.upMenuView() callBack?.upMenuView()
curPageChanged() textChapter.setProgressListener(object : LayoutProgressListener {
callBack?.contentLoadFinish() var available = false
callBack?.onCurrentTextChapterChanged(textChapter, upContent)
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
if (!available && page.containPos(durChapterPos)) {
if (upContent) {
callBack?.upContent(offset, resetPageOffset)
}
curPageChanged()
callBack?.contentLoadFinish()
available = true
}
if (upContent && isScroll) {
if (max(index - 3, 0) < durPageIndex) {
callBack?.upContent(offset, resetPageOffset)
}
}
callBack?.onLayoutPageCompleted(index, page)
}
override fun onLayoutCompleted() {
if (upContent) callBack?.upContent(offset, resetPageOffset)
success?.invoke()
callBack?.onLayoutCompleted()
}
override fun onLayoutException(e: Throwable) {
AppLog.put("ChapterProvider ERROR", e)
appCtx.toastOnUi("ChapterProvider ERROR:\n${e.stackTraceStr}")
callBack?.onLayoutException(e)
}
})
} }
-1 -> { -1 -> {
prevTextChapter?.cancelLayout() prevTextChapter?.cancelLayout()
prevTextChapter = textChapter prevTextChapter = textChapter
if (upContent) { textChapter.setProgressListener(object : LayoutProgressListener {
textChapter.setProgressListener(object : LayoutProgressListener { override fun onLayoutCompleted() {
override fun onLayoutCompleted() { if (upContent) callBack?.upContent(offset, resetPageOffset)
callBack?.upContent(offset, resetPageOffset) success?.invoke()
} }
})
} override fun onLayoutException(e: Throwable) {
AppLog.put("ChapterProvider ERROR", e)
appCtx.toastOnUi("ChapterProvider ERROR:\n${e.stackTraceStr}")
}
})
} }
1 -> { 1 -> {
nextTextChapter?.cancelLayout() nextTextChapter?.cancelLayout()
nextTextChapter = textChapter nextTextChapter = textChapter
if (upContent) { textChapter.setProgressListener(object : LayoutProgressListener {
textChapter.setProgressListener(object : LayoutProgressListener { override fun onLayoutPageCompleted(index: Int, page: TextPage) {
override fun onLayoutPageCompleted(index: Int, page: TextPage) { if (index > 1) {
if (index > 1) { return
return
}
callBack?.upContent(offset, resetPageOffset)
} }
}) if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
override fun onLayoutCompleted() {
success?.invoke()
}
override fun onLayoutException(e: Throwable) {
AppLog.put("ChapterProvider ERROR", e)
appCtx.toastOnUi("ChapterProvider ERROR:\n${e.stackTraceStr}")
}
})
} }
} }
textChapter.createLayout(this@ReadBook, book, contents) textChapter.createLayout(this@ReadBook, book, contents)
}.onError { }.onError {
AppLog.put("ChapterProvider ERROR", it) AppLog.put("ChapterProvider ERROR", it)
appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}") appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
}.onSuccess {
success?.invoke()
} }
} }
@@ -655,9 +718,10 @@ object ReadBook : CoroutineScope by MainScope() {
downloadedChapters.clear() downloadedChapters.clear()
downloadFailChapters.clear() downloadFailChapters.clear()
ImageProvider.clear() ImageProvider.clear()
curTextChapter?.cancelLayout()
} }
interface CallBack { interface CallBack : LayoutProgressListener {
fun upMenuView() fun upMenuView()
fun loadChapterList(book: Book) fun loadChapterList(book: Book)
@@ -675,10 +739,6 @@ object ReadBook : CoroutineScope by MainScope() {
fun upPageAnim() fun upPageAnim()
fun notifyBookChanged() fun notifyBookChanged()
fun onCurrentTextChapterChanged(textChapter: TextChapter, upContent: Boolean = true)
fun resetPageOffset()
} }
} }
@@ -1,6 +1,7 @@
package io.legado.app.model.webBook package io.legado.app.model.webBook
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@@ -131,7 +132,10 @@ object BookContent {
//全文替换 //全文替换
val replaceRegex = contentRule.replaceRegex val replaceRegex = contentRule.replaceRegex
if (!replaceRegex.isNullOrEmpty()) { if (!replaceRegex.isNullOrEmpty()) {
contentStr = contentStr.split(AppPattern.LFRegex)
.joinToString("\n") { it.trim { c -> c <= ' ' || c == ' ' } }
contentStr = analyzeRule.getString(replaceRegex, contentStr) contentStr = analyzeRule.getString(replaceRegex, contentStr)
contentStr = contentStr.split(AppPattern.LFRegex).joinToString("\n") { "  $it" }
} }
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")
@@ -77,7 +77,6 @@ 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.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.read.page.provider.LayoutProgressListener
@@ -115,7 +114,6 @@ 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.throttle
@@ -126,7 +124,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.max
/** /**
* 阅读界面 * 阅读界面
@@ -1373,51 +1370,11 @@ class ReadBookActivity : BaseReadBookActivity(),
binding.readView.autoPager.resume() binding.readView.autoPager.resume()
} }
override fun onCurrentTextChapterChanged(textChapter: TextChapter, upContent: Boolean) {
this.upContent = upContent
textChapter.setProgressListener(this)
}
override fun onLayoutPageCompleted(index: Int, page: TextPage) { override fun onLayoutPageCompleted(index: Int, page: TextPage) {
upSeekBarThrottle.invoke() upSeekBarThrottle.invoke()
if (upContent) {
val durChapterPos = ReadBook.durChapterPos
if (page.containPos(durChapterPos)) {
runOnUiThread {
binding.readView.upContent(0, resetPageOffset = false)
}
}
if (isScroll) {
val pageIndex = ReadBook.durPageIndex
if (max(index - 3, 0) < pageIndex) {
runOnUiThread {
binding.readView.upContent(0, resetPageOffset = false)
}
}
}
}
binding.readView.onLayoutPageCompleted(index, page) binding.readView.onLayoutPageCompleted(index, page)
} }
override fun onLayoutCompleted() {
if (upContent) {
runOnUiThread {
binding.readView.upContent(0, resetPageOffset = false)
}
}
binding.readView.onLayoutCompleted()
}
override fun onLayoutException(e: Throwable) {
AppLog.put("ChapterProvider ERROR", e)
toastOnUi("ChapterProvider ERROR:\n${e.stackTraceStr}")
binding.readView.onLayoutException(e)
}
override fun resetPageOffset() {
binding.readView.resetPageOffset()
}
/* 全文搜索跳转 */ /* 全文搜索跳转 */
private fun skipToSearch(searchResult: SearchResult) { private fun skipToSearch(searchResult: SearchResult) {
val previousResult = binding.searchMenu.previousSearchResult val previousResult = binding.searchMenu.previousSearchResult
@@ -109,21 +109,12 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else { } else {
loadChapterList(book) loadChapterList(book)
} }
} else if (book.isLocal } else if (book.isLocalModified()) {
&& LocalBook.getLastModified(book).getOrDefault(0L) > book.latestChapterTime
) {
loadChapterList(book) loadChapterList(book)
} else if (isSameBook) { } else if (isSameBook) {
if (ReadBook.curTextChapter != null) { ReadBook.loadOrUpContent()
ReadBook.callBack?.upContent(resetPageOffset = false)
} else {
ReadBook.loadContent(resetPageOffset = true)
}
checkLocalBookFileExist(book) checkLocalBookFileExist(book)
} else { } else {
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
}
ReadBook.loadContent(resetPageOffset = false) ReadBook.loadContent(resetPageOffset = false)
checkLocalBookFileExist(book) checkLocalBookFileExist(book)
} }
@@ -130,7 +130,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipHeaderLeft = tipValue ReadTipConfig.tipHeaderLeft = tipValue
tvHeaderLeft.text = ReadTipConfig.tipNames[i] tvHeaderLeft.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2)) postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
} }
} }
llHeaderMiddle.setOnClickListener { llHeaderMiddle.setOnClickListener {
@@ -139,7 +139,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipHeaderMiddle = tipValue ReadTipConfig.tipHeaderMiddle = tipValue
tvHeaderMiddle.text = ReadTipConfig.tipNames[i] tvHeaderMiddle.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2)) postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
} }
} }
llHeaderRight.setOnClickListener { llHeaderRight.setOnClickListener {
@@ -148,7 +148,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipHeaderRight = tipValue ReadTipConfig.tipHeaderRight = tipValue
tvHeaderRight.text = ReadTipConfig.tipNames[i] tvHeaderRight.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2)) postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
} }
} }
llFooterLeft.setOnClickListener { llFooterLeft.setOnClickListener {
@@ -157,7 +157,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipFooterLeft = tipValue ReadTipConfig.tipFooterLeft = tipValue
tvFooterLeft.text = ReadTipConfig.tipNames[i] tvFooterLeft.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2)) postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
} }
} }
llFooterMiddle.setOnClickListener { llFooterMiddle.setOnClickListener {
@@ -166,7 +166,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipFooterMiddle = tipValue ReadTipConfig.tipFooterMiddle = tipValue
tvFooterMiddle.text = ReadTipConfig.tipNames[i] tvFooterMiddle.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2)) postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
} }
} }
llFooterRight.setOnClickListener { llFooterRight.setOnClickListener {
@@ -175,7 +175,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipFooterRight = tipValue ReadTipConfig.tipFooterRight = tipValue
tvFooterRight.text = ReadTipConfig.tipNames[i] tvFooterRight.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2)) postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
} }
} }
llTipColor.setOnClickListener { llTipColor.setOnClickListener {
@@ -536,6 +536,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
} }
private fun upSelectChars() { private fun upSelectChars() {
if (!selectStart.isSelected() || !selectEnd.isSelected()) {
return
}
val last = if (callBack.isScroll) 2 else 0 val last = if (callBack.isScroll) 2 else 0
val textPos = TextPos(0, 0, 0) val textPos = TextPos(0, 0, 0)
for (relativePos in 0..last) { for (relativePos in 0..last) {
@@ -320,7 +320,7 @@ class PageView(context: Context) : FrameLayout(context) {
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize") tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
} else { } else {
val pageSizeInt = pageSize - 1 val pageSizeInt = pageSize
val pageSize = if (pageSizeInt <= 0) "-" else "~$pageSizeInt" val pageSize = if (pageSizeInt <= 0) "-" else "~$pageSizeInt"
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize") tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
@@ -669,10 +669,6 @@ class ReadView(context: Context, attrs: AttributeSet) :
upProgressThrottle.invoke() upProgressThrottle.invoke()
} }
fun resetPageOffset() {
curPage.resetPageOffset()
}
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
@@ -96,8 +96,7 @@ data class TextChapter(
} }
fun isLastIndexCurrent(index: Int): Boolean { fun isLastIndexCurrent(index: Int): Boolean {
// 未完成排版时,最后一页是正在排版中的,需要去掉 return index >= pages.size - 1
return index >= if (isCompleted) pages.size - 1 else pages.size - 2
} }
/** /**
@@ -105,6 +104,7 @@ data class TextChapter(
* @return 已读长度 * @return 已读长度
*/ */
fun getReadLength(pageIndex: Int): Int { fun getReadLength(pageIndex: Int): Int {
if (pageIndex < 0) return 0
return pages[min(pageIndex, lastIndex)].lines.first().chapterPosition return pages[min(pageIndex, lastIndex)].lines.first().chapterPosition
/* /*
var length = 0 var length = 0
@@ -212,18 +212,15 @@ data class TextChapter(
if (pageSize == 0) { if (pageSize == 0) {
return -1 return -1
} }
val size = if (isCompleted) pageSize else pageSize - 1 val bIndex = pages.fastBinarySearchBy(charIndex, 0, pageSize) {
val bIndex = pages.fastBinarySearchBy(charIndex, 0, size) {
it.lines.first().chapterPosition it.lines.first().chapterPosition
} }
val index = abs(bIndex + 1) - 1 val index = abs(bIndex + 1) - 1
if (index == -1) {
return -1
}
// 判断是否已经排版到 charIndex ,没有则返回 -1 // 判断是否已经排版到 charIndex ,没有则返回 -1
if (!isCompleted && index == size - 1) { if (!isCompleted && index == pageSize - 1) {
val line = pages[index].lines.first() val page = pages[index]
val pageEndPos = line.chapterPosition + line.charSize val line = page.lines.first()
val pageEndPos = line.chapterPosition + page.charSize
if (charIndex > pageEndPos) { if (charIndex > pageEndPos) {
return -1 return -1
} }
@@ -287,14 +284,12 @@ data class TextChapter(
} }
override fun onLayoutException(e: Throwable) { override fun onLayoutException(e: Throwable) {
isCompleted = true
listener?.onLayoutException(e) listener?.onLayoutException(e)
listener = null listener = null
} }
fun cancelLayout() { fun cancelLayout() {
layout?.cancel() layout?.cancel()
isCompleted = true
listener = null listener = null
} }
@@ -69,4 +69,8 @@ data class TextPos(
isLast = false isLast = false
} }
fun isSelected(): Boolean {
return lineIndex >= 0 && columnIndex >= 0
}
} }
@@ -67,6 +67,8 @@ class TextChapterLayout(
private val indentCharWidth = ChapterProvider.indentCharWidth private val indentCharWidth = ChapterProvider.indentCharWidth
private val stringBuilder = StringBuilder() private val stringBuilder = StringBuilder()
private var pendingTextPage = TextPage()
private var isCompleted = false private var isCompleted = false
private val job: Coroutine<*> private val job: Coroutine<*>
private val bookChapter inline get() = textChapter.chapter private val bookChapter inline get() = textChapter.chapter
@@ -167,7 +169,6 @@ class TextChapterLayout(
val contents = bookContent.textList val contents = bookContent.textList
var absStartX = paddingLeft var absStartX = paddingLeft
var durY = 0f var durY = 0f
textPages.add(TextPage())
if (ReadBookConfig.titleMode != 2 || bookChapter.isVolume) { if (ReadBookConfig.titleMode != 2 || bookChapter.isVolume) {
//标题非隐藏 //标题非隐藏
displayTitle.splitNotBlank("\n").forEach { text -> displayTitle.splitNotBlank("\n").forEach { text ->
@@ -185,7 +186,7 @@ class TextChapterLayout(
durY = it.second durY = it.second
} }
} }
textPages.last().lines.last().isParagraphEnd = true pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n") stringBuilder.append("\n")
durY += titleBottomSpacing durY += titleBottomSpacing
} }
@@ -244,7 +245,6 @@ class TextChapterLayout(
matcher.group(1)!!, matcher.group(1)!!,
absStartX, absStartX,
durY, durY,
textPages,
contentPaintTextHeight, contentPaintTextHeight,
stringBuilder, stringBuilder,
book.getImageStyle() book.getImageStyle()
@@ -270,10 +270,11 @@ class TextChapterLayout(
} }
} }
} }
textPages.last().lines.last().isParagraphEnd = true pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n") stringBuilder.append("\n")
} }
val textPage = textPages.last() textPages.add(pendingTextPage)
val textPage = pendingTextPage
val endPadding = 20.dpToPx() val endPadding = 20.dpToPx()
val durYPadding = durY + endPadding val durYPadding = durY + endPadding
if (textPage.height < durYPadding) { if (textPage.height < durYPadding) {
@@ -295,7 +296,6 @@ class TextChapterLayout(
src: String, src: String,
x: Int, x: Int,
y: Float, y: Float,
textPages: ArrayList<TextPage>,
textHeight: Float, textHeight: Float,
stringBuilder: StringBuilder, stringBuilder: StringBuilder,
imageStyle: String?, imageStyle: String?,
@@ -305,15 +305,16 @@ class TextChapterLayout(
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource) val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
if (size.width > 0 && size.height > 0) { if (size.width > 0 && size.height > 0) {
if (durY > visibleHeight) { if (durY > visibleHeight) {
val textPage = textPages.last() val textPage = pendingTextPage
if (textPage.height < durY) { if (textPage.height < durY) {
textPage.height = durY textPage.height = durY
} }
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear() stringBuilder.clear()
textPages.add(textPage)
coroutineContext.ensureActive() coroutineContext.ensureActive()
onPageCompleted() onPageCompleted()
textPages.add(TextPage()) pendingTextPage = TextPage()
durY = 0f durY = 0f
} }
var height = size.height var height = size.height
@@ -334,7 +335,11 @@ class TextChapterLayout(
height = visibleHeight height = visibleHeight
} }
if (durY + height > visibleHeight) { if (durY + height > visibleHeight) {
val textPage = textPages.last() val textPage = pendingTextPage
// 双页的 durY 不正确,可能会小于实际高度
if (textPage.height < durY) {
textPage.height = durY
}
if (doublePage && absStartX < viewWidth / 2) { if (doublePage && absStartX < viewWidth / 2) {
//当前页面左列结束 //当前页面左列结束
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
@@ -346,13 +351,10 @@ class TextChapterLayout(
} }
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear() stringBuilder.clear()
textPages.add(textPage)
coroutineContext.ensureActive() coroutineContext.ensureActive()
onPageCompleted() onPageCompleted()
textPages.add(TextPage()) pendingTextPage = TextPage()
}
// 双页的 durY 不正确,可能会小于实际高度
if (textPage.height < durY) {
textPage.height = durY
} }
durY = 0f durY = 0f
} }
@@ -373,7 +375,7 @@ class TextChapterLayout(
) )
calcTextLinePosition(textPages, textLine, stringBuilder.length) calcTextLinePosition(textPages, textLine, stringBuilder.length)
stringBuilder.append(" ") // 确保翻页时索引计算正确 stringBuilder.append(" ") // 确保翻页时索引计算正确
textPages.last().addLine(textLine) pendingTextPage.addLine(textLine)
} }
return absStartX to durY + textHeight * paragraphSpacing / 10f return absStartX to durY + textHeight * paragraphSpacing / 10f
} }
@@ -403,8 +405,8 @@ class TextChapterLayout(
} }
var durY = when { var durY = when {
//标题y轴居中 //标题y轴居中
emptyContent && textPages.size == 1 -> { emptyContent && textPages.isEmpty() -> {
val textPage = textPages.last() val textPage = pendingTextPage
if (textPage.lineSize == 0) { if (textPage.lineSize == 0) {
val ty = (visibleHeight - layout.lineCount * textHeight) / 2 val ty = (visibleHeight - layout.lineCount * textHeight) / 2
if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat() if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat()
@@ -423,7 +425,7 @@ class TextChapterLayout(
} }
} }
isTitle && textPages.size == 1 && textPages.last().lines.isEmpty() -> isTitle && textPages.isEmpty() && pendingTextPage.lines.isEmpty() ->
y + titleTopSpacing y + titleTopSpacing
else -> y else -> y
@@ -431,7 +433,10 @@ class TextChapterLayout(
for (lineIndex in 0 until layout.lineCount) { for (lineIndex in 0 until layout.lineCount) {
val textLine = TextLine(isTitle = isTitle) val textLine = TextLine(isTitle = isTitle)
if (durY + textHeight > visibleHeight) { if (durY + textHeight > visibleHeight) {
val textPage = textPages.last() val textPage = pendingTextPage
if (textPage.height < durY) {
textPage.height = durY
}
if (doublePage && absStartX < viewWidth / 2) { if (doublePage && absStartX < viewWidth / 2) {
//当前页面左列结束 //当前页面左列结束
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
@@ -442,16 +447,14 @@ class TextChapterLayout(
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
} }
textPage.text = stringBuilder.toString() textPage.text = stringBuilder.toString()
textPages.add(textPage)
coroutineContext.ensureActive() coroutineContext.ensureActive()
onPageCompleted() onPageCompleted()
//新建页面 //新建页面
textPages.add(TextPage()) pendingTextPage = TextPage()
stringBuilder.clear() stringBuilder.clear()
absStartX = paddingLeft absStartX = paddingLeft
} }
if (textPage.height < durY) {
textPage.height = durY
}
durY = 0f durY = 0f
} }
val lineStart = layout.getLineStart(lineIndex) val lineStart = layout.getLineStart(lineIndex)
@@ -514,7 +517,7 @@ class TextChapterLayout(
calcTextLinePosition(textPages, textLine, stringBuilder.length) calcTextLinePosition(textPages, textLine, stringBuilder.length)
stringBuilder.append(lineText) stringBuilder.append(lineText)
textLine.upTopBottom(durY, textHeight, fontMetrics) textLine.upTopBottom(durY, textHeight, fontMetrics)
val textPage = textPages.last() val textPage = pendingTextPage
textPage.addLine(textLine) textPage.addLine(textLine)
durY += textHeight * lineSpacingExtra durY += textHeight * lineSpacingExtra
if (textPage.height < durY) { if (textPage.height < durY) {
@@ -530,8 +533,8 @@ class TextChapterLayout(
textLine: TextLine, textLine: TextLine,
sbLength: Int sbLength: Int
) { ) {
val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 } val lastLine = pendingTextPage.lines.lastOrNull { it.paragraphNum > 0 }
?: textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull { it.paragraphNum > 0 } ?: textPages.lastOrNull()?.lines?.lastOrNull { it.paragraphNum > 0 }
val paragraphNum = when { val paragraphNum = when {
lastLine == null -> 1 lastLine == null -> 1
lastLine.isParagraphEnd -> lastLine.paragraphNum + 1 lastLine.isParagraphEnd -> lastLine.paragraphNum + 1
@@ -539,7 +542,7 @@ class TextChapterLayout(
} }
textLine.paragraphNum = paragraphNum textLine.paragraphNum = paragraphNum
textLine.chapterPosition = textLine.chapterPosition =
(textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull()?.run { (textPages.lastOrNull()?.lines?.lastOrNull()?.run {
chapterPosition + charSize + if (isParagraphEnd) 1 else 0 chapterPosition + charSize + if (isParagraphEnd) 1 else 0
} ?: 0) + sbLength } ?: 0) + sbLength
textLine.pagePosition = sbLength textLine.pagePosition = sbLength
@@ -6,7 +6,10 @@ import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Typeface import android.graphics.Typeface
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.StaticLayout import android.text.StaticLayout
import android.text.style.LineHeightSpan
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
@@ -25,6 +28,13 @@ class BatteryView @JvmOverloads constructor(
private val outFrame = Rect() private val outFrame = Rect()
private val polar = Rect() private val polar = Rect()
private val canvasRecorder = CanvasRecorderFactory.create() private val canvasRecorder = CanvasRecorderFactory.create()
private val batterySpan = LineHeightSpan { _, _, _, _, _, fm ->
fm.top = -22
fm.ascent = -28
fm.descent = 7
fm.bottom = 1
fm.leading = 0
}
var isBattery = false var isBattery = false
set(value) { set(value) {
field = value field = value
@@ -58,42 +68,62 @@ class BatteryView @JvmOverloads constructor(
fun setBattery(battery: Int, text: String? = null) { fun setBattery(battery: Int, text: String? = null) {
this.battery = battery this.battery = battery
if (text.isNullOrEmpty()) { if (text.isNullOrEmpty()) {
setText(battery.toString()) setText(getBatteryText(battery.toString()))
} else { } else {
setText("$text $battery") setText(getBatteryText("$text $battery"))
} }
} }
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
canvasRecorder.invalidate()
}
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
canvasRecorder.recordIfNeededThenDraw(canvas, width, height) { canvasRecorder.recordIfNeededThenDraw(canvas, width, height) {
super.onDraw(this) super.onDraw(this)
if (!isBattery) return@recordIfNeededThenDraw if (!isBattery) return@recordIfNeededThenDraw
layout.getLineBounds(0, outFrame) drawBattery(this)
val batteryStart = layout
.getPrimaryHorizontal(text.length - battery.toString().length)
.toInt() + 2.dpToPx()
val batteryEnd = batteryStart +
StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx()
outFrame.set(
batteryStart,
2.dpToPx(),
batteryEnd,
height - 2.dpToPx()
)
val dj = (outFrame.bottom - outFrame.top) / 3
polar.set(
batteryEnd,
outFrame.top + dj,
batteryEnd + 2.dpToPx(),
outFrame.bottom - dj
)
batteryPaint.style = Paint.Style.STROKE
drawRect(outFrame, batteryPaint)
batteryPaint.style = Paint.Style.FILL
drawRect(polar, batteryPaint)
} }
} }
private fun drawBattery(canvas: Canvas) {
layout.getLineBounds(0, outFrame)
val batteryStart = layout
.getPrimaryHorizontal(text.length - battery.toString().length)
.toInt() + 2.dpToPx()
val batteryEnd = batteryStart +
StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx()
outFrame.set(
batteryStart,
2.dpToPx(),
batteryEnd,
height - 2.dpToPx()
)
val dj = (outFrame.bottom - outFrame.top) / 3
polar.set(
batteryEnd,
outFrame.top + dj,
batteryEnd + 2.dpToPx(),
outFrame.bottom - dj
)
batteryPaint.style = Paint.Style.STROKE
canvas.drawRect(outFrame, batteryPaint)
batteryPaint.style = Paint.Style.FILL
canvas.drawRect(polar, batteryPaint)
}
private fun getBatteryText(text: CharSequence?): SpannableStringBuilder? {
if (text == null) {
return null
}
return SpannableStringBuilder(text).apply {
setSpan(batterySpan, 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
override fun invalidate() { override fun invalidate() {
super.invalidate() super.invalidate()
kotlin.runCatching { kotlin.runCatching {