Merge pull request #3731 from gedoor/master

Sync changes from branch master
This commit is contained in:
Xwite 2024-02-19 14:24:10 +08:00 committed by GitHub
commit 92121fbb8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 1833 additions and 955 deletions

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: 简繁转化
url: https://github.com/liuyueyi/quick-chinese-transfer/issues/new
about: 简繁转化问题请优先到quick-chinese-transfer反馈
- name: 讨论 / Discussions - name: 讨论 / Discussions
url: https://github.com/gedoor/legado/discussions url: https://github.com/gedoor/legado/discussions
about: Please ask and answer questions here. about: Please ask and answer questions here.

View File

@ -78,6 +78,7 @@ jobs:
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v3
- name: Build With Gradle - name: Build With Gradle
continue-on-error: true
run: | run: |
if [ ${{ env.type }} == 'release' ]; then if [ ${{ env.type }} == 'release' ]; then
typeName="原包名" typeName="原包名"
@ -91,15 +92,22 @@ jobs:
echo "开始${{ env.product }}$typeName构建" echo "开始${{ env.product }}$typeName构建"
chmod +x gradlew chmod +x gradlew
./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all ./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 }}/apk/
mkdir -p ${{ github.workspace }}/mapping/
for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do
mv "$file" ${{ github.workspace }}/apk/legado_${{ env.product }}_${{ env.VERSIONL }}_$typeName.apk mv "$file" ${{ github.workspace }}/apk/legado_${{ env.product }}_${{ env.VERSIONL }}_$typeName.apk
done done
echo "移动mapping文件"
mkdir -p ${{ github.workspace }}/mapping/
for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/mapping.txt`; do for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/mapping.txt`; do
mv "$file" ${{ github.workspace }}/mapping/mapping.txt mv "$file" ${{ github.workspace }}/mapping/mapping.txt
done 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 - name: Upload App To Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@ -112,6 +120,12 @@ jobs:
name: legado.${{ env.product }}.${{ env.type }}.mapping name: legado.${{ env.product }}.${{ env.type }}.mapping
path: ${{ github.workspace }}/mapping/mapping.txt 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: lanzou:
needs: [ prepare, build ] needs: [ prepare, build ]
if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.lanzou == 'yes' }} if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.lanzou == 'yes' }}

View File

@ -27,7 +27,7 @@ jobs:
with: with:
node-version: 16 node-version: 16
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
id: pnpm-install id: pnpm-install
with: with:

View File

@ -473,6 +473,15 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
tools:node="remove" />
</provider>
<meta-data <meta-data
android:name="channel" android:name="channel"

View File

@ -117,7 +117,6 @@ object PreferKey {
const val welcomeShowIconDark = "welcomeShowIconDark" const val welcomeShowIconDark = "welcomeShowIconDark"
const val pageTouchSlop = "pageTouchSlop" const val pageTouchSlop = "pageTouchSlop"
const val showAddToShelfAlert = "showAddToShelfAlert" const val showAddToShelfAlert = "showAddToShelfAlert"
const val asyncLoadImage = "asyncLoadImage"
const val ignoreAudioFocus = "ignoreAudioFocus" const val ignoreAudioFocus = "ignoreAudioFocus"
const val parallelExportBook = "parallelExportBook" const val parallelExportBook = "parallelExportBook"
const val progressBarBehavior = "progressBarBehavior" const val progressBarBehavior = "progressBarBehavior"

View File

@ -0,0 +1,6 @@
package io.legado.app.help
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
val globalExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() }

View File

@ -137,6 +137,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
} }
} }
val textSelectAble: Boolean
get() = appCtx.getPrefBoolean(PreferKey.textSelectAble, true)
val isTransparentStatusBar: Boolean val isTransparentStatusBar: Boolean
get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true) get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true)
@ -457,8 +460,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val showAddToShelfAlert get() = appCtx.getPrefBoolean(PreferKey.showAddToShelfAlert, true) val showAddToShelfAlert get() = appCtx.getPrefBoolean(PreferKey.showAddToShelfAlert, true)
val asyncLoadImage get() = appCtx.getPrefBoolean(PreferKey.asyncLoadImage, false)
val ignoreAudioFocus get() = appCtx.getPrefBoolean(PreferKey.ignoreAudioFocus, false) val ignoreAudioFocus get() = appCtx.getPrefBoolean(PreferKey.ignoreAudioFocus, false)
val onlyLatestBackup get() = appCtx.getPrefBoolean(PreferKey.onlyLatestBackup, true) val onlyLatestBackup get() = appCtx.getPrefBoolean(PreferKey.onlyLatestBackup, true)

View File

