diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index d8995cec7f39..fd109f3eff14 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -117,7 +117,6 @@ object PreferKey { const val welcomeShowIconDark = "welcomeShowIconDark" const val pageTouchSlop = "pageTouchSlop" const val showAddToShelfAlert = "showAddToShelfAlert" - const val asyncLoadImage = "asyncLoadImage" const val ignoreAudioFocus = "ignoreAudioFocus" const val parallelExportBook = "parallelExportBook" const val progressBarBehavior = "progressBarBehavior" diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 1ce8d8a80263..b719f1d45f45 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -137,6 +137,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { } } + val textSelectAble: Boolean + get() = appCtx.getPrefBoolean(PreferKey.textSelectAble, true) + val isTransparentStatusBar: Boolean get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true) @@ -457,8 +460,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { val showAddToShelfAlert get() = appCtx.getPrefBoolean(PreferKey.showAddToShelfAlert, true) - val asyncLoadImage get() = appCtx.getPrefBoolean(PreferKey.asyncLoadImage, false) - val ignoreAudioFocus get() = appCtx.getPrefBoolean(PreferKey.ignoreAudioFocus, false) val onlyLatestBackup get() = appCtx.getPrefBoolean(PreferKey.onlyLatestBackup, true) 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 1bb69b7aec22..c0a99fb7fa2b 100644 --- a/app/src/main/java/io/legado/app/model/ImageProvider.kt +++ b/app/src/main/java/io/legado/app/model/ImageProvider.kt @@ -2,13 +2,10 @@ package io.legado.app.model import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.os.Build import android.util.Size import androidx.collection.LruCache import io.legado.app.R -import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog.putDebug -import io.legado.app.constant.PageAnim import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.exception.NoStackTraceException @@ -16,10 +13,8 @@ import io.legado.app.help.book.BookHelp import io.legado.app.help.book.isEpub import io.legado.app.help.book.isPdf import io.legado.app.help.config.AppConfig -import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.localBook.EpubFile import io.legado.app.model.localBook.PdfFile -import io.legado.app.utils.BitmapCache import io.legado.app.utils.BitmapUtils import io.legado.app.utils.FileUtils import io.legado.app.utils.SvgUtils @@ -29,8 +24,6 @@ import kotlinx.coroutines.withContext import splitties.init.appCtx import java.io.File import java.io.FileOutputStream -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.min object ImageProvider { @@ -50,12 +43,6 @@ object ImageProvider { } return AppConfig.bitmapCacheSize * M } - private val maxCacheSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { - min(128 * M, Runtime.getRuntime().maxMemory().toInt()) - } else { - 256 * M - } - private val asyncLoadingImages = ConcurrentHashMap.newKeySet() val bitmapLruCache = object : LruCache(cacheSize) { override fun sizeOf(filePath: String, bitmap: Bitmap): Int { @@ -70,8 +57,7 @@ object ImageProvider { ) { //错误图片不能释放,占位用,防止一直重复获取图片 if (oldBitmap != errorBitmap) { - BitmapCache.add(oldBitmap) - //oldBitmap.recycle() + oldBitmap.recycle() //putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath") //putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes") } @@ -160,9 +146,8 @@ object ImageProvider { book: Book, src: String, width: Int, - height: Int? = null, - block: (() -> Unit)? = null - ): Bitmap? { + height: Int? = null + ): Bitmap { //src为空白时 可能被净化替换掉了 或者规则失效 if (book.getUseReplaceRule() && src.isBlank()) { book.setUseReplaceRule(false) @@ -174,32 +159,6 @@ object ImageProvider { //bitmapLruCache的key同一改成缓存文件的路径 val cacheBitmap = getNotRecycled(vFile.absolutePath) if (cacheBitmap != null) return cacheBitmap - if (height != null && AppConfig.asyncLoadImage && ReadBook.pageAnim() == PageAnim.scrollPageAnim) { - if (asyncLoadingImages.contains(vFile.absolutePath)) { - return null - } - asyncLoadingImages.add(vFile.absolutePath) - Coroutine.async { - BitmapUtils.decodeBitmap(vFile.absolutePath, width, height) - ?: SvgUtils.createBitmap(vFile.absolutePath, width, height) - ?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap)) - }.onSuccess { - bitmapLruCache.run { - if (maxSize() < maxCacheSize && size() + it.byteCount > maxSize() && putCount() - evictionCount() < 5) { - resize(min(maxCacheSize, maxSize() + it.byteCount)) - AppLog.put("图片缓存太小,自动扩增至${(maxSize() / M)}MB。") - } - } - bitmapLruCache.put(vFile.absolutePath, it) - }.onError { - //错误图片占位,防止重复获取 - bitmapLruCache.put(vFile.absolutePath, errorBitmap) - }.onFinally { - asyncLoadingImages.remove(vFile.absolutePath) - block?.invoke() - } - return null - } return kotlin.runCatching { val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height) ?: SvgUtils.createBitmap(vFile.absolutePath, width, height) @@ -214,7 +173,6 @@ object ImageProvider { 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 20bb7f48a332..1294454de3d7 100644 --- a/app/src/main/java/io/legado/app/model/ReadBook.kt +++ b/app/src/main/java/io/legado/app/model/ReadBook.kt @@ -29,6 +29,8 @@ import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch import splitties.init.appCtx +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors import kotlin.math.min @@ -63,6 +65,7 @@ object ReadBook : CoroutineScope by MainScope() { val downloadFailChapters = hashMapOf() var contentProcessor: ContentProcessor? = null val downloadScope = CoroutineScope(SupervisorJob() + IO) + val executor: ExecutorService = Executors.newSingleThreadExecutor() //暂时保存跳转前进度 fun saveCurrentBookProcess() { @@ -158,10 +161,10 @@ object ReadBook : CoroutineScope by MainScope() { } fun upReadTime() { - if (!AppConfig.enableReadRecord) { - return - } - Coroutine.async(executeContext = IO) { + executor.execute { + if (!AppConfig.enableReadRecord) { + return@execute + } readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime readStartTime = System.currentTimeMillis() readRecord.lastRead = System.currentTimeMillis() @@ -275,7 +278,7 @@ object ReadBook : CoroutineScope by MainScope() { textChapter.getPage(index - 2)?.recyclePictures() } if (index < pageIndex) { - textChapter.getPage(index + 2)?.recyclePictures() + textChapter.getPage(index + 3)?.recyclePictures() } } durChapterPos = curTextChapter?.getReadLength(index) ?: index @@ -536,8 +539,8 @@ object ReadBook : CoroutineScope by MainScope() { } fun saveRead(pageChanged: Boolean = false) { - Coroutine.async(executeContext = IO) { - val book = book ?: return@async + executor.execute { + val book = book ?: return@execute book.lastCheckCount = 0 book.durChapterTime = System.currentTimeMillis() val chapterChanged = book.durChapterIndex != durChapterIndex @@ -560,26 +563,29 @@ object ReadBook : CoroutineScope by MainScope() { */ private fun preDownload() { if (book?.isLocal == true) return - if (AppConfig.preDownloadNum < 2) { - return - } - preDownloadTask?.cancel() - preDownloadTask = Coroutine.async(executeContext = IO) { - //预下载 - launch { - val maxChapterIndex = min(durChapterIndex + AppConfig.preDownloadNum, chapterSize) - for (i in durChapterIndex.plus(2)..maxChapterIndex) { - if (downloadedChapters.contains(i)) continue - if ((downloadFailChapters[i] ?: 0) >= 3) continue - downloadIndex(i) - } + executor.execute { + if (AppConfig.preDownloadNum < 2) { + return@execute } - launch { - val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum) - for (i in durChapterIndex.minus(2) downTo minChapterIndex) { - if (downloadedChapters.contains(i)) continue - if ((downloadFailChapters[i] ?: 0) >= 3) continue - downloadIndex(i) + preDownloadTask?.cancel() + preDownloadTask = Coroutine.async(executeContext = IO) { + //预下载 + launch { + val maxChapterIndex = + min(durChapterIndex + AppConfig.preDownloadNum, chapterSize) + for (i in durChapterIndex.plus(2)..maxChapterIndex) { + if (downloadedChapters.contains(i)) continue + if ((downloadFailChapters[i] ?: 0) >= 3) continue + downloadIndex(i) + } + } + launch { + val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum) + for (i in durChapterIndex.minus(2) downTo minChapterIndex) { + if (downloadedChapters.contains(i)) continue + if ((downloadFailChapters[i] ?: 0) >= 3) continue + downloadIndex(i) + } } } } diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index 7070861be09c..a8ef1f579073 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -91,7 +91,7 @@ class HttpReadAloudService : BaseReadAloudService(), playIndexJob?.cancel() } - private fun playNext() { + private fun updateNextPos() { readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos paragraphStartPos = 0 if (nowSpeak < contentList.lastIndex) { @@ -355,14 +355,14 @@ class HttpReadAloudService : BaseReadAloudService(), Player.STATE_ENDED -> { // 结束 playErrorNo = 0 - playNext() + updateNextPos() } } } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return - playNext() + updateNextPos() upPlayPos() } @@ -375,7 +375,8 @@ class HttpReadAloudService : BaseReadAloudService(), AppLog.put("朗读连续5次错误, 最后一次错误代码(${error.localizedMessage})", error) ReadAloud.pause(this) } else { - playNext() + updateNextPos() + exoPlayer.seekToNextMediaItem() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt index a7735129b2f4..8496ae09111f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt @@ -58,6 +58,12 @@ abstract class BaseReadBookActivity : override val binding by viewBinding(ActivityBookReadBinding::inflate) override val viewModel by viewModels() var bottomDialog = 0 + set(value) { + if (field != value) { + field = value + onBottomDialogChange() + } + } private val selectBookFolderResult = registerForActivityResult(HandleFileContract()) { it.uri?.let { uri -> ReadBook.book?.let { book -> @@ -94,6 +100,21 @@ abstract class BaseReadBookActivity : } } + private fun onBottomDialogChange() { + when (bottomDialog) { + 0 -> onMenuHide() + 1 -> onMenuShow() + } + } + + open fun onMenuShow() { + + } + + open fun onMenuHide() { + + } + fun showPaddingConfig() { showDialogFragment() } 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 dd3295eb3fa6..55573449e223 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 @@ -76,7 +76,6 @@ import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_C import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.entities.PageDirection -import io.legado.app.ui.book.read.page.provider.TextPageFactory 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 @@ -95,7 +94,6 @@ import io.legado.app.utils.ACache import io.legado.app.utils.Debounce 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 @@ -118,6 +116,7 @@ import io.legado.app.utils.toastOnUi import io.legado.app.utils.visible import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -192,7 +191,6 @@ class ReadBookActivity : BaseReadBookActivity(), } private var menu: Menu? = null private var backupJob: Job? = null - private var keepScreenJon: Job? = null private var tts: TTS? = null val textActionMenu: TextActionMenu by lazy { TextActionMenu(this, this) @@ -202,8 +200,7 @@ class ReadBookActivity : BaseReadBookActivity(), } override val isInitFinish: Boolean get() = viewModel.isInitFinish override val isScroll: Boolean get() = binding.readView.isScroll - override var autoPageProgress = 0 - override var isAutoPage = false + private val isAutoPage get() = binding.readView.isAutoPage override var isShowingSearchResult = false override var isSelectingSearchResult = false set(value) { @@ -212,7 +209,8 @@ class ReadBookActivity : BaseReadBookActivity(), private val timeBatteryReceiver = TimeBatteryReceiver() private var screenTimeOut: Long = 0 private var loadStates: Boolean = false - override val pageFactory: TextPageFactory get() = binding.readView.pageFactory + override val pageFactory get() = binding.readView.pageFactory + override val pageDelegate get() = binding.readView.pageDelegate override val headerHeight: Int get() = binding.readView.curPage.headerHeight private val menuLayoutIsVisible get() = bottomDialog > 0 || binding.readMenu.isVisible private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } } @@ -220,10 +218,9 @@ class ReadBookActivity : BaseReadBookActivity(), 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 val executor = ReadBook.executor //恢复跳转前进度对话框的交互结果 private var confirmRestoreProcess: Boolean? = null @@ -404,6 +401,11 @@ class ReadBookActivity : BaseReadBookActivity(), } } + override fun onNightModeChanged(mode: Int) { + super.onNightModeChanged(mode) + binding.readView.invalidateTextPage() + } + /** * 菜单 */ @@ -907,7 +909,7 @@ class ReadBookActivity : BaseReadBookActivity(), } override fun upMenuView() { - lifecycleScope.launch { + handler.post { binding.readMenu.upBookView() } } @@ -940,9 +942,6 @@ class ReadBookActivity : BaseReadBookActivity(), success: (() -> Unit)? ) { lifecycleScope.launch { - if (relativePosition == 0) { - autoPageProgress = 0 - } binding.readView.upContent(relativePosition, resetPageOffset) upSeekBarProgress() loadStates = false @@ -965,9 +964,11 @@ class ReadBookActivity : BaseReadBookActivity(), */ override fun pageChanged() { pageChanged = true - lifecycleScope.launch { - autoPageProgress = 0 + binding.readView.onPageChange() + handler.post { upSeekBarProgress() + } + executor.execute { startBackupJob() } } @@ -1048,8 +1049,7 @@ class ReadBookActivity : BaseReadBookActivity(), if (isAutoPage) { autoPageStop() } else { - isAutoPage = true - autoPagePlus() + binding.readView.autoPager.start() binding.readMenu.setAutoPage(true) screenTimeOut = -1L screenOffTimerStart() @@ -1058,53 +1058,12 @@ class ReadBookActivity : BaseReadBookActivity(), override fun autoPageStop() { if (isAutoPage) { - isAutoPage = false - autoPageRenderer.stop() - binding.readView.invalidate() - binding.readView.clearNextPageBitmap() + binding.readView.autoPager.stop() binding.readMenu.setAutoPage(false) upScreenTimeOut() } } - private fun autoPagePlus() { - autoPageProgress = 0 - autoPageScrollOffset = 0.0 - autoPageRenderer.start() - } - - private fun doAutoPage(frameTime: Double) { - if (menuLayoutIsVisible) { - return - } - if (binding.readView.run { isScroll && pageDelegate?.isRunning == true }) { - return - } - val readTime = ReadBookConfig.autoReadSpeed * 1000.0 - val height = binding.readView.height - autoPageScrollOffset += height / readTime * frameTime - if (autoPageScrollOffset < 1) { - return - } - val scrollOffset = autoPageScrollOffset.toInt() - autoPageScrollOffset -= scrollOffset - if (binding.readView.isScroll) { - binding.readView.curPage.scroll(-scrollOffset) - } else { - autoPageProgress += scrollOffset - if (autoPageProgress >= height) { - autoPageProgress = 0 - if (!binding.readView.fillPage(PageDirection.NEXT)) { - autoPageStop() - } else { - binding.readView.clearNextPageBitmap() - } - } else { - binding.readView.invalidate() - } - } - } - override fun openSourceEditActivity() { ReadBook.bookSource?.let { sourceEditActivity.launch { @@ -1409,6 +1368,14 @@ class ReadBookActivity : BaseReadBookActivity(), skipToSearch(searchResult) } + override fun onMenuShow() { + binding.readView.autoPager.pause() + } + + override fun onMenuHide() { + binding.readView.autoPager.resume() + } + /* 全文搜索跳转 */ private fun skipToSearch(searchResult: SearchResult) { val previousResult = binding.searchMenu.previousSearchResult @@ -1585,9 +1552,6 @@ class ReadBookActivity : BaseReadBookActivity(), observeEvent(EventBus.UP_SEEK_BAR) { binding.readMenu.upSeekBar() } - observeEvent(EventBus.RECREATE) { - binding.readView.invalidateTextPage() - } } private fun upScreenTimeOut() { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt index bca34bf8bf4b..5e163456cbbb 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt @@ -27,13 +27,38 @@ import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ThemeConfig import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.theme.* +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.buttonDisabledColor +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor import io.legado.app.model.ReadBook import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.browser.WebViewActivity import io.legado.app.ui.widget.seekbar.SeekBarChangeListener -import io.legado.app.utils.* -import splitties.views.* +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.ConstraintModify +import io.legado.app.utils.activity +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.loadAnimation +import io.legado.app.utils.modifyBegin +import io.legado.app.utils.navigationBarGravity +import io.legado.app.utils.navigationBarHeight +import io.legado.app.utils.openUrl +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.startActivity +import io.legado.app.utils.visible +import splitties.views.bottomPadding +import splitties.views.leftPadding +import splitties.views.onClick +import splitties.views.onLongClick +import splitties.views.padding +import splitties.views.rightPadding /** * 阅读界面菜单 @@ -273,6 +298,7 @@ class ReadMenu @JvmOverloads constructor( } fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) { + callBack.onMenuShow() this.visible() binding.titleBar.visible() binding.bottomMenu.visible() @@ -286,6 +312,7 @@ class ReadMenu @JvmOverloads constructor( } fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) { + callBack.onMenuHide() this.onMenuOutEnd = onMenuOutEnd if (this.isVisible) { if (anim) { @@ -558,6 +585,8 @@ class ReadMenu @JvmOverloads constructor( fun payAction() fun disableSource() fun skipToChapter(index: Int) + fun onMenuShow() + fun onMenuHide() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt index e3f0eeadeda8..6edf40f808f6 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt @@ -18,7 +18,13 @@ import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.model.ReadBook import io.legado.app.ui.book.searchContent.SearchResult -import io.legado.app.utils.* +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.activity +import io.legado.app.utils.invisible +import io.legado.app.utils.loadAnimation +import io.legado.app.utils.navigationBarGravity +import io.legado.app.utils.navigationBarHeight +import io.legado.app.utils.visible import splitties.views.bottomPadding import splitties.views.leftPadding import splitties.views.padding @@ -235,6 +241,8 @@ class SearchMenu @JvmOverloads constructor( fun exitSearchMenu() fun showMenuBar() fun navigateToSearch(searchResult: SearchResult, index: Int) + fun onMenuShow() + fun onMenuHide() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt b/app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt new file mode 100644 index 000000000000..7d5ebbd0a0a1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt @@ -0,0 +1,147 @@ +package io.legado.app.ui.book.read.page + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Picture +import android.graphics.Rect +import android.os.Build +import android.os.SystemClock +import androidx.core.graphics.withClip +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.utils.screenshot + +/** + * 自动翻页 + */ +class AutoPager(private val readView: ReadView) { + private var progress = 0 + var isRunning = false + private var isPausing = false + private var scrollOffsetRemain = 0.0 + private var scrollOffset = 0 + private var lastTimeMillis = 0L + private var bitmap: Bitmap? = null + private var picture: Picture? = null + private var pictureIsDirty = true + private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + private val rect = Rect() + private val paint by lazy { Paint() } + + fun start() { + isRunning = true + paint.color = ThemeStore.accentColor + lastTimeMillis = SystemClock.uptimeMillis() + readView.curPage.upSelectAble(false) + readView.invalidate() + } + + fun stop() { + if (!isRunning) { + return + } + isRunning = false + isPausing = false + readView.curPage.upSelectAble(AppConfig.textSelectAble) + readView.invalidate() + reset() + picture = null + } + + fun pause() { + if (!isRunning) { + return + } + isPausing = true + } + + fun resume() { + if (!isRunning) { + return + } + isPausing = false + lastTimeMillis = SystemClock.uptimeMillis() + readView.invalidate() + } + + fun reset() { + progress = 0 + scrollOffsetRemain = 0.0 + scrollOffset = 0 + bitmap?.recycle() + bitmap = null + pictureIsDirty = true + } + + fun onDraw(canvas: Canvas) { + if (!isRunning) { + return + } + + if (readView.isScroll) { + computeOffset() + if (!isPausing) readView.curPage.scroll(-scrollOffset) + } else { + val bottom = progress + val width = readView.width + if (atLeastApi23) { + if (picture == null) { + picture = Picture() + } + if (pictureIsDirty) { + pictureIsDirty = false + readView.nextPage.screenshot(picture!!) + } + canvas.withClip(0, 0, width, bottom) { + drawPicture(picture!!) + } + } else { + if (bitmap == null) { + bitmap = readView.nextPage.screenshot() + } + rect.set(0, 0, width, bottom) + canvas.drawBitmap(bitmap!!, rect, rect, null) + } + canvas.drawRect( + 0f, + bottom.toFloat() - 1, + width.toFloat(), + bottom.toFloat(), + paint + ) + if (!isPausing) readView.invalidate() + computeOffset() + } + + } + + private fun computeOffset() { + + val currentTime = SystemClock.uptimeMillis() + val elapsedTime = currentTime - lastTimeMillis + lastTimeMillis = currentTime + + val readTime = ReadBookConfig.autoReadSpeed * 1000.0 + val height = readView.height + scrollOffsetRemain += height / readTime * elapsedTime + if (scrollOffsetRemain < 1) { + return + } + scrollOffset = scrollOffsetRemain.toInt() + this.scrollOffsetRemain -= scrollOffset + if (!readView.isScroll) { + progress += scrollOffset + if (progress >= height) { + if (!readView.fillPage(PageDirection.NEXT)) { + stop() + } else { + reset() + } + } + } + } + +} 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 3a2fc507e604..cd6eff0fd45d 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,17 +3,15 @@ package io.legado.app.ui.book.read.page import android.content.Context import android.graphics.Canvas import android.graphics.Paint -import android.graphics.RectF import android.util.AttributeSet import android.view.MotionEvent import android.view.View 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.config.AppConfig import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.delegate.PageDelegate 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.TextPos @@ -25,19 +23,18 @@ 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.getCompatColor -import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.showDialogFragment import io.legado.app.utils.toastOnUi +import java.util.concurrent.Executors import kotlin.math.min /** * 阅读内容视图 */ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { - var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true) + var selectAble = AppConfig.textSelectAble val selectedPaint by lazy { Paint().apply { color = context.getCompatColor(R.color.btn_bg_press_2) @@ -45,7 +42,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at } } private var callBack: CallBack - private val visibleRect = RectF() + private val visibleRect = ChapterProvider.visibleRect val selectStart = TextPos(0, 0, 0) private val selectEnd = TextPos(0, 0, 0) var textPage: TextPage = TextPage() @@ -56,10 +53,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at var reverseEndCursor = false //滚动参数 - private val pageFactory: TextPageFactory get() = callBack.pageFactory + private val pageFactory get() = callBack.pageFactory + private val pageDelegate get() = callBack.pageDelegate private var pageOffset = 0 - private val pictureMirror = PictureMirror() - private val isNoAnim get() = ReadBook.pageAnim() == PageAnim.noAnim + private var autoPager: AutoPager? = null + private val renderRunnable by lazy { Runnable { preRenderPage() } } //绘制图片的paint val imagePaint by lazy { @@ -81,40 +79,22 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at invalidate() } - /** - * 更新绘制区域 - */ - fun upVisibleRect() { - visibleRect.set( - ChapterProvider.paddingLeft.toFloat(), - ChapterProvider.paddingTop.toFloat(), - ChapterProvider.visibleRight.toFloat(), - ChapterProvider.visibleBottom.toFloat() - ) - } - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) if (!isMainView) return ChapterProvider.upViewSize(w, h) - upVisibleRect() textPage.format() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) + autoPager?.onDraw(canvas) if (longScreenshot) { canvas.translate(0f, scrollY.toFloat()) } check(!visibleRect.isEmpty) { "visibleRect 为空" } canvas.clipRect(visibleRect) - if (!callBack.isScroll && !isNoAnim) { - pictureMirror.draw(canvas, width, height) { - drawPage(this) - } - } else { - drawPage(canvas) - } + drawPage(canvas) } /** @@ -159,17 +139,20 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at } if (!pageFactory.hasPrev() && pageOffset > 0) { pageOffset = 0 + pageDelegate?.abortAnim() } else if (!pageFactory.hasNext() && pageOffset < 0 && pageOffset + textPage.height < ChapterProvider.visibleHeight ) { val offset = (ChapterProvider.visibleHeight - textPage.height).toInt() pageOffset = min(0, offset) + pageDelegate?.abortAnim() } else if (pageOffset > 0) { if (pageFactory.moveToPrev(true)) { pageOffset -= textPage.height.toInt() } else { pageOffset = 0 + pageDelegate?.abortAnim() } } else if (pageOffset < -textPage.height) { val height = textPage.height @@ -177,14 +160,36 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at pageOffset += height.toInt() } else { pageOffset = -height.toInt() + pageDelegate?.abortAnim() } } invalidate() } - override fun invalidate() { - super.invalidate() - pictureMirror.invalidate() + fun submitPreRenderTask() { + renderThread.submit(renderRunnable) + } + + private fun preRenderPage() { + val view = this + var invalidate = false + pageFactory.run { + hasPrev() && prevPage.preRender(view) + if (curPage.preRender(view)) { + invalidate = true + } + if (hasNext() && nextPage.preRender(view) && callBack.isScroll) { + invalidate = true + } + if (hasNextPlus() && nextPlusPage.preRender(view) && callBack.isScroll + && relativeOffset(2) < ChapterProvider.visibleHeight + ) { + invalidate = true + } + if (invalidate) { + postInvalidate() + } + } } /** @@ -677,6 +682,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at } } + fun setAutoPager(autoPager: AutoPager?) { + this.autoPager = autoPager + } + override fun canScrollVertically(direction: Int): Boolean { return callBack.isScroll && pageFactory.hasNext() } @@ -696,9 +705,18 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at return callBack.onLongScreenshotTouchEvent(event) } + companion object { + private val renderThread by lazy { + Executors.newSingleThreadExecutor { + Thread(it, "TextPageRender") + } + } + } + interface CallBack { val headerHeight: Int val pageFactory: TextPageFactory + val pageDelegate: PageDelegate? val isScroll: Boolean var isSelectingSearchResult: Boolean fun upSelectedStart(x: Float, y: Float, top: Float) 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 3af462e624f0..0e2de7eee508 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 @@ -45,6 +45,8 @@ class PageView(context: Context) : FrameLayout(context) { private var tvBookName: BatteryView? = null private var tvTimeBattery: BatteryView? = null private var tvTimeBatteryP: BatteryView? = null + private var isMainView = false + var isScroll = false val headerHeight: Int get() { @@ -101,7 +103,6 @@ class PageView(context: Context) : FrameLayout(context) { vwTopDivider.gone(llHeader.isGone || !it.showHeaderLine) vwBottomDivider.gone(llFooter.isGone || !it.showFooterLine) } - contentTextView.upVisibleRect() upTime() upBattery(battery) } @@ -273,7 +274,13 @@ class PageView(context: Context) : FrameLayout(context) { * 设置内容 */ fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { - setProgress(textPage) + if (isMainView && !isScroll) { + setProgress(textPage) + } else { + post { + setProgress(textPage) + } + } if (resetPageOffset) { resetPageOffset() } @@ -325,6 +332,14 @@ class PageView(context: Context) : FrameLayout(context) { tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" } + fun setAutoPager(autoPager: AutoPager?) { + binding.contentTextView.setAutoPager(autoPager) + } + + fun submitPreRenderTask() { + binding.contentTextView.submitPreRenderTask() + } + /** * 滚动事件 */ @@ -376,6 +391,7 @@ class PageView(context: Context) : FrameLayout(context) { } fun markAsMainView() { + isMainView = true binding.contentTextView.isMainView = true } 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 1548cdadebed..0b21995d86d1 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 @@ -2,10 +2,7 @@ package io.legado.app.ui.book.read.page import android.annotation.SuppressLint import android.content.Context -import android.graphics.Bitmap import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Rect import android.graphics.RectF import android.os.Build import android.util.AttributeSet @@ -16,7 +13,6 @@ import android.widget.FrameLayout import io.legado.app.constant.PageAnim 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.ReadAloud import io.legado.app.model.ReadBook import io.legado.app.ui.book.read.ContentEditDialog @@ -35,9 +31,7 @@ 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.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.Locale import kotlin.math.abs @@ -104,16 +98,16 @@ class ReadView(context: Context, attrs: AttributeSet) : private val blRect = RectF() private val bcRect = RectF() private val brRect = RectF() - private val autoPageRect by lazy { Rect() } - private val autoPagePint by lazy { Paint().apply { color = context.accentColor } } private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) } - private var nextPageBitmap: Bitmap? = null + val autoPager = AutoPager(this) + val isAutoPage get() = autoPager.isRunning init { addView(nextPage) addView(curPage) addView(prevPage) prevPage.invisible() + nextPage.invisible() curPage.markAsMainView() if (!isInEditMode) { upBg() @@ -149,26 +143,11 @@ class ReadView(context: Context, attrs: AttributeSet) : override fun dispatchDraw(canvas: Canvas) { super.dispatchDraw(canvas) pageDelegate?.onDraw(canvas) - if (!isInEditMode && callBack.isAutoPage && !isScroll) { - // 自动翻页 - val bitmap = nextPageBitmap ?: nextPage.screenshot()?.also { nextPageBitmap = it } - bitmap?.let { - val bottom = callBack.autoPageProgress - autoPageRect.set(0, 0, width, bottom) - canvas.drawBitmap(it, autoPageRect, autoPageRect, null) - canvas.drawRect( - 0f, - bottom.toFloat() - 1, - width.toFloat(), - bottom.toFloat(), - autoPagePint - ) - } - } + autoPager.onDraw(canvas) } override fun computeScroll() { - pageDelegate?.scroll() + pageDelegate?.computeScroll() } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { @@ -262,6 +241,7 @@ class ReadView(context: Context, attrs: AttributeSet) : pageDelegate?.onTouch(event) } pressOnTextSelected = false + autoPager.resume() } } return true @@ -525,11 +505,12 @@ class ReadView(context: Context, attrs: AttributeSet) : } (pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage pageDelegate?.setViewSize(width, height) - if (pageDelegate is NoAnimPageDelegate) { - nextPage.invisible() + if (isScroll) { + curPage.setAutoPager(autoPager) } else { - nextPage.visible() + curPage.setAutoPager(null) } + curPage.isScroll = isScroll } /** @@ -541,12 +522,9 @@ class ReadView(context: Context, attrs: AttributeSet) : post { curPage.setContentDescription(pageFactory.curPage.text) } - if (isScroll && !callBack.isAutoPage) { + if (isScroll && !isAutoPage) { curPage.setContent(pageFactory.curPage, resetPageOffset) } else { - if (callBack.isAutoPage && relativePosition >= 0) { - clearNextPageBitmap() - } when (relativePosition) { -1 -> prevPage.setContent(pageFactory.prevPage) 1 -> nextPage.setContent(pageFactory.nextPage) @@ -649,11 +627,6 @@ class ReadView(context: Context, attrs: AttributeSet) : return curPage.getCurVisibleFirstLine()?.pagePosition ?: 0 } - fun clearNextPageBitmap() { - nextPageBitmap?.recycle() - nextPageBitmap = null - } - fun invalidateTextPage() { pageFactory.run { prevPage.invalidateAll() @@ -663,6 +636,19 @@ class ReadView(context: Context, attrs: AttributeSet) : } } + fun onScrollAnimStart() { + autoPager.pause() + } + + fun onScrollAnimStop() { + autoPager.resume() + } + + fun onPageChange() { + autoPager.reset() + curPage.submitPreRenderTask() + } + override val currentChapter: TextChapter? get() { return if (callBack.isInitFinish) ReadBook.textChapter(0) else null @@ -688,8 +674,6 @@ class ReadView(context: Context, attrs: AttributeSet) : interface CallBack { val isInitFinish: Boolean - val isAutoPage: Boolean - val autoPageProgress: Int fun showActionMenu() fun screenOffTimerStart() fun showTextActionMenu() 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 eb28942d63c0..ceddd279bc80 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 @@ -20,4 +20,5 @@ interface DataSource { fun hasPrevChapter(): Boolean fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt index 7be18c761bb0..31fd7e8db7ab 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -96,7 +96,7 @@ abstract class PageDelegate(protected val readView: ReadView) { viewHeight = height } - fun scroll() { + fun computeScroll() { if (scroller.computeScrollOffset()) { readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) } else if (isStarted) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt index cec16e65ecbd..232aaa925545 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt @@ -9,7 +9,6 @@ import io.legado.app.model.ReadBook import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.provider.ChapterProvider -@Suppress("UnnecessaryVariable") class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) { // 滑动追踪的时间 @@ -22,6 +21,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) { var noAnim: Boolean = false override fun onAnimStart(animationSpeed: Int) { + readView.onScrollAnimStart() //惯性滚动 fling( 0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(), @@ -30,7 +30,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) { } override fun onAnimStop() { - // nothing + readView.onScrollAnimStop() } override fun onTouch(event: MotionEvent) { 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 07cff13df91d..b1f3fb9c6212 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 @@ -4,6 +4,8 @@ package io.legado.app.ui.book.read.page.entities import androidx.annotation.Keep import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.ReplaceRule +import io.legado.app.utils.fastBinarySearchBy +import kotlin.math.abs import kotlin.math.min /** @@ -85,12 +87,15 @@ data class TextChapter( * @return 已读长度 */ fun getReadLength(pageIndex: Int): Int { + return pages[min(pageIndex, lastIndex)].lines.first().chapterPosition + /* var length = 0 val maxIndex = min(pageIndex, pages.size) for (index in 0 until maxIndex) { length += pages[index].charSize } return length + */ } /** @@ -185,6 +190,15 @@ data class TextChapter( * @return 根据索引位置获取所在页 */ fun getPageIndexByCharIndex(charIndex: Int): Int { + val index = pages.fastBinarySearchBy(charIndex) { + it.lines.first().chapterPosition + } + return if (index >= 0) { + index + } else { + abs(index + 1) - 1 + } + /* 相当于以下实现 var length = 0 for (i in pages.indices) { val page = pages[i] @@ -194,6 +208,7 @@ data class TextChapter( } } return pages.lastIndex + */ } fun clearSearchResult() { 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 0f89a6e322df..2683ed97aadd 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 @@ -121,7 +121,7 @@ data class TextLine( return visible } - fun draw(view: ContentTextView, canvas: Canvas) { + fun draw(view: ContentTextView, canvas: Canvas?) { pictureMirror.draw(canvas, view.width, height.toInt()) { drawTextLine(view, this) } 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 1a9d366f7c12..ab9fc3f71a58 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 @@ -263,8 +263,8 @@ data class TextPage( return null } - fun draw(view: ContentTextView, canvas: Canvas) { - pictureMirror.draw(canvas, view.width, height.toInt()) { + fun draw(view: ContentTextView, canvas: Canvas?) { + pictureMirror.drawLocked(canvas, view.width, height.toInt()) { drawPage(view, this) } } @@ -278,6 +278,16 @@ data class TextPage( } } + fun preRender(view: ContentTextView): Boolean { + if (!pictureMirror.isDirty) return false + draw(view, null) + return true + } + + fun isDirty(): Boolean { + return pictureMirror.isDirty + } + fun invalidate() { pictureMirror.invalidate() } 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 39275c8c0376..084d8d3207c7 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 @@ -32,10 +32,7 @@ data class ImageColumn( src, (end - start).toInt(), height.toInt() - ) { - textLine.invalidate() - view.invalidate() - } ?: return + ) val rectF = if (textLine.isImage) { RectF(start, 0f, end, height) 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 46ef5ecd17af..c38d3dc9ca18 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,7 @@ package io.legado.app.ui.book.read.page.provider import android.graphics.Paint.FontMetrics +import android.graphics.RectF import android.graphics.Typeface import android.net.Uri import android.os.Build @@ -133,6 +134,9 @@ object ChapterProvider { var doublePage = false private set + @JvmStatic + var visibleRect = RectF() + private val titleMeasureHelper = TextMeasure(titlePaint) private val contentMeasureHelper = TextMeasure(contentPaint) @@ -176,6 +180,8 @@ object ChapterProvider { durY = it.second } } + textPages.last().lines.last().isParagraphEnd = true + stringBuilder.append("\n") durY += titleBottomSpacing } contents.forEach { content -> @@ -265,6 +271,8 @@ object ChapterProvider { } } } + textPages.last().lines.last().isParagraphEnd = true + stringBuilder.append("\n") } val textPage = textPages.last() val endPadding = 20.dpToPx() @@ -376,6 +384,8 @@ object ChapterProvider { 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 @@ -475,7 +485,6 @@ object ChapterProvider { lineIndex == layout.lineCount - 1 -> { //最后一行 textLine.text = lineText - textLine.isParagraphEnd = true //标题x轴居中 val startX = if ( isTitle && @@ -515,24 +524,8 @@ object ChapterProvider { if (doublePage) { textLine.isLeftLine = absStartX < viewWidth / 2 } - val sbLength = stringBuilder.length + calcTextLinePosition(textPages, textLine, stringBuilder.length) stringBuilder.append(lineText) - if (textLine.isParagraphEnd) { - stringBuilder.append("\n") - } - 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 textLine.upTopBottom(durY, textHeight, fontMetrics) val textPage = textPages.last() textPage.addLine(textLine) @@ -545,6 +538,26 @@ object ChapterProvider { 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 + } + /** * 有缩进,两端对齐 */ @@ -911,6 +924,14 @@ object ChapterProvider { visibleRight = viewWidth - paddingRight visibleBottom = paddingTop + visibleHeight } + + visibleRect.set( + paddingLeft.toFloat(), + paddingTop.toFloat(), + visibleRight.toFloat(), + visibleBottom.toFloat() + ) + } } diff --git a/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt index 63a95e072d78..5004f1cd0c38 100644 --- a/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt @@ -30,6 +30,7 @@ class DetailSeekBar @JvmOverloads constructor( get() = binding.seekBar.progress set(value) { binding.seekBar.progress = value + upValue() } var max: Int get() = binding.seekBar.max diff --git a/app/src/main/java/io/legado/app/utils/BitmapCache.kt b/app/src/main/java/io/legado/app/utils/BitmapCache.kt deleted file mode 100644 index 37b831b8bee9..000000000000 --- a/app/src/main/java/io/legado/app/utils/BitmapCache.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.legado.app.utils - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import java.lang.ref.SoftReference -import java.util.concurrent.ConcurrentHashMap - -object BitmapCache { - - private val reusableBitmaps: MutableSet> = ConcurrentHashMap.newKeySet() - - 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) { - // inBitmap only works with mutable bitmaps, so force the decoder to - // return mutable bitmaps. - options.inMutable = true - - // Try to find a bitmap to use for inBitmap. - getBitmapFromReusableSet(options)?.also { inBitmap -> - // If a suitable bitmap has been found, set it as the value of - // inBitmap. - options.inBitmap = inBitmap - } - } - - - private fun getBitmapFromReusableSet(options: BitmapFactory.Options): Bitmap? { - if (reusableBitmaps.isEmpty()) { - return null - } - val iterator = reusableBitmaps.iterator() - while (iterator.hasNext()) { - val item = iterator.next().get() ?: continue - if (item.isMutable) { - // Check to see it the item can be used for inBitmap. - if (canUseForInBitmap(item, options)) { - // Remove from reusable set so it can't be used again. - iterator.remove() - return item - } - } else { - // Remove from the set if the reference has been cleared. - iterator.remove() - } - } - return null - } - - private fun canUseForInBitmap( - candidate: Bitmap, - targetOptions: BitmapFactory.Options - ): Boolean { - // From Android 4.4 (KitKat) onward we can re-use if the byte size of - // the new bitmap is smaller than the reusable bitmap candidate - // allocation byte count. - val width: Int = targetOptions.outWidth / targetOptions.inSampleSize - val height: Int = targetOptions.outHeight / targetOptions.inSampleSize - val byteCount: Int = width * height * getBytesPerPixel(candidate.config) - return byteCount <= candidate.allocationByteCount - } - - /** - * A helper function to return the byte usage per pixel of a bitmap based on its configuration. - */ - private fun getBytesPerPixel(config: Bitmap.Config): Int { - return when (config) { - Bitmap.Config.ARGB_8888 -> 4 - Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2 - Bitmap.Config.ALPHA_8 -> 1 - else -> 1 - } - } - -} diff --git a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt index e962b5a4313d..80c19e88f7ca 100644 --- a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt +++ b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt @@ -34,7 +34,6 @@ object BitmapUtils { BitmapFactory.decodeFileDescriptor(fis.fd, null, op) op.inSampleSize = calculateInSampleSize(op, width, height) op.inJustDecodeBounds = false - BitmapCache.addInBitmapOptions(op) BitmapFactory.decodeFileDescriptor(fis.fd, null, op) } } 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 3e6f333063ed..e8e7b87d79ef 100644 --- a/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt @@ -7,3 +7,33 @@ fun List.fastSum(): Float { } return sum } + +inline fun List.fastBinarySearch( + fromIndex: Int = 0, + toIndex: Int = size, + comparison: (T) -> Int +): Int { + var low = fromIndex + var high = toIndex - 1 + + while (low <= high) { + val mid = (low + high).ushr(1) // safe from overflows + val midVal = get(mid) + val cmp = comparison(midVal) + + if (cmp < 0) + low = mid + 1 + else if (cmp > 0) + high = mid - 1 + else + return mid // key found + } + return -(low + 1) // key not found +} + +inline fun > List.fastBinarySearchBy( + key: K?, + fromIndex: Int = 0, + toIndex: Int = size, + crossinline selector: (T) -> K? +): Int = fastBinarySearch(fromIndex, toIndex) { compareValues(selector(it), key) } diff --git a/app/src/main/java/io/legado/app/utils/PictureMirror.kt b/app/src/main/java/io/legado/app/utils/PictureMirror.kt index 9a725500dfc4..44e9c01f26db 100644 --- a/app/src/main/java/io/legado/app/utils/PictureMirror.kt +++ b/app/src/main/java/io/legado/app/utils/PictureMirror.kt @@ -4,23 +4,75 @@ import android.graphics.Canvas import android.graphics.Picture import android.os.Build import androidx.core.graphics.record +import java.util.concurrent.locks.ReentrantLock class PictureMirror { - + @Volatile var picture: Picture? = null + + @Volatile + var lock: ReentrantLock? = null + + @Volatile var isDirty = true - inline fun draw(canvas: Canvas, width: Int, height: Int, block: Canvas.() -> Unit) { + inline fun drawLocked( + canvas: Canvas?, + width: Int, + height: Int, + block: Canvas.() -> Unit + ) { + if (atLeastApi23) { + if (picture == null) { + synchronized(this) { + if (picture == null) { + picture = Picture() + } + } + } + if (lock == null) { + synchronized(this) { + if (lock == null) { + lock = ReentrantLock() + } + } + } + val picture = picture!! + val lock = lock!! + if (isDirty) { + if (!lock.tryLock()) return + try { + picture.record(width, height, block) + isDirty = false + } finally { + lock.unlock() + } + } + canvas?.drawPicture(picture) + } else { + canvas?.block() + } + } + + /** + * 非线程安全,多线程调用可能会崩溃 + */ + 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) + isDirty = false } - canvas.drawPicture(picture) + canvas?.drawPicture(picture) } else { - canvas.block() + canvas?.block() } } diff --git a/app/src/main/res/xml/pref_config_read.xml b/app/src/main/res/xml/pref_config_read.xml index 045303a6d2e7..b6fa78c39fdf 100644 --- a/app/src/main/res/xml/pref_config_read.xml +++ b/app/src/main/res/xml/pref_config_read.xml @@ -136,13 +136,6 @@ app:iconSpaceReserved="false" app:isBottomBackground="true" /> - -