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

View File

@ -163,7 +163,22 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
.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 {
return ThemeStore(context)

View File

@ -212,4 +212,9 @@ object ImageProvider {
}.getOrDefault(errorBitmap)
}
fun clear() {
bitmapLruCache.evictAll()
BitmapCache.clear()
}
}

View File

@ -204,7 +204,7 @@ object ReadBook : CoroutineScope by MainScope() {
return hasPrevPage
}
fun moveToNextChapter(upContent: Boolean): Boolean {
fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
if (durChapterIndex < chapterSize - 1) {
durChapterPos = 0
durChapterIndex++
@ -213,11 +213,11 @@ object ReadBook : CoroutineScope by MainScope() {
nextTextChapter = null
if (curTextChapter == null) {
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
callBack?.upContent()
if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent) {
} else if (upContent && upContentInPlace) {
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
callBack?.upContent()
callBack?.upContent(resetPageOffset = false)
}
loadContent(durChapterIndex.plus(1), upContent, false)
saveRead()
@ -233,7 +233,8 @@ object ReadBook : CoroutineScope by MainScope() {
fun moveToPrevChapter(
upContent: Boolean,
toLast: Boolean = true
toLast: Boolean = true,
upContentInPlace: Boolean = true
): Boolean {
if (durChapterIndex > 0) {
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
@ -242,10 +243,10 @@ object ReadBook : CoroutineScope by MainScope() {
curTextChapter = prevTextChapter
prevTextChapter = null
if (curTextChapter == null) {
callBack?.upContent()
if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent) {
callBack?.upContent()
} else if (upContent && upContentInPlace) {
callBack?.upContent(resetPageOffset = false)
}
loadContent(durChapterIndex.minus(1), upContent, false)
saveRead()
@ -267,6 +268,16 @@ object ReadBook : CoroutineScope by MainScope() {
}
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
saveRead(true)
curPageChanged(true)
@ -595,6 +606,7 @@ object ReadBook : CoroutineScope by MainScope() {
coroutineContext.cancelChildren()
downloadedChapters.clear()
downloadFailChapters.clear()
ImageProvider.clear()
}
interface CallBack {

View File

@ -97,6 +97,7 @@ import io.legado.app.utils.LogUtils
import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.SyncedRenderer
import io.legado.app.utils.applyOpenTint
import io.legado.app.utils.buildMainHandler
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import io.legado.app.utils.hexString
@ -218,9 +219,10 @@ class ReadBookActivity : BaseReadBookActivity(),
private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } }
private var bookChanged = false
private var pageChanged = false
private var reloadContent = false
private val autoPageRenderer by lazy { SyncedRenderer { doAutoPage(it) } }
private var autoPageScrollOffset = 0.0
private val handler by lazy { buildMainHandler() }
private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
//恢复跳转前进度对话框的交互结果
private var confirmRestoreProcess: Boolean? = null
@ -265,22 +267,14 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
viewModel.initData(intent) {
initDataSuccess()
upMenu()
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
viewModel.initData(intent ?: return) {
initDataSuccess()
}
}
private fun initDataSuccess() {
upMenu()
if (reloadContent) {
reloadContent = false
ReadBook.loadContent(resetPageOffset = false)
upMenu()
}
}
@ -1532,8 +1526,6 @@ class ReadBookActivity : BaseReadBookActivity(),
if (it) { // 更新内容排版布局
if (isInitFinish) {
ReadBook.loadContent(resetPageOffset = false)
} else {
reloadContent = true
}
} else {
readView.upContent(resetPageOffset = false)
@ -1582,6 +1574,9 @@ class ReadBookActivity : BaseReadBookActivity(),
observeEvent<Boolean>(EventBus.UP_SEEK_BAR) {
binding.readMenu.upSeekBar()
}
observeEvent<String>(EventBus.RECREATE) {
binding.readView.invalidateTextPage()
}
}
private fun upScreenTimeOut() {
@ -1594,17 +1589,16 @@ class ReadBookActivity : BaseReadBookActivity(),
* 重置黑屏时间
*/
override fun screenOffTimerStart() {
keepScreenJon?.cancel()
keepScreenJon = lifecycleScope.launch {
handler.post {
if (screenTimeOut < 0) {
keepScreenOn(true)
return@launch
return@post
}
val t = screenTimeOut - sysScreenOffTime
if (t > 0) {
keepScreenOn(true)
delay(screenTimeOut)
keepScreenOn(false)
handler.removeCallbacks(screenOffRunnable)
handler.postDelayed(screenOffRunnable, screenTimeOut)
} else {
keepScreenOn(false)
}

View File

@ -3,22 +3,16 @@ package io.legado.app.ui.book.read.page
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Picture
import android.graphics.RectF
import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.graphics.record
import androidx.core.graphics.withTranslation
import io.legado.app.R
import io.legado.app.constant.PageAnim
import io.legado.app.constant.PreferKey
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.ReadBookConfig
import io.legado.app.lib.theme.accentColor
import io.legado.app.model.ImageProvider
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.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.TextPageFactory
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.dpToPx
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.showDialogFragment
@ -44,8 +38,7 @@ import kotlin.math.min
*/
class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true)
var upView: ((TextPage) -> Unit)? = null
private val selectedPaint by lazy {
val selectedPaint by lazy {
Paint().apply {
color = context.getCompatColor(R.color.btn_bg_press_2)
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 var pageOffset = 0
private lateinit var picture: Picture
private var pictureIsDirty = true
private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
private val pictureMirror = PictureMirror()
private val isNoAnim get() = ReadBook.pageAnim() == PageAnim.noAnim
//绘制图片的paint
private val imagePaint by lazy {
val imagePaint by lazy {
Paint().apply {
isAntiAlias = AppConfig.useAntiAlias
}
@ -79,9 +70,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
init {
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) {
super.onSizeChanged(w, h, oldw, oldh)
if (!isMainView) return
ChapterProvider.upViewSize(w, h)
ChapterProvider.upViewSize(w, h, oldw, oldh)
upVisibleRect()
textPage.format()
}
@ -119,14 +107,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
canvas.translate(0f, scrollY.toFloat())
}
canvas.clipRect(visibleRect)
if (atLeastApi23 && !callBack.isScroll && !isNoAnim) {
if (pictureIsDirty) {
pictureIsDirty = false
picture.record(width, height) {
drawPage(this)
}
if (!callBack.isScroll && !isNoAnim) {
pictureMirror.draw(canvas, width, height) {
drawPage(this)
}
canvas.drawPicture(picture)
} else {
drawPage(canvas)
}
@ -137,147 +121,34 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
*/
private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0)
var lines = textPage.lines
for (i in lines.indices) {
drawLine(canvas, textPage, lines[i], relativeOffset)
val view = this
canvas.withTranslation(0f, relativeOffset) {
textPage.draw(view, this)
}
if (!callBack.isScroll) return
//滚动翻页
if (!pageFactory.hasNext()) return
val textPage1 = relativePage(1)
relativeOffset = relativeOffset(1)
lines = textPage1.lines
for (i in lines.indices) {
drawLine(canvas, textPage1, lines[i], relativeOffset)
canvas.withTranslation(0f, relativeOffset) {
textPage1.draw(view, this)
}
if (!pageFactory.hasNextPlus()) return
relativeOffset = relativeOffset(2)
if (relativeOffset < ChapterProvider.visibleHeight) {
val textPage2 = relativePage(2)
lines = textPage2.lines
for (i in lines.indices) {
drawLine(canvas, textPage2, lines[i], relativeOffset)
canvas.withTranslation(0f, relativeOffset) {
textPage2.draw(view, this)
}
}
}
/**
* 绘制页面
*/
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) {
if (mOffset == 0) return
@ -294,28 +165,25 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
val offset = (ChapterProvider.visibleHeight - textPage.height).toInt()
pageOffset = min(0, offset)
} else if (pageOffset > 0) {
pageFactory.moveToPrev(true)
textPage = pageFactory.curPage
pageOffset -= textPage.height.toInt()
upView?.invoke(textPage)
contentDescription = textPage.text
if (pageFactory.moveToPrev(true)) {
pageOffset -= textPage.height.toInt()
} else {
pageOffset = 0
}
} else if (pageOffset < -textPage.height) {
pageOffset += textPage.height.toInt()
pageFactory.moveToNext(true)
textPage = pageFactory.curPage
upView?.invoke(textPage)
contentDescription = textPage.text
val height = textPage.height
if (pageFactory.moveToNext(upContent = true)) {
pageOffset += height.toInt()
} else {
pageOffset = -height.toInt()
}
}
invalidate()
}
override fun invalidate() {
super.invalidate()
invalidatePicture()
}
private fun invalidatePicture() {
pictureIsDirty = true
pictureMirror.invalidate()
}
/**
@ -517,9 +385,21 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) return
}
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)) {
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)) {
touched.invoke(
relativeOffset,
@ -529,12 +409,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
return
}
}
val isLast = textLine.columns.first().start < x
val (charIndex, textColumn) = if (isLast) {
textLine.columns.withIndex().last()
} else {
textLine.columns.withIndex().first()
}
val isLast = columns.first().start < x
val charIndex = if (isLast) columns.lastIndex else 0
val textColumn = if (isLast) columns.last() else columns.first()
touched.invoke(
relativeOffset,
TextPos(relativePos, lineIndex, charIndex, false, isLast),
@ -557,7 +434,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break
}
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)) {
val visibleLine = textLine.copy().apply {
lineTop += relativeOffset
@ -580,7 +459,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break
}
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)) {
val visibleLine = textLine.copy().apply {
lineTop += relativeOffset

View File

@ -57,9 +57,6 @@ class PageView(context: Context) : FrameLayout(context) {
if (!isInEditMode) {
upStyle()
}
binding.contentTextView.upView = {
setProgress(it)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {

View File

@ -21,16 +21,25 @@ import io.legado.app.model.ReadAloud
import io.legado.app.model.ReadBook
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.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.TextChapter
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.entities.TextPos
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.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.util.*
import java.util.Locale
import kotlin.math.abs
/**
@ -49,7 +58,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
field = value
upContent()
}
var isScroll = false
override var isScroll = false
val prevPage by lazy { PageView(context) }
val curPage by lazy { PageView(context) }
val nextPage by lazy { PageView(context) }
@ -529,7 +538,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
* @param resetPageOffset 滚动阅读是是否重置位置
*/
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
curPage.setContentDescription(pageFactory.curPage.text)
post {
curPage.setContentDescription(pageFactory.curPage.text)
}
if (isScroll && !callBack.isAutoPage) {
curPage.setContent(pageFactory.curPage, resetPageOffset)
} else {
@ -643,6 +654,15 @@ class ReadView(context: Context, attrs: AttributeSet) :
nextPageBitmap = null
}
fun invalidateTextPage() {
pageFactory.run {
prevPage.invalidateAll()
curPage.invalidateAll()
nextPage.invalidateAll()
nextPlusPage.invalidateAll()
}
}
override val currentChapter: TextChapter?
get() {
return if (callBack.isInitFinish) ReadBook.textChapter(0) else null

View File

@ -13,6 +13,8 @@ interface DataSource {
val prevChapter: TextChapter?
val isScroll: Boolean
fun hasNextChapter(): Boolean
fun hasPrevChapter(): Boolean

View File

@ -186,17 +186,19 @@ data class TextChapter(
*/
fun getPageIndexByCharIndex(charIndex: Int): Int {
var length = 0
pages.forEach {
length += it.charSize
for (i in pages.indices) {
val page = pages[i]
length += page.charSize
if (length > charIndex) {
return it.index
return page.index
}
}
return pages.lastIndex
}
fun clearSearchResult() {
pages.forEach { page ->
for (i in pages.indices) {
val page = pages[i]
page.searchResult.forEach {
it.selected = false
it.isSearchResult = false

View File

@ -1,9 +1,17 @@
package io.legado.app.ui.book.read.page.entities
import android.graphics.Canvas
import android.graphics.Paint.FontMetrics
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.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,
val isTitle: Boolean = false,
var isParagraphEnd: Boolean = false,
var isReadAloud: Boolean = false,
var isImage: Boolean = false
var isImage: Boolean = false,
) {
val columns: List<BaseColumn> get() = textColumns
@ -31,8 +38,20 @@ data class TextLine(
val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f
val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
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) {
column.textLine = this
textColumns.add(column)
}
@ -102,4 +121,50 @@ data class TextLine(
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()
}
}

View File

@ -1,13 +1,17 @@
package io.legado.app.ui.book.read.page.entities
import android.graphics.Canvas
import android.text.Layout
import android.text.StaticLayout
import androidx.annotation.Keep
import androidx.core.graphics.withTranslation
import io.legado.app.R
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.column.TextColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.PictureMirror
import splitties.init.appCtx
import java.text.DecimalFormat
import kotlin.math.min
@ -31,6 +35,7 @@ data class TextPage(
companion object {
val readProgressFormatter = DecimalFormat("0.0%")
val emptyTextPage = TextPage()
}
val lines: List<TextLine> get() = textLines
@ -38,25 +43,29 @@ data class TextPage(
val charSize: Int get() = text.length.coerceAtLeast(1)
val searchResult = hashSetOf<TextColumn>()
var isMsgPage: Boolean = false
var pictureMirror: PictureMirror = PictureMirror()
var doublePage = false
val paragraphs by lazy {
paragraphsInternal
}
val paragraphsInternal: ArrayList<TextParagraph> get() {
val paragraphs = arrayListOf<TextParagraph>()
val lines = textLines.filter { it.paragraphNum > 0 }
val offset = lines.first().paragraphNum - 1
lines.forEach { line ->
if (paragraphs.lastIndex < line.paragraphNum - offset - 1) {
paragraphs.add(TextParagraph(0))
val paragraphsInternal: ArrayList<TextParagraph>
get() {
val paragraphs = arrayListOf<TextParagraph>()
val lines = textLines.filter { it.paragraphNum > 0 }
val offset = lines.first().paragraphNum - 1
lines.forEach { line ->
if (paragraphs.lastIndex < line.paragraphNum - offset - 1) {
paragraphs.add(TextParagraph(0))
}
paragraphs[line.paragraphNum - offset - 1].textLines.add(line)
}
paragraphs[line.paragraphNum - offset - 1].textLines.add(line)
return paragraphs
}
return paragraphs
}
fun addLine(line: TextLine) {
line.textPage = this
textLines.add(line)
}
@ -147,7 +156,7 @@ data class TextPage(
)
x = x1
}
textLines.add(textLine)
addLine(textLine)
}
height = ChapterProvider.visibleHeight.toFloat()
}
@ -158,8 +167,8 @@ data class TextPage(
* 移除朗读标志
*/
fun removePageAloudSpan(): TextPage {
textLines.forEach { textLine ->
textLine.isReadAloud = false
for (i in textLines.indices) {
textLines[i].isReadAloud = false
}
return this
}
@ -254,6 +263,39 @@ data class TextPage(
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 {
return textLines.any { it.isImage } || textLines.isEmpty()
}

View File

@ -1,11 +1,18 @@
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 {
var start: Float
var end: Float
var textLine: TextLine
fun draw(view: ContentTextView, canvas: Canvas)
fun isTouch(x: Float): Boolean {
return x > start && x < end

View File

@ -1,6 +1,10 @@
package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
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
data class ButtonColumn(
override var start: Float,
override var end: Float
) : BaseColumn
override var end: Float,
) : BaseColumn {
override var textLine: TextLine = emptyTextLine
override fun draw(view: ContentTextView, canvas: Canvas) {
}
}

View File

@ -1,6 +1,15 @@
package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import android.graphics.RectF
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 end: Float,
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)
}
}
}

View File

@ -4,6 +4,9 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
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
/**
@ -16,6 +19,16 @@ data class ReviewColumn(
val count: Int = 0
) : 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 {
if (count > 999) {
return@lazy "999"

View File

@ -1,6 +1,13 @@
package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
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 end: Float,
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
) : 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)
}
}
}

View File

@ -1,6 +1,5 @@
package io.legado.app.ui.book.read.page.provider
import android.graphics.Paint
import android.graphics.Paint.FontMetrics
import android.graphics.Typeface
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.utils.RealPathUtil
import io.legado.app.utils.dpToPx
import io.legado.app.utils.fastSum
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.isPad
import io.legado.app.utils.postEvent
import io.legado.app.utils.spToPx
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.textHeight
import io.legado.app.utils.toStringArray
import splitties.init.appCtx
import java.util.LinkedList
import java.util.Locale
@ -47,6 +46,8 @@ object ChapterProvider {
//用于评论按钮的替换
private const val reviewChar = ""
private const val indentChar = " "
@JvmStatic
var viewWidth = 0
private set
@ -104,7 +105,7 @@ object ChapterProvider {
private var indentCharWidth = 0f
@JvmStatic
private var titlePaintTextHeight = 0f
var titlePaintTextHeight = 0f
@JvmStatic
var contentPaintTextHeight = 0f
@ -132,6 +133,9 @@ object ChapterProvider {
var doublePage = false
private set
private val titleMeasureHelper = TextMeasure(titlePaint)
private val contentMeasureHelper = TextMeasure(contentPaint)
init {
upStyle()
}
@ -161,6 +165,7 @@ object ChapterProvider {
textPages,
stringBuilder,
titlePaint,
titleMeasureHelper,
titlePaintTextHeight,
titlePaintFontMetrics,
isTitle = true,
@ -196,6 +201,7 @@ object ChapterProvider {
textPages,
stringBuilder,
contentPaint,
contentMeasureHelper,
contentPaintTextHeight,
contentPaintFontMetrics,
srcList = srcList
@ -217,6 +223,7 @@ object ChapterProvider {
textPages,
stringBuilder,
contentPaint,
contentMeasureHelper,
contentPaintTextHeight,
contentPaintFontMetrics
).let {
@ -224,10 +231,19 @@ object ChapterProvider {
durY = it.second
}
}
durY = setTypeImage(
book, matcher.group(1)!!,
absStartX, durY, textPages, stringBuilder, book.getImageStyle()
)
setTypeImage(
book,
matcher.group(1)!!,
absStartX,
durY,
textPages,
contentPaintTextHeight,
stringBuilder,
book.getImageStyle()
).let {
absStartX = it.first
durY = it.second
}
start = matcher.end()
}
if (start < content.length) {
@ -239,6 +255,7 @@ object ChapterProvider {
textPages,
stringBuilder,
contentPaint,
contentMeasureHelper,
contentPaintTextHeight,
contentPaintFontMetrics
).let {
@ -249,14 +266,22 @@ object ChapterProvider {
}
}
}
textPages.last().height = durY + 20.dpToPx()
textPages.last().text = stringBuilder.toString()
val textPage = textPages.last()
val endPadding = 20.dpToPx()
val durYPadding = durY + endPadding
if (textPage.height < durYPadding) {
textPage.height = durYPadding
} else {
textPage.height += endPadding
}
textPage.text = stringBuilder.toString()
textPages.forEachIndexed { index, item ->
item.index = index
item.pageSize = textPages.size
item.chapterIndex = bookChapter.index
item.chapterSize = chapterSize
item.title = displayTitle
item.doublePage = doublePage
item.upLinesPosition()
}
@ -280,15 +305,19 @@ object ChapterProvider {
x: Int,
y: Float,
textPages: ArrayList<TextPage>,
textHeight: Float,
stringBuilder: StringBuilder,
imageStyle: String?,
): Float {
): Pair<Int, Float> {
var absStartX = x
var durY = y
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
if (size.width > 0 && size.height > 0) {
if (durY > visibleHeight) {
val textPage = textPages.last()
textPage.height = durY
if (textPage.height < durY) {
textPage.height = durY
}
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear()
textPages.add(TextPage())
@ -313,10 +342,23 @@ object ChapterProvider {
}
if (durY + height > visibleHeight) {
val textPage = textPages.last()
textPage.height = durY
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear()
textPages.add(TextPage())
if (doublePage && absStartX < viewWidth / 2) {
//当前页面左列结束
textPage.leftLineSize = textPage.lineSize
absStartX = viewWidth / 2 + paddingLeft
} else {
//当前页面结束
if (textPage.leftLineSize == 0) {
textPage.leftLineSize = textPage.lineSize
}
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear()
textPages.add(TextPage())
}
// 双页的 durY 不正确,可能会小于实际高度
if (textPage.height < durY) {
textPage.height = durY
}
durY = 0f
}
}
@ -336,7 +378,7 @@ object ChapterProvider {
)
textPages.last().addLine(textLine)
}
return durY + paragraphSpacing / 10f
return absStartX to durY + textHeight * paragraphSpacing / 10f
}
/**
@ -350,6 +392,7 @@ object ChapterProvider {
textPages: ArrayList<TextPage>,
stringBuilder: StringBuilder,
textPaint: TextPaint,
measureHelper: TextMeasure,
textHeight: Float,
fontMetrics: FontMetrics,
isTitle: Boolean = false,
@ -358,14 +401,11 @@ object ChapterProvider {
srcList: LinkedList<String>? = null
): Pair<Int, Float> {
var absStartX = x
val widthsArray = FloatArray(text.length)
val layout = if (ReadBookConfig.useZhLayout) {
ZhLayout(text, textPaint, visibleWidth, widthsArray)
ZhLayout(text, textPaint, visibleWidth, measureHelper)
} else {
textPaint.getTextWidths(text, widthsArray)
StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
}
val widthsList = widthsArray.asList()
var durY = when {
//标题y轴居中
emptyContent && textPages.size == 1 -> {
@ -398,6 +438,7 @@ object ChapterProvider {
if (durY + textHeight > visibleHeight) {
val textPage = textPages.last()
if (doublePage && absStartX < viewWidth / 2) {
//当前页面左列结束
textPage.leftLineSize = textPage.lineSize
absStartX = viewWidth / 2 + paddingLeft
} else {
@ -406,32 +447,34 @@ object ChapterProvider {
textPage.leftLineSize = textPage.lineSize
}
textPage.text = stringBuilder.toString()
textPage.height = durY
//新建页面
textPages.add(TextPage())
stringBuilder.clear()
absStartX = paddingLeft
}
if (textPage.height < durY) {
textPage.height = durY
}
durY = 0f
}
val lineStart = layout.getLineStart(lineIndex)
val lineEnd = layout.getLineEnd(lineIndex)
val words = text.substring(lineStart, lineEnd)
val textWidths = widthsList.subList(lineStart, lineEnd)
val desiredWidth = textWidths.sum()
val lineText = text.substring(lineStart, lineEnd)
val (words, widths) = measureHelper.measureTextSplit(lineText)
val desiredWidth = widths.fastSum()
when {
lineIndex == 0 && layout.lineCount > 1 && !isTitle -> {
//第一行 非标题
textLine.text = words
textLine.text = lineText
addCharsToLineFirst(
book, absStartX, textLine, words,
textPaint, desiredWidth, textWidths, srcList
desiredWidth, widths, srcList
)
}
lineIndex == layout.lineCount - 1 -> {
//最后一行
textLine.text = words
textLine.text = lineText
textLine.isParagraphEnd = true
//标题x轴居中
val startX = if (
@ -443,8 +486,8 @@ object ChapterProvider {
0f
}
addCharsToLineNatural(
book, absStartX, textLine, words, textPaint,
startX, !isTitle && lineIndex == 0, textWidths, srcList
book, absStartX, textLine, words,
startX, !isTitle && lineIndex == 0, widths, srcList
)
}
@ -457,20 +500,23 @@ object ChapterProvider {
val startX = (visibleWidth - desiredWidth) / 2
addCharsToLineNatural(
book, absStartX, textLine, words,
textPaint, startX, false, textWidths, srcList
startX, false, widths, srcList
)
} else {
//中间行
textLine.text = words
textLine.text = lineText
addCharsToLineMiddle(
book, absStartX, textLine, words,
textPaint, desiredWidth, 0f, textWidths, srcList
desiredWidth, 0f, widths, srcList
)
}
}
}
if (doublePage) {
textLine.isLeftLine = absStartX < viewWidth / 2
}
val sbLength = stringBuilder.length
stringBuilder.append(words)
stringBuilder.append(lineText)
if (textLine.isParagraphEnd) {
stringBuilder.append("\n")
}
@ -487,10 +533,13 @@ object ChapterProvider {
chapterPosition + charSize + if (isParagraphEnd) 1 else 0
} ?: 0) + sbLength
textLine.pagePosition = sbLength
textPages.last().addLine(textLine)
textLine.upTopBottom(durY, textHeight, fontMetrics)
val textPage = textPages.last()
textPage.addLine(textLine)
durY += textHeight * lineSpacingExtra
textPages.last().height = durY
if (textPage.height < durY) {
textPage.height = durY
}
}
durY += textHeight * paragraphSpacing / 10f
return Pair(absStartX, durY)
@ -503,8 +552,7 @@ object ChapterProvider {
book: Book,
absStartX: Int,
textLine: TextLine,
text: String,
textPaint: TextPaint,
words: List<String>,
/**自然排版长度**/
desiredWidth: Float,
textWidths: List<Float>,
@ -513,17 +561,17 @@ object ChapterProvider {
var x = 0f
if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural(
book, absStartX, textLine, text, textPaint,
book, absStartX, textLine, words,
x, true, textWidths, srcList
)
return
}
val bodyIndent = ReadBookConfig.paragraphIndent
for (char in bodyIndent.toStringArray()) {
for (i in bodyIndent.indices) {
val x1 = x + indentCharWidth
textLine.addColumn(
TextColumn(
charData = char,
charData = indentChar,
start = absStartX + x,
end = absStartX + x1
)
@ -531,12 +579,12 @@ object ChapterProvider {
x = x1
textLine.indentWidth = x
}
if (text.length > bodyIndent.length) {
val text1 = text.substring(bodyIndent.length, text.length)
if (words.size > bodyIndent.length) {
val text1 = words.subList(bodyIndent.length, words.size)
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
addCharsToLineMiddle(
book, absStartX, textLine, text1,
textPaint, desiredWidth, x, textWidths1, srcList
desiredWidth, x, textWidths1, srcList
)
}
}
@ -548,8 +596,7 @@ object ChapterProvider {
book: Book,
absStartX: Int,
textLine: TextLine,
text: String,
textPaint: TextPaint,
words: List<String>,
/**自然排版长度**/
desiredWidth: Float,
/**起始x坐标**/
@ -559,19 +606,19 @@ object ChapterProvider {
) {
if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural(
book, absStartX, textLine, text, textPaint,
book, absStartX, textLine, words,
startX, false, textWidths, srcList
)
return
}
val residualWidth = visibleWidth - desiredWidth
val spaceSize = text.count { it == ' ' }
val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint)
val spaceSize = words.count { it == " " }
if (spaceSize > 1) {
val d = residualWidth / spaceSize
var x = startX
words.forEachIndexed { index, char ->
val cw = widths[index]
for (index in words.indices) {
val char = words[index]
val cw = textWidths[index]
val x1 = if (char == " ") {
if (index != words.lastIndex) (x + cw + d) else (x + cw)
} else {
@ -587,8 +634,9 @@ object ChapterProvider {
val gapCount: Int = words.lastIndex
val d = residualWidth / gapCount
var x = startX
words.forEachIndexed { index, char ->
val cw = widths[index]
for (index in words.indices) {
val char = words[index]
val cw = textWidths[index]
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
addCharToLine(
book, absStartX, textLine, char,
@ -607,8 +655,7 @@ object ChapterProvider {
book: Book,
absStartX: Int,
textLine: TextLine,
text: String,
textPaint: TextPaint,
words: List<String>,
startX: Float,
hasIndent: Boolean,
textWidths: List<Float>,
@ -616,9 +663,9 @@ object ChapterProvider {
) {
val indentLength = ReadBookConfig.paragraphIndent.length
var x = startX
val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint)
words.forEachIndexed { index, char ->
val cw = widths[index]
for (index in words.indices) {
val char = words[index]
val cw = textWidths[index]
val x1 = x + cw
addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
x = x1
@ -731,9 +778,11 @@ object ChapterProvider {
getPaints(typeface).let {
titlePaint = it.first
contentPaint = it.second
reviewPaint.color = contentPaint.color
reviewPaint.textSize = contentPaint.textSize * 0.45f
reviewPaint.textAlign = Paint.Align.CENTER
titleMeasureHelper.setPaint(titlePaint)
contentMeasureHelper.setPaint(contentPaint)
// reviewPaint.color = contentPaint.color
// reviewPaint.textSize = contentPaint.textSize * 0.45f
// reviewPaint.textAlign = Paint.Align.CENTER
}
//间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f
@ -820,12 +869,14 @@ object ChapterProvider {
/**
* 更新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)) {
viewWidth = width
viewHeight = height
upLayout()
postEvent(EventBus.UP_CONFIG, true)
if (oldWidth > 0 && oldHeight > 0) {
postEvent(EventBus.UP_CONFIG, true)
}
}
}

View File

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

View File

@ -34,9 +34,12 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
}
override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) {
return if (hasNext() && currentChapter != null) {
if (currentChapter?.isLastIndex(pageIndex) == true) {
ReadBook.moveToNextChapter(upContent)
return if (hasNext()) {
if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) {
if ((currentChapter == null || isScroll) && nextChapter == null) {
return@with false
}
ReadBook.moveToNextChapter(upContent, false)
} else {
ReadBook.setPageIndex(pageIndex.plus(1))
}
@ -47,10 +50,16 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
}
override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) {
return if (hasPrev() && currentChapter != null) {
return if (hasPrev()) {
if (pageIndex <= 0) {
ReadBook.moveToPrevChapter(upContent)
if (currentChapter == null && prevChapter == null) {
return@with false
}
ReadBook.moveToPrevChapter(upContent, upContentInPlace = false)
} else {
if (currentChapter == null) {
return@with false
}
ReadBook.setPageIndex(pageIndex.minus(1))
}
if (upContent) upContent(resetPageOffset = false)

View File

@ -16,7 +16,7 @@ class ZhLayout(
text: CharSequence,
textPaint: TextPaint,
width: Int,
widthsArray: FloatArray
measureHelper: TextMeasure,
) : Layout(text, textPaint, width, Alignment.ALIGN_NORMAL, 0f, 0f) {
companion object {
private val postPanc = hashSetOf(
@ -50,12 +50,7 @@ class ZhLayout(
init {
var line = 0
curPaint.getTextWidths(text as String, widthsArray)
val (words, widths) = ChapterProvider.getStringArrayAndTextWidths(
text,
widthsArray.asList(),
curPaint
)
val (words, widths) = measureHelper.measureTextSplit(text as String)
var lineW = 0f
var cwPre = 0f
var length = 0

View File

@ -11,6 +11,33 @@ object BitmapCache {
fun add(bitmap: 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) {

View File

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

View File

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