From 5ac5f8d60bd00d8ee6ac071ad20355ca4cb9e658 Mon Sep 17 00:00:00 2001 From: Horis <821938089@qq.com> Date: Mon, 12 Feb 2024 20:28:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/legado/app/lib/theme/ThemeStore.kt | 17 +- .../java/io/legado/app/model/ImageProvider.kt | 5 + .../main/java/io/legado/app/model/ReadBook.kt | 28 ++- .../app/ui/book/read/ReadBookActivity.kt | 30 +-- .../app/ui/book/read/page/ContentTextView.kt | 229 +++++------------- .../legado/app/ui/book/read/page/PageView.kt | 3 - .../legado/app/ui/book/read/page/ReadView.kt | 30 ++- .../app/ui/book/read/page/api/DataSource.kt | 2 + .../ui/book/read/page/entities/TextChapter.kt | 10 +- .../ui/book/read/page/entities/TextLine.kt | 69 +++++- .../ui/book/read/page/entities/TextPage.kt | 68 +++++- .../read/page/entities/column/BaseColumn.kt | 7 + .../read/page/entities/column/ButtonColumn.kt | 13 +- .../read/page/entities/column/ImageColumn.kt | 44 +++- .../read/page/entities/column/ReviewColumn.kt | 13 + .../read/page/entities/column/TextColumn.kt | 48 +++- .../read/page/provider/ChapterProvider.kt | 175 ++++++++----- .../ui/book/read/page/provider/TextMeasure.kt | 146 +++++++++++ .../read/page/provider/TextPageFactory.kt | 19 +- .../ui/book/read/page/provider/ZhLayout.kt | 9 +- .../java/io/legado/app/utils/BitmapCache.kt | 27 +++ .../legado/app/utils/CollectionExtensions.kt | 9 + .../java/io/legado/app/utils/PictureMirror.kt | 40 +++ 23 files changed, 734 insertions(+), 307 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt create mode 100644 app/src/main/java/io/legado/app/utils/CollectionExtensions.kt create mode 100644 app/src/main/java/io/legado/app/utils/PictureMirror.kt diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt index 9c8602eb5..0dab34719 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt @@ -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) diff --git a/app/src/main/java/io/legado/app/model/ImageProvider.kt b/app/src/main/java/io/legado/app/model/ImageProvider.kt index a1320d488..1bb69b7ae 100644 --- a/app/src/main/java/io/legado/app/model/ImageProvider.kt +++ b/app/src/main/java/io/legado/app/model/ImageProvider.kt @@ -212,4 +212,9 @@ object ImageProvider { }.getOrDefault(errorBitmap) } + fun clear() { + bitmapLruCache.evictAll() + BitmapCache.clear() + } + } diff --git a/app/src/main/java/io/legado/app/model/ReadBook.kt b/app/src/main/java/io/legado/app/model/ReadBook.kt index 0d7ee0899..20bb7f48a 100644 --- a/app/src/main/java/io/legado/app/model/ReadBook.kt +++ b/app/src/main/java/io/legado/app/model/ReadBook.kt @@ -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 { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 546846367..68b9e481f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -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(EventBus.UP_SEEK_BAR) { binding.readMenu.upSeekBar() } + observeEvent(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) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index 11fb9d542..cc9ce080d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt index 31656dc56..3af462e62 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt @@ -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) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt index 70833c053..1548cdade 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt b/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt index 22c0d7ab0..eb28942d6 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt @@ -13,6 +13,8 @@ interface DataSource { val prevChapter: TextChapter? + val isScroll: Boolean + fun hasNextChapter(): Boolean fun hasPrevChapter(): Boolean diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt index 09faed73a..07cff13df 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt index 1a14ef62b..0f89a6e32 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -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 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() + } + } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt index b48710b39..1a9d366f7 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt @@ -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 get() = textLines @@ -38,25 +43,29 @@ data class TextPage( val charSize: Int get() = text.length.coerceAtLeast(1) val searchResult = hashSetOf() var isMsgPage: Boolean = false + var pictureMirror: PictureMirror = PictureMirror() + var doublePage = false val paragraphs by lazy { paragraphsInternal } - val paragraphsInternal: ArrayList get() { - val paragraphs = arrayListOf() - 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 + get() { + val paragraphs = arrayListOf() + 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() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt index 6516e5f0d..ed86cd153 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt index 63db76675..abb3ac42a 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt @@ -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 \ No newline at end of file + override var end: Float, +) : BaseColumn { + override var textLine: TextLine = emptyTextLine + override fun draw(view: ContentTextView, canvas: Canvas) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt index d372eda21..39275c8c0 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt @@ -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 \ No newline at end of file +) : 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) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt index 748f1b57c..c1612fb2f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt @@ -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" diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt index 0ffe9d39a..7096c55d1 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt @@ -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 \ No newline at end of file + 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) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt index 760efbc35..8c5ccc06e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt @@ -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, + textHeight: Float, stringBuilder: StringBuilder, imageStyle: String?, - ): Float { + ): Pair { + 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, stringBuilder: StringBuilder, textPaint: TextPaint, + measureHelper: TextMeasure, textHeight: Float, fontMetrics: FontMetrics, isTitle: Boolean = false, @@ -358,14 +401,11 @@ object ChapterProvider { srcList: LinkedList? = null ): Pair { 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, /**自然排版长度**/ desiredWidth: Float, textWidths: List, @@ -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, /**自然排版长度**/ 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, startX: Float, hasIndent: Boolean, textWidths: List, @@ -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) + } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt new file mode 100644 index 000000000..27329769a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt @@ -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() + + 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) { + val charArray = String(codePoints.toIntArray(), 0, codePoints.size).toCharArray() + val widths = FloatArray(charArray.size) + paint.getTextWidths(charArray, 0, charArray.size, widths) + val widthsList = ArrayList(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> { + var needMeasureCodePoints: HashSet? = null + val codePoints = text.toCodePoints() + val size = codePoints.size + val widths = ArrayList(size) + val stringList = ArrayList(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? = 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 { + val codePoints = ArrayList(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) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt index 1bceccd6a..9ff5f77ee 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt @@ -34,9 +34,12 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(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(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) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt index ab8db392f..688725afa 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/utils/BitmapCache.kt b/app/src/main/java/io/legado/app/utils/BitmapCache.kt index 2f2e08411..37b831b8b 100644 --- a/app/src/main/java/io/legado/app/utils/BitmapCache.kt +++ b/app/src/main/java/io/legado/app/utils/BitmapCache.kt @@ -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) { diff --git a/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt new file mode 100644 index 000000000..3e6f33306 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt @@ -0,0 +1,9 @@ +package io.legado.app.utils + +fun List.fastSum(): Float { + var sum = 0f + for (i in indices) { + sum += this[i] + } + return sum +} diff --git a/app/src/main/java/io/legado/app/utils/PictureMirror.kt b/app/src/main/java/io/legado/app/utils/PictureMirror.kt new file mode 100644 index 000000000..9a725500d --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/PictureMirror.kt @@ -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 + } + +}