diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 76602f5b2..ee48d87b8 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
+ - name: 简繁转化
+ url: https://github.com/liuyueyi/quick-chinese-transfer/issues/new
+ about: 简繁转化问题请优先到quick-chinese-transfer反馈
- name: 讨论 / Discussions
url: https://github.com/gedoor/legado/discussions
about: Please ask and answer questions here.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 30eb577ef..5a42a7dbf 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -78,6 +78,7 @@ jobs:
uses: gradle/actions/setup-gradle@v3
- name: Build With Gradle
+ continue-on-error: true
run: |
if [ ${{ env.type }} == 'release' ]; then
typeName="原包名"
@@ -91,15 +92,22 @@ jobs:
echo "开始${{ env.product }}$typeName构建"
chmod +x gradlew
./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all
- echo "修改文件名"
+ - name: Rename outputs apks And Move files
+ run: |
+ echo "修改APK文件名"
mkdir -p ${{ github.workspace }}/apk/
- mkdir -p ${{ github.workspace }}/mapping/
for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do
mv "$file" ${{ github.workspace }}/apk/legado_${{ env.product }}_${{ env.VERSIONL }}_$typeName.apk
done
+ echo "移动mapping文件"
+ mkdir -p ${{ github.workspace }}/mapping/
for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/mapping.txt`; do
mv "$file" ${{ github.workspace }}/mapping/mapping.txt
done
+ echo "移动missing_rules.txt文件"
+ for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/missing_rules.txt`; do
+ mv "$file" ${{ github.workspace }}/mapping/missing_rules.txt
+ done
- name: Upload App To Artifact
uses: actions/upload-artifact@v4
with:
@@ -112,6 +120,12 @@ jobs:
name: legado.${{ env.product }}.${{ env.type }}.mapping
path: ${{ github.workspace }}/mapping/mapping.txt
+ - name: Upload Missing Rules File To Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: legado.${{ env.product }}.${{ env.type }}.mapping.missing_rules
+ path: ${{ github.workspace }}/mapping/missing_rules.txt
+
lanzou:
needs: [ prepare, build ]
if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.lanzou == 'yes' }}
diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
index 09a21638f..506efa577 100644
--- a/.github/workflows/web.yml
+++ b/.github/workflows/web.yml
@@ -27,7 +27,7 @@ jobs:
with:
node-version: 16
- - uses: pnpm/action-setup@v2
+ - uses: pnpm/action-setup@v3
name: Install pnpm
id: pnpm-install
with:
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5163b16c2..a720ba630 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -473,6 +473,15 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
+
+
+
accentColor = accentColor()
+ }
+ }
fun editTheme(context: Context): ThemeStore {
return ThemeStore(context)
diff --git a/app/src/main/java/io/legado/app/model/ImageProvider.kt b/app/src/main/java/io/legado/app/model/ImageProvider.kt
index a1320d488..c0a99fb7f 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)
@@ -212,4 +171,8 @@ object ImageProvider {
}.getOrDefault(errorBitmap)
}
+ fun clear() {
+ bitmapLruCache.evictAll()
+ }
+
}
diff --git a/app/src/main/java/io/legado/app/model/ReadBook.kt b/app/src/main/java/io/legado/app/model/ReadBook.kt
index 0d7ee0899..c5062cfef 100644
--- a/app/src/main/java/io/legado/app/model/ReadBook.kt
+++ b/app/src/main/java/io/legado/app/model/ReadBook.kt
@@ -14,6 +14,7 @@ import io.legado.app.help.book.isLocal
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.help.globalExecutor
import io.legado.app.model.localBook.TextFile
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService
@@ -63,6 +64,7 @@ object ReadBook : CoroutineScope by MainScope() {
val downloadFailChapters = hashMapOf()
var contentProcessor: ContentProcessor? = null
val downloadScope = CoroutineScope(SupervisorJob() + IO)
+ val executor = globalExecutor
//暂时保存跳转前进度
fun saveCurrentBookProcess() {
@@ -158,10 +160,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()
@@ -204,7 +206,7 @@ object ReadBook : CoroutineScope by MainScope() {
return hasPrevPage
}
- fun moveToNextChapter(upContent: Boolean): Boolean {
+ fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
if (durChapterIndex < chapterSize - 1) {
durChapterPos = 0
durChapterIndex++
@@ -213,11 +215,11 @@ object ReadBook : CoroutineScope by MainScope() {
nextTextChapter = null
if (curTextChapter == null) {
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
- callBack?.upContent()
+ if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false)
- } else if (upContent) {
+ } else if (upContent && upContentInPlace) {
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
- callBack?.upContent()
+ callBack?.upContent(resetPageOffset = false)
}
loadContent(durChapterIndex.plus(1), upContent, false)
saveRead()
@@ -233,7 +235,8 @@ object ReadBook : CoroutineScope by MainScope() {
fun moveToPrevChapter(
upContent: Boolean,
- toLast: Boolean = true
+ toLast: Boolean = true,
+ upContentInPlace: Boolean = true
): Boolean {
if (durChapterIndex > 0) {
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
@@ -242,10 +245,10 @@ object ReadBook : CoroutineScope by MainScope() {
curTextChapter = prevTextChapter
prevTextChapter = null
if (curTextChapter == null) {
- callBack?.upContent()
+ if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false)
- } else if (upContent) {
- callBack?.upContent()
+ } else if (upContent && upContentInPlace) {
+ callBack?.upContent(resetPageOffset = false)
}
loadContent(durChapterIndex.minus(1), upContent, false)
saveRead()
@@ -267,6 +270,16 @@ object ReadBook : CoroutineScope by MainScope() {
}
fun setPageIndex(index: Int) {
+ val textChapter = curTextChapter
+ if (textChapter != null) {
+ val pageIndex = durPageIndex
+ if (index > pageIndex) {
+ textChapter.getPage(index - 2)?.recycleRecorders()
+ }
+ if (index < pageIndex) {
+ textChapter.getPage(index + 3)?.recycleRecorders()
+ }
+ }
durChapterPos = curTextChapter?.getReadLength(index) ?: index
saveRead(true)
curPageChanged(true)
@@ -525,8 +538,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
@@ -549,26 +562,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)
+ }
}
}
}
@@ -595,6 +611,7 @@ object ReadBook : CoroutineScope by MainScope() {
coroutineContext.cancelChildren()
downloadedChapters.clear()
downloadFailChapters.clear()
+ ImageProvider.clear()
}
interface CallBack {
diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
index af2eb9cb7..f3162f325 100644
--- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
+++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
@@ -197,7 +197,7 @@ class AnalyzeRule(
}
if (sourceRule.replaceRegex.isNotEmpty() && result is List<*>) {
val newList = ArrayList()
- for (item in result as List<*>) {
+ for (item in result) {
newList.add(replaceRegex(item.toString(), sourceRule))
}
result = newList
@@ -210,12 +210,12 @@ class AnalyzeRule(
}
if (result == null) return null
if (result is String) {
- result = (result as String).split("\n")
+ result = result.split("\n")
}
if (isUrl) {
val urlList = ArrayList()
if (result is List<*>) {
- for (url in result as List<*>) {
+ for (url in result) {
val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString())
if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) {
urlList.add(absoluteURL)
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 7070861be..a8ef1f579 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 a7735129b..8496ae091 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 546846367..6a12f28a4 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
@@ -6,6 +6,7 @@ import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
+import android.os.Looper
import android.view.Gravity
import android.view.InputDevice
import android.view.KeyEvent
@@ -76,7 +77,7 @@ 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.read.page.provider.ChapterProvider
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,8 +96,8 @@ 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
import io.legado.app.utils.getPrefString
import io.legado.app.utils.hexString
@@ -191,7 +192,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)
@@ -201,8 +201,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) {
@@ -211,16 +210,17 @@ 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) } }
private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } }
private var bookChanged = false
private var pageChanged = false
- private var reloadContent = false
- private val autoPageRenderer by lazy { SyncedRenderer { doAutoPage(it) } }
- private var autoPageScrollOffset = 0.0
+ private val handler by lazy { buildMainHandler() }
+ private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
+ private val executor = ReadBook.executor
//恢复跳转前进度对话框的交互结果
private var confirmRestoreProcess: Boolean? = null
@@ -264,23 +264,9 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
- viewModel.initData(intent) {
- initDataSuccess()
- }
- }
-
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- viewModel.initData(intent ?: return) {
- initDataSuccess()
- }
- }
-
- private fun initDataSuccess() {
- upMenu()
- if (reloadContent) {
- reloadContent = false
- ReadBook.loadContent(resetPageOffset = false)
+ Looper.myQueue().addIdleHandler {
+ viewModel.initData(intent) { upMenu() }
+ false
}
}
@@ -401,6 +387,11 @@ class ReadBookActivity : BaseReadBookActivity(),
}
}
+ override fun onNightModeChanged(mode: Int) {
+ super.onNightModeChanged(mode)
+ binding.readView.invalidateTextPage()
+ }
+
/**
* 菜单
*/
@@ -904,7 +895,7 @@ class ReadBookActivity : BaseReadBookActivity(),
}
override fun upMenuView() {
- lifecycleScope.launch {
+ handler.post {
binding.readMenu.upBookView()
}
}
@@ -937,9 +928,6 @@ class ReadBookActivity : BaseReadBookActivity(),
success: (() -> Unit)?
) {
lifecycleScope.launch {
- if (relativePosition == 0) {
- autoPageProgress = 0
- }
binding.readView.upContent(relativePosition, resetPageOffset)
upSeekBarProgress()
loadStates = false
@@ -962,9 +950,11 @@ class ReadBookActivity : BaseReadBookActivity(),
*/
override fun pageChanged() {
pageChanged = true
- lifecycleScope.launch {
- autoPageProgress = 0
+ binding.readView.onPageChange()
+ handler.post {
upSeekBarProgress()
+ }
+ executor.execute {
startBackupJob()
}
}
@@ -1045,8 +1035,7 @@ class ReadBookActivity : BaseReadBookActivity(),
if (isAutoPage) {
autoPageStop()
} else {
- isAutoPage = true
- autoPagePlus()
+ binding.readView.autoPager.start()
binding.readMenu.setAutoPage(true)
screenTimeOut = -1L
screenOffTimerStart()
@@ -1055,53 +1044,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 {
@@ -1350,24 +1298,24 @@ class ReadBookActivity : BaseReadBookActivity(),
when (dialogId) {
TEXT_COLOR -> {
setCurTextColor(color)
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
}
BG_COLOR -> {
setCurBg(0, "#${color.hexString}")
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1))
}
TIP_COLOR -> {
ReadTipConfig.tipColor = color
postEvent(EventBus.TIP_COLOR, "")
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
TIP_DIVIDER_COLOR -> {
ReadTipConfig.tipDividerColor = color
postEvent(EventBus.TIP_COLOR, "")
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
}
@@ -1406,6 +1354,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
@@ -1523,22 +1479,21 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadBook.readAloud(!BaseReadAloudService.pause)
}
}
- observeEvent(EventBus.UP_CONFIG) {
- upSystemUiVisibility()
- readView.upPageSlopSquare()
- readView.upBg()
- readView.upStyle()
- readView.upBgAlpha()
- if (it) { // 更新内容排版布局
- if (isInitFinish) {
- ReadBook.loadContent(resetPageOffset = false)
- } else {
- reloadContent = true
+ observeEvent>(EventBus.UP_CONFIG) {
+ it.forEach { value ->
+ when (value) {
+ 0 -> upSystemUiVisibility()
+ 1 -> readView.upBg()
+ 2 -> readView.upStyle()
+ 3 -> readView.upBgAlpha()
+ 4 -> readView.upPageSlopSquare()
+ 5 -> if (isInitFinish) ReadBook.loadContent(resetPageOffset = false)
+ 6 -> readView.upContent(resetPageOffset = false)
+ 8 -> ChapterProvider.upStyle()
+ 9 -> binding.readView.invalidateTextPage()
+ 10 -> ChapterProvider.upLayout()
}
- } else {
- readView.upContent(resetPageOffset = false)
}
- binding.readMenu.reset()
}
observeEvent(EventBus.ALOUD_STATE) {
if (it == Status.STOP || it == Status.PAUSE) {
@@ -1594,17 +1549,16 @@ class ReadBookActivity : BaseReadBookActivity(),
* 重置黑屏时间
*/
override fun screenOffTimerStart() {
- keepScreenJon?.cancel()
- keepScreenJon = lifecycleScope.launch {
+ handler.post {
if (screenTimeOut < 0) {
keepScreenOn(true)
- return@launch
+ return@post
}
val t = screenTimeOut - sysScreenOffTime
if (t > 0) {
keepScreenOn(true)
- delay(screenTimeOut)
- keepScreenOn(false)
+ handler.removeCallbacks(screenOffRunnable)
+ handler.postDelayed(screenOffRunnable, screenTimeOut)
} else {
keepScreenOn(false)
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
index bca34bf8b..5e163456c 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 e3f0eeade..6edf40f80 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/config/BgAdapter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt
index 1dfb26983..1d3d41d71 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt
@@ -41,7 +41,7 @@ class BgAdapter(context: Context, val textColor: Int) :
this.setOnClickListener {
getItemByLayoutPosition(holder.layoutPosition)?.let {
ReadBookConfig.durConfig.setCurBg(1, it)
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1))
}
}
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
index 2f16f4628..6ecf6c849 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
@@ -33,11 +33,32 @@ import io.legado.app.lib.theme.getSecondaryTextColor
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
-import io.legado.app.utils.*
+import io.legado.app.utils.ColorUtils
+import io.legado.app.utils.FileUtils
+import io.legado.app.utils.GSON
+import io.legado.app.utils.MD5Utils
+import io.legado.app.utils.SelectImageContract
import io.legado.app.utils.compress.ZipUtils
+import io.legado.app.utils.createFileReplace
+import io.legado.app.utils.createFolderReplace
+import io.legado.app.utils.externalCache
+import io.legado.app.utils.externalFiles
+import io.legado.app.utils.getFile
+import io.legado.app.utils.inputStream
+import io.legado.app.utils.isContentScheme
+import io.legado.app.utils.launch
+import io.legado.app.utils.longToast
+import io.legado.app.utils.openOutputStream
+import io.legado.app.utils.outputStream
+import io.legado.app.utils.parseToUri
+import io.legado.app.utils.postEvent
+import io.legado.app.utils.printOnDebug
+import io.legado.app.utils.readBytes
+import io.legado.app.utils.readUri
+import io.legado.app.utils.stackTraceStr
+import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding
import splitties.init.appCtx
-
import java.io.File
import java.io.FileOutputStream
@@ -168,7 +189,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
if (i >= 0) {
ReadBookConfig.durConfig = defaultConfigs[i].copy()
initData()
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
}
}
}
@@ -178,7 +199,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
}
binding.swUnderline.setOnCheckedChangeListener { _, isChecked ->
underline = isChecked
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(9))
}
binding.tvTextColor.setOnClickListener {
ColorPickerDialog.newBuilder()
@@ -214,7 +235,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
}
binding.ivDelete.setOnClickListener {
if (ReadBookConfig.deleteDur()) {
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
dismissAllowingStateLoss()
} else {
toastOnUi("数量已是最少,不能删除.")
@@ -223,11 +244,11 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
binding.sbBgAlpha.setOnSeekBarChangeListener(object : SeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
ReadBookConfig.bgAlpha = progress
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(3))
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(3))
}
})
}
@@ -357,7 +378,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
ReadBookConfig.import(byteArray).getOrThrow()
}.onSuccess {
ReadBookConfig.durConfig = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
toastOnUi("导入成功")
}.onError {
it.printOnDebug()
@@ -378,7 +399,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
inputStream.copyTo(outputStream)
}
ReadBookConfig.durConfig.setCurBg(2, fileName)
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1))
}.onFailure {
appCtx.toastOnUi(it.localizedMessage)
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
index a4ed8c938..1ef45ae8b 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
@@ -107,39 +107,48 @@ class MoreConfigDialog : DialogFragment() {
PreferKey.readBodyToLh -> activity?.recreate()
PreferKey.hideStatusBar -> {
ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(0))
}
+
PreferKey.hideNavigationBar -> {
ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar)
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(0))
}
+
PreferKey.keepLight -> postEvent(key, true)
PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key))
PreferKey.screenOrientation -> {
(activity as? ReadBookActivity)?.setOrientation()
}
+
PreferKey.textFullJustify,
PreferKey.textBottomJustify,
PreferKey.useZhLayout -> {
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(5))
}
+
PreferKey.showBrightnessView -> {
postEvent(PreferKey.showBrightnessView, "")
}
+
PreferKey.expandTextMenu -> {
(activity as? ReadBookActivity)?.textActionMenu?.upMenu()
}
+
PreferKey.doublePageHorizontal -> {
ChapterProvider.upLayout()
ReadBook.loadContent(false)
}
+
PreferKey.showReadTitleAddition,
PreferKey.readBarStyleFollowPage -> {
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
}
+
PreferKey.progressBarBehavior -> {
postEvent(EventBus.UP_SEEK_BAR, true)
}
+
PreferKey.noAnimScrollPage -> {
ReadBook.callBack?.upPageAnim()
}
@@ -152,6 +161,7 @@ class MoreConfigDialog : DialogFragment() {
"clickRegionalConfig" -> {
(activity as? ReadBookActivity)?.showClickRegionalConfig()
}
+
PreferKey.pageTouchSlop -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.page_touch_slop_dialog_title))
@@ -160,7 +170,7 @@ class MoreConfigDialog : DialogFragment() {
.setValue(AppConfig.pageTouchSlop)
.show {
AppConfig.pageTouchSlop = it
- postEvent(EventBus.UP_CONFIG, false)
+ postEvent(EventBus.UP_CONFIG, arrayOf(4))
}
}
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
index d55f28f32..debf0c2a5 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
@@ -63,61 +63,61 @@ class PaddingConfigDialog : BaseDialogFragment(R.layout.dialog_read_padding) {
//正文
dsbPaddingTop.onChanged = {
ReadBookConfig.paddingTop = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
}
dsbPaddingBottom.onChanged = {
ReadBookConfig.paddingBottom = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
}
dsbPaddingLeft.onChanged = {
ReadBookConfig.paddingLeft = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
}
dsbPaddingRight.onChanged = {
ReadBookConfig.paddingRight = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
}
//页眉
dsbHeaderPaddingTop.onChanged = {
ReadBookConfig.headerPaddingTop = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
dsbHeaderPaddingBottom.onChanged = {
ReadBookConfig.headerPaddingBottom = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
dsbHeaderPaddingLeft.onChanged = {
ReadBookConfig.headerPaddingLeft = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
dsbHeaderPaddingRight.onChanged = {
ReadBookConfig.headerPaddingRight = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
//页脚
dsbFooterPaddingTop.onChanged = {
ReadBookConfig.footerPaddingTop = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
dsbFooterPaddingBottom.onChanged = {
ReadBookConfig.footerPaddingBottom = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
dsbFooterPaddingLeft.onChanged = {
ReadBookConfig.footerPaddingLeft = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
dsbFooterPaddingRight.onChanged = {
ReadBookConfig.footerPaddingRight = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
cbShowTopLine.onCheckedChangeListener = { _, isChecked ->
ReadBookConfig.showHeaderLine = isChecked
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
cbShowBottomLine.onCheckedChangeListener = { _, isChecked ->
ReadBookConfig.showFooterLine = isChecked
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
index 1db0195d3..1201cea16 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
@@ -108,10 +108,10 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
private fun initViewEvent() = binding.run {
chineseConverter.onChanged {
ChineseUtils.unLoad(*TransType.entries.toTypedArray())
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(5))
}
textFontWeightConverter.onChanged {
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8))
}
tvTextFont.setOnClickListener {
showDialogFragment()
@@ -122,7 +122,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
items = resources.getStringArray(R.array.indent).toList()
) { _, index ->
ReadBookConfig.paragraphIndent = " ".repeat(index)
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(5))
}
}
tvPadding.setOnClickListener {
@@ -141,40 +141,40 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
cbShareLayout.onCheckedChangeListener = { _, isChecked ->
ReadBookConfig.shareLayout = isChecked
upView()
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
}
dsbTextSize.onChanged = {
ReadBookConfig.textSize = it + 5
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
dsbTextLetterSpacing.onChanged = {
ReadBookConfig.letterSpacing = (it - 50) / 100f
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
dsbLineSize.onChanged = {
ReadBookConfig.lineSpacingExtra = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
dsbParagraphSpacing.onChanged = {
ReadBookConfig.paragraphSpacing = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
}
- private fun changeBg(index: Int) {
+ private fun changeBgTextConfig(index: Int) {
val oldIndex = ReadBookConfig.styleSelect
if (index != oldIndex) {
ReadBookConfig.styleSelect = index
upView()
styleAdapter.notifyItemChanged(oldIndex)
styleAdapter.notifyItemChanged(index)
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
}
}
private fun showBgTextConfig(index: Int): Boolean {
dismissAllowingStateLoss()
- changeBg(index)
+ changeBgTextConfig(index)
callBack?.showBgTextConfig()
return true
}
@@ -200,7 +200,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
override fun selectFont(path: String) {
if (path != ReadBookConfig.textFont) {
ReadBookConfig.textFont = path
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
}
@@ -235,7 +235,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
binding.apply {
ivStyle.setOnClickListener {
if (ivStyle.isInView) {
- changeBg(holder.layoutPosition)
+ changeBgTextConfig(holder.layoutPosition)
}
}
ivStyle.onLongClick(ivStyle.isInView) {
diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt
index cca5a2798..6862c7734 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt
@@ -11,7 +11,12 @@ import io.legado.app.databinding.DialogTipConfigBinding
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ReadTipConfig
import io.legado.app.lib.dialogs.selector
-import io.legado.app.utils.*
+import io.legado.app.utils.checkByIndex
+import io.legado.app.utils.getIndexById
+import io.legado.app.utils.hexString
+import io.legado.app.utils.observeEvent
+import io.legado.app.utils.postEvent
+import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -89,26 +94,26 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
private fun initEvent() = binding.run {
rgTitleMode.setOnCheckedChangeListener { _, checkedId ->
ReadBookConfig.titleMode = rgTitleMode.getIndexById(checkedId)
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(5))
}
dsbTitleSize.onChanged = {
ReadBookConfig.titleSize = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
dsbTitleTop.onChanged = {
ReadBookConfig.titleTopSpacing = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
dsbTitleBottom.onChanged = {
ReadBookConfig.titleBottomSpacing = it
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
}
llHeaderShow.setOnClickListener {
val headerModes = ReadTipConfig.getHeaderModes(requireContext())
context?.selector(items = headerModes.values.toList()) { _, i ->
ReadTipConfig.headerMode = headerModes.keys.toList()[i]
tvHeaderShow.text = headerModes[ReadTipConfig.headerMode]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llFooterShow.setOnClickListener {
@@ -116,7 +121,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
context?.selector(items = footerModes.values.toList()) { _, i ->
ReadTipConfig.footerMode = footerModes.keys.toList()[i]
tvFooterShow.text = footerModes[ReadTipConfig.footerMode]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llHeaderLeft.setOnClickListener {
@@ -125,7 +130,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipHeaderLeft = tipValue
tvHeaderLeft.text = ReadTipConfig.tipNames[i]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llHeaderMiddle.setOnClickListener {
@@ -134,7 +139,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipHeaderMiddle = tipValue
tvHeaderMiddle.text = ReadTipConfig.tipNames[i]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llHeaderRight.setOnClickListener {
@@ -143,7 +148,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipHeaderRight = tipValue
tvHeaderRight.text = ReadTipConfig.tipNames[i]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llFooterLeft.setOnClickListener {
@@ -152,7 +157,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipFooterLeft = tipValue
tvFooterLeft.text = ReadTipConfig.tipNames[i]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llFooterMiddle.setOnClickListener {
@@ -161,7 +166,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipFooterMiddle = tipValue
tvFooterMiddle.text = ReadTipConfig.tipNames[i]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llFooterRight.setOnClickListener {
@@ -170,7 +175,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue)
ReadTipConfig.tipFooterRight = tipValue
tvFooterRight.text = ReadTipConfig.tipNames[i]
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
}
llTipColor.setOnClickListener {
@@ -179,7 +184,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
0 -> {
ReadTipConfig.tipColor = 0
upTvTipColor()
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
1 -> ColorPickerDialog.newBuilder()
.setShowAlphaSlider(false)
@@ -195,7 +200,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
0, 1 -> {
ReadTipConfig.tipDividerColor = i - 1
upTvTipDividerColor()
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(2))
}
2 -> ColorPickerDialog.newBuilder()
.setShowAlphaSlider(false)
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 000000000..5b7b3990b
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt
@@ -0,0 +1,148 @@
+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) {
+ 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.postInvalidate()
+ }
+
+ }
+
+ fun computeOffset() {
+ if (!isRunning) {
+ return
+ }
+
+ 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 11fb9d542..50180f174 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,23 +3,14 @@ package io.legado.app.ui.book.read.page
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
-import android.graphics.Picture
-import android.graphics.RectF
-import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
-import androidx.core.graphics.record
import io.legado.app.R
-import io.legado.app.constant.PageAnim
-import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark
-import io.legado.app.help.book.isImage
import io.legado.app.help.config.AppConfig
-import io.legado.app.help.config.ReadBookConfig
-import io.legado.app.lib.theme.accentColor
-import io.legado.app.model.ImageProvider
import io.legado.app.model.ReadBook
+import io.legado.app.ui.book.read.page.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
@@ -32,27 +23,25 @@ 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.activity
-import io.legado.app.utils.dpToPx
import io.legado.app.utils.getCompatColor
-import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.showDialogFragment
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 upView: ((TextPage) -> Unit)? = null
- private val selectedPaint by lazy {
+ var selectAble = AppConfig.textSelectAble
+ val selectedPaint by lazy {
Paint().apply {
color = context.getCompatColor(R.color.btn_bg_press_2)
style = Paint.Style.FILL
}
}
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()
@@ -63,15 +52,15 @@ 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 lateinit var picture: Picture
- private var pictureIsDirty = true
- private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
- private val isNoAnim get() = ReadBook.pageAnim() == PageAnim.noAnim
+ private var autoPager: AutoPager? = null
+ private var isScroll = false
+ private val renderRunnable by lazy { Runnable { preRenderPage() } }
//绘制图片的paint
- private val imagePaint by lazy {
+ val imagePaint by lazy {
Paint().apply {
isAntiAlias = AppConfig.useAntiAlias
}
@@ -79,9 +68,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
init {
callBack = activity as CallBack
- if (atLeastApi23) {
- picture = Picture()
- }
}
/**
@@ -89,47 +75,29 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
*/
fun setContent(textPage: TextPage) {
this.textPage = textPage
- imagePaint.isAntiAlias = AppConfig.useAntiAlias
- invalidate()
- }
-
- /**
- * 更新绘制区域
- */
- fun upVisibleRect() {
- visibleRect.set(
- ChapterProvider.paddingLeft.toFloat(),
- ChapterProvider.paddingTop.toFloat(),
- ChapterProvider.visibleRight.toFloat(),
- ChapterProvider.visibleBottom.toFloat()
- )
+ // 非滑动翻页动画需要同步重绘,不然翻页可能会出现闪烁
+ if (isScroll) {
+ postInvalidate()
+ } else {
+ invalidate()
+ }
}
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 (atLeastApi23 && !callBack.isScroll && !isNoAnim) {
- if (pictureIsDirty) {
- pictureIsDirty = false
- picture.record(width, height) {
- drawPage(this)
- }
- }
- canvas.drawPicture(picture)
- } else {
- drawPage(canvas)
- }
+ drawPage(canvas)
}
/**
@@ -137,185 +105,94 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
*/
private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0)
- var lines = textPage.lines
- for (i in lines.indices) {
- drawLine(canvas, textPage, lines[i], relativeOffset)
- }
+ textPage.draw(this, canvas, relativeOffset)
if (!callBack.isScroll) return
//滚动翻页
if (!pageFactory.hasNext()) return
val textPage1 = relativePage(1)
- relativeOffset = relativeOffset(1)
- lines = textPage1.lines
- for (i in lines.indices) {
- drawLine(canvas, textPage1, lines[i], relativeOffset)
- }
+ relativeOffset += textPage.height
+ textPage1.draw(this, canvas, relativeOffset)
if (!pageFactory.hasNextPlus()) return
- relativeOffset = relativeOffset(2)
+ relativeOffset += textPage1.height
if (relativeOffset < ChapterProvider.visibleHeight) {
val textPage2 = relativePage(2)
- lines = textPage2.lines
- for (i in lines.indices) {
- drawLine(canvas, textPage2, lines[i], relativeOffset)
- }
+ textPage2.draw(this, canvas, relativeOffset)
}
}
- /**
- * 绘制页面
- */
- private fun drawLine(
- canvas: Canvas,
- textPage: TextPage,
- textLine: TextLine,
- relativeOffset: Float,
- ) {
- val lineTop = textLine.lineTop + relativeOffset
- val lineBase = textLine.lineBase + relativeOffset
- val lineBottom = textLine.lineBottom + relativeOffset
- drawChars(canvas, textPage, textLine, lineTop, lineBase, lineBottom)
- if (ReadBookConfig.underline && ReadBook.book?.isImage != true) {
- drawUnderline(canvas, textLine, relativeOffset)
- }
- }
-
- /**
- * 绘制下划线
- */
- private fun drawUnderline(canvas: Canvas, textLine: TextLine, relativeOffset: Float) {
- val lineY = relativeOffset + textLine.lineBottom - 1.dpToPx()
- canvas.drawLine(
- textLine.lineStart + textLine.indentWidth,
- lineY,
- textLine.lineEnd,
- lineY,
- ChapterProvider.contentPaint
- )
- }
-
- /**
- * 绘制文字
- */
- private fun drawChars(
- canvas: Canvas,
- textPage: TextPage,
- textLine: TextLine,
- lineTop: Float,
- lineBase: Float,
- lineBottom: Float,
- ) {
- val textPaint = if (textLine.isTitle) {
- ChapterProvider.titlePaint
- } else {
- ChapterProvider.contentPaint
- }
- val textColor = if (textLine.isReadAloud) context.accentColor else ReadBookConfig.textColor
- val columns = textLine.columns
- for (i in columns.indices) {
- when (val column = columns[i]) {
- is TextColumn -> {
- if (column.isSearchResult) {
- textPaint.color = context.accentColor
- } else if (textPaint.color != textColor) {
- textPaint.color = textColor
- }
- canvas.drawText(column.charData, column.start, lineBase, textPaint)
- if (column.selected) {
- canvas.drawRect(
- column.start,
- lineTop,
- column.end,
- lineBottom,
- selectedPaint
- )
- }
- }
-
- is ImageColumn -> drawImage(canvas, textPage, textLine, column, lineTop, lineBottom)
- is ReviewColumn -> column.drawToCanvas(canvas, lineBase, textPaint.textSize)
- }
- }
- }
-
- /**
- * 绘制图片
- */
- @Suppress("UNUSED_PARAMETER")
- private fun drawImage(
- canvas: Canvas,
- textPage: TextPage,
- textLine: TextLine,
- column: ImageColumn,
- lineTop: Float,
- lineBottom: Float
- ) {
-
- val book = ReadBook.book ?: return
-
- val bitmap = ImageProvider.getImage(
- book,
- column.src,
- (column.end - column.start).toInt(),
- (lineBottom - lineTop).toInt()
- ) {
- invalidate()
- } ?: return
-
- val rectF = if (textLine.isImage) {
- RectF(column.start, lineTop, column.end, lineBottom)
- } else {
- /*以宽度为基准保持图片的原始比例叠加,当div为负数时,允许高度比字符更高*/
- val h = (column.end - column.start) / bitmap.width * bitmap.height
- val div = (lineBottom - lineTop - h) / 2
- RectF(column.start, lineTop + div, column.end, lineBottom - div)
- }
- kotlin.runCatching {
- canvas.drawBitmap(bitmap, null, rectF, imagePaint)
- }.onFailure { e ->
- context.toastOnUi(e.localizedMessage)
- }
+ override fun computeScroll() {
+ pageDelegate?.computeScroll()
+ autoPager?.computeOffset()
}
/**
* 滚动事件
+ * pageOffset 向上滚动 减小 向下滚动 增大
+ * pageOffset 范围 0 ~ -textPage.height 大于0为上一页,小于-textPage.height为下一页
+ * 以内容显示区域顶端为界,pageOffset的绝对值为textPage上方的高度
+ * pageOffset + textPage.height 为 textPage 下方的高度
*/
fun scroll(mOffset: Int) {
- if (mOffset == 0) return
pageOffset += mOffset
if (longScreenshot) {
scrollY += -mOffset
}
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) {
- pageFactory.moveToPrev(true)
- textPage = pageFactory.curPage
- pageOffset -= textPage.height.toInt()
- upView?.invoke(textPage)
- contentDescription = textPage.text
+ if (pageFactory.moveToPrev(true)) {
+ pageOffset -= textPage.height.toInt()
+ } else {
+ pageOffset = 0
+ pageDelegate?.abortAnim()
+ }
} else if (pageOffset < -textPage.height) {
- pageOffset += textPage.height.toInt()
- pageFactory.moveToNext(true)
- textPage = pageFactory.curPage
- upView?.invoke(textPage)
- contentDescription = textPage.text
+ val height = textPage.height
+ if (pageFactory.moveToNext(upContent = true)) {
+ pageOffset += height.toInt()
+ } else {
+ pageOffset = -height.toInt()
+ pageDelegate?.abortAnim()
+ }
}
- invalidate()
+ postInvalidate()
}
- override fun invalidate() {
- super.invalidate()
- invalidatePicture()
+ fun submitRenderTask() {
+ renderThread.submit(renderRunnable)
}
- private fun invalidatePicture() {
- pictureIsDirty = true
+ private fun preRenderPage() {
+ val view = this
+ var invalidate = false
+ pageFactory.run {
+ if (hasPrev() && prevPage.render(view)) {
+ invalidate = true
+ }
+ if (curPage.render(view)) {
+ invalidate = true
+ }
+ if (hasNext() && nextPage.render(view) && callBack.isScroll) {
+ invalidate = true
+ }
+ if (hasNextPlus() && nextPlusPage.render(view) && callBack.isScroll
+ && relativeOffset(2) < ChapterProvider.visibleHeight
+ ) {
+ invalidate = true
+ }
+ if (invalidate) {
+ postInvalidate()
+ pageDelegate?.postInvalidate()
+ }
+ }
}
/**
@@ -385,7 +262,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
touchRough(x, y) { _, textPos, _, _, column ->
if (column is TextColumn) {
column.selected = true
- invalidate()
select(textPos)
}
}
@@ -517,9 +393,21 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) return
}
val textPage = relativePage(relativePos)
- for ((lineIndex, textLine) in textPage.lines.withIndex()) {
+ for (lineIndex in textPage.lines.indices) {
+ val textLine = textPage.getLine(lineIndex)
if (textLine.isTouchY(y, relativeOffset)) {
- for ((charIndex, textColumn) in textLine.columns.withIndex()) {
+ if (textPage.doublePage) {
+ val halfWidth = width / 2
+ if (textLine.isLeftLine && x > halfWidth) {
+ continue
+ }
+ if (!textLine.isLeftLine && x < halfWidth) {
+ continue
+ }
+ }
+ val columns = textLine.columns
+ for (charIndex in columns.indices) {
+ val textColumn = columns[charIndex]
if (textColumn.isTouch(x)) {
touched.invoke(
relativeOffset,
@@ -529,12 +417,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
return
}
}
- val isLast = textLine.columns.first().start < x
- val (charIndex, textColumn) = if (isLast) {
- textLine.columns.withIndex().last()
- } else {
- textLine.columns.withIndex().first()
- }
+ val isLast = columns.first().start < x
+ val charIndex = if (isLast) columns.lastIndex else 0
+ val textColumn = if (isLast) columns.last() else columns.first()
touched.invoke(
relativeOffset,
TextPos(relativePos, lineIndex, charIndex, false, isLast),
@@ -557,7 +442,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break
}
val textPage = relativePage(relativePos)
- for (textLine in textPage.lines) {
+ val lines = textPage.lines
+ for (i in lines.indices) {
+ val textLine = lines[i]
if (textLine.isVisible(relativeOffset)) {
val visibleLine = textLine.copy().apply {
lineTop += relativeOffset
@@ -580,7 +467,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break
}
val textPage = relativePage(relativePos)
- for (textLine in textPage.lines) {
+ val lines = textPage.lines
+ for (i in lines.indices) {
+ val textLine = lines[i]
if (textLine.isVisible(relativeOffset)) {
val visibleLine = textLine.copy().apply {
lineTop += relativeOffset
@@ -675,7 +564,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
}
}
}
- invalidate()
+ // 由后台线程完成渲染后通知视图重绘
+ submitRenderTask()
}
private fun upSelectedStart(x: Float, y: Float, top: Float) {
@@ -795,6 +685,14 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
}
}
+ fun setAutoPager(autoPager: AutoPager?) {
+ this.autoPager = autoPager
+ }
+
+ fun setIsScroll(value: Boolean) {
+ isScroll = value
+ }
+
override fun canScrollVertically(direction: Int): Boolean {
return callBack.isScroll && pageFactory.hasNext()
}
@@ -814,9 +712,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 31656dc56..f450cb746 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
@@ -22,6 +22,7 @@ import io.legado.app.ui.widget.BatteryView
import io.legado.app.utils.activity
import io.legado.app.utils.dpToPx
import io.legado.app.utils.gone
+import io.legado.app.utils.setTextIfNotEqual
import io.legado.app.utils.statusBarHeight
import splitties.views.backgroundColor
import java.util.Date
@@ -45,6 +46,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() {
@@ -57,9 +60,6 @@ class PageView(context: Context) : FrameLayout(context) {
if (!isInEditMode) {
upStyle()
}
- binding.contentTextView.upView = {
- setProgress(it)
- }
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
@@ -104,7 +104,6 @@ class PageView(context: Context) : FrameLayout(context) {
vwTopDivider.gone(llHeader.isGone || !it.showHeaderLine)
vwBottomDivider.gone(llFooter.isGone || !it.showFooterLine)
}
- contentTextView.upVisibleRect()
upTime()
upBattery(battery)
}
@@ -276,7 +275,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()
}
@@ -302,30 +307,26 @@ class PageView(context: Context) : FrameLayout(context) {
*/
@SuppressLint("SetTextI18n")
fun setProgress(textPage: TextPage) = textPage.apply {
- tvBookName?.apply {
- if (text != ReadBook.book?.name) {
- text = ReadBook.book?.name
- }
- }
- tvTitle?.apply {
- if (text != textPage.title) {
- text = textPage.title
- }
- }
- tvPage?.text = "${index.plus(1)}/$pageSize"
+ tvBookName?.setTextIfNotEqual(ReadBook.book?.name)
+ tvTitle?.setTextIfNotEqual(textPage.title)
+ tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
val readProgress = readProgress
- tvTotalProgress?.apply {
- if (text != readProgress) {
- text = readProgress
- }
- }
- tvTotalProgress1?.apply {
- val progress = "${chapterIndex.plus(1)}/${chapterSize}"
- if (text != progress) {
- text = progress
- }
- }
- tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress"
+ tvTotalProgress?.setTextIfNotEqual(readProgress)
+ tvTotalProgress1?.setTextIfNotEqual("${chapterIndex.plus(1)}/${chapterSize}")
+ tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
+ }
+
+ fun setAutoPager(autoPager: AutoPager?) {
+ binding.contentTextView.setAutoPager(autoPager)
+ }
+
+ fun submitPreRenderTask() {
+ binding.contentTextView.submitRenderTask()
+ }
+
+ fun setIsScroll(value: Boolean) {
+ isScroll = value
+ binding.contentTextView.setIsScroll(value)
}
/**
@@ -379,6 +380,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 70833c053..21b175fff 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,21 +13,27 @@ 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
import io.legado.app.ui.book.read.page.api.DataSource
-import io.legado.app.ui.book.read.page.delegate.*
+import io.legado.app.ui.book.read.page.delegate.CoverPageDelegate
+import io.legado.app.ui.book.read.page.delegate.NoAnimPageDelegate
+import io.legado.app.ui.book.read.page.delegate.PageDelegate
+import io.legado.app.ui.book.read.page.delegate.ScrollPageDelegate
+import io.legado.app.ui.book.read.page.delegate.SimulationPageDelegate
+import io.legado.app.ui.book.read.page.delegate.SlidePageDelegate
import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.entities.TextPos
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.TextPageFactory
-import io.legado.app.utils.*
+import io.legado.app.utils.activity
+import io.legado.app.utils.invisible
+import io.legado.app.utils.showDialogFragment
import java.text.BreakIterator
-import java.util.*
+import java.util.Locale
import kotlin.math.abs
/**
@@ -49,7 +52,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
field = value
upContent()
}
- var isScroll = false
+ override var isScroll = false
val prevPage by lazy { PageView(context) }
val curPage by lazy { PageView(context) }
val nextPage by lazy { PageView(context) }
@@ -95,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()
@@ -140,26 +143,12 @@ 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()
+ autoPager.computeOffset()
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
@@ -253,6 +242,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
pageDelegate?.onTouch(event)
}
pressOnTextSelected = false
+ autoPager.resume()
}
}
return true
@@ -516,11 +506,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.setIsScroll(isScroll)
}
/**
@@ -529,13 +520,12 @@ class ReadView(context: Context, attrs: AttributeSet) :
* @param resetPageOffset 滚动阅读是是否重置位置
*/
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
- curPage.setContentDescription(pageFactory.curPage.text)
- if (isScroll && !callBack.isAutoPage) {
+ post {
+ curPage.setContentDescription(pageFactory.curPage.text)
+ }
+ 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)
@@ -638,9 +628,27 @@ class ReadView(context: Context, attrs: AttributeSet) :
return curPage.getCurVisibleFirstLine()?.pagePosition ?: 0
}
- fun clearNextPageBitmap() {
- nextPageBitmap?.recycle()
- nextPageBitmap = null
+ fun invalidateTextPage() {
+ pageFactory.run {
+ prevPage.invalidateAll()
+ curPage.invalidateAll()
+ nextPage.invalidateAll()
+ nextPlusPage.invalidateAll()
+ }
+ upContent()
+ }
+
+ fun onScrollAnimStart() {
+ autoPager.pause()
+ }
+
+ fun onScrollAnimStop() {
+ autoPager.resume()
+ }
+
+ fun onPageChange() {
+ autoPager.reset()
+ curPage.submitPreRenderTask()
}
override val currentChapter: TextChapter?
@@ -668,8 +676,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 22c0d7ab0..ceddd279b 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt
@@ -13,9 +13,12 @@ interface DataSource {
val prevChapter: TextChapter?
+ val isScroll: Boolean
+
fun hasNextChapter(): Boolean
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/CoverPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt
index f9fb0695c..5e8303312 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt
@@ -1,10 +1,7 @@
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Picture
import android.graphics.drawable.GradientDrawable
-import android.os.Build
import androidx.core.graphics.withClip
import androidx.core.graphics.withTranslation
import io.legado.app.ui.book.read.page.ReadView
@@ -12,26 +9,14 @@ import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.utils.screenshot
class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
- private val bitmapMatrix = Matrix()
private val shadowDrawableR: GradientDrawable
- private lateinit var curPicture: Picture
- private lateinit var prevPicture: Picture
- private lateinit var nextPicture: Picture
-
- private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-
init {
val shadowColors = intArrayOf(0x66111111, 0x00000000)
shadowDrawableR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, shadowColors
)
shadowDrawableR.gradientType = GradientDrawable.LINEAR_GRADIENT
- if (atLeastApi23) {
- curPicture = Picture()
- prevPicture = Picture()
- nextPicture = Picture()
- }
}
override fun onDraw(canvas: Canvas) {
@@ -47,42 +32,21 @@ class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
if (mDirection == PageDirection.PREV) {
if (offsetX <= viewWidth) {
- if (!atLeastApi23) {
- bitmapMatrix.setTranslate(distanceX, 0.toFloat())
- prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
- } else {
- canvas.withTranslation(distanceX) {
- drawPicture(prevPicture)
- }
+ canvas.withTranslation(distanceX) {
+ prevRecorder.draw(canvas)
}
addShadow(distanceX, canvas)
} else {
- if (!atLeastApi23) {
- prevBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
- } else {
- canvas.drawPicture(prevPicture)
- }
+ prevRecorder.draw(canvas)
}
} else if (mDirection == PageDirection.NEXT) {
- if (!atLeastApi23) {
- bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat())
- nextBitmap?.let {
- val width = it.width.toFloat()
- val height = it.height.toFloat()
- canvas.withClip(width + offsetX, 0f, width, height) {
- drawBitmap(it, 0f, 0f, null)
- }
- }
- curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
- } else {
- val width = nextPicture.width.toFloat()
- val height = nextPicture.height.toFloat()
- canvas.withClip(width + offsetX, 0f, width, height) {
- drawPicture(nextPicture)
- }
- canvas.withTranslation(distanceX - viewWidth) {
- drawPicture(curPicture)
- }
+ val width = nextRecorder.width.toFloat()
+ val height = nextRecorder.height.toFloat()
+ canvas.withClip(width + offsetX, 0f, width, height) {
+ nextRecorder.draw(this)
+ }
+ canvas.withTranslation(distanceX - viewWidth) {
+ curRecorder.draw(this)
}
addShadow(distanceX, canvas)
}
@@ -90,18 +54,13 @@ class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
override fun setBitmap() {
when (mDirection) {
- PageDirection.PREV -> if (!atLeastApi23) {
- prevBitmap = prevPage.screenshot(prevBitmap, canvas)
- } else {
- prevPage.screenshot(prevPicture)
+ PageDirection.PREV -> {
+ prevPage.screenshot(prevRecorder)
}
- PageDirection.NEXT -> if (!atLeastApi23) {
- nextBitmap = nextPage.screenshot(nextBitmap, canvas)
- curBitmap = curPage.screenshot(curBitmap, canvas)
- } else {
- nextPage.screenshot(nextPicture)
- curPage.screenshot(curPicture)
+ PageDirection.NEXT -> {
+ nextPage.screenshot(nextRecorder)
+ curPage.screenshot(curRecorder)
}
else -> Unit
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
index 9e8e30f2f..f0f91b3ca 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
@@ -1,18 +1,16 @@
package io.legado.app.ui.book.read.page.delegate
-import android.graphics.Bitmap
-import android.graphics.Canvas
import android.view.MotionEvent
import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
import io.legado.app.utils.screenshot
abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readView) {
- protected var curBitmap: Bitmap? = null
- protected var prevBitmap: Bitmap? = null
- protected var nextBitmap: Bitmap? = null
- protected var canvas: Canvas = Canvas()
+ protected val curRecorder = CanvasRecorderFactory.create()
+ protected val prevRecorder = CanvasRecorderFactory.create()
+ protected val nextRecorder = CanvasRecorderFactory.create()
private val slopSquare get() = readView.pageSlopSquare2
override fun setDirection(direction: PageDirection) {
@@ -23,13 +21,13 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
open fun setBitmap() {
when (mDirection) {
PageDirection.PREV -> {
- prevBitmap = prevPage.screenshot(prevBitmap, canvas)
- curBitmap = curPage.screenshot(curBitmap, canvas)
+ prevPage.screenshot(prevRecorder)
+ curPage.screenshot(curRecorder)
}
PageDirection.NEXT -> {
- nextBitmap = nextPage.screenshot(nextBitmap, canvas)
- curBitmap = curPage.screenshot(curBitmap, canvas)
+ nextPage.screenshot(nextRecorder)
+ curPage.screenshot(curRecorder)
}
else -> Unit
@@ -140,12 +138,9 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
override fun onDestroy() {
super.onDestroy()
- prevBitmap?.recycle()
- prevBitmap = null
- curBitmap?.recycle()
- curBitmap = null
- nextBitmap?.recycle()
- nextBitmap = null
+ prevRecorder.recycle()
+ curRecorder.recycle()
+ nextRecorder.recycle()
}
}
\ 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 7be18c761..38e51d6e7 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() {
+ open fun computeScroll() {
if (scroller.computeScrollOffset()) {
readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) {
@@ -190,6 +190,15 @@ abstract class PageDelegate(protected val readView: ReadView) {
}
}
+ fun postInvalidate() {
+ if (isRunning && this is HorizontalPageDelegate) {
+ readView.post {
+ setBitmap()
+ readView.invalidate()
+ }
+ }
+ }
+
open fun onDestroy() {
// run on destroy
}
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 cec16e65e..2d32b1c92 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) {
@@ -79,7 +79,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
val pointX = event.getX(event.pointerCount - 1)
val pointY = event.getY(event.pointerCount - 1)
if (isMoved) {
- readView.setTouchPoint(pointX, pointY)
+ readView.setTouchPoint(pointX, pointY, false)
}
if (!isMoved) {
val deltaX = (pointX - startX).toInt()
@@ -95,12 +95,22 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
}
}
+ override fun computeScroll() {
+ if (scroller.computeScrollOffset()) {
+ readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat(), false)
+ } else if (isStarted) {
+ onAnimStop()
+ stopScroll()
+ }
+ }
+
override fun onDestroy() {
super.onDestroy()
mVelocity.recycle()
}
override fun abortAnim() {
+ readView.onScrollAnimStop()
isStarted = false
isMoved = false
isRunning = false
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
index 04b7bde03..ebadb8dad 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
@@ -1,13 +1,27 @@
package io.legado.app.ui.book.read.page.delegate
-import android.graphics.*
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PointF
+import android.graphics.Region
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.view.MotionEvent
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection
-import kotlin.math.*
+import io.legado.app.utils.screenshot
+import kotlin.math.abs
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.hypot
+import kotlin.math.min
+import kotlin.math.sin
@Suppress("DEPRECATION")
class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
@@ -86,6 +100,11 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
private val mPaint: Paint = Paint().apply { style = Paint.Style.FILL }
+ private var curBitmap: Bitmap? = null
+ private var prevBitmap: Bitmap? = null
+ private var nextBitmap: Bitmap? = null
+ private var canvas: Canvas = Canvas()
+
init {
//设置颜色数组
val color = intArrayOf(0x333333, -0x4fcccccd)
@@ -122,6 +141,22 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT
}
+ override fun setBitmap() {
+ when (mDirection) {
+ PageDirection.PREV -> {
+ prevBitmap = prevPage.screenshot(prevBitmap, canvas)
+ curBitmap = curPage.screenshot(curBitmap, canvas)
+ }
+
+ PageDirection.NEXT -> {
+ nextBitmap = nextPage.screenshot(nextBitmap, canvas)
+ curBitmap = curPage.screenshot(curBitmap, canvas)
+ }
+
+ else -> Unit
+ }
+ }
+
override fun setViewSize(width: Int, height: Int) {
super.setViewSize(width, height)
mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
@@ -133,6 +168,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
MotionEvent.ACTION_DOWN -> {
calcCornerXY(event.x, event.y)
}
+
MotionEvent.ACTION_MOVE -> {
if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3)
|| mDirection == PageDirection.PREV
@@ -159,10 +195,12 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
} else {
calcCornerXY(viewWidth - startX, viewHeight.toFloat())
}
+
PageDirection.NEXT ->
if (viewWidth / 2 > startX) {
calcCornerXY(viewWidth - startX, startY)
}
+
else -> Unit
}
}
@@ -216,6 +254,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, curBitmap)
}
+
PageDirection.PREV -> {
calcPoints()
drawCurrentPageArea(canvas, prevBitmap)
@@ -223,6 +262,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, prevBitmap)
}
+
else -> return
}
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt
index 103cf3a09..33c6480cb 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt
@@ -1,51 +1,12 @@
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Picture
-import android.os.Build
import androidx.core.graphics.withTranslation
import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection
-import io.legado.app.utils.screenshot
class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
- private val bitmapMatrix = Matrix()
-
- private lateinit var curPicture: Picture
- private lateinit var prevPicture: Picture
- private lateinit var nextPicture: Picture
-
- private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-
- init {
- if (atLeastApi23) {
- curPicture = Picture()
- prevPicture = Picture()
- nextPicture = Picture()
- }
- }
-
- override fun setBitmap() {
- if (!atLeastApi23) {
- return super.setBitmap()
- }
- when (mDirection) {
- PageDirection.PREV -> {
- prevPage.screenshot(prevPicture)
- curPage.screenshot(curPicture)
- }
-
- PageDirection.NEXT -> {
- nextPage.screenshot(nextPicture)
- curPage.screenshot(curPicture)
- }
-
- else -> Unit
- }
- }
-
override fun onAnimStart(animationSpeed: Int) {
val distanceX: Float
when (mDirection) {
@@ -79,32 +40,18 @@ class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
if (!isRunning) return
if (mDirection == PageDirection.PREV) {
- if (!atLeastApi23) {
- bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat())
- curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
- bitmapMatrix.setTranslate(distanceX, 0.toFloat())
- prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
- } else {
- canvas.withTranslation(distanceX + viewWidth) {
- drawPicture(curPicture)
- }
- canvas.withTranslation(distanceX) {
- drawPicture(prevPicture)
- }
+ canvas.withTranslation(distanceX + viewWidth) {
+ curRecorder.draw(this)
+ }
+ canvas.withTranslation(distanceX) {
+ prevRecorder.draw(this)
}
} else if (mDirection == PageDirection.NEXT) {
- if (!atLeastApi23) {
- bitmapMatrix.setTranslate(distanceX, 0.toFloat())
- nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
- bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat())
- curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
- } else {
- canvas.withTranslation(distanceX) {
- drawPicture(nextPicture)
- }
- canvas.withTranslation(distanceX - viewWidth) {
- drawPicture(curPicture)
- }
+ canvas.withTranslation(distanceX) {
+ nextRecorder.draw(this)
+ }
+ canvas.withTranslation(distanceX - viewWidth) {
+ curRecorder.draw(this)
}
}
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt
index 09faed73a..b1f3fb9c6 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,18 +190,30 @@ 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
- pages.forEach {
- length += it.charSize
+ for (i in pages.indices) {
+ val page = pages[i]
+ length += page.charSize
if (length > charIndex) {
- return it.index
+ return page.index
}
}
return pages.lastIndex
+ */
}
fun clearSearchResult() {
- pages.forEach { page ->
+ for (i in pages.indices) {
+ val page = pages[i]
page.searchResult.forEach {
it.selected = false
it.isSearchResult = false
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
index 1a14ef62b..2adc81ea6 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
@@ -1,9 +1,18 @@
package io.legado.app.ui.book.read.page.entities
+import android.graphics.Canvas
import android.graphics.Paint.FontMetrics
import androidx.annotation.Keep
+import io.legado.app.help.book.isImage
+import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.model.ReadBook
+import io.legado.app.ui.book.read.page.ContentTextView
+import io.legado.app.ui.book.read.page.entities.TextPage.Companion.emptyTextPage
import io.legado.app.ui.book.read.page.entities.column.BaseColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
+import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
+import io.legado.app.utils.dpToPx
/**
* 行信息
@@ -22,8 +31,7 @@ data class TextLine(
var pagePosition: Int = 0,
val isTitle: Boolean = false,
var isParagraphEnd: Boolean = false,
- var isReadAloud: Boolean = false,
- var isImage: Boolean = false
+ var isImage: Boolean = false,
) {
val columns: List get() = textColumns
@@ -31,8 +39,20 @@ data class TextLine(
val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f
val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
+ val height: Float inline get() = lineBottom - lineTop
+ val canvasRecorder = CanvasRecorderFactory.create()
+ var isReadAloud: Boolean = false
+ set(value) {
+ if (field != value) {
+ invalidate()
+ }
+ field = value
+ }
+ var textPage: TextPage = emptyTextPage
+ var isLeftLine = true
fun addColumn(column: BaseColumn) {
+ column.textLine = this
textColumns.add(column)
}
@@ -102,4 +122,50 @@ data class TextLine(
return visible
}
+ fun draw(view: ContentTextView, canvas: Canvas) {
+ canvasRecorder.recordIfNeededThenDraw(canvas, view.width, height.toInt()) {
+ drawTextLine(view, this)
+ }
+ }
+
+ private fun drawTextLine(view: ContentTextView, canvas: Canvas) {
+ for (i in columns.indices) {
+ columns[i].draw(view, canvas)
+ }
+ if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) {
+ drawUnderline(canvas)
+ }
+ }
+
+ /**
+ * 绘制下划线
+ */
+ private fun drawUnderline(canvas: Canvas) {
+ val lineY = height - 1.dpToPx()
+ canvas.drawLine(
+ lineStart + indentWidth,
+ lineY,
+ lineEnd,
+ lineY,
+ ChapterProvider.contentPaint
+ )
+ }
+
+ fun invalidate() {
+ invalidateSelf()
+ textPage.invalidate()
+ }
+
+ fun invalidateSelf() {
+ canvasRecorder.invalidate()
+ }
+
+ fun recycleRecorder() {
+ canvasRecorder.recycle()
+ }
+
+ companion object {
+ val emptyTextLine = TextLine()
+ }
+
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
index b48710b39..ef8032b95 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
@@ -1,13 +1,18 @@
package io.legado.app.ui.book.read.page.entities
+import android.graphics.Canvas
import android.text.Layout
import android.text.StaticLayout
import androidx.annotation.Keep
+import androidx.core.graphics.withTranslation
import io.legado.app.R
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook
+import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.column.TextColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
+import io.legado.app.utils.canvasrecorder.recordIfNeeded
import splitties.init.appCtx
import java.text.DecimalFormat
import kotlin.math.min
@@ -31,6 +36,7 @@ data class TextPage(
companion object {
val readProgressFormatter = DecimalFormat("0.0%")
+ val emptyTextPage = TextPage()
}
val lines: List get() = textLines
@@ -38,25 +44,30 @@ data class TextPage(
val charSize: Int get() = text.length.coerceAtLeast(1)
val searchResult = hashSetOf()
var isMsgPage: Boolean = false
+ var canvasRecorder = CanvasRecorderFactory.create(true)
+ var doublePage = false
+ var paddingTop = 0
val paragraphs by lazy {
paragraphsInternal
}
- val paragraphsInternal: ArrayList get() {
- val paragraphs = arrayListOf()
- val lines = textLines.filter { it.paragraphNum > 0 }
- val offset = lines.first().paragraphNum - 1
- lines.forEach { line ->
- if (paragraphs.lastIndex < line.paragraphNum - offset - 1) {
- paragraphs.add(TextParagraph(0))
+ val paragraphsInternal: ArrayList
+ get() {
+ val paragraphs = arrayListOf()
+ val lines = textLines.filter { it.paragraphNum > 0 }
+ val offset = lines.first().paragraphNum - 1
+ lines.forEach { line ->
+ if (paragraphs.lastIndex < line.paragraphNum - offset - 1) {
+ paragraphs.add(TextParagraph(0))
+ }
+ paragraphs[line.paragraphNum - offset - 1].textLines.add(line)
}
- paragraphs[line.paragraphNum - offset - 1].textLines.add(line)
+ return paragraphs
}
- return paragraphs
- }
fun addLine(line: TextLine) {
+ line.textPage = this
textLines.add(line)
}
@@ -147,9 +158,10 @@ data class TextPage(
)
x = x1
}
- textLines.add(textLine)
+ addLine(textLine)
}
height = ChapterProvider.visibleHeight.toFloat()
+ invalidate()
}
return this
}
@@ -158,8 +170,8 @@ data class TextPage(
* 移除朗读标志
*/
fun removePageAloudSpan(): TextPage {
- textLines.forEach { textLine ->
- textLine.isReadAloud = false
+ for (i in textLines.indices) {
+ textLines[i].isReadAloud = false
}
return this
}
@@ -171,7 +183,8 @@ data class TextPage(
fun upPageAloudSpan(aloudSpanStart: Int) {
removePageAloudSpan()
var lineStart = 0
- for ((index, textLine) in textLines.withIndex()) {
+ for (index in textLines.indices) {
+ val textLine = textLines[index]
val lineLength = textLine.text.length + if (textLine.isParagraphEnd) 1 else 0
if (aloudSpanStart > lineStart && aloudSpanStart < lineStart + lineLength) {
for (i in index - 1 downTo 0) {
@@ -254,6 +267,46 @@ data class TextPage(
return null
}
+ fun draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) {
+ render(view)
+ canvas.withTranslation(0f, relativeOffset + paddingTop) {
+ canvasRecorder.draw(this)
+ }
+ }
+
+ private fun drawPage(view: ContentTextView, canvas: Canvas) {
+ for (i in lines.indices) {
+ val line = lines[i]
+ canvas.withTranslation(0f, line.lineTop - paddingTop) {
+ line.draw(view, this)
+ }
+ }
+ }
+
+ fun render(view: ContentTextView): Boolean {
+ return canvasRecorder.recordIfNeeded(view.width, height.toInt()) {
+ drawPage(view, this)
+ }
+ }
+
+ fun invalidate() {
+ canvasRecorder.invalidate()
+ }
+
+ fun invalidateAll() {
+ for (i in lines.indices) {
+ lines[i].invalidateSelf()
+ }
+ invalidate()
+ }
+
+ fun recycleRecorders() {
+ canvasRecorder.recycle()
+ for (i in lines.indices) {
+ lines[i].recycleRecorder()
+ }
+ }
+
fun hasImageOrEmpty(): Boolean {
return textLines.any { it.isImage } || textLines.isEmpty()
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt
index 6516e5f0d..ed86cd153 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt
@@ -1,11 +1,18 @@
package io.legado.app.ui.book.read.page.entities.column
+import android.graphics.Canvas
+import io.legado.app.ui.book.read.page.ContentTextView
+import io.legado.app.ui.book.read.page.entities.TextLine
+
/**
* 列基类
*/
interface BaseColumn {
var start: Float
var end: Float
+ var textLine: TextLine
+
+ fun draw(view: ContentTextView, canvas: Canvas)
fun isTouch(x: Float): Boolean {
return x > start && x < end
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt
index 63db76675..abb3ac42a 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt
@@ -1,6 +1,10 @@
package io.legado.app.ui.book.read.page.entities.column
+import android.graphics.Canvas
import androidx.annotation.Keep
+import io.legado.app.ui.book.read.page.ContentTextView
+import io.legado.app.ui.book.read.page.entities.TextLine
+import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
/**
@@ -9,5 +13,10 @@ import androidx.annotation.Keep
@Keep
data class ButtonColumn(
override var start: Float,
- override var end: Float
-) : BaseColumn
\ No newline at end of file
+ override var end: Float,
+) : BaseColumn {
+ override var textLine: TextLine = emptyTextLine
+ override fun draw(view: ContentTextView, canvas: Canvas) {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt
index d372eda21..084d8d320 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt
@@ -1,6 +1,15 @@
package io.legado.app.ui.book.read.page.entities.column
+import android.graphics.Canvas
+import android.graphics.RectF
import androidx.annotation.Keep
+import io.legado.app.model.ImageProvider
+import io.legado.app.model.ReadBook
+import io.legado.app.ui.book.read.page.ContentTextView
+import io.legado.app.ui.book.read.page.entities.TextLine
+import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
+import io.legado.app.utils.toastOnUi
+import splitties.init.appCtx
/**
* 图片列
@@ -10,4 +19,34 @@ data class ImageColumn(
override var start: Float,
override var end: Float,
var src: String
-) : BaseColumn
\ No newline at end of file
+) : BaseColumn {
+
+ override var textLine: TextLine = emptyTextLine
+ override fun draw(view: ContentTextView, canvas: Canvas) {
+ val book = ReadBook.book ?: return
+
+ val height = textLine.height
+
+ val bitmap = ImageProvider.getImage(
+ book,
+ src,
+ (end - start).toInt(),
+ height.toInt()
+ )
+
+ val rectF = if (textLine.isImage) {
+ RectF(start, 0f, end, height)
+ } else {
+ /*以宽度为基准保持图片的原始比例叠加,当div为负数时,允许高度比字符更高*/
+ val h = (end - start) / bitmap.width * bitmap.height
+ val div = (height - h) / 2
+ RectF(start, div, end, height - div)
+ }
+ kotlin.runCatching {
+ canvas.drawBitmap(bitmap, null, rectF, view.imagePaint)
+ }.onFailure { e ->
+ appCtx.toastOnUi(e.localizedMessage)
+ }
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt
index 748f1b57c..c1612fb2f 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt
@@ -4,6 +4,9 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import androidx.annotation.Keep
+import io.legado.app.ui.book.read.page.ContentTextView
+import io.legado.app.ui.book.read.page.entities.TextLine
+import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
import io.legado.app.ui.book.read.page.provider.ChapterProvider
/**
@@ -16,6 +19,16 @@ data class ReviewColumn(
val count: Int = 0
) : BaseColumn {
+ override var textLine: TextLine = emptyTextLine
+ override fun draw(view: ContentTextView, canvas: Canvas) {
+ val textPaint = if (textLine.isTitle) {
+ ChapterProvider.titlePaint
+ } else {
+ ChapterProvider.contentPaint
+ }
+ drawToCanvas(canvas, textLine.lineBase, textPaint.textSize)
+ }
+
val countText by lazy {
if (count > 999) {
return@lazy "999"
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
index 0ffe9d39a..de204e42d 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt
@@ -1,6 +1,13 @@
package io.legado.app.ui.book.read.page.entities.column
+import android.graphics.Canvas
import androidx.annotation.Keep
+import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.lib.theme.ThemeStore
+import io.legado.app.ui.book.read.page.ContentTextView
+import io.legado.app.ui.book.read.page.entities.TextLine
+import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
+import io.legado.app.ui.book.read.page.provider.ChapterProvider
/**
* 文字列
@@ -10,6 +17,43 @@ data class TextColumn(
override var start: Float,
override var end: Float,
val charData: String,
- var selected: Boolean = false,
+) : BaseColumn {
+
+ override var textLine: TextLine = emptyTextLine
+
+ var selected: Boolean = false
+ set(value) {
+ if (field != value) {
+ textLine.invalidate()
+ }
+ field = value
+ }
var isSearchResult: Boolean = false
-) : BaseColumn
\ No newline at end of file
+ set(value) {
+ if (field != value) {
+ textLine.invalidate()
+ }
+ field = value
+ }
+
+ override fun draw(view: ContentTextView, canvas: Canvas) {
+ val textPaint = if (textLine.isTitle) {
+ ChapterProvider.titlePaint
+ } else {
+ ChapterProvider.contentPaint
+ }
+ if (textLine.isReadAloud || isSearchResult) {
+ synchronized(textPaint) {
+ textPaint.color = ThemeStore.accentColor
+ canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
+ textPaint.color = ReadBookConfig.textColor
+ }
+ } else {
+ canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
+ }
+ if (selected) {
+ canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
+ }
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
index 760efbc35..af63741a2 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,7 +1,7 @@
package io.legado.app.ui.book.read.page.provider
-import android.graphics.Paint
import android.graphics.Paint.FontMetrics
+import android.graphics.RectF
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
@@ -25,13 +25,13 @@ import io.legado.app.ui.book.read.page.entities.column.ReviewColumn
import io.legado.app.ui.book.read.page.entities.column.TextColumn
import io.legado.app.utils.RealPathUtil
import io.legado.app.utils.dpToPx
+import io.legado.app.utils.fastSum
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.isPad
import io.legado.app.utils.postEvent
import io.legado.app.utils.spToPx
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.textHeight
-import io.legado.app.utils.toStringArray
import splitties.init.appCtx
import java.util.LinkedList
import java.util.Locale
@@ -47,6 +47,8 @@ object ChapterProvider {
//用于评论按钮的替换
private const val reviewChar = "▨"
+ private const val indentChar = " "
+
@JvmStatic
var viewWidth = 0
private set
@@ -104,7 +106,7 @@ object ChapterProvider {
private var indentCharWidth = 0f
@JvmStatic
- private var titlePaintTextHeight = 0f
+ var titlePaintTextHeight = 0f
@JvmStatic
var contentPaintTextHeight = 0f
@@ -132,6 +134,9 @@ object ChapterProvider {
var doublePage = false
private set
+ @JvmStatic
+ var visibleRect = RectF()
+
init {
upStyle()
}
@@ -171,6 +176,8 @@ object ChapterProvider {
durY = it.second
}
}
+ textPages.last().lines.last().isParagraphEnd = true
+ stringBuilder.append("\n")
durY += titleBottomSpacing
}
contents.forEach { content ->
@@ -224,10 +231,19 @@ object ChapterProvider {
durY = it.second
}
}
- durY = setTypeImage(
- book, matcher.group(1)!!,
- absStartX, durY, textPages, stringBuilder, book.getImageStyle()
- )
+ setTypeImage(
+ book,
+ matcher.group(1)!!,
+ absStartX,
+ durY,
+ textPages,
+ contentPaintTextHeight,
+ stringBuilder,
+ book.getImageStyle()
+ ).let {
+ absStartX = it.first
+ durY = it.second
+ }
start = matcher.end()
}
if (start < content.length) {
@@ -248,15 +264,26 @@ object ChapterProvider {
}
}
}
+ textPages.last().lines.last().isParagraphEnd = true
+ stringBuilder.append("\n")
}
- textPages.last().height = durY + 20.dpToPx()
- textPages.last().text = stringBuilder.toString()
+ val textPage = textPages.last()
+ val endPadding = 20.dpToPx()
+ val durYPadding = durY + endPadding
+ if (textPage.height < durYPadding) {
+ textPage.height = durYPadding
+ } else {
+ textPage.height += endPadding
+ }
+ textPage.text = stringBuilder.toString()
textPages.forEachIndexed { index, item ->
item.index = index
item.pageSize = textPages.size
item.chapterIndex = bookChapter.index
item.chapterSize = chapterSize
item.title = displayTitle
+ item.doublePage = doublePage
+ item.paddingTop = paddingTop
item.upLinesPosition()
}
@@ -280,15 +307,19 @@ object ChapterProvider {
x: Int,
y: Float,
textPages: ArrayList,
+ textHeight: Float,
stringBuilder: StringBuilder,
imageStyle: String?,
- ): Float {
+ ): Pair {
+ var absStartX = x
var durY = y
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
if (size.width > 0 && size.height > 0) {
if (durY > visibleHeight) {
val textPage = textPages.last()
- textPage.height = durY
+ if (textPage.height < durY) {
+ textPage.height = durY
+ }
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear()
textPages.add(TextPage())
@@ -313,10 +344,23 @@ object ChapterProvider {
}
if (durY + height > visibleHeight) {
val textPage = textPages.last()
- textPage.height = durY
- textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
- stringBuilder.clear()
- textPages.add(TextPage())
+ if (doublePage && absStartX < viewWidth / 2) {
+ //当前页面左列结束
+ textPage.leftLineSize = textPage.lineSize
+ absStartX = viewWidth / 2 + paddingLeft
+ } else {
+ //当前页面结束
+ if (textPage.leftLineSize == 0) {
+ textPage.leftLineSize = textPage.lineSize
+ }
+ textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
+ stringBuilder.clear()
+ textPages.add(TextPage())
+ }
+ // 双页的 durY 不正确,可能会小于实际高度
+ if (textPage.height < durY) {
+ textPage.height = durY
+ }
durY = 0f
}
}
@@ -334,9 +378,11 @@ 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 durY + paragraphSpacing / 10f
+ return absStartX to durY + textHeight * paragraphSpacing / 10f
}
/**
@@ -358,14 +404,11 @@ object ChapterProvider {
srcList: LinkedList? = null
): Pair {
var absStartX = x
- val widthsArray = FloatArray(text.length)
val layout = if (ReadBookConfig.useZhLayout) {
- ZhLayout(text, textPaint, visibleWidth, widthsArray)
+ ZhLayout(text, textPaint, visibleWidth)
} else {
- textPaint.getTextWidths(text, widthsArray)
StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
}
- val widthsList = widthsArray.asList()
var durY = when {
//标题y轴居中
emptyContent && textPages.size == 1 -> {
@@ -398,6 +441,7 @@ object ChapterProvider {
if (durY + textHeight > visibleHeight) {
val textPage = textPages.last()
if (doublePage && absStartX < viewWidth / 2) {
+ //当前页面左列结束
textPage.leftLineSize = textPage.lineSize
absStartX = viewWidth / 2 + paddingLeft
} else {
@@ -406,33 +450,34 @@ object ChapterProvider {
textPage.leftLineSize = textPage.lineSize
}
textPage.text = stringBuilder.toString()
- textPage.height = durY
//新建页面
textPages.add(TextPage())
stringBuilder.clear()
absStartX = paddingLeft
}
+ if (textPage.height < durY) {
+ textPage.height = durY
+ }
durY = 0f
}
val lineStart = layout.getLineStart(lineIndex)
val lineEnd = layout.getLineEnd(lineIndex)
- val words = text.substring(lineStart, lineEnd)
- val textWidths = widthsList.subList(lineStart, lineEnd)
- val desiredWidth = textWidths.sum()
+ val lineText = text.substring(lineStart, lineEnd)
+ val (words, widths) = measureTextSplit(lineText, textPaint)
+ val desiredWidth = widths.fastSum()
when {
lineIndex == 0 && layout.lineCount > 1 && !isTitle -> {
//第一行 非标题
- textLine.text = words
+ textLine.text = lineText
addCharsToLineFirst(
book, absStartX, textLine, words,
- textPaint, desiredWidth, textWidths, srcList
+ desiredWidth, widths, srcList
)
}
lineIndex == layout.lineCount - 1 -> {
//最后一行
- textLine.text = words
- textLine.isParagraphEnd = true
+ textLine.text = lineText
//标题x轴居中
val startX = if (
isTitle &&
@@ -443,8 +488,8 @@ object ChapterProvider {
0f
}
addCharsToLineNatural(
- book, absStartX, textLine, words, textPaint,
- startX, !isTitle && lineIndex == 0, textWidths, srcList
+ book, absStartX, textLine, words,
+ startX, !isTitle && lineIndex == 0, widths, srcList
)
}
@@ -457,45 +502,55 @@ object ChapterProvider {
val startX = (visibleWidth - desiredWidth) / 2
addCharsToLineNatural(
book, absStartX, textLine, words,
- textPaint, startX, false, textWidths, srcList
+ startX, false, widths, srcList
)
} else {
//中间行
- textLine.text = words
+ textLine.text = lineText
addCharsToLineMiddle(
book, absStartX, textLine, words,
- textPaint, desiredWidth, 0f, textWidths, srcList
+ desiredWidth, 0f, widths, srcList
)
}
}
}
- val sbLength = stringBuilder.length
- stringBuilder.append(words)
- if (textLine.isParagraphEnd) {
- stringBuilder.append("\n")
+ if (doublePage) {
+ textLine.isLeftLine = absStartX < viewWidth / 2
}
- 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
- textPages.last().addLine(textLine)
+ calcTextLinePosition(textPages, textLine, stringBuilder.length)
+ stringBuilder.append(lineText)
textLine.upTopBottom(durY, textHeight, fontMetrics)
+ val textPage = textPages.last()
+ textPage.addLine(textLine)
durY += textHeight * lineSpacingExtra
- textPages.last().height = durY
+ if (textPage.height < durY) {
+ textPage.height = durY
+ }
}
durY += textHeight * paragraphSpacing / 10f
return Pair(absStartX, durY)
}
+ 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
+ }
+
/**
* 有缩进,两端对齐
*/
@@ -503,8 +558,7 @@ object ChapterProvider {
book: Book,
absStartX: Int,
textLine: TextLine,
- text: String,
- textPaint: TextPaint,
+ words: List,
/**自然排版长度**/
desiredWidth: Float,
textWidths: List,
@@ -513,17 +567,17 @@ object ChapterProvider {
var x = 0f
if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural(
- book, absStartX, textLine, text, textPaint,
+ book, absStartX, textLine, words,
x, true, textWidths, srcList
)
return
}
val bodyIndent = ReadBookConfig.paragraphIndent
- for (char in bodyIndent.toStringArray()) {
+ for (i in bodyIndent.indices) {
val x1 = x + indentCharWidth
textLine.addColumn(
TextColumn(
- charData = char,
+ charData = indentChar,
start = absStartX + x,
end = absStartX + x1
)
@@ -531,12 +585,12 @@ object ChapterProvider {
x = x1
textLine.indentWidth = x
}
- if (text.length > bodyIndent.length) {
- val text1 = text.substring(bodyIndent.length, text.length)
+ if (words.size > bodyIndent.length) {
+ val text1 = words.subList(bodyIndent.length, words.size)
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
addCharsToLineMiddle(
book, absStartX, textLine, text1,
- textPaint, desiredWidth, x, textWidths1, srcList
+ desiredWidth, x, textWidths1, srcList
)
}
}
@@ -548,8 +602,7 @@ object ChapterProvider {
book: Book,
absStartX: Int,
textLine: TextLine,
- text: String,
- textPaint: TextPaint,
+ words: List,
/**自然排版长度**/
desiredWidth: Float,
/**起始x坐标**/
@@ -559,19 +612,19 @@ object ChapterProvider {
) {
if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural(
- book, absStartX, textLine, text, textPaint,
+ book, absStartX, textLine, words,
startX, false, textWidths, srcList
)
return
}
val residualWidth = visibleWidth - desiredWidth
- val spaceSize = text.count { it == ' ' }
- val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint)
+ val spaceSize = words.count { it == " " }
if (spaceSize > 1) {
val d = residualWidth / spaceSize
var x = startX
- words.forEachIndexed { index, char ->
- val cw = widths[index]
+ for (index in words.indices) {
+ val char = words[index]
+ val cw = textWidths[index]
val x1 = if (char == " ") {
if (index != words.lastIndex) (x + cw + d) else (x + cw)
} else {
@@ -587,8 +640,9 @@ object ChapterProvider {
val gapCount: Int = words.lastIndex
val d = residualWidth / gapCount
var x = startX
- words.forEachIndexed { index, char ->
- val cw = widths[index]
+ for (index in words.indices) {
+ val char = words[index]
+ val cw = textWidths[index]
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
addCharToLine(
book, absStartX, textLine, char,
@@ -607,8 +661,7 @@ object ChapterProvider {
book: Book,
absStartX: Int,
textLine: TextLine,
- text: String,
- textPaint: TextPaint,
+ words: List,
startX: Float,
hasIndent: Boolean,
textWidths: List,
@@ -616,9 +669,9 @@ object ChapterProvider {
) {
val indentLength = ReadBookConfig.paragraphIndent.length
var x = startX
- val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint)
- words.forEachIndexed { index, char ->
- val cw = widths[index]
+ for (index in words.indices) {
+ val char = words[index]
+ val cw = textWidths[index]
val x1 = x + cw
addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
x = x1
@@ -629,39 +682,6 @@ object ChapterProvider {
exceed(absStartX, textLine, words)
}
- fun getStringArrayAndTextWidths(
- text: String,
- textWidths: List,
- textPaint: TextPaint
- ): Pair, List> {
- val charArray = text.toCharArray()
- val strList = ArrayList(text.length)
- val textWidthList = ArrayList(text.length)
- val lastIndex = charArray.lastIndex
- var ca: CharArray? = null
- for (i in textWidths.indices) {
- if (charArray[i].isLowSurrogate()) {
- continue
- }
- val char = if (i + 1 <= lastIndex && charArray[i + 1].isLowSurrogate()) {
- if (ca == null) ca = CharArray(2)
- System.arraycopy(charArray, i, ca, 0, 2)
- String(ca)
- } else {
- charArray[i].toString()
- }
- val w = textWidths[i]
- if (w == 0f && textWidthList.size > 0) {
- textWidthList[textWidthList.lastIndex] = textPaint.measureText(strList.last())
- textWidthList.add(textPaint.measureText(char))
- } else {
- textWidthList.add(w)
- }
- strList.add(char)
- }
- return strList to textWidthList
- }
-
/**
* 添加字符
*/
@@ -723,6 +743,28 @@ object ChapterProvider {
}
}
+ 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
+ }
+
/**
* 更新样式
*/
@@ -731,9 +773,9 @@ object ChapterProvider {
getPaints(typeface).let {
titlePaint = it.first
contentPaint = it.second
- reviewPaint.color = contentPaint.color
- reviewPaint.textSize = contentPaint.textSize * 0.45f
- reviewPaint.textAlign = Paint.Align.CENTER
+// reviewPaint.color = contentPaint.color
+// reviewPaint.textSize = contentPaint.textSize * 0.45f
+// reviewPaint.textAlign = Paint.Align.CENTER
}
//间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f
@@ -825,7 +867,7 @@ object ChapterProvider {
viewWidth = width
viewHeight = height
upLayout()
- postEvent(EventBus.UP_CONFIG, true)
+ postEvent(EventBus.UP_CONFIG, arrayOf(5))
}
}
@@ -862,6 +904,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/book/read/page/provider/TextMeasure.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt
new file mode 100644
index 000000000..4c8e51e9b
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt
@@ -0,0 +1,143 @@
+package io.legado.app.ui.book.read.page.provider
+
+import android.text.TextPaint
+import android.util.SparseArray
+import androidx.core.util.getOrDefault
+import kotlin.math.ceil
+
+class TextMeasure(private var paint: TextPaint) {
+
+ private var chineseCommonWidth = paint.measureText("一")
+ private val asciiWidths = FloatArray(128) { -1f }
+ private val codePointWidths = SparseArray()
+
+ private fun measureCodePoint(codePoint: Int): Float {
+ if (codePoint < 128) {
+ return asciiWidths[codePoint]
+ }
+ // 中文 Unicode 范围 U+4E00 - U+9FA5
+ if (codePoint in 19968 .. 40869) {
+ return chineseCommonWidth
+ }
+ return codePointWidths.getOrDefault(codePoint, -1f)
+ }
+
+ private fun measureCodePoints(codePoints: List) {
+ val charArray = String(codePoints.toIntArray(), 0, codePoints.size).toCharArray()
+ val widths = FloatArray(charArray.size)
+ paint.getTextWidths(charArray, 0, charArray.size, widths)
+ val widthsList = ArrayList(charArray.size)
+ val buf = IntArray(1)
+ for (i in charArray.indices) {
+ if (charArray[i].isLowSurrogate()) continue
+ val width = ceil(widths[i])
+ widthsList.add(width)
+ // 可能需要检查是否不可见字符
+ if (width == 0f && widthsList.size > 1) {
+ val lastIndex = widthsList.lastIndex
+ buf[0] = codePoints[lastIndex - 1]
+ widthsList[lastIndex - 1] = paint.measureText(String(buf, 0, 1))
+ buf[0] = codePoints[lastIndex]
+ widthsList[lastIndex] = paint.measureText(String(buf, 0, 1))
+ }
+ }
+ for (i in codePoints.indices) {
+ val codePoint = codePoints[i]
+ val width = widthsList[i]
+ if (codePoint < 128) {
+ asciiWidths[codePoint] = width
+ } else {
+ codePointWidths[codePoint] = width
+ }
+ }
+ }
+
+
+ fun measureTextSplit(text: String): Pair, ArrayList> {
+ var needMeasureCodePoints: HashSet? = null
+ val codePoints = text.toCodePoints()
+ val size = codePoints.size
+ val widths = ArrayList(size)
+ val stringList = ArrayList(size)
+ val buf = IntArray(1)
+ for (i in codePoints.indices) {
+ val codePoint = codePoints[i]
+ val width = measureCodePoint(codePoint)
+ widths.add(width)
+ if (width == -1f) {
+ if (needMeasureCodePoints == null) {
+ needMeasureCodePoints = hashSetOf()
+ }
+ needMeasureCodePoints.add(codePoint)
+ }
+ buf[0] = codePoint
+ stringList.add(String(buf, 0, 1))
+ }
+ if (!needMeasureCodePoints.isNullOrEmpty()) {
+ measureCodePoints(needMeasureCodePoints.toList())
+ for (i in codePoints.indices) {
+ if (widths[i] == -1f) {
+ widths[i] = measureCodePoint(codePoints[i])
+ }
+ }
+ }
+ return stringList to widths
+ }
+
+ fun measureText(text: String): Float {
+ var textWidth = 0f
+ var needMeasureCodePoints: ArrayList? = null
+ val codePoints = text.toCodePoints()
+ for (i in codePoints.indices) {
+ val codePoint = codePoints[i]
+ val width = measureCodePoint(codePoint)
+ if (width == -1f) {
+ if (needMeasureCodePoints == null) {
+ needMeasureCodePoints = ArrayList()
+ }
+ needMeasureCodePoints.add(codePoint)
+ continue
+ }
+ textWidth += width
+ }
+ if (!needMeasureCodePoints.isNullOrEmpty()) {
+ measureCodePoints(needMeasureCodePoints.toHashSet().toList())
+ for (i in needMeasureCodePoints.indices) {
+ textWidth += measureCodePoint(needMeasureCodePoints[i])
+ }
+ }
+ return textWidth
+ }
+
+ private fun String.toCodePoints(): List {
+ val codePoints = ArrayList(length)
+ val charArray = toCharArray()
+ val size = length
+ var i = 0
+ while (i < size) {
+ val c1 = charArray[i++]
+ var cp = c1.code
+ if (c1.isHighSurrogate() && i < size) {
+ val c2 = charArray[i]
+ if (c2.isLowSurrogate()) {
+ i++
+ cp = Character.toCodePoint(c1, c2)
+ }
+ }
+ codePoints.add(cp)
+ }
+ return codePoints
+ }
+
+ fun setPaint(paint: TextPaint) {
+ this.paint = paint
+ invalidate()
+ }
+
+ private fun invalidate() {
+ chineseCommonWidth = paint.measureText("一")
+ codePointWidths.clear()
+ asciiWidths.fill(-1f)
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt
index 1bceccd6a..f69b022b1 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
@@ -12,7 +12,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource
}
override fun hasNext(): Boolean = with(dataSource) {
- return hasNextChapter() || currentChapter?.isLastIndex(pageIndex) != true
+ return hasNextChapter() || (currentChapter != null && currentChapter?.isLastIndex(pageIndex) != true)
}
override fun hasNextPlus(): Boolean = with(dataSource) {
@@ -34,9 +34,12 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource
}
override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) {
- return if (hasNext() && currentChapter != null) {
- if (currentChapter?.isLastIndex(pageIndex) == true) {
- ReadBook.moveToNextChapter(upContent)
+ return if (hasNext()) {
+ if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) {
+ if ((currentChapter == null || isScroll) && nextChapter == null) {
+ return@with false
+ }
+ ReadBook.moveToNextChapter(upContent, false)
} else {
ReadBook.setPageIndex(pageIndex.plus(1))
}
@@ -47,10 +50,16 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource
}
override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) {
- return if (hasPrev() && currentChapter != null) {
+ return if (hasPrev()) {
if (pageIndex <= 0) {
- ReadBook.moveToPrevChapter(upContent)
+ if (currentChapter == null && prevChapter == null) {
+ return@with false
+ }
+ ReadBook.moveToPrevChapter(upContent, upContentInPlace = false)
} else {
+ if (currentChapter == null) {
+ return@with false
+ }
ReadBook.setPageIndex(pageIndex.minus(1))
}
if (upContent) upContent(resetPageOffset = false)
@@ -76,14 +85,12 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource
return@with TextPage(text = it).format()
}
currentChapter?.let {
+ val pageIndex = pageIndex
if (pageIndex < it.pageSize - 1) {
return@with it.getPage(pageIndex + 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
}
- if (!hasNextChapter()) {
- return@with TextPage(text = "")
- }
nextChapter?.let {
return@with it.getPage(0)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
@@ -96,8 +103,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource
ReadBook.msg?.let {
return@with TextPage(text = it).format()
}
- if (pageIndex > 0) {
- currentChapter?.let {
+ currentChapter?.let {
+ val pageIndex = pageIndex
+ if (pageIndex > 0) {
return@with it.getPage(pageIndex - 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
}
@@ -112,6 +120,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource
override val nextPlusPage: TextPage
get() = with(dataSource) {
currentChapter?.let {
+ val pageIndex = pageIndex
if (pageIndex < it.pageSize - 2) {
return@with it.getPage(pageIndex + 2)?.removePageAloudSpan()
?: TextPage(title = it.title).format()
@@ -124,7 +133,6 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource
return@with nc.getPage(1)?.removePageAloudSpan()
?: TextPage(text = "继续滑动以加载下一章…").format()
}
-
}
return TextPage().format()
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt
index ab8db392f..894c61fc3 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt
@@ -16,7 +16,6 @@ class ZhLayout(
text: CharSequence,
textPaint: TextPaint,
width: Int,
- widthsArray: FloatArray
) : Layout(text, textPaint, width, Alignment.ALIGN_NORMAL, 0f, 0f) {
companion object {
private val postPanc = hashSetOf(
@@ -50,12 +49,7 @@ class ZhLayout(
init {
var line = 0
- curPaint.getTextWidths(text as String, widthsArray)
- val (words, widths) = ChapterProvider.getStringArrayAndTextWidths(
- text,
- widthsArray.asList(),
- curPaint
- )
+ val (words, widths) = ChapterProvider.measureTextSplit(text as String, textPaint)
var lineW = 0f
var cwPre = 0f
var length = 0
diff --git a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt
index 3606b6d72..eb1dca7f5 100644
--- a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt
@@ -10,6 +10,8 @@ import android.text.StaticLayout
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatTextView
+import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
+import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
import io.legado.app.utils.dpToPx
class BatteryView @JvmOverloads constructor(
@@ -22,6 +24,7 @@ class BatteryView @JvmOverloads constructor(
private val batteryPaint = Paint()
private val outFrame = Rect()
private val polar = Rect()
+ private val canvasRecorder = CanvasRecorderFactory.create()
var isBattery = false
set(value) {
field = value
@@ -62,31 +65,40 @@ class BatteryView @JvmOverloads constructor(
}
override fun onDraw(canvas: Canvas) {
- super.onDraw(canvas)
- if (!isBattery) return
- layout.getLineBounds(0, outFrame)
- val batteryStart = layout
- .getPrimaryHorizontal(text.length - battery.toString().length)
- .toInt() + 2.dpToPx()
- val batteryEnd = batteryStart +
- StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx()
- outFrame.set(
- batteryStart,
- 2.dpToPx(),
- batteryEnd,
- height - 2.dpToPx()
- )
- val dj = (outFrame.bottom - outFrame.top) / 3
- polar.set(
- batteryEnd,
- outFrame.top + dj,
- batteryEnd + 2.dpToPx(),
- outFrame.bottom - dj
- )
- batteryPaint.style = Paint.Style.STROKE
- canvas.drawRect(outFrame, batteryPaint)
- batteryPaint.style = Paint.Style.FILL
- canvas.drawRect(polar, batteryPaint)
+ canvasRecorder.recordIfNeededThenDraw(canvas, width, height) {
+ super.onDraw(this)
+ if (!isBattery) return@recordIfNeededThenDraw
+ layout.getLineBounds(0, outFrame)
+ val batteryStart = layout
+ .getPrimaryHorizontal(text.length - battery.toString().length)
+ .toInt() + 2.dpToPx()
+ val batteryEnd = batteryStart +
+ StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx()
+ outFrame.set(
+ batteryStart,
+ 2.dpToPx(),
+ batteryEnd,
+ height - 2.dpToPx()
+ )
+ val dj = (outFrame.bottom - outFrame.top) / 3
+ polar.set(
+ batteryEnd,
+ outFrame.top + dj,
+ batteryEnd + 2.dpToPx(),
+ outFrame.bottom - dj
+ )
+ batteryPaint.style = Paint.Style.STROKE
+ drawRect(outFrame, batteryPaint)
+ batteryPaint.style = Paint.Style.FILL
+ drawRect(polar, batteryPaint)
+ }
+ }
+
+ override fun invalidate() {
+ super.invalidate()
+ kotlin.runCatching {
+ canvasRecorder.invalidate()
+ }
}
}
\ No newline at end of file
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 63a95e072..5004f1cd0 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/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
index 7c42a04c5..bf5126cb2 100644
--- a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt
@@ -36,13 +36,17 @@ class TitleBar @JvmOverloads constructor(
var title: CharSequence?
get() = toolbar.title
set(title) {
- toolbar.title = title
+ if (toolbar.title != title) {
+ toolbar.title = title
+ }
}
var subtitle: CharSequence?
get() = toolbar.subtitle
set(subtitle) {
- toolbar.subtitle = subtitle
+ if (toolbar.subtitle != subtitle) {
+ toolbar.subtitle = subtitle
+ }
}
private val displayHomeAsUp: Boolean
@@ -198,7 +202,7 @@ class TitleBar @JvmOverloads constructor(
toolbar.setSubtitleTextAppearance(context, resId)
}
- fun setTextColor(@ColorInt color: Int){
+ fun setTextColor(@ColorInt color: Int) {
setTitleTextColor(color)
setSubTitleTextColor(color)
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt
index 2882e38ad..333595470 100644
--- a/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt
@@ -12,6 +12,7 @@ import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint
import io.legado.app.utils.setHtml
import io.legado.app.utils.setLayout
+import io.legado.app.utils.setTextAsync
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.noties.markwon.Markwon
import io.noties.markwon.ext.tables.TablePlugin
@@ -75,8 +76,9 @@ class TextDialog() : BaseDialogFragment(R.layout.dialog_text_view) {
.build()
.setMarkdown(binding.textView, content)
}
+
Mode.HTML.name -> binding.textView.setHtml(content)
- else -> binding.textView.text = content
+ else -> binding.textView.setTextAsync(content)
}
time = it.getLong("time", 0L)
}
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 2f2e08411..000000000
--- a/app/src/main/java/io/legado/app/utils/BitmapCache.kt
+++ /dev/null
@@ -1,77 +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))
- }
-
- 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 e962b5a43..80c19e88f 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/ChineseUtils.kt b/app/src/main/java/io/legado/app/utils/ChineseUtils.kt
index d7d01bda9..34744e32c 100644
--- a/app/src/main/java/io/legado/app/utils/ChineseUtils.kt
+++ b/app/src/main/java/io/legado/app/utils/ChineseUtils.kt
@@ -32,26 +32,19 @@ object ChineseUtils {
fun fixT2sDict() {
val dict = DictionaryContainer.getInstance().getDictionary(TransType.TRADITIONAL_TO_SIMPLE)
dict.run {
- remove("劈")
- remove("脊")
- remove("支援")
- remove("沈默")
- remove("類比")
- remove("模擬")
- remove("划槳")
- remove("列根")
- remove("路易斯")
- remove("非同步")
- remove("出租车")
- remove("周杰倫")
+ remove("劈", "脊")
+ remove("支援", "沈默", "類比", "模擬", "划槳", "列根", "先進")
+ remove("路易斯", "非同步", "出租车", "周杰倫")
}
}
- fun BasicDictionary.remove(key: String) {
- if (key.length == 1) {
- chars.remove(key[0])
- } else {
- dict.remove(key)
+ fun BasicDictionary.remove(vararg keys: String) {
+ for (key in keys) {
+ if (key.length == 1) {
+ chars.remove(key[0])
+ } else {
+ dict.remove(key)
+ }
}
}
diff --git a/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt
new file mode 100644
index 000000000..e8e7b87d7
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt
@@ -0,0 +1,39 @@
+package io.legado.app.utils
+
+fun List.fastSum(): Float {
+ var sum = 0f
+ for (i in indices) {
+ sum += this[i]
+ }
+ 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/ViewExtensions.kt b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt
index 03096add4..89a8887bf 100644
--- a/app/src/main/java/io/legado/app/utils/ViewExtensions.kt
+++ b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt
@@ -25,14 +25,20 @@ import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.MenuPopupHelper
+import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.record
import androidx.core.graphics.withTranslation
+import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.get
+import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import io.legado.app.help.config.AppConfig
+import io.legado.app.help.globalExecutor
import io.legado.app.lib.theme.TintHelper
+import io.legado.app.utils.canvasrecorder.CanvasRecorder
+import io.legado.app.utils.canvasrecorder.record
import splitties.systemservices.inputMethodManager
import java.lang.reflect.Field
@@ -176,6 +182,16 @@ fun View.screenshot(picture: Picture) {
}
}
+fun View.screenshot(canvasRecorder: CanvasRecorder) {
+ if (width > 0 && height > 0) {
+ canvasRecorder.record(width, height) {
+ withTranslation(-scrollX.toFloat(), -scrollY.toFloat()) {
+ draw(this)
+ }
+ }
+ }
+}
+
fun View.setPaddingBottom(bottom: Int) {
setPadding(paddingLeft, paddingTop, paddingRight, bottom)
}
@@ -216,6 +232,23 @@ fun TextView.setHtml(html: String) {
}
}
+fun AppCompatTextView.setTextAsync(charSequence: CharSequence) {
+ globalExecutor.execute {
+ val precomputedText = PrecomputedTextCompat.create(
+ charSequence, TextViewCompat.getTextMetricsParams(this),
+ )
+ post {
+ setPrecomputedText(precomputedText)
+ }
+ }
+}
+
+fun TextView.setTextIfNotEqual(charSequence: CharSequence?) {
+ if (text != charSequence) {
+ text = charSequence
+ }
+}
+
@SuppressLint("RestrictedApi")
fun PopupMenu.show(x: Int, y: Int) {
kotlin.runCatching {
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt
new file mode 100644
index 000000000..ee521b0b4
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt
@@ -0,0 +1,36 @@
+package io.legado.app.utils.canvasrecorder
+
+import androidx.annotation.CallSuper
+
+abstract class BaseCanvasRecorder : CanvasRecorder {
+
+ @JvmField
+ protected var isDirty = true
+
+ override fun invalidate() {
+ isDirty = true
+ }
+
+ @CallSuper
+ override fun recycle() {
+ isDirty = true
+ }
+
+ @CallSuper
+ override fun endRecording() {
+ isDirty = false
+ }
+
+ override fun isDirty(): Boolean {
+ return isDirty
+ }
+
+ override fun isLocked(): Boolean {
+ return false
+ }
+
+ override fun needRecord(): Boolean {
+ return isDirty() && !isLocked()
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt
new file mode 100644
index 000000000..904d1bf56
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt
@@ -0,0 +1,27 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+
+interface CanvasRecorder {
+
+ val width: Int
+
+ val height: Int
+
+ fun beginRecording(width: Int, height: Int): Canvas
+
+ fun endRecording()
+
+ fun draw(canvas: Canvas)
+
+ fun invalidate()
+
+ fun recycle()
+
+ fun isDirty(): Boolean
+
+ fun isLocked(): Boolean
+
+ fun needRecord(): Boolean
+
+}
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt
new file mode 100644
index 000000000..b23024e85
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt
@@ -0,0 +1,39 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import android.graphics.Picture
+
+class CanvasRecorderApi23Impl : BaseCanvasRecorder() {
+
+ private var picture: Picture? = null
+
+ override val width get() = picture?.width ?: -1
+ override val height get() = picture?.height ?: -1
+
+ private fun initPicture() {
+ if (picture == null) {
+ picture = Picture()
+ }
+ }
+
+ override fun beginRecording(width: Int, height: Int): Canvas {
+ initPicture()
+ return picture!!.beginRecording(width, height)
+ }
+
+ override fun endRecording() {
+ picture!!.endRecording()
+ super.endRecording()
+ }
+
+ override fun draw(canvas: Canvas) {
+ if (picture == null) return
+ canvas.drawPicture(picture!!)
+ }
+
+ override fun recycle() {
+ super.recycle()
+ picture = null
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt
new file mode 100644
index 000000000..d6d7ddc6f
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt
@@ -0,0 +1,64 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import android.graphics.Picture
+import android.graphics.RenderNode
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.Q)
+class CanvasRecorderApi29Impl : BaseCanvasRecorder() {
+
+ private var renderNode: RenderNode? = null
+ private var picture: Picture? = null
+
+ override val width get() = renderNode?.width ?: -1
+ override val height get() = renderNode?.height ?: -1
+
+ private fun init() {
+ if (renderNode == null) {
+ renderNode = RenderNode("CanvasRecorder")
+ }
+ if (picture == null) {
+ picture = Picture()
+ }
+ }
+
+ private fun flushRenderNode() {
+ val rc = renderNode!!.beginRecording()
+ rc.drawPicture(picture!!)
+ renderNode!!.endRecording()
+ }
+
+ override fun beginRecording(width: Int, height: Int): Canvas {
+ init()
+ renderNode!!.setPosition(0, 0, width, height)
+ return picture!!.beginRecording(width, height)
+ }
+
+ override fun endRecording() {
+ picture!!.endRecording()
+ flushRenderNode()
+ super.endRecording()
+ }
+
+ override fun draw(canvas: Canvas) {
+ if (renderNode == null || picture == null) return
+ if (canvas.isHardwareAccelerated) {
+ if (!renderNode!!.hasDisplayList()) {
+ flushRenderNode()
+ }
+ canvas.drawRenderNode(renderNode!!)
+ } else {
+ canvas.drawPicture(picture!!)
+ }
+ }
+
+ override fun recycle() {
+ super.recycle()
+ renderNode?.discardDisplayList()
+ renderNode = null
+ picture = null
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt
new file mode 100644
index 000000000..9e4ddbff7
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt
@@ -0,0 +1,35 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import androidx.core.graphics.withSave
+
+inline fun CanvasRecorder.recordIfNeeded(
+ width: Int,
+ height: Int,
+ block: Canvas.() -> Unit
+): Boolean {
+ if (!needRecord()) return false
+ record(width, height, block)
+ return true
+}
+
+inline fun CanvasRecorder.record(width: Int, height: Int, block: Canvas.() -> Unit) {
+ val canvas = beginRecording(width, height)
+ try {
+ canvas.withSave {
+ block()
+ }
+ } finally {
+ endRecording()
+ }
+}
+
+inline fun CanvasRecorder.recordIfNeededThenDraw(
+ canvas: Canvas,
+ width: Int,
+ height: Int,
+ block: Canvas.() -> Unit
+) {
+ recordIfNeeded(width, height, block)
+ draw(canvas)
+}
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt
new file mode 100644
index 000000000..038331557
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt
@@ -0,0 +1,23 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.os.Build
+
+object CanvasRecorderFactory {
+
+ private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ private val atLeastApi29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+
+ fun create(locked: Boolean = false): CanvasRecorder {
+ val impl = when {
+ atLeastApi29 -> CanvasRecorderApi29Impl()
+ atLeastApi23 -> CanvasRecorderApi23Impl()
+ else -> CanvasRecorderImpl()
+ }
+ return if (locked) {
+ CanvasRecorderLocked(impl)
+ } else {
+ impl
+ }
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt
new file mode 100644
index 000000000..40ad53151
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt
@@ -0,0 +1,59 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+
+class CanvasRecorderImpl : BaseCanvasRecorder() {
+
+ var bitmap: Bitmap? = null
+ var canvas: Canvas? = null
+
+ override val width get() = bitmap?.width ?: -1
+ override val height get() = bitmap?.height ?: -1
+
+ private fun init(width: Int, height: Int) {
+ if (bitmap == null) {
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ }
+ if (canvas == null) {
+ canvas = Canvas(bitmap!!)
+ }
+ if (bitmap!!.width != width || bitmap!!.height != height) {
+ if (canReconfigure(width, height)) {
+ bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888)
+ } else {
+ bitmap!!.recycle()
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ }
+ canvas!!.setBitmap(bitmap!!)
+ }
+ }
+
+ private fun canReconfigure(width: Int, height: Int): Boolean {
+ return bitmap!!.allocationByteCount >= width * height * 4
+ }
+
+ override fun beginRecording(width: Int, height: Int): Canvas {
+ init(width, height)
+ bitmap!!.eraseColor(Color.TRANSPARENT)
+ return canvas!!
+ }
+
+ override fun endRecording() {
+ bitmap!!.prepareToDraw()
+ super.endRecording()
+ }
+
+ override fun draw(canvas: Canvas) {
+ if (bitmap == null) return
+ canvas.drawBitmap(bitmap!!, 0f, 0f, null)
+ }
+
+ override fun recycle() {
+ super.recycle()
+ bitmap?.recycle()
+ bitmap = null
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt
new file mode 100644
index 000000000..993491abd
--- /dev/null
+++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt
@@ -0,0 +1,52 @@
+package io.legado.app.utils.canvasrecorder
+
+import android.graphics.Canvas
+import java.util.concurrent.locks.ReentrantLock
+
+class CanvasRecorderLocked(private val delegate: CanvasRecorder) :
+ CanvasRecorder by delegate {
+
+ var lock: ReentrantLock? = ReentrantLock()
+
+ private fun initLock() {
+ if (lock == null) {
+ synchronized(this) {
+ if (lock == null) {
+ lock = ReentrantLock()
+ }
+ }
+ }
+ }
+
+ override fun beginRecording(width: Int, height: Int): Canvas {
+ initLock()
+ lock!!.lock()
+ return delegate.beginRecording(width, height)
+ }
+
+ override fun endRecording() {
+ delegate.endRecording()
+ lock!!.unlock()
+ }
+
+ override fun draw(canvas: Canvas) {
+ if (lock == null) return
+ if (!lock!!.tryLock()) return
+ try {
+ delegate.draw(canvas)
+ } finally {
+ lock!!.unlock()
+ }
+ }
+
+ override fun isLocked(): Boolean {
+ if (lock == null) return false
+ return lock!!.isLocked
+ }
+
+ override fun recycle() {
+ delegate.recycle()
+ lock = null
+ }
+
+}
diff --git a/app/src/main/res/xml/pref_config_read.xml b/app/src/main/res/xml/pref_config_read.xml
index 045303a6d..b6fa78c39 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" />
-
-