@ -567,7 +567,6 @@ object ReadBookConfig {
textColorInt = color textColorInt = color
} }
} }
ChapterProvider.upStyle()
} }
fun curTextColor(): Int { fun curTextColor(): Int {

View File

@ -163,7 +163,22 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
.apply() .apply()
} }
companion object { companion object : SharedPreferences.OnSharedPreferenceChangeListener {
init {
prefs(appCtx).registerOnSharedPreferenceChangeListener(this)
}
var accentColor = accentColor()
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?
) {
when (key) {
ThemeStorePrefKeys.KEY_ACCENT_COLOR -> accentColor = accentColor()
}
}
fun editTheme(context: Context): ThemeStore { fun editTheme(context: Context): ThemeStore {
return ThemeStore(context) return ThemeStore(context)

View File

@ -2,13 +2,10 @@ package io.legado.app.model
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Build
import android.util.Size import android.util.Size
import androidx.collection.LruCache import androidx.collection.LruCache
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppLog.putDebug 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.Book
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException 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.isEpub
import io.legado.app.help.book.isPdf import io.legado.app.help.book.isPdf
import io.legado.app.help.config.AppConfig 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.EpubFile
import io.legado.app.model.localBook.PdfFile import io.legado.app.model.localBook.PdfFile
import io.legado.app.utils.BitmapCache
import io.legado.app.utils.BitmapUtils import io.legado.app.utils.BitmapUtils
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.SvgUtils import io.legado.app.utils.SvgUtils
@ -29,8 +24,6 @@ import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.min
object ImageProvider { object ImageProvider {
@ -50,12 +43,6 @@ object ImageProvider {
} }
return AppConfig.bitmapCacheSize * M 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<String>()
val bitmapLruCache = object : LruCache<String, Bitmap>(cacheSize) { val bitmapLruCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(filePath: String, bitmap: Bitmap): Int { override fun sizeOf(filePath: String, bitmap: Bitmap): Int {
@ -70,8 +57,7 @@ object ImageProvider {
) { ) {
//错误图片不能释放,占位用,防止一直重复获取图片 //错误图片不能释放,占位用,防止一直重复获取图片
if (oldBitmap != errorBitmap) { if (oldBitmap != errorBitmap) {
BitmapCache.add(oldBitmap) oldBitmap.recycle()
//oldBitmap.recycle()
//putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath") //putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath")
//putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes") //putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes")
} }
@ -160,9 +146,8 @@ object ImageProvider {
book: Book, book: Book,
src: String, src: String,
width: Int, width: Int,
height: Int? = null, height: Int? = null
block: (() -> Unit)? = null ): Bitmap {
): Bitmap? {
//src为空白时 可能被净化替换掉了 或者规则失效 //src为空白时 可能被净化替换掉了 或者规则失效
if (book.getUseReplaceRule() && src.isBlank()) { if (book.getUseReplaceRule() && src.isBlank()) {
book.setUseReplaceRule(false) book.setUseReplaceRule(false)
@ -174,32 +159,6 @@ object ImageProvider {
//bitmapLruCache的key同一改成缓存文件的路径 //bitmapLruCache的key同一改成缓存文件的路径
val cacheBitmap = getNotRecycled(vFile.absolutePath) val cacheBitmap = getNotRecycled(vFile.absolutePath)
if (cacheBitmap != null) return cacheBitmap 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 { return kotlin.runCatching {
val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height) val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
?: SvgUtils.createBitmap(vFile.absolutePath, width, height) ?: SvgUtils.createBitmap(vFile.absolutePath, width, height)
@ -212,4 +171,8 @@ object ImageProvider {
}.getOrDefault(errorBitmap) }.getOrDefault(errorBitmap)
} }
fun clear() {
bitmapLruCache.evictAll()
}
} }

View File

@ -14,6 +14,7 @@ import io.legado.app.help.book.isLocal
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.coroutine.Coroutine 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.localBook.TextFile
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
@ -63,6 +64,7 @@ object ReadBook : CoroutineScope by MainScope() {
val downloadFailChapters = hashMapOf<Int, Int>() val downloadFailChapters = hashMapOf<Int, Int>()
var contentProcessor: ContentProcessor? = null var contentProcessor: ContentProcessor? = null
val downloadScope = CoroutineScope(SupervisorJob() + IO) val downloadScope = CoroutineScope(SupervisorJob() + IO)
val executor = globalExecutor
//暂时保存跳转前进度 //暂时保存跳转前进度
fun saveCurrentBookProcess() { fun saveCurrentBookProcess() {
@ -158,10 +160,10 @@ object ReadBook : CoroutineScope by MainScope() {
} }
fun upReadTime() { fun upReadTime() {
if (!AppConfig.enableReadRecord) { executor.execute {
return if (!AppConfig.enableReadRecord) {
} return@execute
Coroutine.async(executeContext = IO) { }
readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime
readStartTime = System.currentTimeMillis() readStartTime = System.currentTimeMillis()
readRecord.lastRead = System.currentTimeMillis() readRecord.lastRead = System.currentTimeMillis()
@ -204,7 +206,7 @@ object ReadBook : CoroutineScope by MainScope() {
return hasPrevPage return hasPrevPage
} }
fun moveToNextChapter(upContent: Boolean): Boolean { fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
if (durChapterIndex < chapterSize - 1) { if (durChapterIndex < chapterSize - 1) {
durChapterPos = 0 durChapterPos = 0
durChapterIndex++ durChapterIndex++
@ -213,11 +215,11 @@ object ReadBook : CoroutineScope by MainScope() {
nextTextChapter = null nextTextChapter = null
if (curTextChapter == null) { if (curTextChapter == null) {
AppLog.putDebug("moveToNextChapter-章节未加载,开始加载") AppLog.putDebug("moveToNextChapter-章节未加载,开始加载")
callBack?.upContent() if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false) loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent) { } else if (upContent && upContentInPlace) {
AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图") AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图")
callBack?.upContent() callBack?.upContent(resetPageOffset = false)
} }
loadContent(durChapterIndex.plus(1), upContent, false) loadContent(durChapterIndex.plus(1), upContent, false)
saveRead() saveRead()
@ -233,7 +235,8 @@ object ReadBook : CoroutineScope by MainScope() {
fun moveToPrevChapter( fun moveToPrevChapter(
upContent: Boolean, upContent: Boolean,
toLast: Boolean = true toLast: Boolean = true,
upContentInPlace: Boolean = true
): Boolean { ): Boolean {
if (durChapterIndex > 0) { if (durChapterIndex > 0) {
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0 durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
@ -242,10 +245,10 @@ object ReadBook : CoroutineScope by MainScope() {
curTextChapter = prevTextChapter curTextChapter = prevTextChapter
prevTextChapter = null prevTextChapter = null
if (curTextChapter == null) { if (curTextChapter == null) {
callBack?.upContent() if (upContentInPlace) callBack?.upContent(resetPageOffset = false)
loadContent(durChapterIndex, upContent, resetPageOffset = false) loadContent(durChapterIndex, upContent, resetPageOffset = false)
} else if (upContent) { } else if (upContent && upContentInPlace) {
callBack?.upContent() callBack?.upContent(resetPageOffset = false)
} }
loadContent(durChapterIndex.minus(1), upContent, false) loadContent(durChapterIndex.minus(1), upContent, false)
saveRead() saveRead()
@ -267,6 +270,16 @@ object ReadBook : CoroutineScope by MainScope() {
} }
fun setPageIndex(index: Int) { 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 durChapterPos = curTextChapter?.getReadLength(index) ?: index
saveRead(true) saveRead(true)
curPageChanged(true) curPageChanged(true)
@ -525,8 +538,8 @@ object ReadBook : CoroutineScope by MainScope() {
} }
fun saveRead(pageChanged: Boolean = false) { fun saveRead(pageChanged: Boolean = false) {
Coroutine.async(executeContext = IO) { executor.execute {
val book = book ?: return@async val book = book ?: return@execute
book.lastCheckCount = 0 book.lastCheckCount = 0
book.durChapterTime = System.currentTimeMillis() book.durChapterTime = System.currentTimeMillis()
val chapterChanged = book.durChapterIndex != durChapterIndex val chapterChanged = book.durChapterIndex != durChapterIndex
@ -549,26 +562,29 @@ object ReadBook : CoroutineScope by MainScope() {
*/ */
private fun preDownload() { private fun preDownload() {
if (book?.isLocal == true) return if (book?.isLocal == true) return
if (AppConfig.preDownloadNum < 2) { executor.execute {
return if (AppConfig.preDownloadNum < 2) {
} return@execute
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 { preDownloadTask?.cancel()
val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum) preDownloadTask = Coroutine.async(executeContext = IO) {
for (i in durChapterIndex.minus(2) downTo minChapterIndex) { //预下载
if (downloadedChapters.contains(i)) continue launch {
if ((downloadFailChapters[i] ?: 0) >= 3) continue val maxChapterIndex =
downloadIndex(i) 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() coroutineContext.cancelChildren()
downloadedChapters.clear() downloadedChapters.clear()
downloadFailChapters.clear() downloadFailChapters.clear()
ImageProvider.clear()
} }
interface CallBack { interface CallBack {

View File

@ -197,7 +197,7 @@ class AnalyzeRule(
} }
if (sourceRule.replaceRegex.isNotEmpty() && result is List<*>) { if (sourceRule.replaceRegex.isNotEmpty() && result is List<*>) {
val newList = ArrayList<String>() val newList = ArrayList<String>()
for (item in result as List<*>) { for (item in result) {
newList.add(replaceRegex(item.toString(), sourceRule)) newList.add(replaceRegex(item.toString(), sourceRule))
} }
result = newList result = newList
@ -210,12 +210,12 @@ class AnalyzeRule(
} }
if (result == null) return null if (result == null) return null
if (result is String) { if (result is String) {
result = (result as String).split("\n") result = result.split("\n")
} }
if (isUrl) { if (isUrl) {
val urlList = ArrayList<String>() val urlList = ArrayList<String>()
if (result is List<*>) { if (result is List<*>) {
for (url in result as List<*>) { for (url in result) {
val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString()) val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString())
if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) { if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) {
urlList.add(absoluteURL) urlList.add(absoluteURL)

View File

@ -91,7 +91,7 @@ class HttpReadAloudService : BaseReadAloudService(),
playIndexJob?.cancel() playIndexJob?.cancel()
} }
private fun playNext() { private fun updateNextPos() {
readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos
paragraphStartPos = 0 paragraphStartPos = 0
if (nowSpeak < contentList.lastIndex) { if (nowSpeak < contentList.lastIndex) {
@ -355,14 +355,14 @@ class HttpReadAloudService : BaseReadAloudService(),
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
// 结束 // 结束
playErrorNo = 0 playErrorNo = 0
playNext() updateNextPos()
} }
} }
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return
playNext() updateNextPos()
upPlayPos() upPlayPos()
} }
@ -375,7 +375,8 @@ class HttpReadAloudService : BaseReadAloudService(),
AppLog.put("朗读连续5次错误, 最后一次错误代码(${error.localizedMessage})", error) AppLog.put("朗读连续5次错误, 最后一次错误代码(${error.localizedMessage})", error)
ReadAloud.pause(this) ReadAloud.pause(this)
} else { } else {
playNext() updateNextPos()
exoPlayer.seekToNextMediaItem()
} }
} }

View File

@ -58,6 +58,12 @@ abstract class BaseReadBookActivity :
override val binding by viewBinding(ActivityBookReadBinding::inflate) override val binding by viewBinding(ActivityBookReadBinding::inflate)
override val viewModel by viewModels<ReadBookViewModel>() override val viewModel by viewModels<ReadBookViewModel>()
var bottomDialog = 0 var bottomDialog = 0
set(value) {
if (field != value) {
field = value
onBottomDialogChange()
}
}
private val selectBookFolderResult = registerForActivityResult(HandleFileContract()) { private val selectBookFolderResult = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri -> it.uri?.let { uri ->
ReadBook.book?.let { book -> 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() { fun showPaddingConfig() {
showDialogFragment<PaddingConfigDialog>() showDialogFragment<PaddingConfigDialog>()
} }

View File

@ -6,6 +6,7 @@ import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Looper
import android.view.Gravity import android.view.Gravity
import android.view.InputDevice import android.view.InputDevice
import android.view.KeyEvent 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.ContentTextView
import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.read.page.entities.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.SearchContentActivity
import io.legado.app.ui.book.searchContent.SearchResult import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.ui.book.source.edit.BookSourceEditActivity 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.Debounce
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.StartActivityContract import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.SyncedRenderer
import io.legado.app.utils.applyOpenTint import io.legado.app.utils.applyOpenTint
import io.legado.app.utils.buildMainHandler
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import io.legado.app.utils.hexString import io.legado.app.utils.hexString
@ -191,7 +192,6 @@ class ReadBookActivity : BaseReadBookActivity(),
} }
private var menu: Menu? = null private var menu: Menu? = null
private var backupJob: Job? = null private var backupJob: Job? = null
private var keepScreenJon: Job? = null
private var tts: TTS? = null private var tts: TTS? = null
val textActionMenu: TextActionMenu by lazy { val textActionMenu: TextActionMenu by lazy {
TextActionMenu(this, this) TextActionMenu(this, this)
@ -201,8 +201,7 @@ class ReadBookActivity : BaseReadBookActivity(),
} }
override val isInitFinish: Boolean get() = viewModel.isInitFinish override val isInitFinish: Boolean get() = viewModel.isInitFinish
override val isScroll: Boolean get() = binding.readView.isScroll override val isScroll: Boolean get() = binding.readView.isScroll
override var autoPageProgress = 0 private val isAutoPage get() = binding.readView.isAutoPage
override var isAutoPage = false
override var isShowingSearchResult = false override var isShowingSearchResult = false
override var isSelectingSearchResult = false override var isSelectingSearchResult = false
set(value) { set(value) {
@ -211,16 +210,17 @@ class ReadBookActivity : BaseReadBookActivity(),
private val timeBatteryReceiver = TimeBatteryReceiver() private val timeBatteryReceiver = TimeBatteryReceiver()
private var screenTimeOut: Long = 0 private var screenTimeOut: Long = 0
private var loadStates: Boolean = false 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 override val headerHeight: Int get() = binding.readView.curPage.headerHeight
private val menuLayoutIsVisible get() = bottomDialog > 0 || binding.readMenu.isVisible private val menuLayoutIsVisible get() = bottomDialog > 0 || binding.readMenu.isVisible
private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } } private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } }
private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } } private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } }
private var bookChanged = false private var bookChanged = false
private var pageChanged = false private var pageChanged = false
private var reloadContent = false private val handler by lazy { buildMainHandler() }
private val autoPageRenderer by lazy { SyncedRenderer { doAutoPage(it) } } private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
private var autoPageScrollOffset = 0.0 private val executor = ReadBook.executor
//恢复跳转前进度对话框的交互结果 //恢复跳转前进度对话框的交互结果
private var confirmRestoreProcess: Boolean? = null private var confirmRestoreProcess: Boolean? = null
@ -264,23 +264,9 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
viewModel.initData(intent) { Looper.myQueue().addIdleHandler {
initDataSuccess() viewModel.initData(intent) { upMenu() }
} false
}
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)
} }
} }
@ -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() { override fun upMenuView() {
lifecycleScope.launch { handler.post {
binding.readMenu.upBookView() binding.readMenu.upBookView()
} }
} }
@ -937,9 +928,6 @@ class ReadBookActivity : BaseReadBookActivity(),
success: (() -> Unit)? success: (() -> Unit)?
) { ) {
lifecycleScope.launch { lifecycleScope.launch {
if (relativePosition == 0) {
autoPageProgress = 0
}
binding.readView.upContent(relativePosition, resetPageOffset) binding.readView.upContent(relativePosition, resetPageOffset)
upSeekBarProgress() upSeekBarProgress()
loadStates = false loadStates = false
@ -962,9 +950,11 @@ class ReadBookActivity : BaseReadBookActivity(),
*/ */
override fun pageChanged() { override fun pageChanged() {
pageChanged = true pageChanged = true
lifecycleScope.launch { binding.readView.onPageChange()
autoPageProgress = 0 handler.post {
upSeekBarProgress() upSeekBarProgress()
}
executor.execute {
startBackupJob() startBackupJob()
} }
} }
@ -1045,8 +1035,7 @@ class ReadBookActivity : BaseReadBookActivity(),
if (isAutoPage) { if (isAutoPage) {
autoPageStop() autoPageStop()
} else { } else {
isAutoPage = true binding.readView.autoPager.start()
autoPagePlus()
binding.readMenu.setAutoPage(true) binding.readMenu.setAutoPage(true)
screenTimeOut = -1L screenTimeOut = -1L
screenOffTimerStart() screenOffTimerStart()
@ -1055,53 +1044,12 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun autoPageStop() { override fun autoPageStop() {
if (isAutoPage) { if (isAutoPage) {
isAutoPage = false binding.readView.autoPager.stop()
autoPageRenderer.stop()
binding.readView.invalidate()
binding.readView.clearNextPageBitmap()
binding.readMenu.setAutoPage(false) binding.readMenu.setAutoPage(false)
upScreenTimeOut() 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() { override fun openSourceEditActivity() {
ReadBook.bookSource?.let { ReadBook.bookSource?.let {
sourceEditActivity.launch { sourceEditActivity.launch {
@ -1350,24 +1298,24 @@ class ReadBookActivity : BaseReadBookActivity(),
when (dialogId) { when (dialogId) {
TEXT_COLOR -> { TEXT_COLOR -> {
setCurTextColor(color) setCurTextColor(color)
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, arrayOf(2, 6))
} }
BG_COLOR -> { BG_COLOR -> {
setCurBg(0, "#${color.hexString}") setCurBg(0, "#${color.hexString}")
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, arrayOf(1))
} }
TIP_COLOR -> { TIP_COLOR -> {
ReadTipConfig.tipColor = color ReadTipConfig.tipColor = color
postEvent(EventBus.TIP_COLOR, "") postEvent(EventBus.TIP_COLOR, "")
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
TIP_DIVIDER_COLOR -> { TIP_DIVIDER_COLOR -> {
ReadTipConfig.tipDividerColor = color ReadTipConfig.tipDividerColor = color
postEvent(EventBus.TIP_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) skipToSearch(searchResult)
} }
override fun onMenuShow() {
binding.readView.autoPager.pause()
}
override fun onMenuHide() {
binding.readView.autoPager.resume()
}
/* 全文搜索跳转 */ /* 全文搜索跳转 */
private fun skipToSearch(searchResult: SearchResult) { private fun skipToSearch(searchResult: SearchResult) {
val previousResult = binding.searchMenu.previousSearchResult val previousResult = binding.searchMenu.previousSearchResult
@ -1523,22 +1479,21 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadBook.readAloud(!BaseReadAloudService.pause) ReadBook.readAloud(!BaseReadAloudService.pause)
} }
} }
observeEvent<Boolean>(EventBus.UP_CONFIG) { observeEvent<Array<Int>>(EventBus.UP_CONFIG) {
upSystemUiVisibility() it.forEach { value ->
readView.upPageSlopSquare() when (value) {
readView.upBg() 0 -> upSystemUiVisibility()
readView.upStyle() 1 -> readView.upBg()
readView.upBgAlpha() 2 -> readView.upStyle()
if (it) { // 更新内容排版布局 3 -> readView.upBgAlpha()
if (isInitFinish) { 4 -> readView.upPageSlopSquare()
ReadBook.loadContent(resetPageOffset = false) 5 -> if (isInitFinish) ReadBook.loadContent(resetPageOffset = false)
} else { 6 -> readView.upContent(resetPageOffset = false)
reloadContent = true 8 -> ChapterProvider.upStyle()
9 -> binding.readView.invalidateTextPage()
10 -> ChapterProvider.upLayout()
} }
} else {
readView.upContent(resetPageOffset = false)
} }
binding.readMenu.reset()
} }
observeEvent<Int>(EventBus.ALOUD_STATE) { observeEvent<Int>(EventBus.ALOUD_STATE) {
if (it == Status.STOP || it == Status.PAUSE) { if (it == Status.STOP || it == Status.PAUSE) {
@ -1594,17 +1549,16 @@ class ReadBookActivity : BaseReadBookActivity(),
* 重置黑屏时间 * 重置黑屏时间
*/ */
override fun screenOffTimerStart() { override fun screenOffTimerStart() {
keepScreenJon?.cancel() handler.post {
keepScreenJon = lifecycleScope.launch {
if (screenTimeOut < 0) { if (screenTimeOut < 0) {
keepScreenOn(true) keepScreenOn(true)
return@launch return@post
} }
val t = screenTimeOut - sysScreenOffTime val t = screenTimeOut - sysScreenOffTime
if (t > 0) { if (t > 0) {
keepScreenOn(true) keepScreenOn(true)
delay(screenTimeOut) handler.removeCallbacks(screenOffRunnable)
keepScreenOn(false) handler.postDelayed(screenOffRunnable, screenTimeOut)
} else { } else {
keepScreenOn(false) keepScreenOn(false)
} }

View File

@ -27,13 +27,38 @@ import io.legado.app.help.config.LocalConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig import io.legado.app.help.config.ThemeConfig
import io.legado.app.lib.dialogs.alert 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.model.ReadBook
import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.browser.WebViewActivity import io.legado.app.ui.browser.WebViewActivity
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.* import io.legado.app.utils.ColorUtils
import splitties.views.* 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) { fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
callBack.onMenuShow()
this.visible() this.visible()
binding.titleBar.visible() binding.titleBar.visible()
binding.bottomMenu.visible() binding.bottomMenu.visible()
@ -286,6 +312,7 @@ class ReadMenu @JvmOverloads constructor(
} }
fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) { fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) {
callBack.onMenuHide()
this.onMenuOutEnd = onMenuOutEnd this.onMenuOutEnd = onMenuOutEnd
if (this.isVisible) { if (this.isVisible) {
if (anim) { if (anim) {
@ -558,6 +585,8 @@ class ReadMenu @JvmOverloads constructor(
fun payAction() fun payAction()
fun disableSource() fun disableSource()
fun skipToChapter(index: Int) fun skipToChapter(index: Int)
fun onMenuShow()
fun onMenuHide()
} }
} }

View File

@ -18,7 +18,13 @@ import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.searchContent.SearchResult 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.bottomPadding
import splitties.views.leftPadding import splitties.views.leftPadding
import splitties.views.padding import splitties.views.padding
@ -235,6 +241,8 @@ class SearchMenu @JvmOverloads constructor(
fun exitSearchMenu() fun exitSearchMenu()
fun showMenuBar() fun showMenuBar()
fun navigateToSearch(searchResult: SearchResult, index: Int) fun navigateToSearch(searchResult: SearchResult, index: Int)
fun onMenuShow()
fun onMenuHide()
} }
} }

View File

@ -41,7 +41,7 @@ class BgAdapter(context: Context, val textColor: Int) :
this.setOnClickListener { this.setOnClickListener {
getItemByLayoutPosition(holder.layoutPosition)?.let { getItemByLayoutPosition(holder.layoutPosition)?.let {
ReadBookConfig.durConfig.setCurBg(1, it) ReadBookConfig.durConfig.setCurBg(1, it)
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, arrayOf(1))
} }
} }
} }

View File

@ -33,11 +33,32 @@ import io.legado.app.lib.theme.getSecondaryTextColor
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.file.HandleFileContract import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener 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.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 io.legado.app.utils.viewbindingdelegate.viewBinding
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -168,7 +189,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
if (i >= 0) { if (i >= 0) {
ReadBookConfig.durConfig = defaultConfigs[i].copy() ReadBookConfig.durConfig = defaultConfigs[i].copy()
initData() 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 -> binding.swUnderline.setOnCheckedChangeListener { _, isChecked ->
underline = isChecked underline = isChecked
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(9))
} }
binding.tvTextColor.setOnClickListener { binding.tvTextColor.setOnClickListener {
ColorPickerDialog.newBuilder() ColorPickerDialog.newBuilder()
@ -214,7 +235,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
} }
binding.ivDelete.setOnClickListener { binding.ivDelete.setOnClickListener {
if (ReadBookConfig.deleteDur()) { if (ReadBookConfig.deleteDur()) {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
dismissAllowingStateLoss() dismissAllowingStateLoss()
} else { } else {
toastOnUi("数量已是最少,不能删除.") toastOnUi("数量已是最少,不能删除.")
@ -223,11 +244,11 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
binding.sbBgAlpha.setOnSeekBarChangeListener(object : SeekBarChangeListener { binding.sbBgAlpha.setOnSeekBarChangeListener(object : SeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
ReadBookConfig.bgAlpha = progress ReadBookConfig.bgAlpha = progress
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, arrayOf(3))
} }
override fun onStopTrackingTouch(seekBar: SeekBar) { 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() ReadBookConfig.import(byteArray).getOrThrow()
}.onSuccess { }.onSuccess {
ReadBookConfig.durConfig = it ReadBookConfig.durConfig = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
toastOnUi("导入成功") toastOnUi("导入成功")
}.onError { }.onError {
it.printOnDebug() it.printOnDebug()
@ -378,7 +399,7 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
inputStream.copyTo(outputStream) inputStream.copyTo(outputStream)
} }
ReadBookConfig.durConfig.setCurBg(2, fileName) ReadBookConfig.durConfig.setCurBg(2, fileName)
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, arrayOf(1))
}.onFailure { }.onFailure {
appCtx.toastOnUi(it.localizedMessage) appCtx.toastOnUi(it.localizedMessage)
} }

View File

@ -107,39 +107,48 @@ class MoreConfigDialog : DialogFragment() {
PreferKey.readBodyToLh -> activity?.recreate() PreferKey.readBodyToLh -> activity?.recreate()
PreferKey.hideStatusBar -> { PreferKey.hideStatusBar -> {
ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar) ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(0))
} }
PreferKey.hideNavigationBar -> { PreferKey.hideNavigationBar -> {
ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar) ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar)
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(0))
} }
PreferKey.keepLight -> postEvent(key, true) PreferKey.keepLight -> postEvent(key, true)
PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key)) PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key))
PreferKey.screenOrientation -> { PreferKey.screenOrientation -> {
(activity as? ReadBookActivity)?.setOrientation() (activity as? ReadBookActivity)?.setOrientation()
} }
PreferKey.textFullJustify, PreferKey.textFullJustify,
PreferKey.textBottomJustify, PreferKey.textBottomJustify,
PreferKey.useZhLayout -> { PreferKey.useZhLayout -> {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(5))
} }
PreferKey.showBrightnessView -> { PreferKey.showBrightnessView -> {
postEvent(PreferKey.showBrightnessView, "") postEvent(PreferKey.showBrightnessView, "")
} }
PreferKey.expandTextMenu -> { PreferKey.expandTextMenu -> {
(activity as? ReadBookActivity)?.textActionMenu?.upMenu() (activity as? ReadBookActivity)?.textActionMenu?.upMenu()
} }
PreferKey.doublePageHorizontal -> { PreferKey.doublePageHorizontal -> {
ChapterProvider.upLayout() ChapterProvider.upLayout()
ReadBook.loadContent(false) ReadBook.loadContent(false)
} }
PreferKey.showReadTitleAddition, PreferKey.showReadTitleAddition,
PreferKey.readBarStyleFollowPage -> { PreferKey.readBarStyleFollowPage -> {
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
} }
PreferKey.progressBarBehavior -> { PreferKey.progressBarBehavior -> {
postEvent(EventBus.UP_SEEK_BAR, true) postEvent(EventBus.UP_SEEK_BAR, true)
} }
PreferKey.noAnimScrollPage -> { PreferKey.noAnimScrollPage -> {
ReadBook.callBack?.upPageAnim() ReadBook.callBack?.upPageAnim()
} }
@ -152,6 +161,7 @@ class MoreConfigDialog : DialogFragment() {
"clickRegionalConfig" -> { "clickRegionalConfig" -> {
(activity as? ReadBookActivity)?.showClickRegionalConfig() (activity as? ReadBookActivity)?.showClickRegionalConfig()
} }
PreferKey.pageTouchSlop -> { PreferKey.pageTouchSlop -> {
NumberPickerDialog(requireContext()) NumberPickerDialog(requireContext())
.setTitle(getString(R.string.page_touch_slop_dialog_title)) .setTitle(getString(R.string.page_touch_slop_dialog_title))
@ -160,7 +170,7 @@ class MoreConfigDialog : DialogFragment() {
.setValue(AppConfig.pageTouchSlop) .setValue(AppConfig.pageTouchSlop)
.show { .show {
AppConfig.pageTouchSlop = it AppConfig.pageTouchSlop = it
postEvent(EventBus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, arrayOf(4))
} }
} }
} }

View File

@ -63,61 +63,61 @@ class PaddingConfigDialog : BaseDialogFragment(R.layout.dialog_read_padding) {
//正文 //正文
dsbPaddingTop.onChanged = { dsbPaddingTop.onChanged = {
ReadBookConfig.paddingTop = it ReadBookConfig.paddingTop = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
} }
dsbPaddingBottom.onChanged = { dsbPaddingBottom.onChanged = {
ReadBookConfig.paddingBottom = it ReadBookConfig.paddingBottom = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
} }
dsbPaddingLeft.onChanged = { dsbPaddingLeft.onChanged = {
ReadBookConfig.paddingLeft = it ReadBookConfig.paddingLeft = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
} }
dsbPaddingRight.onChanged = { dsbPaddingRight.onChanged = {
ReadBookConfig.paddingRight = it ReadBookConfig.paddingRight = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(10, 5))
} }
//页眉 //页眉
dsbHeaderPaddingTop.onChanged = { dsbHeaderPaddingTop.onChanged = {
ReadBookConfig.headerPaddingTop = it ReadBookConfig.headerPaddingTop = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
dsbHeaderPaddingBottom.onChanged = { dsbHeaderPaddingBottom.onChanged = {
ReadBookConfig.headerPaddingBottom = it ReadBookConfig.headerPaddingBottom = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
dsbHeaderPaddingLeft.onChanged = { dsbHeaderPaddingLeft.onChanged = {
ReadBookConfig.headerPaddingLeft = it ReadBookConfig.headerPaddingLeft = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
dsbHeaderPaddingRight.onChanged = { dsbHeaderPaddingRight.onChanged = {
ReadBookConfig.headerPaddingRight = it ReadBookConfig.headerPaddingRight = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
//页脚 //页脚
dsbFooterPaddingTop.onChanged = { dsbFooterPaddingTop.onChanged = {
ReadBookConfig.footerPaddingTop = it ReadBookConfig.footerPaddingTop = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
dsbFooterPaddingBottom.onChanged = { dsbFooterPaddingBottom.onChanged = {
ReadBookConfig.footerPaddingBottom = it ReadBookConfig.footerPaddingBottom = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
dsbFooterPaddingLeft.onChanged = { dsbFooterPaddingLeft.onChanged = {
ReadBookConfig.footerPaddingLeft = it ReadBookConfig.footerPaddingLeft = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
dsbFooterPaddingRight.onChanged = { dsbFooterPaddingRight.onChanged = {
ReadBookConfig.footerPaddingRight = it ReadBookConfig.footerPaddingRight = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
cbShowTopLine.onCheckedChangeListener = { _, isChecked -> cbShowTopLine.onCheckedChangeListener = { _, isChecked ->
ReadBookConfig.showHeaderLine = isChecked ReadBookConfig.showHeaderLine = isChecked
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
cbShowBottomLine.onCheckedChangeListener = { _, isChecked -> cbShowBottomLine.onCheckedChangeListener = { _, isChecked ->
ReadBookConfig.showFooterLine = isChecked ReadBookConfig.showFooterLine = isChecked
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }

View File

@ -108,10 +108,10 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
private fun initViewEvent() = binding.run { private fun initViewEvent() = binding.run {
chineseConverter.onChanged { chineseConverter.onChanged {
ChineseUtils.unLoad(*TransType.entries.toTypedArray()) ChineseUtils.unLoad(*TransType.entries.toTypedArray())
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(5))
} }
textFontWeightConverter.onChanged { textFontWeightConverter.onChanged {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(8))
} }
tvTextFont.setOnClickListener { tvTextFont.setOnClickListener {
showDialogFragment<FontSelectDialog>() showDialogFragment<FontSelectDialog>()
@ -122,7 +122,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
items = resources.getStringArray(R.array.indent).toList() items = resources.getStringArray(R.array.indent).toList()
) { _, index -> ) { _, index ->
ReadBookConfig.paragraphIndent = " ".repeat(index) ReadBookConfig.paragraphIndent = " ".repeat(index)
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(5))
} }
} }
tvPadding.setOnClickListener { tvPadding.setOnClickListener {
@ -141,40 +141,40 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
cbShareLayout.onCheckedChangeListener = { _, isChecked -> cbShareLayout.onCheckedChangeListener = { _, isChecked ->
ReadBookConfig.shareLayout = isChecked ReadBookConfig.shareLayout = isChecked
upView() upView()
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
} }
dsbTextSize.onChanged = { dsbTextSize.onChanged = {
ReadBookConfig.textSize = it + 5 ReadBookConfig.textSize = it + 5
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
} }
dsbTextLetterSpacing.onChanged = { dsbTextLetterSpacing.onChanged = {
ReadBookConfig.letterSpacing = (it - 50) / 100f ReadBookConfig.letterSpacing = (it - 50) / 100f
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
} }
dsbLineSize.onChanged = { dsbLineSize.onChanged = {
ReadBookConfig.lineSpacingExtra = it ReadBookConfig.lineSpacingExtra = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
} }
dsbParagraphSpacing.onChanged = { dsbParagraphSpacing.onChanged = {
ReadBookConfig.paragraphSpacing = it 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 val oldIndex = ReadBookConfig.styleSelect
if (index != oldIndex) { if (index != oldIndex) {
ReadBookConfig.styleSelect = index ReadBookConfig.styleSelect = index
upView() upView()
styleAdapter.notifyItemChanged(oldIndex) styleAdapter.notifyItemChanged(oldIndex)
styleAdapter.notifyItemChanged(index) styleAdapter.notifyItemChanged(index)
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(1, 2, 5))
} }
} }
private fun showBgTextConfig(index: Int): Boolean { private fun showBgTextConfig(index: Int): Boolean {
dismissAllowingStateLoss() dismissAllowingStateLoss()
changeBg(index) changeBgTextConfig(index)
callBack?.showBgTextConfig() callBack?.showBgTextConfig()
return true return true
} }
@ -200,7 +200,7 @@ class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style),
override fun selectFont(path: String) { override fun selectFont(path: String) {
if (path != ReadBookConfig.textFont) { if (path != ReadBookConfig.textFont) {
ReadBookConfig.textFont = path 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 { binding.apply {
ivStyle.setOnClickListener { ivStyle.setOnClickListener {
if (ivStyle.isInView) { if (ivStyle.isInView) {
changeBg(holder.layoutPosition) changeBgTextConfig(holder.layoutPosition)
} }
} }
ivStyle.onLongClick(ivStyle.isInView) { ivStyle.onLongClick(ivStyle.isInView) {

View File

@ -11,7 +11,12 @@ import io.legado.app.databinding.DialogTipConfigBinding
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ReadTipConfig import io.legado.app.help.config.ReadTipConfig
import io.legado.app.lib.dialogs.selector 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 import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -89,26 +94,26 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
private fun initEvent() = binding.run { private fun initEvent() = binding.run {
rgTitleMode.setOnCheckedChangeListener { _, checkedId -> rgTitleMode.setOnCheckedChangeListener { _, checkedId ->
ReadBookConfig.titleMode = rgTitleMode.getIndexById(checkedId) ReadBookConfig.titleMode = rgTitleMode.getIndexById(checkedId)
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(5))
} }
dsbTitleSize.onChanged = { dsbTitleSize.onChanged = {
ReadBookConfig.titleSize = it ReadBookConfig.titleSize = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
} }
dsbTitleTop.onChanged = { dsbTitleTop.onChanged = {
ReadBookConfig.titleTopSpacing = it ReadBookConfig.titleTopSpacing = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
} }
dsbTitleBottom.onChanged = { dsbTitleBottom.onChanged = {
ReadBookConfig.titleBottomSpacing = it ReadBookConfig.titleBottomSpacing = it
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(8, 5))
} }
llHeaderShow.setOnClickListener { llHeaderShow.setOnClickListener {
val headerModes = ReadTipConfig.getHeaderModes(requireContext()) val headerModes = ReadTipConfig.getHeaderModes(requireContext())
context?.selector(items = headerModes.values.toList()) { _, i -> context?.selector(items = headerModes.values.toList()) { _, i ->
ReadTipConfig.headerMode = headerModes.keys.toList()[i] ReadTipConfig.headerMode = headerModes.keys.toList()[i]
tvHeaderShow.text = headerModes[ReadTipConfig.headerMode] tvHeaderShow.text = headerModes[ReadTipConfig.headerMode]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llFooterShow.setOnClickListener { llFooterShow.setOnClickListener {
@ -116,7 +121,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
context?.selector(items = footerModes.values.toList()) { _, i -> context?.selector(items = footerModes.values.toList()) { _, i ->
ReadTipConfig.footerMode = footerModes.keys.toList()[i] ReadTipConfig.footerMode = footerModes.keys.toList()[i]
tvFooterShow.text = footerModes[ReadTipConfig.footerMode] tvFooterShow.text = footerModes[ReadTipConfig.footerMode]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llHeaderLeft.setOnClickListener { llHeaderLeft.setOnClickListener {
@ -125,7 +130,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipHeaderLeft = tipValue ReadTipConfig.tipHeaderLeft = tipValue
tvHeaderLeft.text = ReadTipConfig.tipNames[i] tvHeaderLeft.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llHeaderMiddle.setOnClickListener { llHeaderMiddle.setOnClickListener {
@ -134,7 +139,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipHeaderMiddle = tipValue ReadTipConfig.tipHeaderMiddle = tipValue
tvHeaderMiddle.text = ReadTipConfig.tipNames[i] tvHeaderMiddle.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llHeaderRight.setOnClickListener { llHeaderRight.setOnClickListener {
@ -143,7 +148,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipHeaderRight = tipValue ReadTipConfig.tipHeaderRight = tipValue
tvHeaderRight.text = ReadTipConfig.tipNames[i] tvHeaderRight.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llFooterLeft.setOnClickListener { llFooterLeft.setOnClickListener {
@ -152,7 +157,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipFooterLeft = tipValue ReadTipConfig.tipFooterLeft = tipValue
tvFooterLeft.text = ReadTipConfig.tipNames[i] tvFooterLeft.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llFooterMiddle.setOnClickListener { llFooterMiddle.setOnClickListener {
@ -161,7 +166,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipFooterMiddle = tipValue ReadTipConfig.tipFooterMiddle = tipValue
tvFooterMiddle.text = ReadTipConfig.tipNames[i] tvFooterMiddle.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llFooterRight.setOnClickListener { llFooterRight.setOnClickListener {
@ -170,7 +175,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
clearRepeat(tipValue) clearRepeat(tipValue)
ReadTipConfig.tipFooterRight = tipValue ReadTipConfig.tipFooterRight = tipValue
tvFooterRight.text = ReadTipConfig.tipNames[i] tvFooterRight.text = ReadTipConfig.tipNames[i]
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
} }
llTipColor.setOnClickListener { llTipColor.setOnClickListener {
@ -179,7 +184,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
0 -> { 0 -> {
ReadTipConfig.tipColor = 0 ReadTipConfig.tipColor = 0
upTvTipColor() upTvTipColor()
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
1 -> ColorPickerDialog.newBuilder() 1 -> ColorPickerDialog.newBuilder()
.setShowAlphaSlider(false) .setShowAlphaSlider(false)
@ -195,7 +200,7 @@ class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) {
0, 1 -> { 0, 1 -> {
ReadTipConfig.tipDividerColor = i - 1 ReadTipConfig.tipDividerColor = i - 1
upTvTipDividerColor() upTvTipDividerColor()
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(2))
} }
2 -> ColorPickerDialog.newBuilder() 2 -> ColorPickerDialog.newBuilder()
.setShowAlphaSlider(false) .setShowAlphaSlider(false)

View File

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

View File

@ -3,23 +3,14 @@ package io.legado.app.ui.book.read.page
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Picture
import android.graphics.RectF
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.core.graphics.record
import io.legado.app.R 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.data.entities.Bookmark
import io.legado.app.help.book.isImage
import io.legado.app.help.config.AppConfig 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.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.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage 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.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.book.read.page.provider.TextPageFactory
import io.legado.app.ui.widget.dialog.PhotoDialog import io.legado.app.ui.widget.dialog.PhotoDialog
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.dpToPx
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import java.util.concurrent.Executors
import kotlin.math.min import kotlin.math.min
/** /**
* 阅读内容视图 * 阅读内容视图
*/ */
class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true) var selectAble = AppConfig.textSelectAble
var upView: ((TextPage) -> Unit)? = null val selectedPaint by lazy {
private val selectedPaint by lazy {
Paint().apply { Paint().apply {
color = context.getCompatColor(R.color.btn_bg_press_2) color = context.getCompatColor(R.color.btn_bg_press_2)
style = Paint.Style.FILL style = Paint.Style.FILL
} }
} }
private var callBack: CallBack private var callBack: CallBack
private val visibleRect = RectF() private val visibleRect = ChapterProvider.visibleRect
val selectStart = TextPos(0, 0, 0) val selectStart = TextPos(0, 0, 0)
private val selectEnd = TextPos(0, 0, 0) private val selectEnd = TextPos(0, 0, 0)
var textPage: TextPage = TextPage() var textPage: TextPage = TextPage()
@ -63,15 +52,15 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
var reverseEndCursor = false 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 var pageOffset = 0
private lateinit var picture: Picture private var autoPager: AutoPager? = null
private var pictureIsDirty = true private var isScroll = false
private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M private val renderRunnable by lazy { Runnable { preRenderPage() } }
private val isNoAnim get() = ReadBook.pageAnim() == PageAnim.noAnim
//绘制图片的paint //绘制图片的paint
private val imagePaint by lazy { val imagePaint by lazy {
Paint().apply { Paint().apply {
isAntiAlias = AppConfig.useAntiAlias isAntiAlias = AppConfig.useAntiAlias
} }
@ -79,9 +68,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
init { init {
callBack = activity as CallBack 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) { fun setContent(textPage: TextPage) {
this.textPage = textPage this.textPage = textPage
imagePaint.isAntiAlias = AppConfig.useAntiAlias // 非滑动翻页动画需要同步重绘,不然翻页可能会出现闪烁
invalidate() if (isScroll) {
} postInvalidate()
} else {
/** invalidate()
* 更新绘制区域 }
*/
fun upVisibleRect() {
visibleRect.set(
ChapterProvider.paddingLeft.toFloat(),
ChapterProvider.paddingTop.toFloat(),
ChapterProvider.visibleRight.toFloat(),
ChapterProvider.visibleBottom.toFloat()
)
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
if (!isMainView) return
ChapterProvider.upViewSize(w, h) ChapterProvider.upViewSize(w, h)
upVisibleRect()
textPage.format() textPage.format()
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
autoPager?.onDraw(canvas)
if (longScreenshot) { if (longScreenshot) {
canvas.translate(0f, scrollY.toFloat()) canvas.translate(0f, scrollY.toFloat())
} }
check(!visibleRect.isEmpty) { "visibleRect 为空" }
canvas.clipRect(visibleRect) canvas.clipRect(visibleRect)
if (atLeastApi23 && !callBack.isScroll && !isNoAnim) { drawPage(canvas)
if (pictureIsDirty) {
pictureIsDirty = false
picture.record(width, height) {
drawPage(this)
}
}
canvas.drawPicture(picture)
} else {
drawPage(canvas)
}
} }
/** /**
@ -137,185 +105,94 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
*/ */
private fun drawPage(canvas: Canvas) { private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0) var relativeOffset = relativeOffset(0)
var lines = textPage.lines textPage.draw(this, canvas, relativeOffset)
for (i in lines.indices) {
drawLine(canvas, textPage, lines[i], relativeOffset)
}
if (!callBack.isScroll) return if (!callBack.isScroll) return
//滚动翻页 //滚动翻页
if (!pageFactory.hasNext()) return if (!pageFactory.hasNext()) return
val textPage1 = relativePage(1) val textPage1 = relativePage(1)
relativeOffset = relativeOffset(1) relativeOffset += textPage.height
lines = textPage1.lines textPage1.draw(this, canvas, relativeOffset)
for (i in lines.indices) {
drawLine(canvas, textPage1, lines[i], relativeOffset)
}
if (!pageFactory.hasNextPlus()) return if (!pageFactory.hasNextPlus()) return
relativeOffset = relativeOffset(2) relativeOffset += textPage1.height
if (relativeOffset < ChapterProvider.visibleHeight) { if (relativeOffset < ChapterProvider.visibleHeight) {
val textPage2 = relativePage(2) val textPage2 = relativePage(2)
lines = textPage2.lines textPage2.draw(this, canvas, relativeOffset)
for (i in lines.indices) {
drawLine(canvas, textPage2, lines[i], relativeOffset)
}
} }
} }
/** override fun computeScroll() {
* 绘制页面 pageDelegate?.computeScroll()
*/ autoPager?.computeOffset()
private fun drawLine(
canvas: Canvas,
textPage: TextPage,
textLine: TextLine,
relativeOffset: Float,
) {
val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(canvas, textPage, textLine, lineTop, lineBase, lineBottom)
if (ReadBookConfig.underline && ReadBook.book?.isImage != true) {
drawUnderline(canvas, textLine, relativeOffset)
}
}
/**
* 绘制下划线
*/
private fun drawUnderline(canvas: Canvas, textLine: TextLine, relativeOffset: Float) {
val lineY = relativeOffset + textLine.lineBottom - 1.dpToPx()
canvas.drawLine(
textLine.lineStart + textLine.indentWidth,
lineY,
textLine.lineEnd,
lineY,
ChapterProvider.contentPaint
)
}
/**
* 绘制文字
*/
private fun drawChars(
canvas: Canvas,
textPage: TextPage,
textLine: TextLine,
lineTop: Float,
lineBase: Float,
lineBottom: Float,
) {
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
val textColor = if (textLine.isReadAloud) context.accentColor else ReadBookConfig.textColor
val columns = textLine.columns
for (i in columns.indices) {
when (val column = columns[i]) {
is TextColumn -> {
if (column.isSearchResult) {
textPaint.color = context.accentColor
} else if (textPaint.color != textColor) {
textPaint.color = textColor
}
canvas.drawText(column.charData, column.start, lineBase, textPaint)
if (column.selected) {
canvas.drawRect(
column.start,
lineTop,
column.end,
lineBottom,
selectedPaint
)
}
}
is ImageColumn -> drawImage(canvas, textPage, textLine, column, lineTop, lineBottom)
is ReviewColumn -> column.drawToCanvas(canvas, lineBase, textPaint.textSize)
}
}
}
/**
* 绘制图片
*/
@Suppress("UNUSED_PARAMETER")
private fun drawImage(
canvas: Canvas,
textPage: TextPage,
textLine: TextLine,
column: ImageColumn,
lineTop: Float,
lineBottom: Float
) {
val book = ReadBook.book ?: return
val bitmap = ImageProvider.getImage(
book,
column.src,
(column.end - column.start).toInt(),
(lineBottom - lineTop).toInt()
) {
invalidate()
} ?: return
val rectF = if (textLine.isImage) {
RectF(column.start, lineTop, column.end, lineBottom)
} else {
/*以宽度为基准保持图片的原始比例叠加当div为负数时允许高度比字符更高*/
val h = (column.end - column.start) / bitmap.width * bitmap.height
val div = (lineBottom - lineTop - h) / 2
RectF(column.start, lineTop + div, column.end, lineBottom - div)
}
kotlin.runCatching {
canvas.drawBitmap(bitmap, null, rectF, imagePaint)
}.onFailure { e ->
context.toastOnUi(e.localizedMessage)
}
} }
/** /**
* 滚动事件 * 滚动事件
* pageOffset 向上滚动 减小 向下滚动 增大
* pageOffset 范围 0 ~ -textPage.height 大于0为上一页小于-textPage.height为下一页
* 以内容显示区域顶端为界pageOffset的绝对值为textPage上方的高度
* pageOffset + textPage.height textPage 下方的高度
*/ */
fun scroll(mOffset: Int) { fun scroll(mOffset: Int) {
if (mOffset == 0) return
pageOffset += mOffset pageOffset += mOffset
if (longScreenshot) { if (longScreenshot) {
scrollY += -mOffset scrollY += -mOffset
} }
if (!pageFactory.hasPrev() && pageOffset > 0) { if (!pageFactory.hasPrev() && pageOffset > 0) {
pageOffset = 0 pageOffset = 0
pageDelegate?.abortAnim()
} else if (!pageFactory.hasNext() } else if (!pageFactory.hasNext()
&& pageOffset < 0 && pageOffset < 0
&& pageOffset + textPage.height < ChapterProvider.visibleHeight && pageOffset + textPage.height < ChapterProvider.visibleHeight
) { ) {
val offset = (ChapterProvider.visibleHeight - textPage.height).toInt() val offset = (ChapterProvider.visibleHeight - textPage.height).toInt()
pageOffset = min(0, offset) pageOffset = min(0, offset)
pageDelegate?.abortAnim()
} else if (pageOffset > 0) { } else if (pageOffset > 0) {
pageFactory.moveToPrev(true) if (pageFactory.moveToPrev(true)) {
textPage = pageFactory.curPage pageOffset -= textPage.height.toInt()
pageOffset -= textPage.height.toInt() } else {
upView?.invoke(textPage) pageOffset = 0
contentDescription = textPage.text pageDelegate?.abortAnim()
}
} else if (pageOffset < -textPage.height) { } else if (pageOffset < -textPage.height) {
pageOffset += textPage.height.toInt() val height = textPage.height
pageFactory.moveToNext(true) if (pageFactory.moveToNext(upContent = true)) {
textPage = pageFactory.curPage pageOffset += height.toInt()
upView?.invoke(textPage) } else {
contentDescription = textPage.text pageOffset = -height.toInt()
pageDelegate?.abortAnim()
}
} }
invalidate() postInvalidate()
} }
override fun invalidate() { fun submitRenderTask() {
super.invalidate() renderThread.submit(renderRunnable)
invalidatePicture()
} }
private fun invalidatePicture() { private fun preRenderPage() {
pictureIsDirty = true 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 -> touchRough(x, y) { _, textPos, _, _, column ->
if (column is TextColumn) { if (column is TextColumn) {
column.selected = true column.selected = true
invalidate()
select(textPos) select(textPos)
} }
} }
@ -517,9 +393,21 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) return if (relativeOffset >= ChapterProvider.visibleHeight) return
} }
val textPage = relativePage(relativePos) 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)) { 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)) { if (textColumn.isTouch(x)) {
touched.invoke( touched.invoke(
relativeOffset, relativeOffset,
@ -529,12 +417,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
return return
} }
} }
val isLast = textLine.columns.first().start < x val isLast = columns.first().start < x
val (charIndex, textColumn) = if (isLast) { val charIndex = if (isLast) columns.lastIndex else 0
textLine.columns.withIndex().last() val textColumn = if (isLast) columns.last() else columns.first()
} else {
textLine.columns.withIndex().first()
}
touched.invoke( touched.invoke(
relativeOffset, relativeOffset,
TextPos(relativePos, lineIndex, charIndex, false, isLast), TextPos(relativePos, lineIndex, charIndex, false, isLast),
@ -557,7 +442,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break if (relativeOffset >= ChapterProvider.visibleHeight) break
} }
val textPage = relativePage(relativePos) 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)) { if (textLine.isVisible(relativeOffset)) {
val visibleLine = textLine.copy().apply { val visibleLine = textLine.copy().apply {
lineTop += relativeOffset lineTop += relativeOffset
@ -580,7 +467,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (relativeOffset >= ChapterProvider.visibleHeight) break if (relativeOffset >= ChapterProvider.visibleHeight) break
} }
val textPage = relativePage(relativePos) 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)) { if (textLine.isVisible(relativeOffset)) {
val visibleLine = textLine.copy().apply { val visibleLine = textLine.copy().apply {
lineTop += relativeOffset 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) { 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 { override fun canScrollVertically(direction: Int): Boolean {
return callBack.isScroll && pageFactory.hasNext() return callBack.isScroll && pageFactory.hasNext()
} }
@ -814,9 +712,18 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
return callBack.onLongScreenshotTouchEvent(event) return callBack.onLongScreenshotTouchEvent(event)
} }
companion object {
private val renderThread by lazy {
Executors.newSingleThreadExecutor {
Thread(it, "TextPageRender")
}
}
}
interface CallBack { interface CallBack {
val headerHeight: Int val headerHeight: Int
val pageFactory: TextPageFactory val pageFactory: TextPageFactory
val pageDelegate: PageDelegate?
val isScroll: Boolean val isScroll: Boolean
var isSelectingSearchResult: Boolean var isSelectingSearchResult: Boolean
fun upSelectedStart(x: Float, y: Float, top: Float) fun upSelectedStart(x: Float, y: Float, top: Float)

View File

@ -22,6 +22,7 @@ import io.legado.app.ui.widget.BatteryView
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.dpToPx import io.legado.app.utils.dpToPx
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.setTextIfNotEqual
import io.legado.app.utils.statusBarHeight import io.legado.app.utils.statusBarHeight
import splitties.views.backgroundColor import splitties.views.backgroundColor
import java.util.Date import java.util.Date
@ -45,6 +46,8 @@ class PageView(context: Context) : FrameLayout(context) {
private var tvBookName: BatteryView? = null private var tvBookName: BatteryView? = null
private var tvTimeBattery: BatteryView? = null private var tvTimeBattery: BatteryView? = null
private var tvTimeBatteryP: BatteryView? = null private var tvTimeBatteryP: BatteryView? = null
private var isMainView = false
var isScroll = false
val headerHeight: Int val headerHeight: Int
get() { get() {
@ -57,9 +60,6 @@ class PageView(context: Context) : FrameLayout(context) {
if (!isInEditMode) { if (!isInEditMode) {
upStyle() upStyle()
} }
binding.contentTextView.upView = {
setProgress(it)
}
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 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) vwTopDivider.gone(llHeader.isGone || !it.showHeaderLine)
vwBottomDivider.gone(llFooter.isGone || !it.showFooterLine) vwBottomDivider.gone(llFooter.isGone || !it.showFooterLine)
} }
contentTextView.upVisibleRect()
upTime() upTime()
upBattery(battery) upBattery(battery)
} }
@ -276,7 +275,13 @@ class PageView(context: Context) : FrameLayout(context) {
* 设置内容 * 设置内容
*/ */
fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
setProgress(textPage) if (isMainView && !isScroll) {
setProgress(textPage)
} else {
post {
setProgress(textPage)
}
}
if (resetPageOffset) { if (resetPageOffset) {
resetPageOffset() resetPageOffset()
} }
@ -302,30 +307,26 @@ class PageView(context: Context) : FrameLayout(context) {
*/ */
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun setProgress(textPage: TextPage) = textPage.apply { fun setProgress(textPage: TextPage) = textPage.apply {
tvBookName?.apply { tvBookName?.setTextIfNotEqual(ReadBook.book?.name)
if (text != ReadBook.book?.name) { tvTitle?.setTextIfNotEqual(textPage.title)
text = ReadBook.book?.name tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize")
}
}
tvTitle?.apply {
if (text != textPage.title) {
text = textPage.title
}
}
tvPage?.text = "${index.plus(1)}/$pageSize"
val readProgress = readProgress val readProgress = readProgress
tvTotalProgress?.apply { tvTotalProgress?.setTextIfNotEqual(readProgress)
if (text != readProgress) { tvTotalProgress1?.setTextIfNotEqual("${chapterIndex.plus(1)}/${chapterSize}")
text = readProgress tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress")
} }
}
tvTotalProgress1?.apply { fun setAutoPager(autoPager: AutoPager?) {
val progress = "${chapterIndex.plus(1)}/${chapterSize}" binding.contentTextView.setAutoPager(autoPager)
if (text != progress) { }
text = progress
} fun submitPreRenderTask() {
} binding.contentTextView.submitRenderTask()
tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" }
fun setIsScroll(value: Boolean) {
isScroll = value
binding.contentTextView.setIsScroll(value)
} }
/** /**
@ -379,6 +380,7 @@ class PageView(context: Context) : FrameLayout(context) {
} }
fun markAsMainView() { fun markAsMainView() {
isMainView = true
binding.contentTextView.isMainView = true binding.contentTextView.isMainView = true
} }

View File

@ -2,10 +2,7 @@ package io.legado.app.ui.book.read.page
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.os.Build import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
@ -16,21 +13,27 @@ import android.widget.FrameLayout
import io.legado.app.constant.PageAnim import io.legado.app.constant.PageAnim
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig 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.ReadAloud
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.ContentEditDialog 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.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.PageDirection
import io.legado.app.ui.book.read.page.entities.TextChapter 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.TextPage
import io.legado.app.ui.book.read.page.entities.TextPos 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.ChapterProvider
import io.legado.app.ui.book.read.page.provider.TextPageFactory 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.text.BreakIterator
import java.util.* import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
/** /**
@ -49,7 +52,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
field = value field = value
upContent() upContent()
} }
var isScroll = false override var isScroll = false
val prevPage by lazy { PageView(context) } val prevPage by lazy { PageView(context) }
val curPage by lazy { PageView(context) } val curPage by lazy { PageView(context) }
val nextPage 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 blRect = RectF()
private val bcRect = RectF() private val bcRect = RectF()
private val brRect = 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 val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) }
private var nextPageBitmap: Bitmap? = null val autoPager = AutoPager(this)
val isAutoPage get() = autoPager.isRunning
init { init {
addView(nextPage) addView(nextPage)
addView(curPage) addView(curPage)
addView(prevPage) addView(prevPage)
prevPage.invisible() prevPage.invisible()
nextPage.invisible()
curPage.markAsMainView() curPage.markAsMainView()
if (!isInEditMode) { if (!isInEditMode) {
upBg() upBg()
@ -140,26 +143,12 @@ class ReadView(context: Context, attrs: AttributeSet) :
override fun dispatchDraw(canvas: Canvas) { override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
pageDelegate?.onDraw(canvas) pageDelegate?.onDraw(canvas)
if (!isInEditMode && callBack.isAutoPage && !isScroll) { autoPager.onDraw(canvas)
// 自动翻页
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
)
}
}
} }
override fun computeScroll() { override fun computeScroll() {
pageDelegate?.scroll() pageDelegate?.computeScroll()
autoPager.computeOffset()
} }
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
@ -253,6 +242,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
pageDelegate?.onTouch(event) pageDelegate?.onTouch(event)
} }
pressOnTextSelected = false pressOnTextSelected = false
autoPager.resume()
} }
} }
return true return true
@ -516,11 +506,12 @@ class ReadView(context: Context, attrs: AttributeSet) :
} }
(pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage (pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage
pageDelegate?.setViewSize(width, height) pageDelegate?.setViewSize(width, height)
if (pageDelegate is NoAnimPageDelegate) { if (isScroll) {
nextPage.invisible() curPage.setAutoPager(autoPager)
} else { } else {
nextPage.visible() curPage.setAutoPager(null)
} }
curPage.setIsScroll(isScroll)
} }
/** /**
@ -529,13 +520,12 @@ class ReadView(context: Context, attrs: AttributeSet) :
* @param resetPageOffset 滚动阅读是是否重置位置 * @param resetPageOffset 滚动阅读是是否重置位置
*/ */
override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
curPage.setContentDescription(pageFactory.curPage.text) post {
if (isScroll && !callBack.isAutoPage) { curPage.setContentDescription(pageFactory.curPage.text)
}
if (isScroll && !isAutoPage) {
curPage.setContent(pageFactory.curPage, resetPageOffset) curPage.setContent(pageFactory.curPage, resetPageOffset)
} else { } else {
if (callBack.isAutoPage && relativePosition >= 0) {
clearNextPageBitmap()
}
when (relativePosition) { when (relativePosition) {
-1 -> prevPage.setContent(pageFactory.prevPage) -1 -> prevPage.setContent(pageFactory.prevPage)
1 -> nextPage.setContent(pageFactory.nextPage) 1 -> nextPage.setContent(pageFactory.nextPage)
@ -638,9 +628,27 @@ class ReadView(context: Context, attrs: AttributeSet) :
return curPage.getCurVisibleFirstLine()?.pagePosition ?: 0 return curPage.getCurVisibleFirstLine()?.pagePosition ?: 0
} }
fun clearNextPageBitmap() { fun invalidateTextPage() {
nextPageBitmap?.recycle() pageFactory.run {
nextPageBitmap = null 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? override val currentChapter: TextChapter?
@ -668,8 +676,6 @@ class ReadView(context: Context, attrs: AttributeSet) :
interface CallBack { interface CallBack {
val isInitFinish: Boolean val isInitFinish: Boolean
val isAutoPage: Boolean
val autoPageProgress: Int
fun showActionMenu() fun showActionMenu()
fun screenOffTimerStart() fun screenOffTimerStart()
fun showTextActionMenu() fun showTextActionMenu()

View File

@ -13,9 +13,12 @@ interface DataSource {
val prevChapter: TextChapter? val prevChapter: TextChapter?
val isScroll: Boolean
fun hasNextChapter(): Boolean fun hasNextChapter(): Boolean
fun hasPrevChapter(): Boolean fun hasPrevChapter(): Boolean
fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
} }

View File

@ -1,10 +1,7 @@
package io.legado.app.ui.book.read.page.delegate package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Picture
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Build
import androidx.core.graphics.withClip import androidx.core.graphics.withClip
import androidx.core.graphics.withTranslation import androidx.core.graphics.withTranslation
import io.legado.app.ui.book.read.page.ReadView 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 import io.legado.app.utils.screenshot
class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
private val bitmapMatrix = Matrix()
private val shadowDrawableR: GradientDrawable 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 { init {
val shadowColors = intArrayOf(0x66111111, 0x00000000) val shadowColors = intArrayOf(0x66111111, 0x00000000)
shadowDrawableR = GradientDrawable( shadowDrawableR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, shadowColors GradientDrawable.Orientation.LEFT_RIGHT, shadowColors
) )
shadowDrawableR.gradientType = GradientDrawable.LINEAR_GRADIENT shadowDrawableR.gradientType = GradientDrawable.LINEAR_GRADIENT
if (atLeastApi23) {
curPicture = Picture()
prevPicture = Picture()
nextPicture = Picture()
}
} }
override fun onDraw(canvas: Canvas) { 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 val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
if (mDirection == PageDirection.PREV) { if (mDirection == PageDirection.PREV) {
if (offsetX <= viewWidth) { if (offsetX <= viewWidth) {
if (!atLeastApi23) { canvas.withTranslation(distanceX) {
bitmapMatrix.setTranslate(distanceX, 0.toFloat()) prevRecorder.draw(canvas)
prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) }
} else {
canvas.withTranslation(distanceX) {
drawPicture(prevPicture)
}
} }
addShadow(distanceX, canvas) addShadow(distanceX, canvas)
} else { } else {
if (!atLeastApi23) { prevRecorder.draw(canvas)
prevBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
} else {
canvas.drawPicture(prevPicture)
}
} }
} else if (mDirection == PageDirection.NEXT) { } else if (mDirection == PageDirection.NEXT) {
if (!atLeastApi23) { val width = nextRecorder.width.toFloat()
bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) val height = nextRecorder.height.toFloat()
nextBitmap?.let { canvas.withClip(width + offsetX, 0f, width, height) {
val width = it.width.toFloat() nextRecorder.draw(this)
val height = it.height.toFloat() }
canvas.withClip(width + offsetX, 0f, width, height) { canvas.withTranslation(distanceX - viewWidth) {
drawBitmap(it, 0f, 0f, null) curRecorder.draw(this)
}
}
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)
}
} }
addShadow(distanceX, canvas) addShadow(distanceX, canvas)
} }
@ -90,18 +54,13 @@ class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
override fun setBitmap() { override fun setBitmap() {
when (mDirection) { when (mDirection) {
PageDirection.PREV -> if (!atLeastApi23) { PageDirection.PREV -> {
prevBitmap = prevPage.screenshot(prevBitmap, canvas) prevPage.screenshot(prevRecorder)
} else {
prevPage.screenshot(prevPicture)
} }
PageDirection.NEXT -> if (!atLeastApi23) { PageDirection.NEXT -> {
nextBitmap = nextPage.screenshot(nextBitmap, canvas) nextPage.screenshot(nextRecorder)
curBitmap = curPage.screenshot(curBitmap, canvas) curPage.screenshot(curRecorder)
} else {
nextPage.screenshot(nextPicture)
curPage.screenshot(curPicture)
} }
else -> Unit else -> Unit

View File

@ -1,18 +1,16 @@
package io.legado.app.ui.book.read.page.delegate package io.legado.app.ui.book.read.page.delegate
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.MotionEvent import android.view.MotionEvent
import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
import io.legado.app.utils.screenshot import io.legado.app.utils.screenshot
abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readView) { abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readView) {
protected var curBitmap: Bitmap? = null protected val curRecorder = CanvasRecorderFactory.create()
protected var prevBitmap: Bitmap? = null protected val prevRecorder = CanvasRecorderFactory.create()
protected var nextBitmap: Bitmap? = null protected val nextRecorder = CanvasRecorderFactory.create()
protected var canvas: Canvas = Canvas()
private val slopSquare get() = readView.pageSlopSquare2 private val slopSquare get() = readView.pageSlopSquare2
override fun setDirection(direction: PageDirection) { override fun setDirection(direction: PageDirection) {
@ -23,13 +21,13 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
open fun setBitmap() { open fun setBitmap() {
when (mDirection) { when (mDirection) {
PageDirection.PREV -> { PageDirection.PREV -> {
prevBitmap = prevPage.screenshot(prevBitmap, canvas) prevPage.screenshot(prevRecorder)
curBitmap = curPage.screenshot(curBitmap, canvas) curPage.screenshot(curRecorder)
} }
PageDirection.NEXT -> { PageDirection.NEXT -> {
nextBitmap = nextPage.screenshot(nextBitmap, canvas) nextPage.screenshot(nextRecorder)
curBitmap = curPage.screenshot(curBitmap, canvas) curPage.screenshot(curRecorder)
} }
else -> Unit else -> Unit
@ -140,12 +138,9 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
prevBitmap?.recycle() prevRecorder.recycle()
prevBitmap = null curRecorder.recycle()
curBitmap?.recycle() nextRecorder.recycle()
curBitmap = null
nextBitmap?.recycle()
nextBitmap = null
} }
} }

View File

@ -96,7 +96,7 @@ abstract class PageDelegate(protected val readView: ReadView) {
viewHeight = height viewHeight = height
} }
fun scroll() { open fun computeScroll() {
if (scroller.computeScrollOffset()) { if (scroller.computeScrollOffset()) {
readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) { } 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() { open fun onDestroy() {
// run on destroy // run on destroy
} }

View File

@ -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.ReadView
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
@Suppress("UnnecessaryVariable")
class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) { class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
// 滑动追踪的时间 // 滑动追踪的时间
@ -22,6 +21,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
var noAnim: Boolean = false var noAnim: Boolean = false
override fun onAnimStart(animationSpeed: Int) { override fun onAnimStart(animationSpeed: Int) {
readView.onScrollAnimStart()
//惯性滚动 //惯性滚动
fling( fling(
0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(), 0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(),
@ -30,7 +30,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
} }
override fun onAnimStop() { override fun onAnimStop() {
// nothing readView.onScrollAnimStop()
} }
override fun onTouch(event: MotionEvent) { override fun onTouch(event: MotionEvent) {
@ -79,7 +79,7 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
val pointX = event.getX(event.pointerCount - 1) val pointX = event.getX(event.pointerCount - 1)
val pointY = event.getY(event.pointerCount - 1) val pointY = event.getY(event.pointerCount - 1)
if (isMoved) { if (isMoved) {
readView.setTouchPoint(pointX, pointY) readView.setTouchPoint(pointX, pointY, false)
} }
if (!isMoved) { if (!isMoved) {
val deltaX = (pointX - startX).toInt() 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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
mVelocity.recycle() mVelocity.recycle()
} }
override fun abortAnim() { override fun abortAnim() {
readView.onScrollAnimStop()
isStarted = false isStarted = false
isMoved = false isMoved = false
isRunning = false isRunning = false

View File

@ -1,13 +1,27 @@
package io.legado.app.ui.book.read.page.delegate 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.graphics.drawable.GradientDrawable
import android.os.Build import android.os.Build
import android.view.MotionEvent import android.view.MotionEvent
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.read.page.entities.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") @Suppress("DEPRECATION")
class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { 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 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 { init {
//设置颜色数组 //设置颜色数组
val color = intArrayOf(0x333333, -0x4fcccccd) val color = intArrayOf(0x333333, -0x4fcccccd)
@ -122,6 +141,22 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT 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) { override fun setViewSize(width: Int, height: Int) {
super.setViewSize(width, height) super.setViewSize(width, height)
mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat() mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
@ -133,6 +168,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
calcCornerXY(event.x, event.y) calcCornerXY(event.x, event.y)
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3) if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3)
|| mDirection == PageDirection.PREV || mDirection == PageDirection.PREV
@ -159,10 +195,12 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
} else { } else {
calcCornerXY(viewWidth - startX, viewHeight.toFloat()) calcCornerXY(viewWidth - startX, viewHeight.toFloat())
} }
PageDirection.NEXT -> PageDirection.NEXT ->
if (viewWidth / 2 > startX) { if (viewWidth / 2 > startX) {
calcCornerXY(viewWidth - startX, startY) calcCornerXY(viewWidth - startX, startY)
} }
else -> Unit else -> Unit
} }
} }
@ -216,6 +254,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
drawCurrentPageShadow(canvas) drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, curBitmap) drawCurrentBackArea(canvas, curBitmap)
} }
PageDirection.PREV -> { PageDirection.PREV -> {
calcPoints() calcPoints()
drawCurrentPageArea(canvas, prevBitmap) drawCurrentPageArea(canvas, prevBitmap)
@ -223,6 +262,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
drawCurrentPageShadow(canvas) drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, prevBitmap) drawCurrentBackArea(canvas, prevBitmap)
} }
else -> return else -> return
} }
} }

View File

@ -1,51 +1,12 @@
package io.legado.app.ui.book.read.page.delegate package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Picture
import android.os.Build
import androidx.core.graphics.withTranslation import androidx.core.graphics.withTranslation
import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.utils.screenshot
class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { 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) { override fun onAnimStart(animationSpeed: Int) {
val distanceX: Float val distanceX: Float
when (mDirection) { when (mDirection) {
@ -79,32 +40,18 @@ class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth
if (!isRunning) return if (!isRunning) return
if (mDirection == PageDirection.PREV) { if (mDirection == PageDirection.PREV) {
if (!atLeastApi23) { canvas.withTranslation(distanceX + viewWidth) {
bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat()) curRecorder.draw(this)
curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } }
bitmapMatrix.setTranslate(distanceX, 0.toFloat()) canvas.withTranslation(distanceX) {
prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } prevRecorder.draw(this)
} else {
canvas.withTranslation(distanceX + viewWidth) {
drawPicture(curPicture)
}
canvas.withTranslation(distanceX) {
drawPicture(prevPicture)
}
} }
} else if (mDirection == PageDirection.NEXT) { } else if (mDirection == PageDirection.NEXT) {
if (!atLeastApi23) { canvas.withTranslation(distanceX) {
bitmapMatrix.setTranslate(distanceX, 0.toFloat()) nextRecorder.draw(this)
nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } }
bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) canvas.withTranslation(distanceX - viewWidth) {
curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } curRecorder.draw(this)
} else {
canvas.withTranslation(distanceX) {
drawPicture(nextPicture)
}
canvas.withTranslation(distanceX - viewWidth) {
drawPicture(curPicture)
}
} }
} }
} }

View File

@ -4,6 +4,8 @@ package io.legado.app.ui.book.read.page.entities
import androidx.annotation.Keep import androidx.annotation.Keep
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.fastBinarySearchBy
import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
/** /**
@ -85,12 +87,15 @@ data class TextChapter(
* @return 已读长度 * @return 已读长度
*/ */
fun getReadLength(pageIndex: Int): Int { fun getReadLength(pageIndex: Int): Int {
return pages[min(pageIndex, lastIndex)].lines.first().chapterPosition
/*
var length = 0 var length = 0
val maxIndex = min(pageIndex, pages.size) val maxIndex = min(pageIndex, pages.size)
for (index in 0 until maxIndex) { for (index in 0 until maxIndex) {
length += pages[index].charSize length += pages[index].charSize
} }
return length return length
*/
} }
/** /**
@ -185,18 +190,30 @@ data class TextChapter(
* @return 根据索引位置获取所在页 * @return 根据索引位置获取所在页
*/ */
fun getPageIndexByCharIndex(charIndex: Int): Int { 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 var length = 0
pages.forEach { for (i in pages.indices) {
length += it.charSize val page = pages[i]
length += page.charSize
if (length > charIndex) { if (length > charIndex) {
return it.index return page.index
} }
} }
return pages.lastIndex return pages.lastIndex
*/
} }
fun clearSearchResult() { fun clearSearchResult() {
pages.forEach { page -> for (i in pages.indices) {
val page = pages[i]
page.searchResult.forEach { page.searchResult.forEach {
it.selected = false it.selected = false
it.isSearchResult = false it.isSearchResult = false

View File

@ -1,9 +1,18 @@
package io.legado.app.ui.book.read.page.entities package io.legado.app.ui.book.read.page.entities
import android.graphics.Canvas
import android.graphics.Paint.FontMetrics import android.graphics.Paint.FontMetrics
import androidx.annotation.Keep 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.entities.column.BaseColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider 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, var pagePosition: Int = 0,
val isTitle: Boolean = false, val isTitle: Boolean = false,
var isParagraphEnd: Boolean = false, var isParagraphEnd: Boolean = false,
var isReadAloud: Boolean = false, var isImage: Boolean = false,
var isImage: Boolean = false
) { ) {
val columns: List<BaseColumn> get() = textColumns val columns: List<BaseColumn> get() = textColumns
@ -31,8 +39,20 @@ data class TextLine(
val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f
val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize 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) { fun addColumn(column: BaseColumn) {
column.textLine = this
textColumns.add(column) textColumns.add(column)
} }
@ -102,4 +122,50 @@ data class TextLine(
return visible 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()
}
} }

View File

@ -1,13 +1,18 @@
package io.legado.app.ui.book.read.page.entities package io.legado.app.ui.book.read.page.entities
import android.graphics.Canvas
import android.text.Layout import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.graphics.withTranslation
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook 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.entities.column.TextColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider 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 splitties.init.appCtx
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.min import kotlin.math.min
@ -31,6 +36,7 @@ data class TextPage(
companion object { companion object {
val readProgressFormatter = DecimalFormat("0.0%") val readProgressFormatter = DecimalFormat("0.0%")
val emptyTextPage = TextPage()
} }
val lines: List<TextLine> get() = textLines val lines: List<TextLine> get() = textLines
@ -38,25 +44,30 @@ data class TextPage(
val charSize: Int get() = text.length.coerceAtLeast(1) val charSize: Int get() = text.length.coerceAtLeast(1)
val searchResult = hashSetOf<TextColumn>() val searchResult = hashSetOf<TextColumn>()
var isMsgPage: Boolean = false var isMsgPage: Boolean = false
var canvasRecorder = CanvasRecorderFactory.create(true)
var doublePage = false
var paddingTop = 0
val paragraphs by lazy { val paragraphs by lazy {
paragraphsInternal paragraphsInternal
} }
val paragraphsInternal: ArrayList<TextParagraph> get() { val paragraphsInternal: ArrayList<TextParagraph>
val paragraphs = arrayListOf<TextParagraph>() get() {
val lines = textLines.filter { it.paragraphNum > 0 } val paragraphs = arrayListOf<TextParagraph>()
val offset = lines.first().paragraphNum - 1 val lines = textLines.filter { it.paragraphNum > 0 }
lines.forEach { line -> val offset = lines.first().paragraphNum - 1
if (paragraphs.lastIndex < line.paragraphNum - offset - 1) { lines.forEach { line ->
paragraphs.add(TextParagraph(0)) 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) { fun addLine(line: TextLine) {
line.textPage = this
textLines.add(line) textLines.add(line)
} }
@ -147,9 +158,10 @@ data class TextPage(
) )
x = x1 x = x1
} }
textLines.add(textLine) addLine(textLine)
} }
height = ChapterProvider.visibleHeight.toFloat() height = ChapterProvider.visibleHeight.toFloat()
invalidate()
} }
return this return this
} }
@ -158,8 +170,8 @@ data class TextPage(
* 移除朗读标志 * 移除朗读标志
*/ */
fun removePageAloudSpan(): TextPage { fun removePageAloudSpan(): TextPage {
textLines.forEach { textLine -> for (i in textLines.indices) {
textLine.isReadAloud = false textLines[i].isReadAloud = false
} }
return this return this
} }
@ -171,7 +183,8 @@ data class TextPage(
fun upPageAloudSpan(aloudSpanStart: Int) { fun upPageAloudSpan(aloudSpanStart: Int) {
removePageAloudSpan() removePageAloudSpan()
var lineStart = 0 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 val lineLength = textLine.text.length + if (textLine.isParagraphEnd) 1 else 0
if (aloudSpanStart > lineStart && aloudSpanStart < lineStart + lineLength) { if (aloudSpanStart > lineStart && aloudSpanStart < lineStart + lineLength) {
for (i in index - 1 downTo 0) { for (i in index - 1 downTo 0) {
@ -254,6 +267,46 @@ data class TextPage(
return null 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 { fun hasImageOrEmpty(): Boolean {
return textLines.any { it.isImage } || textLines.isEmpty() return textLines.any { it.isImage } || textLines.isEmpty()
} }

View File

@ -1,11 +1,18 @@
package io.legado.app.ui.book.read.page.entities.column 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 { interface BaseColumn {
var start: Float var start: Float
var end: Float var end: Float
var textLine: TextLine
fun draw(view: ContentTextView, canvas: Canvas)
fun isTouch(x: Float): Boolean { fun isTouch(x: Float): Boolean {
return x > start && x < end return x > start && x < end

View File

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

View File

@ -1,6 +1,15 @@
package io.legado.app.ui.book.read.page.entities.column package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import android.graphics.RectF
import androidx.annotation.Keep 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 start: Float,
override var end: Float, override var end: Float,
var src: String var src: String
) : BaseColumn ) : BaseColumn {
override var textLine: TextLine = emptyTextLine
override fun draw(view: ContentTextView, canvas: Canvas) {
val book = ReadBook.book ?: return
val height = textLine.height
val bitmap = ImageProvider.getImage(
book,
src,
(end - start).toInt(),
height.toInt()
)
val rectF = if (textLine.isImage) {
RectF(start, 0f, end, height)
} else {
/*以宽度为基准保持图片的原始比例叠加当div为负数时允许高度比字符更高*/
val h = (end - start) / bitmap.width * bitmap.height
val div = (height - h) / 2
RectF(start, div, end, height - div)
}
kotlin.runCatching {
canvas.drawBitmap(bitmap, null, rectF, view.imagePaint)
}.onFailure { e ->
appCtx.toastOnUi(e.localizedMessage)
}
}
}

View File

@ -4,6 +4,9 @@ import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Path import android.graphics.Path
import androidx.annotation.Keep 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 import io.legado.app.ui.book.read.page.provider.ChapterProvider
/** /**
@ -16,6 +19,16 @@ data class ReviewColumn(
val count: Int = 0 val count: Int = 0
) : BaseColumn { ) : 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 { val countText by lazy {
if (count > 999) { if (count > 999) {
return@lazy "999" return@lazy "999"

View File

@ -1,6 +1,13 @@
package io.legado.app.ui.book.read.page.entities.column package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import androidx.annotation.Keep 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 start: Float,
override var end: Float, override var end: Float,
val charData: String, 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 var isSearchResult: Boolean = false
) : BaseColumn set(value) {
if (field != value) {
textLine.invalidate()
}
field = value
}
override fun draw(view: ContentTextView, canvas: Canvas) {
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
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)
}
}
}

View File

@ -1,7 +1,7 @@
package io.legado.app.ui.book.read.page.provider package io.legado.app.ui.book.read.page.provider
import android.graphics.Paint
import android.graphics.Paint.FontMetrics import android.graphics.Paint.FontMetrics
import android.graphics.RectF
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.Build 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.ui.book.read.page.entities.column.TextColumn
import io.legado.app.utils.RealPathUtil import io.legado.app.utils.RealPathUtil
import io.legado.app.utils.dpToPx import io.legado.app.utils.dpToPx
import io.legado.app.utils.fastSum
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.isPad import io.legado.app.utils.isPad
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.spToPx import io.legado.app.utils.spToPx
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.textHeight import io.legado.app.utils.textHeight
import io.legado.app.utils.toStringArray
import splitties.init.appCtx import splitties.init.appCtx
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
@ -47,6 +47,8 @@ object ChapterProvider {
//用于评论按钮的替换 //用于评论按钮的替换
private const val reviewChar = "" private const val reviewChar = ""
private const val indentChar = " "
@JvmStatic @JvmStatic
var viewWidth = 0 var viewWidth = 0
private set private set
@ -104,7 +106,7 @@ object ChapterProvider {
private var indentCharWidth = 0f private var indentCharWidth = 0f
@JvmStatic @JvmStatic
private var titlePaintTextHeight = 0f var titlePaintTextHeight = 0f
@JvmStatic @JvmStatic
var contentPaintTextHeight = 0f var contentPaintTextHeight = 0f
@ -132,6 +134,9 @@ object ChapterProvider {
var doublePage = false var doublePage = false
private set private set
@JvmStatic
var visibleRect = RectF()
init { init {
upStyle() upStyle()
} }
@ -171,6 +176,8 @@ object ChapterProvider {
durY = it.second durY = it.second
} }
} }
textPages.last().lines.last().isParagraphEnd = true
stringBuilder.append("\n")
durY += titleBottomSpacing durY += titleBottomSpacing
} }
contents.forEach { content -> contents.forEach { content ->
@ -224,10 +231,19 @@ object ChapterProvider {
durY = it.second durY = it.second
} }
} }
durY = setTypeImage( setTypeImage(
book, matcher.group(1)!!, book,
absStartX, durY, textPages, stringBuilder, book.getImageStyle() matcher.group(1)!!,
) absStartX,
durY,
textPages,
contentPaintTextHeight,
stringBuilder,
book.getImageStyle()
).let {
absStartX = it.first
durY = it.second
}
start = matcher.end() start = matcher.end()
} }
if (start < content.length) { 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() val textPage = textPages.last()
textPages.last().text = stringBuilder.toString() 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 -> textPages.forEachIndexed { index, item ->
item.index = index item.index = index
item.pageSize = textPages.size item.pageSize = textPages.size
item.chapterIndex = bookChapter.index item.chapterIndex = bookChapter.index
item.chapterSize = chapterSize item.chapterSize = chapterSize
item.title = displayTitle item.title = displayTitle
item.doublePage = doublePage
item.paddingTop = paddingTop
item.upLinesPosition() item.upLinesPosition()
} }
@ -280,15 +307,19 @@ object ChapterProvider {
x: Int, x: Int,
y: Float, y: Float,
textPages: ArrayList<TextPage>, textPages: ArrayList<TextPage>,
textHeight: Float,
stringBuilder: StringBuilder, stringBuilder: StringBuilder,
imageStyle: String?, imageStyle: String?,
): Float { ): Pair<Int, Float> {
var absStartX = x
var durY = y var durY = y
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource) val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
if (size.width > 0 && size.height > 0) { if (size.width > 0 && size.height > 0) {
if (durY > visibleHeight) { if (durY > visibleHeight) {
val textPage = textPages.last() val textPage = textPages.last()
textPage.height = durY if (textPage.height < durY) {
textPage.height = durY
}
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" }
stringBuilder.clear() stringBuilder.clear()
textPages.add(TextPage()) textPages.add(TextPage())
@ -313,10 +344,23 @@ object ChapterProvider {
} }
if (durY + height > visibleHeight) { if (durY + height > visibleHeight) {
val textPage = textPages.last() val textPage = textPages.last()
textPage.height = durY if (doublePage && absStartX < viewWidth / 2) {
textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } //当前页面左列结束
stringBuilder.clear() textPage.leftLineSize = textPage.lineSize
textPages.add(TextPage()) 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 durY = 0f
} }
} }
@ -334,9 +378,11 @@ object ChapterProvider {
textLine.addColumn( textLine.addColumn(
ImageColumn(start = x + start, end = x + end, src = src) ImageColumn(start = x + start, end = x + end, src = src)
) )
calcTextLinePosition(textPages, textLine, stringBuilder.length)
stringBuilder.append(" ") // 确保翻页时索引计算正确
textPages.last().addLine(textLine) textPages.last().addLine(textLine)
} }
return durY + paragraphSpacing / 10f return absStartX to durY + textHeight * paragraphSpacing / 10f
} }
/** /**
@ -358,14 +404,11 @@ object ChapterProvider {
srcList: LinkedList<String>? = null srcList: LinkedList<String>? = null
): Pair<Int, Float> { ): Pair<Int, Float> {
var absStartX = x var absStartX = x
val widthsArray = FloatArray(text.length)
val layout = if (ReadBookConfig.useZhLayout) { val layout = if (ReadBookConfig.useZhLayout) {
ZhLayout(text, textPaint, visibleWidth, widthsArray) ZhLayout(text, textPaint, visibleWidth)
} else { } else {
textPaint.getTextWidths(text, widthsArray)
StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true) StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
} }
val widthsList = widthsArray.asList()
var durY = when { var durY = when {
//标题y轴居中 //标题y轴居中
emptyContent && textPages.size == 1 -> { emptyContent && textPages.size == 1 -> {
@ -398,6 +441,7 @@ object ChapterProvider {
if (durY + textHeight > visibleHeight) { if (durY + textHeight > visibleHeight) {
val textPage = textPages.last() val textPage = textPages.last()
if (doublePage && absStartX < viewWidth / 2) { if (doublePage && absStartX < viewWidth / 2) {
//当前页面左列结束
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
absStartX = viewWidth / 2 + paddingLeft absStartX = viewWidth / 2 + paddingLeft
} else { } else {
@ -406,33 +450,34 @@ object ChapterProvider {
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
} }
textPage.text = stringBuilder.toString() textPage.text = stringBuilder.toString()
textPage.height = durY
//新建页面 //新建页面
textPages.add(TextPage()) textPages.add(TextPage())
stringBuilder.clear() stringBuilder.clear()
absStartX = paddingLeft absStartX = paddingLeft
} }
if (textPage.height < durY) {
textPage.height = durY
}
durY = 0f durY = 0f
} }
val lineStart = layout.getLineStart(lineIndex) val lineStart = layout.getLineStart(lineIndex)
val lineEnd = layout.getLineEnd(lineIndex) val lineEnd = layout.getLineEnd(lineIndex)
val words = text.substring(lineStart, lineEnd) val lineText = text.substring(lineStart, lineEnd)
val textWidths = widthsList.subList(lineStart, lineEnd) val (words, widths) = measureTextSplit(lineText, textPaint)
val desiredWidth = textWidths.sum() val desiredWidth = widths.fastSum()
when { when {
lineIndex == 0 && layout.lineCount > 1 && !isTitle -> { lineIndex == 0 && layout.lineCount > 1 && !isTitle -> {
//第一行 非标题 //第一行 非标题
textLine.text = words textLine.text = lineText
addCharsToLineFirst( addCharsToLineFirst(
book, absStartX, textLine, words, book, absStartX, textLine, words,
textPaint, desiredWidth, textWidths, srcList desiredWidth, widths, srcList
) )
} }
lineIndex == layout.lineCount - 1 -> { lineIndex == layout.lineCount - 1 -> {
//最后一行 //最后一行
textLine.text = words textLine.text = lineText
textLine.isParagraphEnd = true
//标题x轴居中 //标题x轴居中
val startX = if ( val startX = if (
isTitle && isTitle &&
@ -443,8 +488,8 @@ object ChapterProvider {
0f 0f
} }
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, words, textPaint, book, absStartX, textLine, words,
startX, !isTitle && lineIndex == 0, textWidths, srcList startX, !isTitle && lineIndex == 0, widths, srcList
) )
} }
@ -457,45 +502,55 @@ object ChapterProvider {
val startX = (visibleWidth - desiredWidth) / 2 val startX = (visibleWidth - desiredWidth) / 2
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, words, book, absStartX, textLine, words,
textPaint, startX, false, textWidths, srcList startX, false, widths, srcList
) )
} else { } else {
//中间行 //中间行
textLine.text = words textLine.text = lineText
addCharsToLineMiddle( addCharsToLineMiddle(
book, absStartX, textLine, words, book, absStartX, textLine, words,
textPaint, desiredWidth, 0f, textWidths, srcList desiredWidth, 0f, widths, srcList
) )
} }
} }
} }
val sbLength = stringBuilder.length if (doublePage) {
stringBuilder.append(words) textLine.isLeftLine = absStartX < viewWidth / 2
if (textLine.isParagraphEnd) {
stringBuilder.append("\n")
} }
val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 } calcTextLinePosition(textPages, textLine, stringBuilder.length)
?: textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull { it.paragraphNum > 0 } stringBuilder.append(lineText)
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)
textLine.upTopBottom(durY, textHeight, fontMetrics) textLine.upTopBottom(durY, textHeight, fontMetrics)
val textPage = textPages.last()
textPage.addLine(textLine)
durY += textHeight * lineSpacingExtra durY += textHeight * lineSpacingExtra
textPages.last().height = durY if (textPage.height < durY) {
textPage.height = durY
}
} }
durY += textHeight * paragraphSpacing / 10f durY += textHeight * paragraphSpacing / 10f
return Pair(absStartX, durY) return Pair(absStartX, durY)
} }
private fun calcTextLinePosition(
textPages: ArrayList<TextPage>,
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, book: Book,
absStartX: Int, absStartX: Int,
textLine: TextLine, textLine: TextLine,
text: String, words: List<String>,
textPaint: TextPaint,
/**自然排版长度**/ /**自然排版长度**/
desiredWidth: Float, desiredWidth: Float,
textWidths: List<Float>, textWidths: List<Float>,
@ -513,17 +567,17 @@ object ChapterProvider {
var x = 0f var x = 0f
if (!ReadBookConfig.textFullJustify) { if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, text, textPaint, book, absStartX, textLine, words,
x, true, textWidths, srcList x, true, textWidths, srcList
) )
return return
} }
val bodyIndent = ReadBookConfig.paragraphIndent val bodyIndent = ReadBookConfig.paragraphIndent
for (char in bodyIndent.toStringArray()) { for (i in bodyIndent.indices) {
val x1 = x + indentCharWidth val x1 = x + indentCharWidth
textLine.addColumn( textLine.addColumn(
TextColumn( TextColumn(
charData = char, charData = indentChar,
start = absStartX + x, start = absStartX + x,
end = absStartX + x1 end = absStartX + x1
) )
@ -531,12 +585,12 @@ object ChapterProvider {
x = x1 x = x1
textLine.indentWidth = x textLine.indentWidth = x
} }
if (text.length > bodyIndent.length) { if (words.size > bodyIndent.length) {
val text1 = text.substring(bodyIndent.length, text.length) val text1 = words.subList(bodyIndent.length, words.size)
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size) val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
addCharsToLineMiddle( addCharsToLineMiddle(
book, absStartX, textLine, text1, book, absStartX, textLine, text1,
textPaint, desiredWidth, x, textWidths1, srcList desiredWidth, x, textWidths1, srcList
) )
} }
} }
@ -548,8 +602,7 @@ object ChapterProvider {
book: Book, book: Book,
absStartX: Int, absStartX: Int,
textLine: TextLine, textLine: TextLine,
text: String, words: List<String>,
textPaint: TextPaint,
/**自然排版长度**/ /**自然排版长度**/
desiredWidth: Float, desiredWidth: Float,
/**起始x坐标**/ /**起始x坐标**/
@ -559,19 +612,19 @@ object ChapterProvider {
) { ) {
if (!ReadBookConfig.textFullJustify) { if (!ReadBookConfig.textFullJustify) {
addCharsToLineNatural( addCharsToLineNatural(
book, absStartX, textLine, text, textPaint, book, absStartX, textLine, words,
startX, false, textWidths, srcList startX, false, textWidths, srcList
) )
return return
} }
val residualWidth = visibleWidth - desiredWidth val residualWidth = visibleWidth - desiredWidth
val spaceSize = text.count { it == ' ' } val spaceSize = words.count { it == " " }
val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint)
if (spaceSize > 1) { if (spaceSize > 1) {
val d = residualWidth / spaceSize val d = residualWidth / spaceSize
var x = startX var x = startX
words.forEachIndexed { index, char -> for (index in words.indices) {
val cw = widths[index] val char = words[index]
val cw = textWidths[index]
val x1 = if (char == " ") { val x1 = if (char == " ") {
if (index != words.lastIndex) (x + cw + d) else (x + cw) if (index != words.lastIndex) (x + cw + d) else (x + cw)
} else { } else {
@ -587,8 +640,9 @@ object ChapterProvider {
val gapCount: Int = words.lastIndex val gapCount: Int = words.lastIndex
val d = residualWidth / gapCount val d = residualWidth / gapCount
var x = startX var x = startX
words.forEachIndexed { index, char -> for (index in words.indices) {
val cw = widths[index] val char = words[index]
val cw = textWidths[index]
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw) val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
addCharToLine( addCharToLine(
book, absStartX, textLine, char, book, absStartX, textLine, char,
@ -607,8 +661,7 @@ object ChapterProvider {
book: Book, book: Book,
absStartX: Int, absStartX: Int,
textLine: TextLine, textLine: TextLine,
text: String, words: List<String>,
textPaint: TextPaint,
startX: Float, startX: Float,
hasIndent: Boolean, hasIndent: Boolean,
textWidths: List<Float>, textWidths: List<Float>,
@ -616,9 +669,9 @@ object ChapterProvider {
) { ) {
val indentLength = ReadBookConfig.paragraphIndent.length val indentLength = ReadBookConfig.paragraphIndent.length
var x = startX var x = startX
val (words, widths) = getStringArrayAndTextWidths(text, textWidths, textPaint) for (index in words.indices) {
words.forEachIndexed { index, char -> val char = words[index]
val cw = widths[index] val cw = textWidths[index]
val x1 = x + cw val x1 = x + cw
addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList) addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
x = x1 x = x1
@ -629,39 +682,6 @@ object ChapterProvider {
exceed(absStartX, textLine, words) exceed(absStartX, textLine, words)
} }
fun getStringArrayAndTextWidths(
text: String,
textWidths: List<Float>,
textPaint: TextPaint
): Pair<List<String>, List<Float>> {
val charArray = text.toCharArray()
val strList = ArrayList<String>(text.length)
val textWidthList = ArrayList<Float>(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<String>, ArrayList<Float>> {
val length = text.length
val widthsArray = FloatArray(length)
paint.getTextWidths(text, widthsArray)
val clusterCount = widthsArray.count { it > 0f }
val widths = ArrayList<Float>(clusterCount)
val stringList = ArrayList<String>(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 { getPaints(typeface).let {
titlePaint = it.first titlePaint = it.first
contentPaint = it.second contentPaint = it.second
reviewPaint.color = contentPaint.color // reviewPaint.color = contentPaint.color
reviewPaint.textSize = contentPaint.textSize * 0.45f // reviewPaint.textSize = contentPaint.textSize * 0.45f
reviewPaint.textAlign = Paint.Align.CENTER // reviewPaint.textAlign = Paint.Align.CENTER
} }
//间距 //间距
lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f
@ -825,7 +867,7 @@ object ChapterProvider {
viewWidth = width viewWidth = width
viewHeight = height viewHeight = height
upLayout() upLayout()
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, arrayOf(5))
} }
} }
@ -862,6 +904,14 @@ object ChapterProvider {
visibleRight = viewWidth - paddingRight visibleRight = viewWidth - paddingRight
visibleBottom = paddingTop + visibleHeight visibleBottom = paddingTop + visibleHeight
} }
visibleRect.set(
paddingLeft.toFloat(),
paddingTop.toFloat(),
visibleRight.toFloat(),
visibleBottom.toFloat()
)
} }
} }

View File

@ -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<Float>()
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<Int>) {
val charArray = String(codePoints.toIntArray(), 0, codePoints.size).toCharArray()
val widths = FloatArray(charArray.size)
paint.getTextWidths(charArray, 0, charArray.size, widths)
val widthsList = ArrayList<Float>(charArray.size)
val buf = IntArray(1)
for (i in charArray.indices) {
if (charArray[i].isLowSurrogate()) continue
val width = ceil(widths[i])
widthsList.add(width)
// 可能需要检查是否不可见字符
if (width == 0f && widthsList.size > 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<String>, ArrayList<Float>> {
var needMeasureCodePoints: HashSet<Int>? = null
val codePoints = text.toCodePoints()
val size = codePoints.size
val widths = ArrayList<Float>(size)
val stringList = ArrayList<String>(size)
val buf = IntArray(1)
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = measureCodePoint(codePoint)
widths.add(width)
if (width == -1f) {
if (needMeasureCodePoints == null) {
needMeasureCodePoints = hashSetOf()
}
needMeasureCodePoints.add(codePoint)
}
buf[0] = codePoint
stringList.add(String(buf, 0, 1))
}
if (!needMeasureCodePoints.isNullOrEmpty()) {
measureCodePoints(needMeasureCodePoints.toList())
for (i in codePoints.indices) {
if (widths[i] == -1f) {
widths[i] = measureCodePoint(codePoints[i])
}
}
}
return stringList to widths
}
fun measureText(text: String): Float {
var textWidth = 0f
var needMeasureCodePoints: ArrayList<Int>? = null
val codePoints = text.toCodePoints()
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = measureCodePoint(codePoint)
if (width == -1f) {
if (needMeasureCodePoints == null) {
needMeasureCodePoints = ArrayList()
}
needMeasureCodePoints.add(codePoint)
continue
}
textWidth += width
}
if (!needMeasureCodePoints.isNullOrEmpty()) {
measureCodePoints(needMeasureCodePoints.toHashSet().toList())
for (i in needMeasureCodePoints.indices) {
textWidth += measureCodePoint(needMeasureCodePoints[i])
}
}
return textWidth
}
private fun String.toCodePoints(): List<Int> {
val codePoints = ArrayList<Int>(length)
val charArray = toCharArray()
val size = length
var i = 0
while (i < size) {
val c1 = charArray[i++]
var cp = c1.code
if (c1.isHighSurrogate() && i < size) {
val c2 = charArray[i]
if (c2.isLowSurrogate()) {
i++
cp = Character.toCodePoint(c1, c2)
}
}
codePoints.add(cp)
}
return codePoints
}
fun setPaint(paint: TextPaint) {
this.paint = paint
invalidate()
}
private fun invalidate() {
chineseCommonWidth = paint.measureText("")
codePointWidths.clear()
asciiWidths.fill(-1f)
}
}

View File

@ -12,7 +12,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} }
override fun hasNext(): Boolean = with(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) { override fun hasNextPlus(): Boolean = with(dataSource) {
@ -34,9 +34,12 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} }
override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) { override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) {
return if (hasNext() && currentChapter != null) { return if (hasNext()) {
if (currentChapter?.isLastIndex(pageIndex) == true) { if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) {
ReadBook.moveToNextChapter(upContent) if ((currentChapter == null || isScroll) && nextChapter == null) {
return@with false
}
ReadBook.moveToNextChapter(upContent, false)
} else { } else {
ReadBook.setPageIndex(pageIndex.plus(1)) ReadBook.setPageIndex(pageIndex.plus(1))
} }
@ -47,10 +50,16 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
} }
override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) { override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) {
return if (hasPrev() && currentChapter != null) { return if (hasPrev()) {
if (pageIndex <= 0) { if (pageIndex <= 0) {
ReadBook.moveToPrevChapter(upContent) if (currentChapter == null && prevChapter == null) {
return@with false
}
ReadBook.moveToPrevChapter(upContent, upContentInPlace = false)
} else { } else {
if (currentChapter == null) {
return@with false
}
ReadBook.setPageIndex(pageIndex.minus(1)) ReadBook.setPageIndex(pageIndex.minus(1))
} }
if (upContent) upContent(resetPageOffset = false) if (upContent) upContent(resetPageOffset = false)
@ -76,14 +85,12 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
return@with TextPage(text = it).format() return@with TextPage(text = it).format()
} }
currentChapter?.let { currentChapter?.let {
val pageIndex = pageIndex
if (pageIndex < it.pageSize - 1) { if (pageIndex < it.pageSize - 1) {
return@with it.getPage(pageIndex + 1)?.removePageAloudSpan() return@with it.getPage(pageIndex + 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
} }
} }
if (!hasNextChapter()) {
return@with TextPage(text = "")
}
nextChapter?.let { nextChapter?.let {
return@with it.getPage(0)?.removePageAloudSpan() return@with it.getPage(0)?.removePageAloudSpan()
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
@ -96,8 +103,9 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
ReadBook.msg?.let { ReadBook.msg?.let {
return@with TextPage(text = it).format() 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() return@with it.getPage(pageIndex - 1)?.removePageAloudSpan()
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
} }
@ -112,6 +120,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
override val nextPlusPage: TextPage override val nextPlusPage: TextPage
get() = with(dataSource) { get() = with(dataSource) {
currentChapter?.let { currentChapter?.let {
val pageIndex = pageIndex
if (pageIndex < it.pageSize - 2) { if (pageIndex < it.pageSize - 2) {
return@with it.getPage(pageIndex + 2)?.removePageAloudSpan() return@with it.getPage(pageIndex + 2)?.removePageAloudSpan()
?: TextPage(title = it.title).format() ?: TextPage(title = it.title).format()
@ -124,7 +133,6 @@ class TextPageFactory(dataSource: DataSource) : PageFactory<TextPage>(dataSource
return@with nc.getPage(1)?.removePageAloudSpan() return@with nc.getPage(1)?.removePageAloudSpan()
?: TextPage(text = "继续滑动以加载下一章…").format() ?: TextPage(text = "继续滑动以加载下一章…").format()
} }
} }
return TextPage().format() return TextPage().format()
} }

View File

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

View File

@ -10,6 +10,8 @@ import android.text.StaticLayout
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatTextView 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 import io.legado.app.utils.dpToPx
class BatteryView @JvmOverloads constructor( class BatteryView @JvmOverloads constructor(
@ -22,6 +24,7 @@ class BatteryView @JvmOverloads constructor(
private val batteryPaint = Paint() private val batteryPaint = Paint()
private val outFrame = Rect() private val outFrame = Rect()
private val polar = Rect() private val polar = Rect()
private val canvasRecorder = CanvasRecorderFactory.create()
var isBattery = false var isBattery = false
set(value) { set(value) {
field = value field = value
@ -62,31 +65,40 @@ class BatteryView @JvmOverloads constructor(
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) canvasRecorder.recordIfNeededThenDraw(canvas, width, height) {
if (!isBattery) return super.onDraw(this)
layout.getLineBounds(0, outFrame) if (!isBattery) return@recordIfNeededThenDraw
val batteryStart = layout layout.getLineBounds(0, outFrame)
.getPrimaryHorizontal(text.length - battery.toString().length) val batteryStart = layout
.toInt() + 2.dpToPx() .getPrimaryHorizontal(text.length - battery.toString().length)
val batteryEnd = batteryStart + .toInt() + 2.dpToPx()
StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx() val batteryEnd = batteryStart +
outFrame.set( StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx()
batteryStart, outFrame.set(
2.dpToPx(), batteryStart,
batteryEnd, 2.dpToPx(),
height - 2.dpToPx() batteryEnd,
) height - 2.dpToPx()
val dj = (outFrame.bottom - outFrame.top) / 3 )
polar.set( val dj = (outFrame.bottom - outFrame.top) / 3
batteryEnd, polar.set(
outFrame.top + dj, batteryEnd,
batteryEnd + 2.dpToPx(), outFrame.top + dj,
outFrame.bottom - dj batteryEnd + 2.dpToPx(),
) outFrame.bottom - dj
batteryPaint.style = Paint.Style.STROKE )
canvas.drawRect(outFrame, batteryPaint) batteryPaint.style = Paint.Style.STROKE
batteryPaint.style = Paint.Style.FILL drawRect(outFrame, batteryPaint)
canvas.drawRect(polar, batteryPaint) batteryPaint.style = Paint.Style.FILL
drawRect(polar, batteryPaint)
}
}
override fun invalidate() {
super.invalidate()
kotlin.runCatching {
canvasRecorder.invalidate()
}
} }
} }

View File

@ -30,6 +30,7 @@ class DetailSeekBar @JvmOverloads constructor(
get() = binding.seekBar.progress get() = binding.seekBar.progress
set(value) { set(value) {
binding.seekBar.progress = value binding.seekBar.progress = value
upValue()
} }
var max: Int var max: Int
get() = binding.seekBar.max get() = binding.seekBar.max

View File

@ -36,13 +36,17 @@ class TitleBar @JvmOverloads constructor(
var title: CharSequence? var title: CharSequence?
get() = toolbar.title get() = toolbar.title
set(title) { set(title) {
toolbar.title = title if (toolbar.title != title) {
toolbar.title = title
}
} }
var subtitle: CharSequence? var subtitle: CharSequence?
get() = toolbar.subtitle get() = toolbar.subtitle
set(subtitle) { set(subtitle) {
toolbar.subtitle = subtitle if (toolbar.subtitle != subtitle) {
toolbar.subtitle = subtitle
}
} }
private val displayHomeAsUp: Boolean private val displayHomeAsUp: Boolean
@ -198,7 +202,7 @@ class TitleBar @JvmOverloads constructor(
toolbar.setSubtitleTextAppearance(context, resId) toolbar.setSubtitleTextAppearance(context, resId)
} }
fun setTextColor(@ColorInt color: Int){ fun setTextColor(@ColorInt color: Int) {
setTitleTextColor(color) setTitleTextColor(color)
setSubTitleTextColor(color) setSubTitleTextColor(color)
} }

View File

@ -12,6 +12,7 @@ import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.setHtml import io.legado.app.utils.setHtml
import io.legado.app.utils.setLayout import io.legado.app.utils.setLayout
import io.legado.app.utils.setTextAsync
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.ext.tables.TablePlugin import io.noties.markwon.ext.tables.TablePlugin
@ -75,8 +76,9 @@ class TextDialog() : BaseDialogFragment(R.layout.dialog_text_view) {
.build() .build()
.setMarkdown(binding.textView, content) .setMarkdown(binding.textView, content)
} }
Mode.HTML.name -> binding.textView.setHtml(content) Mode.HTML.name -> binding.textView.setHtml(content)
else -> binding.textView.text = content else -> binding.textView.setTextAsync(content)
} }
time = it.getLong("time", 0L) time = it.getLong("time", 0L)
} }

View File

@ -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<SoftReference<Bitmap>> = 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
}
}
}

View File

@ -34,7 +34,6 @@ object BitmapUtils {
BitmapFactory.decodeFileDescriptor(fis.fd, null, op) BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
op.inSampleSize = calculateInSampleSize(op, width, height) op.inSampleSize = calculateInSampleSize(op, width, height)
op.inJustDecodeBounds = false op.inJustDecodeBounds = false
BitmapCache.addInBitmapOptions(op)
BitmapFactory.decodeFileDescriptor(fis.fd, null, op) BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
} }
} }

View File

@ -32,26 +32,19 @@ object ChineseUtils {
fun fixT2sDict() { fun fixT2sDict() {
val dict = DictionaryContainer.getInstance().getDictionary(TransType.TRADITIONAL_TO_SIMPLE) val dict = DictionaryContainer.getInstance().getDictionary(TransType.TRADITIONAL_TO_SIMPLE)
dict.run { dict.run {
remove("") remove("", "")
remove("") remove("支援", "沈默", "類比", "模擬", "划槳", "列根", "先進")
remove("支援") remove("路易斯", "非同步", "出租车", "周杰倫")
remove("沈默")
remove("類比")
remove("模擬")
remove("划槳")
remove("列根")
remove("路易斯")
remove("非同步")
remove("出租车")
remove("周杰倫")
} }
} }
fun BasicDictionary.remove(key: String) { fun BasicDictionary.remove(vararg keys: String) {
if (key.length == 1) { for (key in keys) {
chars.remove(key[0]) if (key.length == 1) {
} else { chars.remove(key[0])
dict.remove(key) } else {
dict.remove(key)
}
} }
} }

View File

@ -0,0 +1,39 @@
package io.legado.app.utils
fun List<Float>.fastSum(): Float {
var sum = 0f
for (i in indices) {
sum += this[i]
}
return sum
}
inline fun <T> List<T>.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 <T, K : Comparable<K>> List<T>.fastBinarySearchBy(
key: K?,
fromIndex: Int = 0,
toIndex: Int = size,
crossinline selector: (T) -> K?
): Int = fastBinarySearch(fromIndex, toIndex) { compareValues(selector(it), key) }

View File

@ -25,14 +25,20 @@ import android.widget.TextView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.MenuPopupHelper import androidx.appcompat.view.menu.MenuPopupHelper
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.record import androidx.core.graphics.record
import androidx.core.graphics.withTranslation import androidx.core.graphics.withTranslation
import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.get import androidx.core.view.get
import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.globalExecutor
import io.legado.app.lib.theme.TintHelper 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 splitties.systemservices.inputMethodManager
import java.lang.reflect.Field 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) { fun View.setPaddingBottom(bottom: Int) {
setPadding(paddingLeft, paddingTop, paddingRight, bottom) 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") @SuppressLint("RestrictedApi")
fun PopupMenu.show(x: Int, y: Int) { fun PopupMenu.show(x: Int, y: Int) {
kotlin.runCatching { kotlin.runCatching {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -136,13 +136,6 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:isBottomBackground="true" /> app:isBottomBackground="true" />
<io.legado.app.lib.prefs.SwitchPreference
android:defaultValue="false"
android:key="asyncLoadImage"
android:title="@string/async_load_image"
app:iconSpaceReserved="false"
app:isBottomBackground="true" />
<io.legado.app.lib.prefs.SwitchPreference <io.legado.app.lib.prefs.SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="noAnimScrollPage" android:key="noAnimScrollPage"