From 946cd1ab945a8eb6aa81a3b7cb36cde5ca474c87 Mon Sep 17 00:00:00 2001 From: Horis <821938089@qq.com> Date: Wed, 21 Feb 2024 23:34:36 +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 --- .../java/io/legado/app/data/entities/Book.kt | 4 + .../java/io/legado/app/help/book/BookHelp.kt | 2 +- .../main/java/io/legado/app/model/ReadBook.kt | 23 +- .../app/ui/book/read/ReadBookActivity.kt | 33 +- .../app/ui/book/read/page/ContentTextView.kt | 2 +- .../legado/app/ui/book/read/page/PageView.kt | 11 +- .../legado/app/ui/book/read/page/ReadView.kt | 40 +- .../ui/book/read/page/entities/TextChapter.kt | 97 ++- .../ui/book/read/page/entities/TextPage.kt | 28 +- .../read/page/provider/ChapterProvider.kt | 52 +- .../page/provider/LayoutProgressListener.kt | 22 + .../read/page/provider/TextChapterLayout.kt | 752 ++++++++++++++++++ .../read/page/provider/TextPageFactory.kt | 19 +- .../legado/app/utils/CollectionExtensions.kt | 6 + .../main/java/io/legado/app/utils/Debounce.kt | 8 + .../main/java/io/legado/app/utils/Throttle.kt | 7 + 16 files changed, 1059 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt create mode 100644 app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index 9bd6c2355..49f35f985 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -282,6 +282,10 @@ data class Book( } } + fun getBookSource(): BookSource? { + return appDb.bookSourceDao.getBookSource(origin) + } + fun toSearchBook() = SearchBook( name = name, author = author, diff --git a/app/src/main/java/io/legado/app/help/book/BookHelp.kt b/app/src/main/java/io/legado/app/help/book/BookHelp.kt index 99fbdafcd..10b034cc4 100644 --- a/app/src/main/java/io/legado/app/help/book/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/book/BookHelp.kt @@ -124,7 +124,7 @@ object BookHelp { ) { try { saveText(book, bookChapter, content) - saveImages(bookSource, book, bookChapter, content) + //saveImages(bookSource, book, bookChapter, content) postEvent(EventBus.SAVE_CONTENT, Pair(book, bookChapter)) } catch (e: Exception) { e.printStackTrace() 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 c5062cfef..01c222b75 100644 --- a/app/src/main/java/io/legado/app/model/ReadBook.kt +++ b/app/src/main/java/io/legado/app/model/ReadBook.kt @@ -145,6 +145,9 @@ object ReadBook : CoroutineScope by MainScope() { } fun clearTextChapter() { + prevTextChapter?.cancelLayout() + curTextChapter?.cancelLayout() + nextTextChapter?.cancelLayout() prevTextChapter = null curTextChapter = null nextTextChapter = null @@ -210,6 +213,7 @@ object ReadBook : CoroutineScope by MainScope() { if (durChapterIndex < chapterSize - 1) { durChapterPos = 0 durChapterIndex++ + prevTextChapter?.cancelLayout() prevTextChapter = curTextChapter curTextChapter = nextTextChapter nextTextChapter = null @@ -226,6 +230,7 @@ object ReadBook : CoroutineScope by MainScope() { callBack?.upMenuView() AppLog.putDebug("moveToNextChapter-curPageChanged()") curPageChanged() + curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it) } return true } else { AppLog.putDebug("跳转下一章失败,没有下一章") @@ -241,6 +246,7 @@ object ReadBook : CoroutineScope by MainScope() { if (durChapterIndex > 0) { durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0 durChapterIndex-- + nextTextChapter?.cancelLayout() nextTextChapter = curTextChapter curTextChapter = prevTextChapter prevTextChapter = null @@ -254,6 +260,7 @@ object ReadBook : CoroutineScope by MainScope() { saveRead() callBack?.upMenuView() curPageChanged() + curTextChapter?.let { callBack?.onCurrentTextChapterChanged(it) } return true } else { return false @@ -477,23 +484,33 @@ object ReadBook : CoroutineScope by MainScope() { ) val contents = contentProcessor .getContent(book, chapter, content, includeTitle = false) - val textChapter = ChapterProvider - .getTextChapter(book, chapter, displayTitle, contents, chapterSize) + val textChapter = ChapterProvider.getTextChapterAsync( + book, + chapter, + displayTitle, + contents, + chapterSize, + this@ReadBook + ) when (val offset = chapter.index - durChapterIndex) { 0 -> { + curTextChapter?.cancelLayout() curTextChapter = textChapter if (upContent) callBack?.upContent(offset, resetPageOffset) callBack?.upMenuView() curPageChanged() callBack?.contentLoadFinish() + callBack?.onCurrentTextChapterChanged(textChapter) } -1 -> { + prevTextChapter?.cancelLayout() prevTextChapter = textChapter if (upContent) callBack?.upContent(offset, resetPageOffset) } 1 -> { + nextTextChapter?.cancelLayout() nextTextChapter = textChapter if (upContent) callBack?.upContent(offset, resetPageOffset) } @@ -632,6 +649,8 @@ object ReadBook : CoroutineScope by MainScope() { fun upPageAnim() fun notifyBookChanged() + + fun onCurrentTextChapterChanged(textChapter: TextChapter) } } 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 93f68e07a..e410af1c4 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 @@ -77,7 +77,10 @@ import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_C import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.LayoutProgressListener import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.searchContent.SearchResult import io.legado.app.ui.book.source.edit.BookSourceEditActivity @@ -112,8 +115,10 @@ import io.legado.app.utils.observeEvent import io.legado.app.utils.observeEventSticky import io.legado.app.utils.postEvent import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.stackTraceStr import io.legado.app.utils.startActivity import io.legado.app.utils.sysScreenOffTime +import io.legado.app.utils.throttle import io.legado.app.utils.toastOnUi import io.legado.app.utils.visible import kotlinx.coroutines.Dispatchers.IO @@ -139,7 +144,8 @@ class ReadBookActivity : BaseReadBookActivity(), ReadBook.CallBack, AutoReadDialog.CallBack, TxtTocRuleDialog.CallBack, - ColorPickerDialogListener { + ColorPickerDialogListener, + LayoutProgressListener { private val tocActivity = registerForActivityResult(TocActivityResult()) { @@ -221,6 +227,12 @@ class ReadBookActivity : BaseReadBookActivity(), private val handler by lazy { buildMainHandler() } private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } } private val executor = ReadBook.executor + private val upSeekBarThrotle = throttle(200) { + runOnUiThread { + upSeekBarProgress() + binding.readMenu.upSeekBar() + } + } //恢复跳转前进度对话框的交互结果 private var confirmRestoreProcess: Boolean? = null @@ -1357,6 +1369,25 @@ class ReadBookActivity : BaseReadBookActivity(), binding.readView.autoPager.resume() } + override fun onCurrentTextChapterChanged(textChapter: TextChapter) { + textChapter.setProgressListener(this) + } + + override fun onLayoutPageCompleted(index: Int, page: TextPage) { + upSeekBarThrotle.invoke() + binding.readView.onLayoutPageCompleted(index, page) + } + + override fun onLayoutCompleted() { + binding.readView.onLayoutCompleted() + } + + override fun onLayoutException(e: Throwable) { + AppLog.put("ChapterProvider ERROR", e) + toastOnUi("ChapterProvider ERROR:\n${e.stackTraceStr}") + binding.readView.onLayoutException(e) + } + /* 全文搜索跳转 */ private fun skipToSearch(searchResult: SearchResult) { val previousResult = binding.searchMenu.previousSearchResult 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 d9831f401..aba5502db 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 @@ -655,7 +655,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at fun createBookmark(): Bookmark? { val page = relativePage(selectStart.relativePagePos) - page.getTextChapter()?.let { chapter -> + page.getTextChapter().let { chapter -> ReadBook.book?.let { book -> return book.createBookMark().apply { chapterIndex = page.chapterIndex 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 b4a44ddbb..c08339218 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 @@ -309,11 +309,18 @@ class PageView(context: Context) : FrameLayout(context) { fun setProgress(textPage: TextPage) = textPage.apply { tvBookName?.setTextIfNotEqual(ReadBook.book?.name) tvTitle?.setTextIfNotEqual(textPage.title) - tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize") val readProgress = readProgress tvTotalProgress?.setTextIfNotEqual(readProgress) tvTotalProgress1?.setTextIfNotEqual("${chapterIndex.plus(1)}/${chapterSize}") - tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") + if (textChapter.isCompleted) { + tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") + tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize") + } else { + val pageSizeInt = pageSize - 1 + val pageSize = if (pageSizeInt <= 0) "-" else "~$pageSizeInt" + tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") + tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize") + } } fun setAutoPager(autoPager: AutoPager?) { 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 927dd2834..7e999db03 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 @@ -28,10 +28,12 @@ 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.LayoutProgressListener import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.utils.activity import io.legado.app.utils.invisible import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.throttle import java.text.BreakIterator import java.util.Locale import kotlin.math.abs @@ -41,7 +43,7 @@ import kotlin.math.abs */ class ReadView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs), - DataSource { + DataSource, LayoutProgressListener { val callBack: CallBack get() = activity as CallBack var pageFactory: TextPageFactory = TextPageFactory(this) @@ -99,6 +101,7 @@ class ReadView(context: Context, attrs: AttributeSet) : private val bcRect = RectF() private val brRect = RectF() private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) } + private val upProgressThrottle = throttle(200) { post { upProgress() } } val autoPager = AutoPager(this) val isAutoPage get() = autoPager.isRunning @@ -539,6 +542,10 @@ class ReadView(context: Context, attrs: AttributeSet) : callBack.screenOffTimerStart() } + private fun upProgress() { + curPage.setProgress(pageFactory.curPage) + } + /** * 更新滑动距离 */ @@ -654,6 +661,37 @@ class ReadView(context: Context, attrs: AttributeSet) : curPage.submitRenderTask() } + override fun onLayoutPageCompleted(index: Int, page: TextPage) { + val line = page.lines.first() + val durChapterPos = ReadBook.durChapterPos + val startPos = line.chapterPosition + val endPos = line.chapterPosition + line.charSize + if (durChapterPos in startPos.., val chaptersSize: Int, val sameTitleRemoved: Boolean, val isVip: Boolean, val isPay: Boolean, //起效的替换规则 val effectiveReplaceRules: List? -) { +) : LayoutProgressListener { + + private val textPages = arrayListOf() + val pages: List get() = textPages + + var layout: TextChapterLayout? = null fun getPage(index: Int): TextPage? { return pages.getOrNull(index) @@ -42,6 +51,10 @@ data class TextChapter( val pageSize: Int get() = pages.size + var listener: LayoutProgressListener? = null + + var isCompleted = false + val paragraphs by lazy { paragraphsInternal } @@ -79,7 +92,12 @@ data class TextChapter( * @return 是否是最后一页 */ fun isLastIndex(index: Int): Boolean { - return index >= pages.size - 1 + return isCompleted && index >= pages.size - 1 + } + + fun isLastIndexCurrent(index: Int): Boolean { + // 未完成排版时,最后一页是正在排版中的,需要去掉 + return index >= if (isCompleted) pages.size - 1 else pages.size - 2 } /** @@ -190,15 +208,28 @@ data class TextChapter( * @return 根据索引位置获取所在页 */ fun getPageIndexByCharIndex(charIndex: Int): Int { - val index = pages.fastBinarySearchBy(charIndex) { + val pageSize = pages.size + if (pageSize == 0) { + return -1 + } + val size = if (isCompleted) pageSize else pageSize - 1 + val bIndex = pages.fastBinarySearchBy(charIndex, 0, size) { it.lines.first().chapterPosition } - return if (index >= 0) { - index - } else { - abs(index + 1) - 1 + val index = abs(bIndex + 1) - 1 + if (index == -1) { + return -1 } - /* 相当于以下实现 + // 判断是否已经排版到 charIndex ,没有则返回 -1 + if (!isCompleted && index == size - 1) { + val line = pages[index].lines.first() + val pageEndPos = line.chapterPosition + line.charSize + if (charIndex > pageEndPos) { + return -1 + } + } + return index + /* var length = 0 for (i in pages.indices) { val page = pages[i] @@ -222,4 +253,52 @@ data class TextChapter( } } + fun createLayout(scope: CoroutineScope, book: Book, bookContent: BookContent) { + textPages.clear() + layout = TextChapterLayout( + scope, + this, + textPages, + book, + bookContent, + ) + } + + fun setProgressListener(l: LayoutProgressListener) { + if (isCompleted) { + l.onLayoutCompleted() + } else { + listener = l + } + } + + override fun onLayoutPageCompleted(index: Int, page: TextPage) { + listener?.onLayoutPageCompleted(index, page) + } + + override fun onLayoutCompleted() { + isCompleted = true + listener?.onLayoutCompleted() + listener = null + } + + override fun onLayoutException(e: Throwable) { + listener?.onLayoutException(e) + listener = null + } + + fun cancelLayout() { + layout?.cancel() + } + + companion object { + val emptyTextChapter = TextChapter( + BookChapter(), -1, "emptyTextChapter", -1, + sameTitleRemoved = false, + isVip = false, + isPay = false, + null + ).apply { isCompleted = true } + } + } 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 ef8032b95..9867dcb5f 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 @@ -7,8 +7,8 @@ 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.TextChapter.Companion.emptyTextChapter 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.canvasrecorder.CanvasRecorderFactory @@ -27,7 +27,6 @@ data class TextPage( var text: String = appCtx.getString(R.string.data_loading), var title: String = appCtx.getString(R.string.data_loading), private val textLines: ArrayList = arrayListOf(), - var pageSize: Int = 0, var chapterSize: Int = 0, var chapterIndex: Int = 0, var height: Float = 0f, @@ -47,6 +46,10 @@ data class TextPage( var canvasRecorder = CanvasRecorderFactory.create(true) var doublePage = false var paddingTop = 0 + var isCompleted = false + @JvmField + var textChapter = emptyTextChapter + val pageSize get() = textChapter.pageSize val paragraphs by lazy { paragraphsInternal @@ -162,6 +165,7 @@ data class TextPage( } height = ChapterProvider.visibleHeight.toFloat() invalidate() + isCompleted = true } return this } @@ -248,23 +252,8 @@ data class TextPage( /** * @return 页面所在章节 */ - fun getTextChapter(): TextChapter? { - ReadBook.curTextChapter?.let { - if (it.position == chapterIndex) { - return it - } - } - ReadBook.nextTextChapter?.let { - if (it.position == chapterIndex) { - return it - } - } - ReadBook.prevTextChapter?.let { - if (it.position == chapterIndex) { - return it - } - } - return null + fun getTextChapter(): TextChapter { + return textChapter } fun draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) { @@ -284,6 +273,7 @@ data class TextPage( } fun render(view: ContentTextView): Boolean { + if (!isCompleted) return false return canvasRecorder.recordIfNeeded(view.width, height.toInt()) { drawPage(view, this) } 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 5f913cb97..8b88fa75e 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 @@ -32,6 +32,7 @@ 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 kotlinx.coroutines.CoroutineScope import splitties.init.appCtx import java.util.LinkedList import java.util.Locale @@ -42,12 +43,12 @@ import java.util.Locale @Suppress("DEPRECATION", "ConstPropertyName") object ChapterProvider { //用于图片字的替换 - private const val srcReplaceChar = "▩" + const val srcReplaceChar = "▩" //用于评论按钮的替换 - private const val reviewChar = "▨" + const val reviewChar = "▨" - private const val indentChar = " " + const val indentChar = " " @JvmStatic var viewWidth = 0 @@ -94,25 +95,31 @@ object ChapterProvider { private set @JvmStatic - private var paragraphSpacing = 0 + var paragraphSpacing = 0 + private set @JvmStatic - private var titleTopSpacing = 0 + var titleTopSpacing = 0 + private set @JvmStatic - private var titleBottomSpacing = 0 + var titleBottomSpacing = 0 + private set @JvmStatic - private var indentCharWidth = 0f + var indentCharWidth = 0f + private set @JvmStatic var titlePaintTextHeight = 0f + private set @JvmStatic var contentPaintTextHeight = 0f + private set @JvmStatic - private var titlePaintFontMetrics = FontMetrics() + var titlePaintFontMetrics = FontMetrics() @JvmStatic var contentPaintFontMetrics = FontMetrics() @@ -278,7 +285,7 @@ object ChapterProvider { textPage.text = stringBuilder.toString() textPages.forEachIndexed { index, item -> item.index = index - item.pageSize = textPages.size + //item.pageSize = textPages.size item.chapterIndex = bookChapter.index item.chapterSize = chapterSize item.title = displayTitle @@ -290,7 +297,8 @@ object ChapterProvider { return TextChapter( bookChapter, bookChapter.index, displayTitle, - textPages, chapterSize, + //textPages, + chapterSize, bookContent.sameTitleRemoved, bookChapter.isVip, bookChapter.isPay, @@ -298,6 +306,30 @@ object ChapterProvider { ) } + fun getTextChapterAsync( + book: Book, + bookChapter: BookChapter, + displayTitle: String, + bookContent: BookContent, + chapterSize: Int, + scope: CoroutineScope + ): TextChapter { + + val textChapter = TextChapter( + bookChapter, + bookChapter.index, displayTitle, + chapterSize, + bookContent.sameTitleRemoved, + bookChapter.isVip, + bookChapter.isPay, + bookContent.effectiveReplaceRules + ).apply { + createLayout(scope, book, bookContent) + } + + return textChapter + } + /** * 排版图片 */ diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt new file mode 100644 index 000000000..26ada6c5e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt @@ -0,0 +1,22 @@ +package io.legado.app.ui.book.read.page.provider + +import io.legado.app.ui.book.read.page.entities.TextPage + +interface LayoutProgressListener { + + /** + * 单页排版完成 + */ + fun onLayoutPageCompleted(index: Int, page: TextPage) + + /** + * 全部排版完成 + */ + fun onLayoutCompleted() + + /** + * 排版出现异常 + */ + fun onLayoutException(e: Throwable) + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt new file mode 100644 index 000000000..f37ed0c48 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt @@ -0,0 +1,752 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Paint +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.book.BookContent +import io.legado.app.help.book.BookHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.ImageProvider +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.entities.column.ImageColumn +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.dpToPx +import io.legado.app.utils.fastSum +import io.legado.app.utils.splitNotBlank +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.util.LinkedList +import java.util.Locale + +class TextChapterLayout( + scope: CoroutineScope, + private val textChapter: TextChapter, + private val textPages: ArrayList, + private val book: Book, + private val bookContent: BookContent, +) { + + @Volatile + private var listener: LayoutProgressListener? = textChapter + + private val paddingLeft = ChapterProvider.paddingLeft + private val paddingTop = ChapterProvider.paddingTop + + private val titlePaint = ChapterProvider.titlePaint + private val titlePaintTextHeight = ChapterProvider.titlePaintTextHeight + private val titlePaintFontMetrics = ChapterProvider.titlePaintFontMetrics + + private val contentPaint = ChapterProvider.contentPaint + private val contentPaintTextHeight = ChapterProvider.contentPaintTextHeight + private val contentPaintFontMetrics = ChapterProvider.contentPaintFontMetrics + + private val titleTopSpacing = ChapterProvider.titleTopSpacing + private val titleBottomSpacing = ChapterProvider.titleBottomSpacing + private val lineSpacingExtra = ChapterProvider.lineSpacingExtra + private val paragraphSpacing = ChapterProvider.paragraphSpacing + + private val visibleHeight = ChapterProvider.visibleHeight + private val visibleWidth = ChapterProvider.visibleWidth + + private val viewWidth = ChapterProvider.viewWidth + private val doublePage = ChapterProvider.doublePage + private val indentCharWidth = ChapterProvider.indentCharWidth + private val stringBuilder = StringBuilder() + + private var isCompleted = false + private var exception: Throwable? = null + private val job: Coroutine<*> + private val bookChapter inline get() = textChapter.chapter + private val displayTitle inline get() = textChapter.title + private val chaptersSize inline get() = textChapter.chaptersSize + + + init { + job = Coroutine.async(scope) { + launch { + val bookSource = book.getBookSource() ?: return@launch + BookHelp.saveImages(bookSource, book, bookChapter, bookContent.toString()) + } + getTextChapter(book, bookChapter, displayTitle, bookContent) + }.onError { + exception = it + onException(it) + }.onFinally { + isCompleted = true + } + } + + fun setProgressListener(l: LayoutProgressListener) { + try { + if (isCompleted) { + l.onLayoutCompleted() + } else if (exception != null) { + l.onLayoutException(exception!!) + } else { + listener = l + } + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } + } + + fun cancel() { + job.cancel() + } + + private fun onPageCompleted() { + val textPage = textPages.last() + textPage.index = textPages.lastIndex + textPage.chapterIndex = bookChapter.index + textPage.chapterSize = chaptersSize + textPage.title = displayTitle + textPage.doublePage = doublePage + textPage.paddingTop = paddingTop + textPage.isCompleted = true + textPage.textChapter = textChapter + textPage.upLinesPosition() + try { + listener?.onLayoutPageCompleted(textPages.lastIndex, textPage) + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } + } + + private fun onCompleted() { + try { + listener?.onLayoutCompleted() + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } finally { + listener = null + } + } + + private fun onException(e: Throwable) { + if (e is CancellationException) { + return + } + try { + listener?.onLayoutException(e) + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } finally { + listener = null + } + } + + /** + * 获取拆分完的章节数据 + */ + private suspend fun getTextChapter( + book: Book, + bookChapter: BookChapter, + displayTitle: String, + bookContent: BookContent, + ) { + val contents = bookContent.textList + var absStartX = paddingLeft + var durY = 0f + textPages.add(TextPage()) + if (ReadBookConfig.titleMode != 2 || bookChapter.isVolume) { + //标题非隐藏 + displayTitle.splitNotBlank("\n").forEach { text -> + setTypeText( + book, absStartX, durY, + if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text, + titlePaint, + titlePaintTextHeight, + titlePaintFontMetrics, + isTitle = true, + emptyContent = contents.isEmpty(), + isVolumeTitle = bookChapter.isVolume + ).let { + absStartX = it.first + durY = it.second + } + } + textPages.last().lines.last().isParagraphEnd = true + stringBuilder.append("\n") + durY += titleBottomSpacing + } + val sb = StringBuffer() + contents.forEach { content -> + if (book.getImageStyle().equals(Book.imgStyleText, true)) { + //图片样式为文字嵌入类型 + var text = content.replace(ChapterProvider.srcReplaceChar, "▣") + val srcList = LinkedList() + sb.setLength(0) + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { src -> + srcList.add(src) + matcher.appendReplacement(sb, ChapterProvider.srcReplaceChar) + } + } + matcher.appendTail(sb) + text = sb.toString() + setTypeText( + book, + absStartX, + durY, + text, + contentPaint, + contentPaintTextHeight, + contentPaintFontMetrics, + srcList = srcList + ).let { + absStartX = it.first + durY = it.second + } + } else { + val matcher = AppPattern.imgPattern.matcher(content) + var start = 0 + while (matcher.find()) { + val text = content.substring(start, matcher.start()) + if (text.isNotBlank()) { + setTypeText( + book, + absStartX, + durY, + text, + contentPaint, + contentPaintTextHeight, + contentPaintFontMetrics + ).let { + absStartX = it.first + durY = it.second + } + } + 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) { + val text = content.substring(start, content.length) + if (text.isNotBlank()) { + setTypeText( + book, absStartX, durY, + if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text, + contentPaint, + contentPaintTextHeight, + contentPaintFontMetrics + ).let { + absStartX = it.first + durY = it.second + } + } + } + } + textPages.last().lines.last().isParagraphEnd = true + stringBuilder.append("\n") + } + 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() + onPageCompleted() + onCompleted() + } + + /** + * 排版图片 + */ + private suspend fun setTypeImage( + book: Book, + src: String, + x: Int, + y: Float, + textPages: ArrayList, + textHeight: Float, + stringBuilder: StringBuilder, + imageStyle: String?, + ): 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() + if (textPage.height < durY) { + textPage.height = durY + } + textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } + stringBuilder.clear() + onPageCompleted() + textPages.add(TextPage()) + durY = 0f + } + var height = size.height + var width = size.width + when (imageStyle?.uppercase(Locale.ROOT)) { + Book.imgStyleFull -> { + width = visibleWidth + height = size.height * visibleWidth / size.width + } + + else -> { + if (size.width > visibleWidth) { + height = size.height * visibleWidth / size.width + width = visibleWidth + } + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + if (durY + height > visibleHeight) { + val textPage = textPages.last() + 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() + onPageCompleted() + textPages.add(TextPage()) + } + // 双页的 durY 不正确,可能会小于实际高度 + if (textPage.height < durY) { + textPage.height = durY + } + durY = 0f + } + } + } + val textLine = TextLine(isImage = true) + textLine.lineTop = durY + paddingTop + durY += height + textLine.lineBottom = durY + paddingTop + val (start, end) = if (visibleWidth > width) { + val adjustWidth = (visibleWidth - width) / 2f + Pair(adjustWidth, adjustWidth + width) + } else { + Pair(0f, width.toFloat()) + } + textLine.addColumn( + ImageColumn(start = x + start, end = x + end, src = src) + ) + calcTextLinePosition(textPages, textLine, stringBuilder.length) + stringBuilder.append(" ") // 确保翻页时索引计算正确 + textPages.last().addLine(textLine) + } + return absStartX to durY + textHeight * paragraphSpacing / 10f + } + + /** + * 排版文字 + */ + @Suppress("DEPRECATION") + private suspend fun setTypeText( + book: Book, + x: Int, + y: Float, + text: String, + textPaint: TextPaint, + textHeight: Float, + fontMetrics: Paint.FontMetrics, + isTitle: Boolean = false, + emptyContent: Boolean = false, + isVolumeTitle: Boolean = false, + srcList: LinkedList? = null + ): Pair { + var absStartX = x + val layout = if (ReadBookConfig.useZhLayout) { + ZhLayout(text, textPaint, visibleWidth) + } else { + StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true) + } + var durY = when { + //标题y轴居中 + emptyContent && textPages.size == 1 -> { + val textPage = textPages.last() + if (textPage.lineSize == 0) { + val ty = (visibleHeight - layout.lineCount * textHeight) / 2 + if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat() + } else { + var textLayoutHeight = layout.lineCount * textHeight + val fistLine = textPage.getLine(0) + if (fistLine.lineTop < textLayoutHeight + titleTopSpacing) { + textLayoutHeight = fistLine.lineTop - titleTopSpacing + } + textPage.lines.forEach { + it.lineTop -= textLayoutHeight + it.lineBase -= textLayoutHeight + it.lineBottom -= textLayoutHeight + } + y - textLayoutHeight + } + } + + isTitle && textPages.size == 1 && textPages.last().lines.isEmpty() -> + y + titleTopSpacing + + else -> y + } + for (lineIndex in 0 until layout.lineCount) { + val textLine = TextLine(isTitle = isTitle) + if (durY + textHeight > visibleHeight) { + val textPage = textPages.last() + 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() + onPageCompleted() + //新建页面 + 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 lineText = text.substring(lineStart, lineEnd) + val (words, widths) = measureTextSplit(lineText, textPaint) + val desiredWidth = widths.fastSum() + when { + lineIndex == 0 && layout.lineCount > 1 && !isTitle -> { + //第一行 非标题 + textLine.text = lineText + addCharsToLineFirst( + book, absStartX, textLine, words, + desiredWidth, widths, srcList + ) + } + + lineIndex == layout.lineCount - 1 -> { + //最后一行 + textLine.text = lineText + //标题x轴居中 + val startX = if ( + isTitle && + (ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle) + ) { + (visibleWidth - desiredWidth) / 2 + } else { + 0f + } + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, !isTitle && lineIndex == 0, widths, srcList + ) + } + + else -> { + if ( + isTitle && + (ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle) + ) { + //标题居中 + val startX = (visibleWidth - desiredWidth) / 2 + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, false, widths, srcList + ) + } else { + //中间行 + textLine.text = lineText + addCharsToLineMiddle( + book, absStartX, textLine, words, + desiredWidth, 0f, widths, srcList + ) + } + } + } + if (doublePage) { + textLine.isLeftLine = absStartX < viewWidth / 2 + } + calcTextLinePosition(textPages, textLine, stringBuilder.length) + stringBuilder.append(lineText) + textLine.upTopBottom(durY, textHeight, fontMetrics) + val textPage = textPages.last() + textPage.addLine(textLine) + durY += textHeight * lineSpacingExtra + if (textPage.height < durY) { + textPage.height = durY + } + } + durY += textHeight * paragraphSpacing / 10f + return Pair(absStartX, durY) + } + + private fun calcTextLinePosition( + textPages: ArrayList, + textLine: TextLine, + sbLength: Int + ) { + val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 } + ?: textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull { it.paragraphNum > 0 } + val paragraphNum = when { + lastLine == null -> 1 + lastLine.isParagraphEnd -> lastLine.paragraphNum + 1 + else -> lastLine.paragraphNum + } + textLine.paragraphNum = paragraphNum + textLine.chapterPosition = + (textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull()?.run { + chapterPosition + charSize + if (isParagraphEnd) 1 else 0 + } ?: 0) + sbLength + textLine.pagePosition = sbLength + } + + /** + * 有缩进,两端对齐 + */ + private suspend fun addCharsToLineFirst( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + /**自然排版长度**/ + desiredWidth: Float, + textWidths: List, + srcList: LinkedList? + ) { + var x = 0f + if (!ReadBookConfig.textFullJustify) { + addCharsToLineNatural( + book, absStartX, textLine, words, + x, true, textWidths, srcList + ) + return + } + val bodyIndent = ReadBookConfig.paragraphIndent + for (i in bodyIndent.indices) { + val x1 = x + indentCharWidth + textLine.addColumn( + TextColumn( + charData = ChapterProvider.indentChar, + start = absStartX + x, + end = absStartX + x1 + ) + ) + x = x1 + textLine.indentWidth = x + } + 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, + desiredWidth, x, textWidths1, srcList + ) + } + } + + /** + * 无缩进,两端对齐 + */ + private suspend fun addCharsToLineMiddle( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + /**自然排版长度**/ + desiredWidth: Float, + /**起始x坐标**/ + startX: Float, + textWidths: List, + srcList: LinkedList? + ) { + if (!ReadBookConfig.textFullJustify) { + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, false, textWidths, srcList + ) + return + } + val residualWidth = visibleWidth - desiredWidth + val spaceSize = words.count { it == " " } + if (spaceSize > 1) { + val d = residualWidth / spaceSize + var x = startX + 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 { + (x + cw) + } + addCharToLine( + book, absStartX, textLine, char, + x, x1, index + 1 == words.size, srcList + ) + x = x1 + } + } else { + val gapCount: Int = words.lastIndex + val d = residualWidth / gapCount + var x = startX + 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, + x, x1, index + 1 == words.size, srcList + ) + x = x1 + } + } + exceed(absStartX, textLine, words) + } + + /** + * 自然排列 + */ + private suspend fun addCharsToLineNatural( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + startX: Float, + hasIndent: Boolean, + textWidths: List, + srcList: LinkedList? + ) { + val indentLength = ReadBookConfig.paragraphIndent.length + var x = startX + 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 + if (hasIndent && index == indentLength - 1) { + textLine.indentWidth = x + } + } + exceed(absStartX, textLine, words) + } + + /** + * 添加字符 + */ + private suspend fun addCharToLine( + book: Book, + absStartX: Int, + textLine: TextLine, + char: String, + xStart: Float, + xEnd: Float, + isLineEnd: Boolean, + srcList: LinkedList? + ) { + val column = when { + srcList != null && char == ChapterProvider.srcReplaceChar -> { + val src = srcList.removeFirst() + ImageProvider.cacheImage(book, src, ReadBook.bookSource) + ImageColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + src = src + ) + } + + isLineEnd && char == ChapterProvider.reviewChar -> { + ReviewColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + count = 100 + ) + } + + else -> { + TextColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + charData = char + ) + } + } + textLine.addColumn(column) + } + + /** + * 超出边界处理 + */ + private fun exceed(absStartX: Int, textLine: TextLine, words: List) { + val visibleEnd = absStartX + visibleWidth + val endX = textLine.columns.lastOrNull()?.end ?: return + if (endX > visibleEnd) { + val cc = (endX - visibleEnd) / words.size + for (i in 0..words.lastIndex) { + textLine.getColumnReverseAt(i).let { + val py = cc * (words.size - i) + it.start -= py + it.end -= py + } + } + } + } + + private fun measureTextSplit( + text: String, + paint: TextPaint + ): Pair, ArrayList> { + val length = text.length + val widthsArray = FloatArray(length) + paint.getTextWidths(text, widthsArray) + val clusterCount = widthsArray.count { it > 0f } + val widths = ArrayList(clusterCount) + val stringList = ArrayList(clusterCount) + var i = 0 + while (i < length) { + val clusterBaseIndex = i++ + widths.add(widthsArray[clusterBaseIndex]) + while (i < length && widthsArray[i] == 0f) { + i++ + } + stringList.add(text.substring(clusterBaseIndex, i)) + } + return stringList to widths + } + +} 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 f69b022b1..b51c8228c 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 @@ -35,12 +35,16 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) { return if (hasNext()) { + val pageIndex = pageIndex if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) { if ((currentChapter == null || isScroll) && nextChapter == null) { return@with false } ReadBook.moveToNextChapter(upContent, false) } else { + if (pageIndex < 0 || currentChapter?.isLastIndexCurrent(pageIndex) == true) { + return@with false + } ReadBook.setPageIndex(pageIndex.plus(1)) } if (upContent) upContent(resetPageOffset = false) @@ -55,6 +59,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource if (currentChapter == null && prevChapter == null) { return@with false } + if (prevChapter != null && prevChapter?.isCompleted == false) { + return@with false + } ReadBook.moveToPrevChapter(upContent, upContentInPlace = false) } else { if (currentChapter == null) { @@ -74,7 +81,8 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource return@with TextPage(text = it).format() } currentChapter?.let { - return@with it.getPage(pageIndex) ?: TextPage(title = it.title).format() + return@with it.getPage(pageIndex) + ?: TextPage(title = it.title).apply { textChapter = it }.format() } return TextPage().format() } @@ -90,6 +98,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource return@with it.getPage(pageIndex + 1)?.removePageAloudSpan() ?: TextPage(title = it.title).format() } + if (!it.isCompleted) { + return@with TextPage(title = it.title).format() + } } nextChapter?.let { return@with it.getPage(0)?.removePageAloudSpan() @@ -109,6 +120,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource return@with it.getPage(pageIndex - 1)?.removePageAloudSpan() ?: TextPage(title = it.title).format() } + if (!it.isCompleted) { + return@with TextPage(title = it.title).format() + } } prevChapter?.let { return@with it.lastPage?.removePageAloudSpan() @@ -125,6 +139,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource return@with it.getPage(pageIndex + 2)?.removePageAloudSpan() ?: TextPage(title = it.title).format() } + if (!it.isCompleted) { + return@with TextPage(title = it.title).format() + } nextChapter?.let { nc -> if (pageIndex < it.pageSize - 1) { return@with nc.getPage(0)?.removePageAloudSpan() diff --git a/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt index e8e7b87d7..0dfb7a8c6 100644 --- a/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt @@ -13,6 +13,12 @@ inline fun List.fastBinarySearch( toIndex: Int = size, comparison: (T) -> Int ): Int { + when { + fromIndex > toIndex -> throw IllegalArgumentException("fromIndex ($fromIndex) is greater than toIndex ($toIndex).") + fromIndex < 0 -> throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than zero.") + toIndex > size -> throw IndexOutOfBoundsException("toIndex ($toIndex) is greater than size ($size).") + } + var low = fromIndex var high = toIndex - 1 diff --git a/app/src/main/java/io/legado/app/utils/Debounce.kt b/app/src/main/java/io/legado/app/utils/Debounce.kt index 0d5527746..ab1e4cc3f 100644 --- a/app/src/main/java/io/legado/app/utils/Debounce.kt +++ b/app/src/main/java/io/legado/app/utils/Debounce.kt @@ -119,3 +119,11 @@ open class Debounce( } } + +fun debounce( + wait: Long = 0L, + maxWait: Long = -1L, + leading: Boolean = false, + trailing: Boolean = true, + func: () -> T +) = Debounce(wait, maxWait, leading, trailing, func) diff --git a/app/src/main/java/io/legado/app/utils/Throttle.kt b/app/src/main/java/io/legado/app/utils/Throttle.kt index 079c9d7d8..bd8cc8266 100644 --- a/app/src/main/java/io/legado/app/utils/Throttle.kt +++ b/app/src/main/java/io/legado/app/utils/Throttle.kt @@ -7,3 +7,10 @@ class Throttle( trailing: Boolean = true, func: () -> T ) : Debounce(wait, wait, leading, trailing, func) + +fun throttle( + wait: Long = 0L, + leading: Boolean = true, + trailing: Boolean = true, + func: () -> T +) = Throttle(wait, leading, trailing, func)