Merge pull request #3731 from gedoor/master

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

View File

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

View File

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

View File

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

View File

@ -473,6 +473,15 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</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
android:name="channel"

View File

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

View File

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

View File

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

View File

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

View File

@ -163,7 +163,22 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
.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 {
return ThemeStore(context)

View File

@ -2,13 +2,10 @@ package io.legado.app.model
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.util.Size
import androidx.collection.LruCache
import io.legado.app.R
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppLog.putDebug
import io.legado.app.constant.PageAnim
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException
@ -16,10 +13,8 @@ import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isEpub
import io.legado.app.help.book.isPdf
import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.localBook.EpubFile
import io.legado.app.model.localBook.PdfFile
import io.legado.app.utils.BitmapCache
import io.legado.app.utils.BitmapUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.SvgUtils
@ -29,8 +24,6 @@ import kotlinx.coroutines.withContext
import splitties.init.appCtx
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.min
object ImageProvider {
@ -50,12 +43,6 @@ object ImageProvider {
}
return AppConfig.bitmapCacheSize * M
}
private val maxCacheSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
min(128 * M, Runtime.getRuntime().maxMemory().toInt())
} else {
256 * M
}
private val asyncLoadingImages = ConcurrentHashMap.newKeySet<String>()
val bitmapLruCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(filePath: String, bitmap: Bitmap): Int {
@ -70,8 +57,7 @@ object ImageProvider {
) {
//错误图片不能释放,占位用,防止一直重复获取图片
if (oldBitmap != errorBitmap) {
BitmapCache.add(oldBitmap)
//oldBitmap.recycle()
oldBitmap.recycle()
//putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath")
//putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes")
}
@ -160,9 +146,8 @@ object ImageProvider {
book: Book,
src: String,
width: Int,
height: Int? = null,
block: (() -> Unit)? = null
): Bitmap? {
height: Int? = null
): Bitmap {
//src为空白时 可能被净化替换掉了 或者规则失效
if (book.getUseReplaceRule() && src.isBlank()) {
book.setUseReplaceRule(false)
@ -174,32 +159,6 @@ object ImageProvider {
//bitmapLruCache的key同一改成缓存文件的路径
val cacheBitmap = getNotRecycled(vFile.absolutePath)
if (cacheBitmap != null) return cacheBitmap
if (height != null && AppConfig.asyncLoadImage && ReadBook.pageAnim() == PageAnim.scrollPageAnim) {
if (asyncLoadingImages.contains(vFile.absolutePath)) {
return null
}
asyncLoadingImages.add(vFile.absolutePath)
Coroutine.async {
BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
?: SvgUtils.createBitmap(vFile.absolutePath, width, height)
?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap))
}.onSuccess {
bitmapLruCache.run {
if (maxSize() < maxCacheSize && size() + it.byteCount > maxSize() && putCount() - evictionCount() < 5) {
resize(min(maxCacheSize, maxSize() + it.byteCount))
AppLog.put("图片缓存太小,自动扩增至${(maxSize() / M)}MB。")
}
}
bitmapLruCache.put(vFile.absolutePath, it)
}.onError {
//错误图片占位,防止重复获取
bitmapLruCache.put(vFile.absolutePath, errorBitmap)
}.onFinally {
asyncLoadingImages.remove(vFile.absolutePath)
block?.invoke()
}
return null
}
return kotlin.runCatching {
val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
?: SvgUtils.createBitmap(vFile.absolutePath, width, height)
@ -212,4 +171,8 @@ object ImageProvider {
}.getOrDefault(errorBitmap)
}
fun clear() {
bitmapLruCache.evictAll()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,13 +27,38 @@ import io.legado.app.help.config.LocalConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.*
import io.legado.app.lib.theme.Selector
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.buttonDisabledColor
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.browser.WebViewActivity
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.*
import splitties.views.*
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.ConstraintModify
import io.legado.app.utils.activity
import io.legado.app.utils.dpToPx
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.gone
import io.legado.app.utils.invisible
import io.legado.app.utils.loadAnimation
import io.legado.app.utils.modifyBegin
import io.legado.app.utils.navigationBarGravity
import io.legado.app.utils.navigationBarHeight
import io.legado.app.utils.openUrl
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.startActivity
import io.legado.app.utils.visible
import splitties.views.bottomPadding
import splitties.views.leftPadding
import splitties.views.onClick
import splitties.views.onLongClick
import splitties.views.padding
import splitties.views.rightPadding
/**
* 阅读界面菜单
@ -273,6 +298,7 @@ class ReadMenu @JvmOverloads constructor(
}
fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
callBack.onMenuShow()
this.visible()
binding.titleBar.visible()
binding.bottomMenu.visible()
@ -286,6 +312,7 @@ class ReadMenu @JvmOverloads constructor(
}
fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) {
callBack.onMenuHide()
this.onMenuOutEnd = onMenuOutEnd
if (this.isVisible) {
if (anim) {
@ -558,6 +585,8 @@ class ReadMenu @JvmOverloads constructor(
fun payAction()
fun disableSource()
fun skipToChapter(index: Int)
fun onMenuShow()
fun onMenuHide()
}
}

View File

@ -18,7 +18,13 @@ import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.utils.*
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.activity
import io.legado.app.utils.invisible
import io.legado.app.utils.loadAnimation
import io.legado.app.utils.navigationBarGravity
import io.legado.app.utils.navigationBarHeight
import io.legado.app.utils.visible
import splitties.views.bottomPadding
import splitties.views.leftPadding
import splitties.views.padding
@ -235,6 +241,8 @@ class SearchMenu @JvmOverloads constructor(
fun exitSearchMenu()
fun showMenuBar()
fun navigateToSearch(searchResult: SearchResult, index: Int)
fun onMenuShow()
fun onMenuHide()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,148 @@
package io.legado.app.ui.book.read.page
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Picture
import android.graphics.Rect
import android.os.Build
import android.os.SystemClock
import androidx.core.graphics.withClip
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.utils.screenshot
/**
* 自动翻页
*/
class AutoPager(private val readView: ReadView) {
private var progress = 0
var isRunning = false
private var isPausing = false
private var scrollOffsetRemain = 0.0
private var scrollOffset = 0
private var lastTimeMillis = 0L
private var bitmap: Bitmap? = null
private var picture: Picture? = null
private var pictureIsDirty = true
private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
private val rect = Rect()
private val paint by lazy { Paint() }
fun start() {
isRunning = true
paint.color = ThemeStore.accentColor
lastTimeMillis = SystemClock.uptimeMillis()
readView.curPage.upSelectAble(false)
readView.invalidate()
}
fun stop() {
if (!isRunning) {
return
}
isRunning = false
isPausing = false
readView.curPage.upSelectAble(AppConfig.textSelectAble)
readView.invalidate()
reset()
picture = null
}
fun pause() {
if (!isRunning) {
return
}
isPausing = true
}
fun resume() {
if (!isRunning) {
return
}
isPausing = false
lastTimeMillis = SystemClock.uptimeMillis()
readView.invalidate()
}
fun reset() {
progress = 0
scrollOffsetRemain = 0.0
scrollOffset = 0
bitmap?.recycle()
bitmap = null
pictureIsDirty = true
}
fun onDraw(canvas: Canvas) {
if (!isRunning) {
return
}
if (readView.isScroll) {
if (!isPausing) readView.curPage.scroll(-scrollOffset)
} else {
val bottom = progress
val width = readView.width
if (atLeastApi23) {
if (picture == null) {
picture = Picture()
}
if (pictureIsDirty) {
pictureIsDirty = false
readView.nextPage.screenshot(picture!!)
}
canvas.withClip(0, 0, width, bottom) {
drawPicture(picture!!)
}
} else {
if (bitmap == null) {
bitmap = readView.nextPage.screenshot()
}
rect.set(0, 0, width, bottom)
canvas.drawBitmap(bitmap!!, rect, rect, null)
}
canvas.drawRect(
0f,
bottom.toFloat() - 1,
width.toFloat(),
bottom.toFloat(),
paint
)
if (!isPausing) readView.postInvalidate()
}
}
fun computeOffset() {
if (!isRunning) {
return
}
val currentTime = SystemClock.uptimeMillis()
val elapsedTime = currentTime - lastTimeMillis
lastTimeMillis = currentTime
val readTime = ReadBookConfig.autoReadSpeed * 1000.0
val height = readView.height
scrollOffsetRemain += height / readTime * elapsedTime
if (scrollOffsetRemain < 1) {
return
}
scrollOffset = scrollOffsetRemain.toInt()
this.scrollOffsetRemain -= scrollOffset
if (!readView.isScroll) {
progress += scrollOffset
if (progress >= height) {
if (!readView.fillPage(PageDirection.NEXT)) {
stop()
} else {
reset()
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -96,7 +96,7 @@ abstract class PageDelegate(protected val readView: ReadView) {
viewHeight = height
}
fun scroll() {
open fun computeScroll() {
if (scroller.computeScrollOffset()) {
readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) {
@ -190,6 +190,15 @@ abstract class PageDelegate(protected val readView: ReadView) {
}
}
fun postInvalidate() {
if (isRunning && this is HorizontalPageDelegate) {
readView.post {
setBitmap()
readView.invalidate()
}
}
}
open fun onDestroy() {
// run on destroy
}

View File

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

View File

@ -1,13 +1,27 @@
package io.legado.app.ui.book.read.page.delegate
import android.graphics.*
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF
import android.graphics.Region
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.view.MotionEvent
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection
import kotlin.math.*
import io.legado.app.utils.screenshot
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.hypot
import kotlin.math.min
import kotlin.math.sin
@Suppress("DEPRECATION")
class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) {
@ -86,6 +100,11 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
private val mPaint: Paint = Paint().apply { style = Paint.Style.FILL }
private var curBitmap: Bitmap? = null
private var prevBitmap: Bitmap? = null
private var nextBitmap: Bitmap? = null
private var canvas: Canvas = Canvas()
init {
//设置颜色数组
val color = intArrayOf(0x333333, -0x4fcccccd)
@ -122,6 +141,22 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT
}
override fun setBitmap() {
when (mDirection) {
PageDirection.PREV -> {
prevBitmap = prevPage.screenshot(prevBitmap, canvas)
curBitmap = curPage.screenshot(curBitmap, canvas)
}
PageDirection.NEXT -> {
nextBitmap = nextPage.screenshot(nextBitmap, canvas)
curBitmap = curPage.screenshot(curBitmap, canvas)
}
else -> Unit
}
}
override fun setViewSize(width: Int, height: Int) {
super.setViewSize(width, height)
mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
@ -133,6 +168,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
MotionEvent.ACTION_DOWN -> {
calcCornerXY(event.x, event.y)
}
MotionEvent.ACTION_MOVE -> {
if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3)
|| mDirection == PageDirection.PREV
@ -159,10 +195,12 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
} else {
calcCornerXY(viewWidth - startX, viewHeight.toFloat())
}
PageDirection.NEXT ->
if (viewWidth / 2 > startX) {
calcCornerXY(viewWidth - startX, startY)
}
else -> Unit
}
}
@ -216,6 +254,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, curBitmap)
}
PageDirection.PREV -> {
calcPoints()
drawCurrentPageArea(canvas, prevBitmap)
@ -223,6 +262,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, prevBitmap)
}
else -> return
}
}

View File

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

View File

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

View File

@ -1,9 +1,18 @@
package io.legado.app.ui.book.read.page.entities
import android.graphics.Canvas
import android.graphics.Paint.FontMetrics
import androidx.annotation.Keep
import io.legado.app.help.book.isImage
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextPage.Companion.emptyTextPage
import io.legado.app.ui.book.read.page.entities.column.BaseColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
import io.legado.app.utils.dpToPx
/**
* 行信息
@ -22,8 +31,7 @@ data class TextLine(
var pagePosition: Int = 0,
val isTitle: Boolean = false,
var isParagraphEnd: Boolean = false,
var isReadAloud: Boolean = false,
var isImage: Boolean = false
var isImage: Boolean = false,
) {
val columns: List<BaseColumn> get() = textColumns
@ -31,8 +39,20 @@ data class TextLine(
val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f
val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
val height: Float inline get() = lineBottom - lineTop
val canvasRecorder = CanvasRecorderFactory.create()
var isReadAloud: Boolean = false
set(value) {
if (field != value) {
invalidate()
}
field = value
}
var textPage: TextPage = emptyTextPage
var isLeftLine = true
fun addColumn(column: BaseColumn) {
column.textLine = this
textColumns.add(column)
}
@ -102,4 +122,50 @@ data class TextLine(
return visible
}
fun draw(view: ContentTextView, canvas: Canvas) {
canvasRecorder.recordIfNeededThenDraw(canvas, view.width, height.toInt()) {
drawTextLine(view, this)
}
}
private fun drawTextLine(view: ContentTextView, canvas: Canvas) {
for (i in columns.indices) {
columns[i].draw(view, canvas)
}
if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) {
drawUnderline(canvas)
}
}
/**
* 绘制下划线
*/
private fun drawUnderline(canvas: Canvas) {
val lineY = height - 1.dpToPx()
canvas.drawLine(
lineStart + indentWidth,
lineY,
lineEnd,
lineY,
ChapterProvider.contentPaint
)
}
fun invalidate() {
invalidateSelf()
textPage.invalidate()
}
fun invalidateSelf() {
canvasRecorder.invalidate()
}
fun recycleRecorder() {
canvasRecorder.recycle()
}
companion object {
val emptyTextLine = TextLine()
}
}

View File

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

View File

@ -1,11 +1,18 @@
package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
/**
* 列基类
*/
interface BaseColumn {
var start: Float
var end: Float
var textLine: TextLine
fun draw(view: ContentTextView, canvas: Canvas)
fun isTouch(x: Float): Boolean {
return x > start && x < end

View File

@ -1,6 +1,10 @@
package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import androidx.annotation.Keep
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
/**
@ -9,5 +13,10 @@ import androidx.annotation.Keep
@Keep
data class ButtonColumn(
override var start: Float,
override var end: Float
) : BaseColumn
override var end: Float,
) : BaseColumn {
override var textLine: TextLine = emptyTextLine
override fun draw(view: ContentTextView, canvas: Canvas) {
}
}

View File

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

View File

@ -4,6 +4,9 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import androidx.annotation.Keep
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
import io.legado.app.ui.book.read.page.provider.ChapterProvider
/**
@ -16,6 +19,16 @@ data class ReviewColumn(
val count: Int = 0
) : BaseColumn {
override var textLine: TextLine = emptyTextLine
override fun draw(view: ContentTextView, canvas: Canvas) {
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
drawToCanvas(canvas, textLine.lineBase, textPaint.textSize)
}
val countText by lazy {
if (count > 999) {
return@lazy "999"

View File

@ -1,6 +1,13 @@
package io.legado.app.ui.book.read.page.entities.column
import android.graphics.Canvas
import androidx.annotation.Keep
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
import io.legado.app.ui.book.read.page.provider.ChapterProvider
/**
* 文字列
@ -10,6 +17,43 @@ data class TextColumn(
override var start: Float,
override var end: Float,
val charData: String,
var selected: Boolean = false,
) : BaseColumn {
override var textLine: TextLine = emptyTextLine
var selected: Boolean = false
set(value) {
if (field != value) {
textLine.invalidate()
}
field = value
}
var isSearchResult: Boolean = false
) : BaseColumn
set(value) {
if (field != value) {
textLine.invalidate()
}
field = value
}
override fun draw(view: ContentTextView, canvas: Canvas) {
val textPaint = if (textLine.isTitle) {
ChapterProvider.titlePaint
} else {
ChapterProvider.contentPaint
}
if (textLine.isReadAloud || isSearchResult) {
synchronized(textPaint) {
textPaint.color = ThemeStore.accentColor
canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
textPaint.color = ReadBookConfig.textColor
}
} else {
canvas.drawText(charData, start, textLine.lineBase - textLine.lineTop, textPaint)
}
if (selected) {
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
}
}
}

View File

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

View File

@ -0,0 +1,143 @@
package io.legado.app.ui.book.read.page.provider
import android.text.TextPaint
import android.util.SparseArray
import androidx.core.util.getOrDefault
import kotlin.math.ceil
class TextMeasure(private var paint: TextPaint) {
private var chineseCommonWidth = paint.measureText("")
private val asciiWidths = FloatArray(128) { -1f }
private val codePointWidths = SparseArray<Float>()
private fun measureCodePoint(codePoint: Int): Float {
if (codePoint < 128) {
return asciiWidths[codePoint]
}
// 中文 Unicode 范围 U+4E00 - U+9FA5
if (codePoint in 19968 .. 40869) {
return chineseCommonWidth
}
return codePointWidths.getOrDefault(codePoint, -1f)
}
private fun measureCodePoints(codePoints: List<Int>) {
val charArray = String(codePoints.toIntArray(), 0, codePoints.size).toCharArray()
val widths = FloatArray(charArray.size)
paint.getTextWidths(charArray, 0, charArray.size, widths)
val widthsList = ArrayList<Float>(charArray.size)
val buf = IntArray(1)
for (i in charArray.indices) {
if (charArray[i].isLowSurrogate()) continue
val width = ceil(widths[i])
widthsList.add(width)
// 可能需要检查是否不可见字符
if (width == 0f && widthsList.size > 1) {
val lastIndex = widthsList.lastIndex
buf[0] = codePoints[lastIndex - 1]
widthsList[lastIndex - 1] = paint.measureText(String(buf, 0, 1))
buf[0] = codePoints[lastIndex]
widthsList[lastIndex] = paint.measureText(String(buf, 0, 1))
}
}
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = widthsList[i]
if (codePoint < 128) {
asciiWidths[codePoint] = width
} else {
codePointWidths[codePoint] = width
}
}
}
fun measureTextSplit(text: String): Pair<ArrayList<String>, ArrayList<Float>> {
var needMeasureCodePoints: HashSet<Int>? = null
val codePoints = text.toCodePoints()
val size = codePoints.size
val widths = ArrayList<Float>(size)
val stringList = ArrayList<String>(size)
val buf = IntArray(1)
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = measureCodePoint(codePoint)
widths.add(width)
if (width == -1f) {
if (needMeasureCodePoints == null) {
needMeasureCodePoints = hashSetOf()
}
needMeasureCodePoints.add(codePoint)
}
buf[0] = codePoint
stringList.add(String(buf, 0, 1))
}
if (!needMeasureCodePoints.isNullOrEmpty()) {
measureCodePoints(needMeasureCodePoints.toList())
for (i in codePoints.indices) {
if (widths[i] == -1f) {
widths[i] = measureCodePoint(codePoints[i])
}
}
}
return stringList to widths
}
fun measureText(text: String): Float {
var textWidth = 0f
var needMeasureCodePoints: ArrayList<Int>? = null
val codePoints = text.toCodePoints()
for (i in codePoints.indices) {
val codePoint = codePoints[i]
val width = measureCodePoint(codePoint)
if (width == -1f) {
if (needMeasureCodePoints == null) {
needMeasureCodePoints = ArrayList()
}
needMeasureCodePoints.add(codePoint)
continue
}
textWidth += width
}
if (!needMeasureCodePoints.isNullOrEmpty()) {
measureCodePoints(needMeasureCodePoints.toHashSet().toList())
for (i in needMeasureCodePoints.indices) {
textWidth += measureCodePoint(needMeasureCodePoints[i])
}
}
return textWidth
}
private fun String.toCodePoints(): List<Int> {
val codePoints = ArrayList<Int>(length)
val charArray = toCharArray()
val size = length
var i = 0
while (i < size) {
val c1 = charArray[i++]
var cp = c1.code
if (c1.isHighSurrogate() && i < size) {
val c2 = charArray[i]
if (c2.isLowSurrogate()) {
i++
cp = Character.toCodePoint(c1, c2)
}
}
codePoints.add(cp)
}
return codePoints
}
fun setPaint(paint: TextPaint) {
this.paint = paint
invalidate()
}
private fun invalidate() {
chineseCommonWidth = paint.measureText("")
codePointWidths.clear()
asciiWidths.fill(-1f)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,77 +0,0 @@
package io.legado.app.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.lang.ref.SoftReference
import java.util.concurrent.ConcurrentHashMap
object BitmapCache {
private val reusableBitmaps: MutableSet<SoftReference<Bitmap>> = ConcurrentHashMap.newKeySet()
fun add(bitmap: Bitmap) {
reusableBitmaps.add(SoftReference(bitmap))
}
fun addInBitmapOptions(options: BitmapFactory.Options) {
// inBitmap only works with mutable bitmaps, so force the decoder to
// return mutable bitmaps.
options.inMutable = true
// Try to find a bitmap to use for inBitmap.
getBitmapFromReusableSet(options)?.also { inBitmap ->
// If a suitable bitmap has been found, set it as the value of
// inBitmap.
options.inBitmap = inBitmap
}
}
private fun getBitmapFromReusableSet(options: BitmapFactory.Options): Bitmap? {
if (reusableBitmaps.isEmpty()) {
return null
}
val iterator = reusableBitmaps.iterator()
while (iterator.hasNext()) {
val item = iterator.next().get() ?: continue
if (item.isMutable) {
// Check to see it the item can be used for inBitmap.
if (canUseForInBitmap(item, options)) {
// Remove from reusable set so it can't be used again.
iterator.remove()
return item
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove()
}
}
return null
}
private fun canUseForInBitmap(
candidate: Bitmap,
targetOptions: BitmapFactory.Options
): Boolean {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
// the new bitmap is smaller than the reusable bitmap candidate
// allocation byte count.
val width: Int = targetOptions.outWidth / targetOptions.inSampleSize
val height: Int = targetOptions.outHeight / targetOptions.inSampleSize
val byteCount: Int = width * height * getBytesPerPixel(candidate.config)
return byteCount <= candidate.allocationByteCount
}
/**
* A helper function to return the byte usage per pixel of a bitmap based on its configuration.
*/
private fun getBytesPerPixel(config: Bitmap.Config): Int {
return when (config) {
Bitmap.Config.ARGB_8888 -> 4
Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2
Bitmap.Config.ALPHA_8 -> 1
else -> 1
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,39 @@
package io.legado.app.utils
fun List<Float>.fastSum(): Float {
var sum = 0f
for (i in indices) {
sum += this[i]
}
return sum
}
inline fun <T> List<T>.fastBinarySearch(
fromIndex: Int = 0,
toIndex: Int = size,
comparison: (T) -> Int
): Int {
var low = fromIndex
var high = toIndex - 1
while (low <= high) {
val mid = (low + high).ushr(1) // safe from overflows
val midVal = get(mid)
val cmp = comparison(midVal)
if (cmp < 0)
low = mid + 1
else if (cmp > 0)
high = mid - 1
else
return mid // key found
}
return -(low + 1) // key not found
}
inline fun <T, K : Comparable<K>> List<T>.fastBinarySearchBy(
key: K?,
fromIndex: Int = 0,
toIndex: Int = size,
crossinline selector: (T) -> K?
): Int = fastBinarySearch(fromIndex, toIndex) { compareValues(selector(it), key) }

View File

@ -25,14 +25,20 @@ import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.MenuPopupHelper
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.record
import androidx.core.graphics.withTranslation
import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.get
import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import io.legado.app.help.config.AppConfig
import io.legado.app.help.globalExecutor
import io.legado.app.lib.theme.TintHelper
import io.legado.app.utils.canvasrecorder.CanvasRecorder
import io.legado.app.utils.canvasrecorder.record
import splitties.systemservices.inputMethodManager
import java.lang.reflect.Field
@ -176,6 +182,16 @@ fun View.screenshot(picture: Picture) {
}
}
fun View.screenshot(canvasRecorder: CanvasRecorder) {
if (width > 0 && height > 0) {
canvasRecorder.record(width, height) {
withTranslation(-scrollX.toFloat(), -scrollY.toFloat()) {
draw(this)
}
}
}
}
fun View.setPaddingBottom(bottom: Int) {
setPadding(paddingLeft, paddingTop, paddingRight, bottom)
}
@ -216,6 +232,23 @@ fun TextView.setHtml(html: String) {
}
}
fun AppCompatTextView.setTextAsync(charSequence: CharSequence) {
globalExecutor.execute {
val precomputedText = PrecomputedTextCompat.create(
charSequence, TextViewCompat.getTextMetricsParams(this),
)
post {
setPrecomputedText(precomputedText)
}
}
}
fun TextView.setTextIfNotEqual(charSequence: CharSequence?) {
if (text != charSequence) {
text = charSequence
}
}
@SuppressLint("RestrictedApi")
fun PopupMenu.show(x: Int, y: Int) {
kotlin.runCatching {

View File

@ -0,0 +1,36 @@
package io.legado.app.utils.canvasrecorder
import androidx.annotation.CallSuper
abstract class BaseCanvasRecorder : CanvasRecorder {
@JvmField
protected var isDirty = true
override fun invalidate() {
isDirty = true
}
@CallSuper
override fun recycle() {
isDirty = true
}
@CallSuper
override fun endRecording() {
isDirty = false
}
override fun isDirty(): Boolean {
return isDirty
}
override fun isLocked(): Boolean {
return false
}
override fun needRecord(): Boolean {
return isDirty() && !isLocked()
}
}

View File

@ -0,0 +1,27 @@
package io.legado.app.utils.canvasrecorder
import android.graphics.Canvas
interface CanvasRecorder {
val width: Int
val height: Int
fun beginRecording(width: Int, height: Int): Canvas
fun endRecording()
fun draw(canvas: Canvas)
fun invalidate()
fun recycle()
fun isDirty(): Boolean
fun isLocked(): Boolean
fun needRecord(): Boolean
}

View File

@ -0,0 +1,39 @@
package io.legado.app.utils.canvasrecorder
import android.graphics.Canvas
import android.graphics.Picture
class CanvasRecorderApi23Impl : BaseCanvasRecorder() {
private var picture: Picture? = null
override val width get() = picture?.width ?: -1
override val height get() = picture?.height ?: -1
private fun initPicture() {
if (picture == null) {
picture = Picture()
}
}
override fun beginRecording(width: Int, height: Int): Canvas {
initPicture()
return picture!!.beginRecording(width, height)
}
override fun endRecording() {
picture!!.endRecording()
super.endRecording()
}
override fun draw(canvas: Canvas) {
if (picture == null) return
canvas.drawPicture(picture!!)
}
override fun recycle() {
super.recycle()
picture = null
}
}

View File

@ -0,0 +1,64 @@
package io.legado.app.utils.canvasrecorder
import android.graphics.Canvas
import android.graphics.Picture
import android.graphics.RenderNode
import android.os.Build
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.Q)
class CanvasRecorderApi29Impl : BaseCanvasRecorder() {
private var renderNode: RenderNode? = null
private var picture: Picture? = null
override val width get() = renderNode?.width ?: -1
override val height get() = renderNode?.height ?: -1
private fun init() {
if (renderNode == null) {
renderNode = RenderNode("CanvasRecorder")
}
if (picture == null) {
picture = Picture()
}
}
private fun flushRenderNode() {
val rc = renderNode!!.beginRecording()
rc.drawPicture(picture!!)
renderNode!!.endRecording()
}
override fun beginRecording(width: Int, height: Int): Canvas {
init()
renderNode!!.setPosition(0, 0, width, height)
return picture!!.beginRecording(width, height)
}
override fun endRecording() {
picture!!.endRecording()
flushRenderNode()
super.endRecording()
}
override fun draw(canvas: Canvas) {
if (renderNode == null || picture == null) return
if (canvas.isHardwareAccelerated) {
if (!renderNode!!.hasDisplayList()) {
flushRenderNode()
}
canvas.drawRenderNode(renderNode!!)
} else {
canvas.drawPicture(picture!!)
}
}
override fun recycle() {
super.recycle()
renderNode?.discardDisplayList()
renderNode = null
picture = null
}
}

View File

@ -0,0 +1,35 @@
package io.legado.app.utils.canvasrecorder
import android.graphics.Canvas
import androidx.core.graphics.withSave
inline fun CanvasRecorder.recordIfNeeded(
width: Int,
height: Int,
block: Canvas.() -> Unit
): Boolean {
if (!needRecord()) return false
record(width, height, block)
return true
}
inline fun CanvasRecorder.record(width: Int, height: Int, block: Canvas.() -> Unit) {
val canvas = beginRecording(width, height)
try {
canvas.withSave {
block()
}
} finally {
endRecording()
}
}
inline fun CanvasRecorder.recordIfNeededThenDraw(
canvas: Canvas,
width: Int,
height: Int,
block: Canvas.() -> Unit
) {
recordIfNeeded(width, height, block)
draw(canvas)
}

View File

@ -0,0 +1,23 @@
package io.legado.app.utils.canvasrecorder
import android.os.Build
object CanvasRecorderFactory {
private val atLeastApi23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
private val atLeastApi29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
fun create(locked: Boolean = false): CanvasRecorder {
val impl = when {
atLeastApi29 -> CanvasRecorderApi29Impl()
atLeastApi23 -> CanvasRecorderApi23Impl()
else -> CanvasRecorderImpl()
}
return if (locked) {
CanvasRecorderLocked(impl)
} else {
impl
}
}
}

View File

@ -0,0 +1,59 @@
package io.legado.app.utils.canvasrecorder
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
class CanvasRecorderImpl : BaseCanvasRecorder() {
var bitmap: Bitmap? = null
var canvas: Canvas? = null
override val width get() = bitmap?.width ?: -1
override val height get() = bitmap?.height ?: -1
private fun init(width: Int, height: Int) {
if (bitmap == null) {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
}
if (canvas == null) {
canvas = Canvas(bitmap!!)
}
if (bitmap!!.width != width || bitmap!!.height != height) {
if (canReconfigure(width, height)) {
bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888)
} else {
bitmap!!.recycle()
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
}
canvas!!.setBitmap(bitmap!!)
}
}
private fun canReconfigure(width: Int, height: Int): Boolean {
return bitmap!!.allocationByteCount >= width * height * 4
}
override fun beginRecording(width: Int, height: Int): Canvas {
init(width, height)
bitmap!!.eraseColor(Color.TRANSPARENT)
return canvas!!
}
override fun endRecording() {
bitmap!!.prepareToDraw()
super.endRecording()
}
override fun draw(canvas: Canvas) {
if (bitmap == null) return
canvas.drawBitmap(bitmap!!, 0f, 0f, null)
}
override fun recycle() {
super.recycle()
bitmap?.recycle()
bitmap = null
}
}

View File

@ -0,0 +1,52 @@
package io.legado.app.utils.canvasrecorder
import android.graphics.Canvas
import java.util.concurrent.locks.ReentrantLock
class CanvasRecorderLocked(private val delegate: CanvasRecorder) :
CanvasRecorder by delegate {
var lock: ReentrantLock? = ReentrantLock()
private fun initLock() {
if (lock == null) {
synchronized(this) {
if (lock == null) {
lock = ReentrantLock()
}
}
}
}
override fun beginRecording(width: Int, height: Int): Canvas {
initLock()
lock!!.lock()
return delegate.beginRecording(width, height)
}
override fun endRecording() {
delegate.endRecording()
lock!!.unlock()
}
override fun draw(canvas: Canvas) {
if (lock == null) return
if (!lock!!.tryLock()) return
try {
delegate.draw(canvas)
} finally {
lock!!.unlock()
}
}
override fun isLocked(): Boolean {
if (lock == null) return false
return lock!!.isLocked
}
override fun recycle() {
delegate.recycle()
lock = null
}
}

View File

@ -136,13 +136,6 @@
app:iconSpaceReserved="false"
app: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
android:defaultValue="false"
android:key="noAnimScrollPage"