mirror of
https://github.com/gedoor/legado.git
synced 2024-07-19 01:17:25 +08:00
Merge pull request #3731 from gedoor/master
Sync changes from branch master
This commit is contained in:
commit
92121fbb8c
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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.
|
||||||
|
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
@ -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' }}
|
||||||
|
2
.github/workflows/web.yml
vendored
2
.github/workflows/web.yml
vendored
@ -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:
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
6
app/src/main/java/io/legado/app/help/ExecutorService.kt
Normal file
6
app/src/main/java/io/legado/app/help/ExecutorService.kt
Normal 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() }
|
@ -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)
|
||||||
|
@ -567,7 +567,6 @@ object ReadBookConfig {
|
|||||||
textColorInt = color
|
textColorInt = color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChapterProvider.upStyle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun curTextColor(): Int {
|
fun curTextColor(): Int {
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
148
app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt
Normal file
148
app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) }
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user