Merge branch 'gedoor:master' into master

This commit is contained in:
huolibao 2024-02-25 10:13:45 +08:00 committed by GitHub
commit 35dc7dd728
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 225 additions and 173 deletions

View File

@ -52,4 +52,6 @@ object AppPattern {
val spaceRegex = "\\s+".toRegex()
val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex()
val LFRegex = "\n".toRegex()
}

View File

@ -2,6 +2,7 @@ package io.legado.app.constant
import androidx.annotation.IntDef
@Suppress("ConstPropertyName")
object PageAnim {
const val coverPageAnim = 0

View File

@ -16,10 +16,12 @@ import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isEpub
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.config.AppConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.GSON
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.fromJsonObject
@ -286,6 +288,10 @@ data class Book(
return appDb.bookSourceDao.getBookSource(origin)
}
fun isLocalModified(): Boolean {
return isLocal && LocalBook.getLastModified(this).getOrDefault(0L) > latestChapterTime
}
fun toSearchBook() = SearchBook(
name = name,
author = author,

View File

@ -1,6 +1,7 @@
package io.legado.app.model
import io.legado.app.constant.AppLog
import io.legado.app.constant.PageAnim.scrollPageAnim
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
@ -32,6 +33,7 @@ import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import splitties.init.appCtx
import kotlin.math.max
import kotlin.math.min
@ -88,7 +90,7 @@ object ReadBook : CoroutineScope by MainScope() {
readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
contentProcessor = ContentProcessor.get(book)
durChapterIndex = book.durChapterIndex
durChapterIndex = min(book.durChapterIndex, chapterSize - 1).coerceAtLeast(0)
durChapterPos = book.durChapterPos
isLocalBook = book.isLocal
clearTextChapter()
@ -111,6 +113,15 @@ object ReadBook : CoroutineScope by MainScope() {
durChapterPos = book.durChapterPos
clearTextChapter()
}
if (curTextChapter?.isCompleted == false) {
curTextChapter = null
}
if (nextTextChapter?.isCompleted == false) {
nextTextChapter = null
}
if (prevTextChapter?.isCompleted == false) {
prevTextChapter = null
}
callBack?.upMenuView()
upWebBook(book)
synchronized(this) {
@ -221,18 +232,17 @@ object ReadBook : CoroutineScope by MainScope() {
nextTextChapter = null
if (curTextChapter == null) {
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
if (upContentInPlace) callBack?.upContent()
loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent && upContentInPlace) {
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
callBack?.upContent(resetPageOffset = false)
callBack?.upContent()
}
loadContent(durChapterIndex.plus(1), upContent, false)
saveRead()
callBack?.upMenuView()
AppLog.putDebug("moveToNextChapter-curPageChanged()")
curPageChanged()
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it, upContent) }
return true
} else {
AppLog.putDebug("跳转下一章失败,没有下一章")
@ -253,16 +263,15 @@ object ReadBook : CoroutineScope by MainScope() {
curTextChapter = prevTextChapter
prevTextChapter = null
if (curTextChapter == null) {
if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
if (upContentInPlace) callBack?.upContent()
loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent && upContentInPlace) {
callBack?.upContent(resetPageOffset = false)
callBack?.upContent()
}
loadContent(durChapterIndex.minus(1), upContent, false)
saveRead()
callBack?.upMenuView()
curPageChanged()
curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it, upContent) }
return true
} else {
return false
@ -312,7 +321,7 @@ object ReadBook : CoroutineScope by MainScope() {
*/
private fun curPageChanged(pageChanged: Boolean = false) {
callBack?.pageChanged()
if (BaseReadAloudService.isRun) {
if (BaseReadAloudService.isRun && isLayoutAvailable) {
val scrollPageAnim = pageAnim() == 3
if (scrollPageAnim && pageChanged) {
ReadAloud.pause(appCtx)
@ -341,6 +350,13 @@ object ReadBook : CoroutineScope by MainScope() {
return curTextChapter?.getPageIndexByCharIndex(durChapterPos) ?: durChapterPos
}
/**
* 是否排版到了当前阅读位置
*/
val isLayoutAvailable inline get() = durPageIndex >= 0
val isScroll inline get() = pageAnim() == scrollPageAnim
/**
* chapterOnDur: 0为当前页,1为下一页,-1为上一页
*/
@ -369,6 +385,20 @@ object ReadBook : CoroutineScope by MainScope() {
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 章节序号
@ -392,10 +422,9 @@ object ReadBook : CoroutineScope by MainScope() {
chapter,
it,
upContent,
resetPageOffset
) {
success?.invoke()
}
resetPageOffset,
success
)
} ?: download(
downloadScope,
chapter,
@ -456,9 +485,8 @@ object ReadBook : CoroutineScope by MainScope() {
chapter,
"加载正文失败\n$msg",
resetPageOffset = resetPageOffset,
) {
success?.invoke()
}
success = success
)
}
}
@ -505,48 +533,83 @@ object ReadBook : CoroutineScope by MainScope() {
0 -> {
curTextChapter?.cancelLayout()
curTextChapter = textChapter
if (resetPageOffset) {
callBack?.resetPageOffset()
}
callBack?.upMenuView()
curPageChanged()
callBack?.contentLoadFinish()
callBack?.onCurrentTextChapterChanged(textChapter, upContent)
textChapter.setProgressListener(object : LayoutProgressListener {
var available = false
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 -> {
prevTextChapter?.cancelLayout()
prevTextChapter = textChapter
if (upContent) {
textChapter.setProgressListener(object : LayoutProgressListener {
override fun onLayoutCompleted() {
callBack?.upContent(offset, resetPageOffset)
}
})
}
textChapter.setProgressListener(object : LayoutProgressListener {
override fun onLayoutCompleted() {
if (upContent) callBack?.upContent(offset, resetPageOffset)
success?.invoke()
}
override fun onLayoutException(e: Throwable) {
AppLog.put("ChapterProvider ERROR", e)
appCtx.toastOnUi("ChapterProvider ERROR:\n${e.stackTraceStr}")
}
})
}
1 -> {
nextTextChapter?.cancelLayout()
nextTextChapter = textChapter
if (upContent) {
textChapter.setProgressListener(object : LayoutProgressListener {
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
if (index > 1) {
return
}
callBack?.upContent(offset, resetPageOffset)
textChapter.setProgressListener(object : LayoutProgressListener {
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
if (index > 1) {
return
}
})
}
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)
}.onError {
AppLog.put("ChapterProvider ERROR", it)
appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
}.onSuccess {
success?.invoke()
}
}
@ -655,9 +718,10 @@ object ReadBook : CoroutineScope by MainScope() {
downloadedChapters.clear()
downloadFailChapters.clear()
ImageProvider.clear()
curTextChapter?.cancelLayout()
}
interface CallBack {
interface CallBack : LayoutProgressListener {
fun upMenuView()
fun loadChapterList(book: Book)
@ -675,10 +739,6 @@ object ReadBook : CoroutineScope by MainScope() {
fun upPageAnim()
fun notifyBookChanged()
fun onCurrentTextChapterChanged(textChapter: TextChapter, upContent: Boolean = true)
fun resetPageOffset()
}
}

View File

@ -1,6 +1,7 @@
package io.legado.app.model.webBook
import io.legado.app.R
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
@ -131,7 +132,10 @@ object BookContent {
//全文替换
val replaceRegex = contentRule.replaceRegex
if (!replaceRegex.isNullOrEmpty()) {
contentStr = contentStr.split(AppPattern.LFRegex)
.joinToString("\n") { it.trim { c -> c <= ' ' || c == ' ' } }
contentStr = analyzeRule.getString(replaceRegex, contentStr)
contentStr = contentStr.split(AppPattern.LFRegex).joinToString("\n") { "  $it" }
}
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")

View File

@ -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.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
@ -115,7 +114,6 @@ 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
@ -126,7 +124,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
/**
* 阅读界面
@ -1373,51 +1370,11 @@ class ReadBookActivity : BaseReadBookActivity(),
binding.readView.autoPager.resume()
}
override fun onCurrentTextChapterChanged(textChapter: TextChapter, upContent: Boolean) {
this.upContent = upContent
textChapter.setProgressListener(this)
}
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
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)
}
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) {
val previousResult = binding.searchMenu.previousSearchResult

View File

@ -109,21 +109,12 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else {
loadChapterList(book)
}
} else if (book.isLocal
&& LocalBook.getLastModified(book).getOrDefault(0L) > book.latestChapterTime
) {
} else if (book.isLocalModified()) {
loadChapterList(book)
} else if (isSameBook) {
if (ReadBook.curTextChapter != null) {
ReadBook.callBack?.upContent(resetPageOffset = false)
} else {
ReadBook.loadContent(resetPageOffset = true)
}
ReadBook.loadOrUpContent()
checkLocalBookFileExist(book)
} else {
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
}
ReadBook.loadContent(resetPageOffset = false)
checkLocalBookFileExist(book)
}

View File

@ -130,7 +130,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipHeaderLeft = tipValue
tvHeaderLeft.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2))
postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
}
}
llHeaderMiddle.setOnClickListener {
@ -139,7 +139,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipHeaderMiddle = tipValue
tvHeaderMiddle.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2))
postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
}
}
llHeaderRight.setOnClickListener {
@ -148,7 +148,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipHeaderRight = tipValue
tvHeaderRight.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2))
postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
}
}
llFooterLeft.setOnClickListener {
@ -157,7 +157,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipFooterLeft = tipValue
tvFooterLeft.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2))
postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
}
}
llFooterMiddle.setOnClickListener {
@ -166,7 +166,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipFooterMiddle = tipValue
tvFooterMiddle.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2))
postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
}
}
llFooterRight.setOnClickListener {
@ -175,7 +175,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipFooterRight = tipValue
tvFooterRight.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, arrayOf(2))
postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
}
}
llTipColor.setOnClickListener {

View File

@ -536,6 +536,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
}
private fun upSelectChars() {
if (!selectStart.isSelected() || !selectEnd.isSelected()) {
return
}
val last = if (callBack.isScroll) 2 else 0
val textPos = TextPos(0, 0, 0)
for (relativePos in 0..last) {

View File

@ -320,7 +320,7 @@ class PageView(context: Context) : FrameLayout(context) {
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
} else {
val pageSizeInt = pageSize - 1
val pageSizeInt = pageSize
val pageSize = if (pageSizeInt <= 0) "-" else "~$pageSizeInt"
tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")

View File

@ -669,10 +669,6 @@ class ReadView(context: Context, attrs: AttributeSet) :
upProgressThrottle.invoke()
}
fun resetPageOffset() {
curPage.resetPageOffset()
}
override val currentChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(0) else null

View File

@ -96,8 +96,7 @@ data class TextChapter(
}
fun isLastIndexCurrent(index: Int): Boolean {
// 未完成排版时,最后一页是正在排版中的,需要去掉
return index >= if (isCompleted) pages.size - 1 else pages.size - 2
return index >= pages.size - 1
}
/**
@ -105,6 +104,7 @@ data class TextChapter(
* @return 已读长度
*/
fun getReadLength(pageIndex: Int): Int {
if (pageIndex < 0) return 0
return pages[min(pageIndex, lastIndex)].lines.first().chapterPosition
/*
var length = 0
@ -212,18 +212,15 @@ data class TextChapter(
if (pageSize == 0) {
return -1
}
val size = if (isCompleted) pageSize else pageSize - 1
val bIndex = pages.fastBinarySearchBy(charIndex, 0, size) {
val bIndex = pages.fastBinarySearchBy(charIndex, 0, pageSize) {
it.lines.first().chapterPosition
}
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 (!isCompleted && index == pageSize - 1) {
val page = pages[index]
val line = page.lines.first()
val pageEndPos = line.chapterPosition + page.charSize
if (charIndex > pageEndPos) {
return -1
}
@ -287,14 +284,12 @@ data class TextChapter(
}
override fun onLayoutException(e: Throwable) {
isCompleted = true
listener?.onLayoutException(e)
listener = null
}
fun cancelLayout() {
layout?.cancel()
isCompleted = true
listener = null
}

View File

@ -69,4 +69,8 @@ data class TextPos(
isLast = false
}
fun isSelected(): Boolean {
return lineIndex >= 0 && columnIndex >= 0
}
}

View File

@ -67,6 +67,8 @@ class TextChapterLayout(
private val indentCharWidth = ChapterProvider.indentCharWidth
private val stringBuilder = StringBuilder()
private var pendingTextPage = TextPage()
private var isCompleted = false
private val job: Coroutine<*>
private val bookChapter inline get() = textChapter.chapter
@ -167,7 +169,6 @@ class TextChapterLayout(
val contents = bookContent.textList
var absStartX = paddingLeft
var durY = 0f
textPages.add(TextPage())
if (ReadBookConfig.titleMode != 2 || bookChapter.isVolume) {
//标题非隐藏
displayTitle.splitNotBlank("\n").forEach { text ->
@ -185,7 +186,7 @@ class TextChapterLayout(
durY = it.second
}
}
textPages.last().lines.last().isParagraphEnd = true
pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n")
durY += titleBottomSpacing
}
@ -244,7 +245,6 @@ class TextChapterLayout(
matcher.group(1)!!,
absStartX,
durY,
textPages,
contentPaintTextHeight,
stringBuilder,
book.getImageStyle()
@ -270,10 +270,11 @@ class TextChapterLayout(
}
}
}
textPages.last().lines.last().isParagraphEnd = true
pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n")
}
val textPage = textPages.last()
textPages.add(pendingTextPage)
val textPage = pendingTextPage
val endPadding = 20.dpToPx()
val durYPadding = durY + endPadding
if (textPage.height < durYPadding) {
@ -295,7 +296,6 @@ class TextChapterLayout(
src: String,
x: Int,
y: Float,
textPages: ArrayList<TextPage>,
textHeight: Float,
stringBuilder: StringBuilder,
imageStyle: String?,
@ -305,15 +305,16 @@ class TextChapterLayout(
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
if (size.width > 0 && size.height > 0) {
if (durY > visibleHeight) {
val textPage = textPages.last()
val textPage = pendingTextPage
if (textPage.height < durY) {
textPage.height = durY
}
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear()
textPages.add(textPage)
coroutineContext.ensureActive()
onPageCompleted()
textPages.add(TextPage())
pendingTextPage = TextPage()
durY = 0f
}
var height = size.height
@ -334,7 +335,11 @@ class TextChapterLayout(
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) {
//当前页面左列结束
textPage.leftLineSize = textPage.lineSize
@ -346,13 +351,10 @@ class TextChapterLayout(
}
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear()
textPages.add(textPage)
coroutineContext.ensureActive()
onPageCompleted()
textPages.add(TextPage())
}
// 双页的 durY 不正确,可能会小于实际高度
if (textPage.height < durY) {
textPage.height = durY
pendingTextPage = TextPage()
}
durY = 0f
}
@ -373,7 +375,7 @@ class TextChapterLayout(
)
calcTextLinePosition(textPages, textLine, stringBuilder.length)
stringBuilder.append(" ") // 确保翻页时索引计算正确
textPages.last().addLine(textLine)
pendingTextPage.addLine(textLine)
}
return absStartX to durY + textHeight * paragraphSpacing / 10f
}
@ -403,8 +405,8 @@ class TextChapterLayout(
}
var durY = when {
//标题y轴居中
emptyContent && textPages.size == 1 -> {
val textPage = textPages.last()
emptyContent && textPages.isEmpty() -> {
val textPage = pendingTextPage
if (textPage.lineSize == 0) {
val ty = (visibleHeight - layout.lineCount * textHeight) / 2
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
else -> y
@ -431,7 +433,10 @@ class TextChapterLayout(
for (lineIndex in 0 until layout.lineCount) {
val textLine = TextLine(isTitle = isTitle)
if (durY + textHeight > visibleHeight) {
val textPage = textPages.last()
val textPage = pendingTextPage
if (textPage.height < durY) {
textPage.height = durY
}
if (doublePage && absStartX < viewWidth / 2) {
//当前页面左列结束
textPage.leftLineSize = textPage.lineSize
@ -442,16 +447,14 @@ class TextChapterLayout(
textPage.leftLineSize = textPage.lineSize
}
textPage.text = stringBuilder.toString()
textPages.add(textPage)
coroutineContext.ensureActive()
onPageCompleted()
//新建页面
textPages.add(TextPage())
pendingTextPage = TextPage()
stringBuilder.clear()
absStartX = paddingLeft
}
if (textPage.height < durY) {
textPage.height = durY
}
durY = 0f
}
val lineStart = layout.getLineStart(lineIndex)
@ -514,7 +517,7 @@ class TextChapterLayout(
calcTextLinePosition(textPages, textLine, stringBuilder.length)
stringBuilder.append(lineText)
textLine.upTopBottom(durY, textHeight, fontMetrics)
val textPage = textPages.last()
val textPage = pendingTextPage
textPage.addLine(textLine)
durY += textHeight * lineSpacingExtra
if (textPage.height < durY) {
@ -530,8 +533,8 @@ class TextChapterLayout(
textLine: TextLine,
sbLength: Int
) {
val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 }
?: textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull { it.paragraphNum > 0 }
val lastLine = pendingTextPage.lines.lastOrNull { it.paragraphNum > 0 }
?: textPages.lastOrNull()?.lines?.lastOrNull { it.paragraphNum > 0 }
val paragraphNum = when {
lastLine == null -> 1
lastLine.isParagraphEnd -> lastLine.paragraphNum + 1
@ -539,7 +542,7 @@ class TextChapterLayout(
}
textLine.paragraphNum = paragraphNum
textLine.chapterPosition =
(textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull()?.run {
(textPages.lastOrNull()?.lines?.lastOrNull()?.run {
chapterPosition + charSize + if (isParagraphEnd) 1 else 0
} ?: 0) + sbLength
textLine.pagePosition = sbLength

View File

@ -6,7 +6,10 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.StaticLayout
import android.text.style.LineHeightSpan
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatTextView
@ -25,6 +28,13 @@ class BatteryView @JvmOverloads constructor(
private val outFrame = Rect()
private val polar = Rect()
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
set(value) {
field = value
@ -58,42 +68,62 @@ class BatteryView @JvmOverloads constructor(
fun setBattery(battery: Int, text: String? = null) {
this.battery = battery
if (text.isNullOrEmpty()) {
setText(battery.toString())
setText(getBatteryText(battery.toString()))
} 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) {
canvasRecorder.recordIfNeededThenDraw(canvas, width, height) {
super.onDraw(this)
if (!isBattery) return@recordIfNeededThenDraw
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
drawRect(outFrame, batteryPaint)
batteryPaint.style = Paint.Style.FILL
drawRect(polar, batteryPaint)
drawBattery(this)
}
}
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() {
super.invalidate()
kotlin.runCatching {