This commit is contained in:
Horis
2024-02-12 20:28:31 +08:00
parent b79f59acca
commit 5ac5f8d60b
23 changed files with 734 additions and 307 deletions
@@ -163,7 +163,22 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
.apply() .apply()
} }
companion object { companion object : SharedPreferences.OnSharedPreferenceChangeListener {
init {
prefs(appCtx).registerOnSharedPreferenceChangeListener(this)
}
var accentColor = accentColor()
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?
) {
when (key) {
ThemeStorePrefKeys.KEY_ACCENT_COLOR -> accentColor = accentColor()
}
}
fun editTheme(context: Context): ThemeStore { fun editTheme(context: Context): ThemeStore {
return ThemeStore(context) return ThemeStore(context)
@@ -212,4 +212,9 @@ object ImageProvider {
}.getOrDefault(errorBitmap) }.getOrDefault(errorBitmap)
} }
fun clear() {
bitmapLruCache.evictAll()
BitmapCache.clear()
}
} }
@@ -204,7 +204,7 @@ object ReadBook : CoroutineScope by MainScope() {
return hasPrevPage return hasPrevPage
} }
fun moveToNextChapter(upContent: Boolean): Boolean { fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
if (durChapterIndex < chapterSize - 1) { if (durChapterIndex < chapterSize - 1) {
durChapterPos = 0 durChapterPos = 0
durChapterIndex++ durChapterIndex++
@@ -213,11 +213,11 @@ object ReadBook : CoroutineScope by MainScope() {
nextTextChapter = null nextTextChapter = null
if (curTextChapter == null) { if (curTextChapter == null) {
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载") AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
callBack?.upContent() if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false) loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent) { } else if (upContent && upContentInPlace) {
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图") AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
callBack?.upContent() callBack?.upContent(resetPageOffset = false)
} }
loadContent(durChapterIndex.plus(1), upContent, false) loadContent(durChapterIndex.plus(1), upContent, false)
saveRead() saveRead()
@@ -233,7 +233,8 @@ object ReadBook : CoroutineScope by MainScope() {
fun moveToPrevChapter( fun moveToPrevChapter(
upContent: Boolean, upContent: Boolean,
toLast: Boolean = true toLast: Boolean = true,
upContentInPlace: Boolean = true
): Boolean { ): Boolean {
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
@@ -242,10 +243,10 @@ object ReadBook : CoroutineScope by MainScope() {
curTextChapter = prevTextChapter curTextChapter = prevTextChapter
prevTextChapter = null prevTextChapter = null
if (curTextChapter == null) { if (curTextChapter == null) {
callBack?.upContent() if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false) loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent) { } else if (upContent && upContentInPlace) {
callBack?.upContent() callBack?.upContent(resetPageOffset = false)
} }
loadContent(durChapterIndex.minus(1), upContent, false) loadContent(durChapterIndex.minus(1), upContent, false)
saveRead() saveRead()
@@ -267,6 +268,16 @@ object ReadBook : CoroutineScope by MainScope() {
} }
fun setPageIndex(index: Int) { fun setPageIndex(index: Int) {
val textChapter = curTextChapter
if (textChapter != null) {
val pageIndex = durPageIndex
if (index > pageIndex) {
textChapter.getPage(index - 2)?.recyclePictures()
}
if (index < pageIndex) {
textChapter.getPage(index + 2)?.recyclePictures()
}
}
durChapterPos = curTextChapter?.getReadLength(index) ?: index durChapterPos = curTextChapter?.getReadLength(index) ?: index
saveRead(true) saveRead(true)
curPageChanged(true) curPageChanged(true)
@@ -595,6 +606,7 @@ object ReadBook : CoroutineScope by MainScope() {
coroutineContext.cancelChildren() coroutineContext.cancelChildren()
downloadedChapters.clear() downloadedChapters.clear()
downloadFailChapters.clear() downloadFailChapters.clear()
ImageProvider.clear()
} }
interface CallBack { interface CallBack {
@@ -97,6 +97,7 @@ import io.legado.app.utils.LogUtils
import io.legado.app.utils.StartActivityContract import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.SyncedRenderer import io.legado.app.utils.SyncedRenderer
import io.legado.app.utils.applyOpenTint import io.legado.app.utils.applyOpenTint
import io.legado.app.utils.buildMainHandler
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import io.legado.app.utils.hexString import io.legado.app.utils.hexString
@@ -218,9 +219,10 @@ class ReadBookActivity : BaseReadBookActivity(),
private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } } private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } }
private var bookChanged = false private var bookChanged = false
private var pageChanged = false private var pageChanged = false
private var reloadContent = false
private val autoPageRenderer by lazy { SyncedRenderer { doAutoPage(it) } } private val autoPageRenderer by lazy { SyncedRenderer { doAutoPage(it) } }
private var autoPageScrollOffset = 0.0 private var autoPageScrollOffset = 0.0
private val handler by lazy { buildMainHandler() }
private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
//恢复跳转前进度对话框的交互结果 //恢复跳转前进度对话框的交互结果
private var confirmRestoreProcess: Boolean? = null private var confirmRestoreProcess: Boolean? = null
@@ -265,22 +267,14 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
viewModel.initData(intent) { viewModel.initData(intent) {
initDataSuccess() upMenu()
} }
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
viewModel.initData(intent ?: return) { viewModel.initData(intent ?: return) {
initDataSuccess()
}
}
private fun initDataSuccess() {
upMenu() upMenu()
if (reloadContent) {
reloadContent = false
ReadBook.loadContent(resetPageOffset = false)
} }
} }
@@ -1532,8 +1526,6 @@ class ReadBookActivity : BaseReadBookActivity(),
if (it) { // 更新内容排版布局 if (it) { // 更新内容排版布局
if (isInitFinish) { if (isInitFinish) {
ReadBook.loadContent(resetPageOffset = false) ReadBook.loadContent(resetPageOffset = false)
} else {
reloadContent = true
} }
} else { } else {
readView.upContent(resetPageOffset = false) readView.upContent(resetPageOffset = false)
@@ -1582,6 +1574,9 @@ class ReadBookActivity : BaseReadBookActivity(),
observeEvent<Boolean>(EventBus.UP_SEEK_BAR) { observeEvent<Boolean>(EventBus.UP_SEEK_BAR) {
binding.readMenu.upSeekBar() binding.readMenu.upSeekBar()
} }
observeEvent<String>(EventBus.RECREATE) {
binding.readView.invalidateTextPage()
}
} }
private fun upScreenTimeOut() { private fun upScreenTimeOut() {
@@ -1594,17 +1589,16 @@ class ReadBookActivity : BaseReadBookActivity(),
* 重置黑屏时间 * 重置黑屏时间
*/ */
override fun screenOffTimerStart() { override fun screenOffTimerStart() {
keepScreenJon?.cancel() handler.post {
keepScreenJon = lifecycleScope.launch {
if (screenTimeOut < 0) { if (screenTimeOut < 0) {
keepScreenOn(true) keepScreenOn(true)
return@launch return@post
} }
val t = screenTimeOut - sysScreenOffTime val t = screenTimeOut - sysScreenOffTime
if (t > 0) { if (t > 0) {
keepScreenOn(true) keepScreenOn(true)
delay(screenTimeOut) handler.removeCallbacks(screenOffRunnable)
keepScreenOn(false) handler.postDelayed(screenOffRunnable, screenTimeOut)
} else { } else {
keepScreenOn(false) keepScreenOn(false)
} }
@@ -3,22 +3,16 @@ package io.legado.app.ui.book.read.page
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Picture
import android.graphics.RectF import android.graphics.RectF
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.core.graphics.record import androidx.core.graphics.withTranslation
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PageAnim import io.legado.app.constant.PageAnim
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.help.book.isImage
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.lib.theme.accentColor
import io.legado.app.model.ImageProvider
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.entities.TextLine 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.TextPage
@@ -31,8 +25,8 @@ 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.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.ui.widget.dialog.PhotoDialog import io.legado.app.ui.widget.dialog.PhotoDialog
import io.legado.app.utils.PictureMirror
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.dpToPx
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
@@ -44,8 +38,7 @@ import kotlin.math.min
*/ */
class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true) var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true)
var upView: ((TextPage) -> Unit)? = null val selectedPaint by lazy {
private val selectedPaint by lazy {
Paint().apply { Paint().apply {
color = context.getCompatColor(R.color.btn_bg_press_2) color = context.getCompatColor(R.color.btn_bg_press_2)
style = Paint.Style.FILL style = Paint.Style.FILL
@@ -65,13 +58,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
//滚动参数 //滚动参数
private val pageFactory: TextPageFactory get() = callBack.pageFactory private val pageFactory: TextPageFactory get() = callBack.pageFactory
private var pageOffset = 0 private var pageOffset = 0
private lateinit var picture: Picture private val pictureMirror = PictureMirror()
private var pictureIsDirty = true
private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
private val isNoAnim get() = ReadBook.pageAnim() == PageAnim.noAnim private val isNoAnim get() = ReadBook.pageAnim() == PageAnim.noAnim
//绘制图片的paint //绘制图片的paint
private val imagePaint by lazy { val imagePaint by lazy {
Paint().apply { Paint().apply {
isAntiAlias = AppConfig.useAntiAlias isAntiAlias = AppConfig.useAntiAlias
} }
@@ -79,9 +70,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
init { init {
callBack = activity as CallBack callBack = activity as CallBack
if (atLeastApi23) {
picture = Picture()
}
} }
/** /**
@@ -108,7 +96,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
if (!isMainView) return if (!isMainView) return
ChapterProvider.upViewSize(w, h) ChapterProvider.upViewSize(w, h, oldw, oldh)
upVisibleRect() upVisibleRect()
textPage.format() textPage.format()
} }
@@ -119,14 +107,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
canvas.translate(0f, scrollY.toFloat()) canvas.translate(0f, scrollY.toFloat())
} }
canvas.clipRect(visibleRect) canvas.clipRect(visibleRect)
if (atLeastApi23 && !callBack.isScroll && !isNoAnim) { if (!callBack.isScroll && !isNoAnim) {
if (pictureIsDirty) { pictureMirror.draw(canvas, width, height) {
pictureIsDirty = false
picture.record(width, height) {
drawPage(this) drawPage(this)
} }
}
canvas.drawPicture(picture)
} else { } else {
drawPage(canvas) drawPage(canvas)
} }
@@ -137,147 +121,34 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
*/ */
private fun drawPage(canvas: Canvas) { private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0) var relativeOffset = relativeOffset(0)
var lines = textPage.lines val view = this
for (i in lines.indices) { canvas.withTranslation(0f, relativeOffset) {
drawLine(canvas, textPage, lines[i], relativeOffset) textPage.draw(view, this)
} }
if (!callBack.isScroll) return if (!callBack.isScroll) return
//滚动翻页 //滚动翻页
if (!pageFactory.hasNext()) return if (!pageFactory.hasNext()) return
val textPage1 = relativePage(1) val textPage1 = relativePage(1)
relativeOffset = relativeOffset(1) relativeOffset = relativeOffset(1)
lines = textPage1.lines canvas.withTranslation(0f, relativeOffset) {
for (i in lines.indices) { textPage1.draw(view, this)
drawLine(canvas, textPage1, lines[i], relativeOffset)
} }
if (!pageFactory.hasNextPlus()) return if (!pageFactory.hasNextPlus()) return
relativeOffset = relativeOffset(2) relativeOffset = relativeOffset(2)
if (relativeOffset < ChapterProvider.visibleHeight) { if (relativeOffset < ChapterProvider.visibleHeight) {
val textPage2 = relativePage(2) val textPage2 = relativePage(2)
lines = textPage2.lines canvas.withTranslation(0f, relativeOffset) {
for (i in lines.indices) { textPage2.draw(view, this)
drawLine(canvas, textPage2, lines[i], relativeOffset)
} }
} }
} }
/**
* 绘制页面
*/
private fun drawLine(
canvas: Canvas,
textPage: TextPage,
textLine: TextLine,
relativeOffset: Float,
) {
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(canvas, textPage, textLine, lineTop, lineBase, lineBottom)
if (ReadBookConfig.underline && ReadBook.book?.isImage != true) {
drawUnderline(canvas, textLine, relativeOffset)
}
}
/**
* 绘制下划线
*/
private fun drawUnderline(canvas: Canvas, textLine: TextLine, relativeOffset: Float) {
val lineY = relativeOffset + textLine.lineBottom - 1.dpToPx()
canvas.drawLine(
textLine.lineStart + textLine.indentWidth,
lineY,
textLine.lineEnd,
lineY,
ChapterProvider.contentPaint
)
}
/**
* 绘制文字
*/
private fun drawChars(
canvas: Canvas,
textPage: TextPage,
textLine: TextLine,
lineTop: Float,
lineBase: Float,
lineBottom: Float,
) {
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
val textColor = if (textLine.isReadAloud) context.accentColor else ReadBookConfig.textColor
val columns = textLine.columns
for (i in columns.indices) {
when (val column = columns[i]) {
is TextColumn -> {
if (column.isSearchResult) {
textPaint.color = context.accentColor
} else if (textPaint.color != textColor) {
textPaint.color = textColor
}
canvas.drawText(column.charData, column.start, lineBase, textPaint)
if (column.selected) {
canvas.drawRect(
column.start,
lineTop,
column.end,
lineBottom,
selectedPaint
)
}
}
is ImageColumn -> drawImage(canvas, textPage, textLine, column, lineTop, lineBottom)
is ReviewColumn -> column.drawToCanvas(canvas, lineBase, textPaint.textSize)
}
}
}
/**
* 绘制图片
*/
@Suppress("UNUSED_PARAMETER")
private fun drawImage(
canvas: Canvas,
textPage: TextPage,
textLine: TextLine,
column: ImageColumn,
lineTop: Float,
lineBottom: Float
) {
val book = ReadBook.book ?: return
val bitmap = ImageProvider.getImage(
book,
column.src,
(column.end - column.start).toInt(),
(lineBottom - lineTop).toInt()
) {
invalidate()
} ?: return
val rectF = if (textLine.isImage) {
RectF(column.start, lineTop, column.end, lineBottom)
} else {
/*以宽度为基准保持图片的原始比例叠加,当div为负数时,允许高度比字符更高*/
val h = (column.end - column.start) / bitmap.width * bitmap.height
val div = (lineBottom - lineTop - h) / 2
RectF(column.start, lineTop + div, column.end, lineBottom - div)
}
kotlin.runCatching {
canvas.drawBitmap(bitmap, null, rectF, imagePaint)
}.onFailure { e ->
context.toastOnUi(e.localizedMessage)
}
}
/** /**
* 滚动事件 * 滚动事件
* pageOffset 向上滚动 减小 向下滚动 增大
* pageOffset 范围 0 ~ -textPage.height 大于0为上一页,小于-textPage.height为下一页
* 以内容显示区域顶端为界,pageOffset的绝对值为textPage上方的高度
* pageOffset + textPage.height 为 textPage 下方的高度
*/ */
fun scroll(mOffset: Int) { fun scroll(mOffset: Int) {
if (mOffset == 0) return if (mOffset == 0) return
@@ -294,28 +165,25 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
val offset = (ChapterProvider.visibleHeight - textPage.height).toInt() val offset = (ChapterProvider.visibleHeight - textPage.height).toInt()
pageOffset = min(0, offset) pageOffset = min(0, offset)
} else if (pageOffset > 0) { } else if (pageOffset > 0) {
pageFactory.moveToPrev(true) if (pageFactory.moveToPrev(true)) {
textPage = pageFactory.curPage
pageOffset -= textPage.height.toInt() pageOffset -= textPage.height.toInt()
upView?.invoke(textPage) } else {
contentDescription = textPage.text pageOffset = 0
}
} else if (pageOffset < -textPage.height) { } else if (pageOffset < -textPage.height) {
pageOffset += textPage.height.toInt() val height = textPage.height
pageFactory.moveToNext(true) if (pageFactory.moveToNext(upContent = true)) {
textPage = pageFactory.curPage pageOffset += height.toInt()
upView?.invoke(textPage) } else {
contentDescription = textPage.text pageOffset = -height.toInt()
}
} }
invalidate() invalidate()
} }
override fun invalidate() { override fun invalidate() {
super.invalidate() super.invalidate()
invalidatePicture() pictureMirror.invalidate()
}
private fun invalidatePicture() {
pictureIsDirty = true
} }
/** /**
@@ -517,9 +385,21 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) return if (relativeOffset >= ChapterProvider.visibleHeight) return
} }
val textPage = relativePage(relativePos) val textPage = relativePage(relativePos)
for ((lineIndex, textLine) in textPage.lines.withIndex()) { for (lineIndex in textPage.lines.indices) {
val textLine = textPage.getLine(lineIndex)
if (textLine.isTouchY(y, relativeOffset)) { if (textLine.isTouchY(y, relativeOffset)) {
for ((charIndex, textColumn) in textLine.columns.withIndex()) { if (textPage.doublePage) {
val halfWidth = width / 2
if (textLine.isLeftLine && x > halfWidth) {
continue
}
if (!textLine.isLeftLine && x < halfWidth) {
continue
}
}
val columns = textLine.columns
for (charIndex in columns.indices) {
val textColumn = columns[charIndex]
if (textColumn.isTouch(x)) { if (textColumn.isTouch(x)) {
touched.invoke( touched.invoke(
relativeOffset, relativeOffset,
@@ -529,12 +409,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
return return
} }
} }
val isLast = textLine.columns.first().start < x val isLast = columns.first().start < x
val (charIndex, textColumn) = if (isLast) { val charIndex = if (isLast) columns.lastIndex else 0
textLine.columns.withIndex().last() val textColumn = if (isLast) columns.last() else columns.first()
} else {
textLine.columns.withIndex().first()
}
touched.invoke( touched.invoke(
relativeOffset, relativeOffset,
TextPos(relativePos, lineIndex, charIndex, false, isLast), TextPos(relativePos, lineIndex, charIndex, false, isLast),
@@ -557,7 +434,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break if (relativeOffset >= ChapterProvider.visibleHeight) break
} }
val textPage = relativePage(relativePos) val textPage = relativePage(relativePos)
for (textLine in textPage.lines) { val lines = textPage.lines
for (i in lines.indices) {
val textLine = lines[i]
if (textLine.isVisible(relativeOffset)) { if (textLine.isVisible(relativeOffset)) {
val visibleLine = textLine.copy().apply { val visibleLine = textLine.copy().apply {
lineTop += relativeOffset lineTop += relativeOffset
@@ -580,7 +459,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break if (relativeOffset >= ChapterProvider.visibleHeight) break
} }
val textPage = relativePage(relativePos) val textPage = relativePage(relativePos)
for (textLine in textPage.lines) { val lines = textPage.lines
for (i in lines.indices) {
val textLine = lines[i]
if (textLine.isVisible(relativeOffset)) { if (textLine.isVisible(relativeOffset)) {
val visibleLine = textLine.copy().apply { val visibleLine = textLine.copy().apply {
lineTop += relativeOffset lineTop += relativeOffset
@@ -57,9 +57,6 @@ class PageView(context: Context) : FrameLayout(context) {
if (!isInEditMode) { if (!isInEditMode) {
upStyle() upStyle()
} }
binding.contentTextView.upView = {
setProgress(it)
}
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@@ -21,16 +21,25 @@ import io.legado.app.model.ReadAloud
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.ContentEditDialog import io.legado.app.ui.book.read.ContentEditDialog
import io.legado.app.ui.book.read.page.api.DataSource import io.legado.app.ui.book.read.page.api.DataSource
import io.legado.app.ui.book.read.page.delegate.* import io.legado.app.ui.book.read.page.delegate.CoverPageDelegate
import io.legado.app.ui.book.read.page.delegate.NoAnimPageDelegate
import io.legado.app.ui.book.read.page.delegate.PageDelegate
import io.legado.app.ui.book.read.page.delegate.ScrollPageDelegate
import io.legado.app.ui.book.read.page.delegate.SimulationPageDelegate
import io.legado.app.ui.book.read.page.delegate.SlidePageDelegate
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.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.TextPageFactory import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.utils.* import io.legado.app.utils.activity
import io.legado.app.utils.invisible
import io.legado.app.utils.screenshot
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.visible
import java.text.BreakIterator import java.text.BreakIterator
import java.util.* import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
/** /**
@@ -49,7 +58,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
field = value field = value
upContent() upContent()
} }
var isScroll = false override var isScroll = false
val prevPage by lazy { PageView(context) } val prevPage by lazy { PageView(context) }
val curPage by lazy { PageView(context) } val curPage by lazy { PageView(context) }
val nextPage by lazy { PageView(context) } val nextPage by lazy { PageView(context) }
@@ -529,7 +538,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
* @param resetPageOffset 滚动阅读是是否重置位置 * @param resetPageOffset 滚动阅读是是否重置位置
*/ */
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
post {
curPage.setContentDescription(pageFactory.curPage.text) curPage.setContentDescription(pageFactory.curPage.text)
}
if (isScroll && !callBack.isAutoPage) { if (isScroll && !callBack.isAutoPage) {
curPage.setContent(pageFactory.curPage, resetPageOffset) curPage.setContent(pageFactory.curPage, resetPageOffset)
} else { } else {
@@ -643,6 +654,15 @@ class ReadView(context: Context, attrs: AttributeSet) :
nextPageBitmap = null nextPageBitmap = null
} }
fun invalidateTextPage() {
pageFactory.run {
prevPage.invalidateAll()
curPage.invalidateAll()
nextPage.invalidateAll()
nextPlusPage.invalidateAll()
}
}
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
@@ -13,6 +13,8 @@ interface DataSource {
val prevChapter: TextChapter? val prevChapter: TextChapter?
val isScroll: Boolean
fun hasNextChapter(): Boolean fun hasNextChapter(): Boolean
fun hasPrevChapter(): Boolean fun hasPrevChapter(): Boolean
@@ -186,17 +186,19 @@ data class TextChapter(
*/ */
fun getPageIndexByCharIndex(charIndex: Int): Int { fun getPageIndexByCharIndex(charIndex: Int): Int {
var length = 0 var length = 0
pages.forEach { for (i in pages.indices) {
length += it.charSize val page = pages[i]
length += page.charSize
if (length > charIndex) { if (length > charIndex) {
return it.index return page.index
} }
} }
return pages.lastIndex return pages.lastIndex
} }
fun clearSearchResult() { fun clearSearchResult() {
pages.forEach { page -> for (i in pages.indices) {
val page = pages[i]
page.searchResult.forEach { page.searchResult.forEach {
it.selected = false it.selected = false
it.isSearchResult = false it.isSearchResult = false
@@ -1,9 +1,17 @@
package io.legado.app.ui.book.read.page.entities package io.legado.app.ui.book.read.page.entities
import android.graphics.Canvas
import android.graphics.Paint.FontMetrics import android.graphics.Paint.FontMetrics
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.help.book.isImage
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextPage.Companion.emptyTextPage
import io.legado.app.ui.book.read.page.entities.column.BaseColumn import io.legado.app.ui.book.read.page.entities.column.BaseColumn
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.PictureMirror
import io.legado.app.utils.dpToPx
/** /**
* 行信息 * 行信息
@@ -22,8 +30,7 @@ data class TextLine(
var pagePosition: Int = 0, var pagePosition: Int = 0,
val isTitle: Boolean = false, val isTitle: Boolean = false,
var isParagraphEnd: Boolean = false, var isParagraphEnd: Boolean = false,
var isReadAloud: Boolean = false, var isImage: Boolean = false,
var isImage: Boolean = false
) { ) {
val columns: List<BaseColumn> get() = textColumns val columns: List<BaseColumn> get() = textColumns
@@ -31,8 +38,20 @@ data class TextLine(
val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f
val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
val height: Float inline get() = lineBottom - lineTop
val pictureMirror: PictureMirror = PictureMirror()
var isReadAloud: Boolean = false
set(value) {
if (field != value) {
invalidate()
}
field = value
}
var textPage: TextPage = emptyTextPage
var isLeftLine = true
fun addColumn(column: BaseColumn) { fun addColumn(column: BaseColumn) {
column.textLine = this
textColumns.add(column) textColumns.add(column)
} }
@@ -102,4 +121,50 @@ data class TextLine(
return visible return visible
} }
fun draw(view: ContentTextView, canvas: Canvas) {
pictureMirror.draw(canvas, view.width, height.toInt()) {
drawTextLine(view, this)
}
}
private fun drawTextLine(view: ContentTextView, canvas: Canvas) {
for (i in columns.indices) {
columns[i].draw(view, canvas)
}
if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) {
drawUnderline(canvas)
}
}
/**
* 绘制下划线
*/
private fun drawUnderline(canvas: Canvas) {
val lineY = height - 1.dpToPx()
canvas.drawLine(
lineStart + indentWidth,
lineY,
lineEnd,
lineY,
ChapterProvider.contentPaint
)
}
fun invalidate() {
invalidateSelf()
textPage.invalidate()
}
fun invalidateSelf() {
pictureMirror.invalidate()
}
fun recyclePicture() {
pictureMirror.recycle()
}
companion object {
val emptyTextLine = TextLine()
}
} }
@@ -1,13 +1,17 @@
package io.legado.app.ui.book.read.page.entities package io.legado.app.ui.book.read.page.entities
import android.graphics.Canvas
import android.text.Layout import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import androidx.annotation.Keep import androidx.annotation.Keep
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.model.ReadBook
import io.legado.app.ui.book.read.page.ContentTextView
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.PictureMirror
import splitties.init.appCtx import splitties.init.appCtx
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.min import kotlin.math.min
@@ -31,6 +35,7 @@ data class TextPage(
companion object { companion object {
val readProgressFormatter = DecimalFormat("0.0%") val readProgressFormatter = DecimalFormat("0.0%")
val emptyTextPage = TextPage()
} }
val lines: List<TextLine> get() = textLines val lines: List<TextLine> get() = textLines
@@ -38,12 +43,15 @@ data class TextPage(
val charSize: Int get() = text.length.coerceAtLeast(1) val charSize: Int get() = text.length.coerceAtLeast(1)
val searchResult = hashSetOf<TextColumn>() val searchResult = hashSetOf<TextColumn>()
var isMsgPage: Boolean = false var isMsgPage: Boolean = false
var pictureMirror: PictureMirror = PictureMirror()
var doublePage = false
val paragraphs by lazy { val paragraphs by lazy {
paragraphsInternal paragraphsInternal
} }
val paragraphsInternal: ArrayList<TextParagraph> get() { val paragraphsInternal: ArrayList<TextParagraph>
get() {
val paragraphs = arrayListOf<TextParagraph>() val paragraphs = arrayListOf<TextParagraph>()
val lines = textLines.filter { it.paragraphNum > 0 } val lines = textLines.filter { it.paragraphNum > 0 }
val offset = lines.first().paragraphNum - 1 val offset = lines.first().paragraphNum - 1
@@ -57,6 +65,7 @@ data class TextPage(
} }
fun addLine(line: TextLine) { fun addLine(line: TextLine) {
line.textPage = this
textLines.add(line) textLines.add(line)
} }
@@ -147,7 +156,7 @@ data class TextPage(
) )
x = x1 x = x1
} }
textLines.add(textLine) addLine(textLine)
} }
height = ChapterProvider.visibleHeight.toFloat() height = ChapterProvider.visibleHeight.toFloat()
} }
@@ -158,8 +167,8 @@ data class TextPage(
* 移除朗读标志 * 移除朗读标志
*/ */
fun removePageAloudSpan(): TextPage { fun removePageAloudSpan(): TextPage {
textLines.forEach { textLine -> for (i in textLines.indices) {
textLine.isReadAloud = false textLines[i].isReadAloud = false
} }
return this return this
} }
@@ -254,6 +263,39 @@ data class TextPage(
return null return null
} }
fun draw(view: ContentTextView, canvas: Canvas) {
pictureMirror.draw(canvas, view.width, height.toInt()) {
drawPage(view, this)
}
}
private fun drawPage(view: ContentTextView, canvas: Canvas) {
for (i in lines.indices) {
val line = lines[i]
canvas.withTranslation(0f, line.lineTop) {
line.draw(view, this)
}
}
}
fun invalidate() {
pictureMirror.invalidate()
}
fun invalidateAll() {
for (i in lines.indices) {
lines[i].invalidateSelf()
}
invalidate()
}
fun recyclePictures() {
pictureMirror.recycle()
for (i in lines.indices) {
lines[i].recyclePicture()
}
}
fun hasImageOrEmpty(): Boolean { fun hasImageOrEmpty(): Boolean {
return textLines.any { it.isImage } || textLines.isEmpty() return textLines.any { it.isImage } || textLines.isEmpty()
} }
@@ -1,11 +1,18 @@
package io.legado.app.ui.book.read.page.entities.column package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
/** /**
* 列基类 * 列基类
*/ */
interface BaseColumn { interface BaseColumn {
var start: Float var start: Float
var end: Float var end: Float
var textLine: TextLine
fun draw(view: ContentTextView, canvas: Canvas)
fun isTouch(x: Float): Boolean { fun isTouch(x: Float): Boolean {
return x > start && x < end return x > start && x < end
@@ -1,6 +1,10 @@
package io.legado.app.ui.book.read.page.entities.column package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
/** /**
@@ -9,5 +13,10 @@ import androidx.annotation.Keep
@Keep @Keep
data class ButtonColumn( data class ButtonColumn(
override var start: Float, override var start: Float,
override var end: Float override var end: Float,
) : BaseColumn ) : BaseColumn {
override var textLine: TextLine = emptyTextLine
override fun draw(view: ContentTextView, canvas: Canvas) {
}
}
@@ -1,6 +1,15 @@
package io.legado.app.ui.book.read.page.entities.column package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import android.graphics.RectF
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.model.ImageProvider
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
/** /**
* 图片列 * 图片列
@@ -10,4 +19,37 @@ data class ImageColumn(
override var start: Float, override var start: Float,
override var end: Float, override var end: Float,
var src: String var src: String
) : BaseColumn ) : BaseColumn {
override var textLine: TextLine = emptyTextLine
override fun draw(view: ContentTextView, canvas: Canvas) {
val book = ReadBook.book ?: return
val height = textLine.height
val bitmap = ImageProvider.getImage(
book,
src,
(end - start).toInt(),
height.toInt()
) {
textLine.invalidate()
view.invalidate()
} ?: return
val rectF = if (textLine.isImage) {
RectF(start, 0f, end, height)
} else {
/*以宽度为基准保持图片的原始比例叠加,当div为负数时,允许高度比字符更高*/
val h = (end - start) / bitmap.width * bitmap.height
val div = (height - h) / 2
RectF(start, div, end, height - div)
}
kotlin.runCatching {
canvas.drawBitmap(bitmap, null, rectF, view.imagePaint)
}.onFailure { e ->
appCtx.toastOnUi(e.localizedMessage)
}
}
}
@@ -4,6 +4,9 @@ import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Path import android.graphics.Path
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
/** /**
@@ -16,6 +19,16 @@ data class ReviewColumn(
val count: Int = 0 val count: Int = 0
) : BaseColumn { ) : BaseColumn {
override var textLine: TextLine = emptyTextLine
override fun draw(view: ContentTextView, canvas: Canvas) {
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
drawToCanvas(canvas, textLine.lineBase, textPaint.textSize)
}
val countText by lazy { val countText by lazy {
if (count > 999) { if (count > 999) {
return@lazy "999" return@lazy "999"
@@ -1,6 +1,13 @@
package io.legado.app.ui.book.read.page.entities.column package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
import io.legado.app.ui.book.read.page.provider.ChapterProvider
/** /**
* 文字列 * 文字列
@@ -10,6 +17,43 @@ data class TextColumn(
override var start: Float, override var start: Float,
override var end: Float, override var end: Float,
val charData: String, val charData: String,
var selected: Boolean = false, ) : BaseColumn {
override var textLine: TextLine = emptyTextLine
var selected: Boolean = false
set(value) {
if (field != value) {
textLine.invalidate()
}
field = value
}
var isSearchResult: Boolean = false var isSearchResult: Boolean = false
) : BaseColumn set(value) {
if (field != value) {
textLine.invalidate()
}
field = value
}
override fun draw(view: ContentTextView, canvas: Canvas) {
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
val textColor = if (textLine.isReadAloud || isSearchResult) {
ThemeStore.accentColor
} else {
ReadBookConfig.textColor
}
if (textPaint.color != textColor) {
textPaint.color = textColor
}
canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
if (selected) {
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
}
}
}
@@ -1,6 +1,5 @@
package io.legado.app.ui.book.read.page.provider package io.legado.app.ui.book.read.page.provider
import android.graphics.Paint
import android.graphics.Paint.FontMetrics import android.graphics.Paint.FontMetrics
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
@@ -25,13 +24,13 @@ 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.ui.book.read.page.entities.column.TextColumn
import io.legado.app.utils.RealPathUtil import io.legado.app.utils.RealPathUtil
import io.legado.app.utils.dpToPx import io.legado.app.utils.dpToPx
import io.legado.app.utils.fastSum
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.isPad import io.legado.app.utils.isPad
import io.legado.app.utils.postEvent 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 io.legado.app.utils.toStringArray
import splitties.init.appCtx import splitties.init.appCtx
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
@@ -47,6 +46,8 @@ object ChapterProvider {
//用于评论按钮的替换 //用于评论按钮的替换
private const val reviewChar = "▨" private const val reviewChar = "▨"
private const val indentChar = " "
@JvmStatic @JvmStatic
var viewWidth = 0 var viewWidth = 0
private set private set
@@ -104,7 +105,7 @@ object ChapterProvider {
private var indentCharWidth = 0f private var indentCharWidth = 0f
@JvmStatic @JvmStatic
private var titlePaintTextHeight = 0f var titlePaintTextHeight = 0f
@JvmStatic @JvmStatic
var contentPaintTextHeight = 0f var contentPaintTextHeight = 0f
@@ -132,6 +133,9 @@ object ChapterProvider {
var doublePage = false var doublePage = false
private set private set
private val titleMeasureHelper = TextMeasure(titlePaint)
private val contentMeasureHelper = TextMeasure(contentPaint)
init { init {
upStyle() upStyle()
} }
@@ -161,6 +165,7 @@ object ChapterProvider {
textPages, textPages,
stringBuilder, stringBuilder,
titlePaint, titlePaint,
titleMeasureHelper,
titlePaintTextHeight, titlePaintTextHeight,
titlePaintFontMetrics, titlePaintFontMetrics,
isTitle = true, isTitle = true,
@@ -196,6 +201,7 @@ object ChapterProvider {
textPages, textPages,
stringBuilder, stringBuilder,
contentPaint, contentPaint,
contentMeasureHelper,
contentPaintTextHeight, contentPaintTextHeight,
contentPaintFontMetrics, contentPaintFontMetrics,
srcList = srcList srcList = srcList
@@ -217,6 +223,7 @@ object ChapterProvider {
textPages, textPages,
stringBuilder, stringBuilder,
contentPaint, contentPaint,
contentMeasureHelper,
contentPaintTextHeight, contentPaintTextHeight,
contentPaintFontMetrics contentPaintFontMetrics
).let { ).let {
@@ -224,10 +231,19 @@ object ChapterProvider {
durY = it.second durY = it.second
} }
} }
durY = setTypeImage( setTypeImage(
book, matcher.group(1)!!, book,
absStartX, durY, textPages, stringBuilder, book.getImageStyle() matcher.group(1)!!,
) absStartX,
durY,
textPages,
contentPaintTextHeight,
stringBuilder,
book.getImageStyle()
).let {
absStartX = it.first
durY = it.second
}
start = matcher.end() start = matcher.end()
} }
if (start < content.length) { if (start < content.length) {
@@ -239,6 +255,7 @@ object ChapterProvider {
textPages, textPages,
stringBuilder, stringBuilder,
contentPaint, contentPaint,
contentMeasureHelper,
contentPaintTextHeight, contentPaintTextHeight,
contentPaintFontMetrics contentPaintFontMetrics
).let { ).let {
@@ -249,14 +266,22 @@ object ChapterProvider {
} }
} }
} }
textPages.last().height = durY + 20.dpToPx() val textPage = textPages.last()
textPages.last().text = stringBuilder.toString() val endPadding = 20.dpToPx()
val durYPadding = durY + endPadding
if (textPage.height < durYPadding) {
textPage.height = durYPadding
} else {
textPage.height += endPadding
}
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
item.doublePage = doublePage
item.upLinesPosition() item.upLinesPosition()
} }
@@ -280,15 +305,19 @@ object ChapterProvider {
x: Int, x: Int,
y: Float, y: Float,
textPages: ArrayList<TextPage>, textPages: ArrayList<TextPage>,
textHeight: Float,
stringBuilder: StringBuilder, stringBuilder: StringBuilder,
imageStyle: String?, imageStyle: String?,
): Float { ): Pair<Int, Float> {
var absStartX = x
var durY = y var durY = y
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 = textPages.last()
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()) textPages.add(TextPage())
@@ -313,10 +342,23 @@ object ChapterProvider {
} }
if (durY + height > visibleHeight) { if (durY + height > visibleHeight) {
val textPage = textPages.last() val textPage = textPages.last()
textPage.height = durY 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 { "本页无文字内容" } textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear() stringBuilder.clear()
textPages.add(TextPage()) textPages.add(TextPage())
}
// 双页的 durY 不正确,可能会小于实际高度
if (textPage.height < durY) {
textPage.height = durY
}
durY = 0f durY = 0f
} }
} }
@@ -336,7 +378,7 @@ object ChapterProvider {
) )
textPages.last().addLine(textLine) textPages.last().addLine(textLine)
} }
return durY + paragraphSpacing / 10f return absStartX to durY + textHeight * paragraphSpacing / 10f
} }
/** /**
@@ -350,6 +392,7 @@ object ChapterProvider {
textPages: ArrayList<TextPage>, textPages: ArrayList<TextPage>,
stringBuilder: StringBuilder, stringBuilder: StringBuilder,
textPaint: TextPaint, textPaint: TextPaint,
measureHelper: TextMeasure,
textHeight: Float, textHeight: Float,
fontMetrics: FontMetrics, fontMetrics: FontMetrics,
isTitle: Boolean = false, isTitle: Boolean = false,
@@ -358,14 +401,11 @@ object ChapterProvider {
srcList: LinkedList<String>? = null srcList: LinkedList<String>? = null
): Pair<Int, Float> { ): Pair<Int, Float> {
var absStartX = x var absStartX = x
val widthsArray = FloatArray(text.length)
val layout = if (ReadBookConfig.useZhLayout) { val layout = if (ReadBookConfig.useZhLayout) {
ZhLayout(text, textPaint, visibleWidth, widthsArray) ZhLayout(text, textPaint, visibleWidth, measureHelper)
} else { } else {
textPaint.getTextWidths(text, widthsArray)
StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true) StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
} }
val widthsList = widthsArray.asList()
var durY = when { var durY = when {
//标题y轴居中 //标题y轴居中
emptyContent && textPages.size == 1 -> { emptyContent && textPages.size == 1 -> {
@@ -398,6 +438,7 @@ object ChapterProvider {
if (durY + textHeight > visibleHeight) { if (durY + textHeight > visibleHeight) {
val textPage = textPages.last() val textPage = textPages.last()
if (doublePage && absStartX < viewWidth / 2) { if (doublePage && absStartX < viewWidth / 2) {
//当前页面左列结束
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
absStartX = viewWidth / 2 + paddingLeft absStartX = viewWidth / 2 + paddingLeft
} else { } else {
@@ -406,32 +447,34 @@ object ChapterProvider {
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
} }
textPage.text = stringBuilder.toString() textPage.text = stringBuilder.toString()
textPage.height = durY
//新建页面 //新建页面
textPages.add(TextPage()) textPages.add(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)
val lineEnd = layout.getLineEnd(lineIndex) val lineEnd = layout.getLineEnd(lineIndex)
val words = text.substring(lineStart, lineEnd) val lineText = text.substring(lineStart, lineEnd)
val textWidths = widthsList.subList(lineStart, lineEnd) val (words, widths) = measureHelper.measureTextSplit(lineText)
val desiredWidth = textWidths.sum() val desiredWidth = widths.fastSum()
when { when {
lineIndex == 0 && layout.lineCount > 1 && !isTitle -> { lineIndex == 0 && layout.lineCount > 1 && !isTitle -> {
//第一行 非标题 //第一行 非标题
textLine.text = words textLine.text = lineText
addCharsToLineFirst( addCharsToLineFirst(
book, absStartX, textLine, words, book, absStartX, textLine, words,
textPaint, desiredWidth, textWidths, srcList desiredWidth, widths, srcList
) )
} }
lineIndex == layout.lineCount - 1 -> { lineIndex == layout.lineCount - 1 -> {
//最后一行 //最后一行
textLine.text = words textLine.text = lineText
textLine.isParagraphEnd = true textLine.isParagraphEnd = true
//标题x轴居中 //标题x轴居中
val startX = if ( val startX = if (
@@ -443,8 +486,8 @@ object ChapterProvider {
0f 0f
} }
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, words, textPaint, book, absStartX, textLine, words,
startX, !isTitle && lineIndex == 0, textWidths, srcList startX, !isTitle && lineIndex == 0, widths, srcList
) )
} }
@@ -457,20 +500,23 @@ object ChapterProvider {
val startX = (visibleWidth - desiredWidth) / 2 val startX = (visibleWidth - desiredWidth) / 2
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, words, book, absStartX, textLine, words,
textPaint, startX, false, textWidths, srcList startX, false, widths, srcList
) )
} else { } else {
//中间行 //中间行
textLine.text = words textLine.text = lineText
addCharsToLineMiddle( addCharsToLineMiddle(
book, absStartX, textLine, words, book, absStartX, textLine, words,
textPaint, desiredWidth, 0f, textWidths, srcList desiredWidth, 0f, widths, srcList
) )
} }
} }
} }
if (doublePage) {
textLine.isLeftLine = absStartX < viewWidth / 2
}
val sbLength = stringBuilder.length val sbLength = stringBuilder.length
stringBuilder.append(words) stringBuilder.append(lineText)
if (textLine.isParagraphEnd) { if (textLine.isParagraphEnd) {
stringBuilder.append("\n") stringBuilder.append("\n")
} }
@@ -487,10 +533,13 @@ object ChapterProvider {
chapterPosition + charSize + if (isParagraphEnd) 1 else 0 chapterPosition + charSize + if (isParagraphEnd) 1 else 0
} ?: 0) + sbLength } ?: 0) + sbLength
textLine.pagePosition = sbLength textLine.pagePosition = sbLength
textPages.last().addLine(textLine)
textLine.upTopBottom(durY, textHeight, fontMetrics) textLine.upTopBottom(durY, textHeight, fontMetrics)
val textPage = textPages.last()
textPage.addLine(textLine)
durY += textHeight * lineSpacingExtra durY += textHeight * lineSpacingExtra
textPages.last().height = durY if (textPage.height < durY) {
textPage.height = durY
}
} }
durY += textHeight * paragraphSpacing / 10f durY += textHeight * paragraphSpacing / 10f
return Pair(absStartX, durY) return Pair(absStartX, durY)
@@ -503,8 +552,7 @@ object ChapterProvider {
book: Book, book: Book,
absStartX: Int, absStartX: Int,
textLine: TextLine, textLine: TextLine,
text: String, words: List<String>,
textPaint: TextPaint,
/**自然排版长度**/ /**自然排版长度**/
desiredWidth: Float, desiredWidth: Float,
textWidths: List<Float>, textWidths: List<Float>,
@@ -513,17 +561,17 @@ object ChapterProvider {
var x = 0f var x = 0f
if (!ReadBookConfig.textFullJustify) { if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, text, textPaint, book, absStartX, textLine, words,
x, true, textWidths, srcList x, true, textWidths, srcList
) )
return return
} }
val bodyIndent = ReadBookConfig.paragraphIndent val bodyIndent = ReadBookConfig.paragraphIndent
for (char in bodyIndent.toStringArray()) { for (i in bodyIndent.indices) {
val x1 = x + indentCharWidth val x1 = x + indentCharWidth
textLine.addColumn( textLine.addColumn(
TextColumn( TextColumn(
charData = char, charData = indentChar,
start = absStartX + x, start = absStartX + x,
end = absStartX + x1 end = absStartX + x1
) )
@@ -531,12 +579,12 @@ object ChapterProvider {
x = x1 x = x1
textLine.indentWidth = x textLine.indentWidth = x
} }
if (text.length > bodyIndent.length) { if (words.size > bodyIndent.length) {
val text1 = text.substring(bodyIndent.length, text.length) val text1 = words.subList(bodyIndent.length, words.size)
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size) val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
addCharsToLineMiddle( addCharsToLineMiddle(
book, absStartX, textLine, text1, book, absStartX, textLine, text1,
textPaint, desiredWidth, x, textWidths1, srcList desiredWidth, x, textWidths1, srcList
) )
} }
} }
@@ -548,8 +596,7 @@ object ChapterProvider {
book: Book, book: Book,
absStartX: Int, absStartX: Int,
textLine: TextLine, textLine: TextLine,
text: String, words: List<String>,
textPaint: TextPaint,
/**自然排版长度**/ /**自然排版长度**/
desiredWidth: Float, desiredWidth: Float,
/**起始x坐标**/ /**起始x坐标**/
@@ -559,19 +606,19 @@ object ChapterProvider {
) { ) {
if (!ReadBookConfig.textFullJustify) { if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, text, textPaint, book, absStartX, textLine, words,
startX, false, textWidths, srcList startX, false, textWidths, srcList
) )
return return
} }
val residualWidth = visibleWidth - desiredWidth val residualWidth = visibleWidth - desiredWidth
val spaceSize = text.count { it == ' ' } val spaceSize = words.count { it == " " }
val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint)
if (spaceSize > 1) { if (spaceSize > 1) {
val d = residualWidth / spaceSize val d = residualWidth / spaceSize
var x = startX var x = startX
words.forEachIndexed { index, char -> for (index in words.indices) {
val cw = widths[index] val char = words[index]
val cw = textWidths[index]
val x1 = if (char == " ") { val x1 = if (char == " ") {
if (index != words.lastIndex) (x + cw + d) else (x + cw) if (index != words.lastIndex) (x + cw + d) else (x + cw)
} else { } else {
@@ -587,8 +634,9 @@ object ChapterProvider {
val gapCount: Int = words.lastIndex val gapCount: Int = words.lastIndex
val d = residualWidth / gapCount val d = residualWidth / gapCount
var x = startX var x = startX
words.forEachIndexed { index, char -> for (index in words.indices) {
val cw = widths[index] val char = words[index]
val cw = textWidths[index]
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw) val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
addCharToLine( addCharToLine(
book, absStartX, textLine, char, book, absStartX, textLine, char,
@@ -607,8 +655,7 @@ object ChapterProvider {
book: Book, book: Book,
absStartX: Int, absStartX: Int,
textLine: TextLine, textLine: TextLine,
text: String, words: List<String>,
textPaint: TextPaint,
startX: Float, startX: Float,
hasIndent: Boolean, hasIndent: Boolean,
textWidths: List<Float>, textWidths: List<Float>,
@@ -616,9 +663,9 @@ object ChapterProvider {
) { ) {
val indentLength = ReadBookConfig.paragraphIndent.length val indentLength = ReadBookConfig.paragraphIndent.length
var x = startX var x = startX
val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint) for (index in words.indices) {
words.forEachIndexed { index, char -> val char = words[index]
val cw = widths[index] val cw = textWidths[index]
val x1 = x + cw val x1 = x + cw
addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList) addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
x = x1 x = x1
@@ -731,9 +778,11 @@ object ChapterProvider {
getPaints(typeface).let { getPaints(typeface).let {
titlePaint = it.first titlePaint = it.first
contentPaint = it.second contentPaint = it.second
reviewPaint.color = contentPaint.color titleMeasureHelper.setPaint(titlePaint)
reviewPaint.textSize = contentPaint.textSize * 0.45f contentMeasureHelper.setPaint(contentPaint)
reviewPaint.textAlign = Paint.Align.CENTER // reviewPaint.color = contentPaint.color
// reviewPaint.textSize = contentPaint.textSize * 0.45f
// reviewPaint.textAlign = Paint.Align.CENTER
} }
//间距 //间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f
@@ -820,14 +869,16 @@ object ChapterProvider {
/** /**
* 更新View尺寸 * 更新View尺寸
*/ */
fun upViewSize(width: Int, height: Int) { fun upViewSize(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
if (width > 0 && height > 0 && (width != viewWidth || height != viewHeight)) { if (width > 0 && height > 0 && (width != viewWidth || height != viewHeight)) {
viewWidth = width viewWidth = width
viewHeight = height viewHeight = height
upLayout() upLayout()
if (oldWidth > 0 && oldHeight > 0) {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
} }
}
/** /**
* 更新绘制尺寸 * 更新绘制尺寸
@@ -0,0 +1,146 @@
package io.legado.app.ui.book.read.page.provider
import android.text.TextPaint
import android.util.SparseArray
import androidx.core.util.getOrDefault
import java.util.BitSet
import kotlin.math.ceil
class TextMeasure(private var paint: TextPaint) {
private var chineseCommonWidth = paint.measureText("一")
private val chineseCommonWidthBitSet = BitSet()
private val asciiWidths = FloatArray(128) { -1f }
private val codePointWidths = SparseArray<Float>()
private fun measureCodePoint(codePoint: Int): Float {
if (codePoint < 128) {
return asciiWidths[codePoint]
}
if (chineseCommonWidthBitSet[codePoint]) {
return chineseCommonWidth
}
return codePointWidths.getOrDefault(codePoint, -1f)
}
private fun measureCodePoints(codePoints: List<Int>) {
val charArray = String(codePoints.toIntArray(), 0, codePoints.size).toCharArray()
val widths = FloatArray(charArray.size)
paint.getTextWidths(charArray, 0, charArray.size, widths)
val widthsList = ArrayList<Float>(charArray.size)
val buf = IntArray(1)
for (i in charArray.indices) {
if (charArray[i].isLowSurrogate()) continue
val width = ceil(widths[i])
widthsList.add(width)
if (width == 0f && widthsList.size > 0) {
val lastIndex = widthsList.lastIndex
buf[0] = codePoints[lastIndex - 1]
widthsList[lastIndex - 1] = paint.measureText(String(buf, 0, 1))
buf[0] = codePoints[lastIndex]
widthsList[lastIndex] = paint.measureText(String(buf, 0, 1))
}
}
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = widthsList[i]
if (codePoint < 128) {
asciiWidths[codePoint] = width
} else if (width == chineseCommonWidth) {
chineseCommonWidthBitSet.set(codePoint)
} else {
codePointWidths[codePoint] = width
}
}
}
fun measureTextSplit(text: String): Pair<ArrayList<String>, ArrayList<Float>> {
var needMeasureCodePoints: HashSet<Int>? = null
val codePoints = text.toCodePoints()
val size = codePoints.size
val widths = ArrayList<Float>(size)
val stringList = ArrayList<String>(size)
val buf = IntArray(1)
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = measureCodePoint(codePoint)
widths.add(width)
if (width == -1f) {
if (needMeasureCodePoints == null) {
needMeasureCodePoints = hashSetOf()
}
needMeasureCodePoints.add(codePoint)
}
buf[0] = codePoint
stringList.add(String(buf, 0, 1))
}
if (!needMeasureCodePoints.isNullOrEmpty()) {
measureCodePoints(needMeasureCodePoints.toList())
for (i in codePoints.indices) {
if (widths[i] == -1f) {
widths[i] = measureCodePoint(codePoints[i])
}
}
}
return stringList to widths
}
fun measureText(text: String): Float {
var textWidth = 0f
var needMeasureCodePoints: ArrayList<Int>? = null
val codePoints = text.toCodePoints()
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = measureCodePoint(codePoint)
if (width == -1f) {
if (needMeasureCodePoints == null) {
needMeasureCodePoints = ArrayList()
}
needMeasureCodePoints.add(codePoint)
continue
}
textWidth += width
}
if (!needMeasureCodePoints.isNullOrEmpty()) {
measureCodePoints(needMeasureCodePoints.toHashSet().toList())
for (i in needMeasureCodePoints.indices) {
textWidth += measureCodePoint(needMeasureCodePoints[i])
}
}
return textWidth
}
private fun String.toCodePoints(): List<Int> {
val codePoints = ArrayList<Int>(length)
val charArray = toCharArray()
val size = length
var i = 0
while (i < size) {
val c1 = charArray[i++]
var cp = c1.code
if (c1.isHighSurrogate() && i < size) {
val c2 = charArray[i]
if (c2.isLowSurrogate()) {
i++
cp = Character.toCodePoint(c1, c2)
}
}
codePoints.add(cp)
}
return codePoints
}
fun setPaint(paint: TextPaint) {
this.paint = paint
invalidate()
}
private fun invalidate() {
chineseCommonWidth = paint.measureText("一")
chineseCommonWidthBitSet.clear()
codePointWidths.clear()
asciiWidths.fill(-1f)
}
}
@@ -34,9 +34,12 @@ 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() && currentChapter != null) { return if (hasNext()) {
if (currentChapter?.isLastIndex(pageIndex) == true) { if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) {
ReadBook.moveToNextChapter(upContent) if ((currentChapter == null || isScroll) && nextChapter == null) {
return@with false
}
ReadBook.moveToNextChapter(upContent, false)
} else { } else {
ReadBook.setPageIndex(pageIndex.plus(1)) ReadBook.setPageIndex(pageIndex.plus(1))
} }
@@ -47,10 +50,16 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} }
override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) { override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) {
return if (hasPrev() && currentChapter != null) { return if (hasPrev()) {
if (pageIndex <= 0) { if (pageIndex <= 0) {
ReadBook.moveToPrevChapter(upContent) if (currentChapter == null && prevChapter == null) {
return@with false
}
ReadBook.moveToPrevChapter(upContent, upContentInPlace = false)
} else { } else {
if (currentChapter == null) {
return@with false
}
ReadBook.setPageIndex(pageIndex.minus(1)) ReadBook.setPageIndex(pageIndex.minus(1))
} }
if (upContent) upContent(resetPageOffset = false) if (upContent) upContent(resetPageOffset = false)
@@ -16,7 +16,7 @@ class ZhLayout(
text: CharSequence, text: CharSequence,
textPaint: TextPaint, textPaint: TextPaint,
width: Int, width: Int,
widthsArray: FloatArray measureHelper: TextMeasure,
) : Layout(text, textPaint, width, Alignment.ALIGN_NORMAL, 0f, 0f) { ) : Layout(text, textPaint, width, Alignment.ALIGN_NORMAL, 0f, 0f) {
companion object { companion object {
private val postPanc = hashSetOf( private val postPanc = hashSetOf(
@@ -50,12 +50,7 @@ class ZhLayout(
init { init {
var line = 0 var line = 0
curPaint.getTextWidths(text as String, widthsArray) val (words, widths) = measureHelper.measureTextSplit(text as String)
val (words, widths) = ChapterProvider.getStringArrayAndTextWidths(
text,
widthsArray.asList(),
curPaint
)
var lineW = 0f var lineW = 0f
var cwPre = 0f var cwPre = 0f
var length = 0 var length = 0
@@ -11,6 +11,33 @@ object BitmapCache {
fun add(bitmap: Bitmap) { fun add(bitmap: Bitmap) {
reusableBitmaps.add(SoftReference(bitmap)) reusableBitmaps.add(SoftReference(bitmap))
trimSize()
}
fun clear() {
if (reusableBitmaps.isEmpty()) {
return
}
val iterator = reusableBitmaps.iterator()
while (iterator.hasNext()) {
val item = iterator.next().get() ?: continue
item.recycle()
iterator.remove()
}
}
private fun trimSize() {
var byteCount = 0
val iterator = reusableBitmaps.iterator()
while (iterator.hasNext()) {
val item = iterator.next().get() ?: continue
if (byteCount > 128 * 1024 * 1024) {
item.recycle()
iterator.remove()
} else {
byteCount += item.byteCount
}
}
} }
fun addInBitmapOptions(options: BitmapFactory.Options) { fun addInBitmapOptions(options: BitmapFactory.Options) {
@@ -0,0 +1,9 @@
package io.legado.app.utils
fun List<Float>.fastSum(): Float {
var sum = 0f
for (i in indices) {
sum += this[i]
}
return sum
}
@@ -0,0 +1,40 @@
package io.legado.app.utils
import android.graphics.Canvas
import android.graphics.Picture
import android.os.Build
import androidx.core.graphics.record
class PictureMirror {
var picture: Picture? = null
var isDirty = true
inline fun draw(canvas: Canvas, width: Int, height: Int, block: Canvas.() -> Unit) {
if (atLeastApi23) {
if (picture == null) picture = Picture()
val picture = picture!!
if (isDirty) {
isDirty = false
picture.record(width, height, block)
}
canvas.drawPicture(picture)
} else {
canvas.block()
}
}
fun invalidate() {
isDirty = true
}
fun recycle() {
picture = null
isDirty = true
}
companion object {
val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
}