* [修复] 透明主题阅读详情页FAB变黑和书架标签右侧半透明框问题 (@Piktowo) (#665)

Issue #659, #661

* [修复] 透明主题控件点击出现实色框问题,补全 surfaceContainer 系列颜色定义及主题映射 (@Piktowo) (#665) 

Issue #659

* 修复 TOC 自动定位竞态导致章节选取错误,修复书架共享元素返回动画位置错误 (@Piktowo) (#665) 

Issue #664, #647

* [修复] 阅读页背景Bitmap提前recycle导致的崩溃 (@Piktowo) (#665)
This commit is contained in:
Piktowo authored and GitHub committed 2026-03-05 01:23:11 +08:00
1 parent 8956f1d682
commit 2363e95702
19 files changed
+62 -25

No files matched your search

@@ -106,7 +106,7 @@ object ReadBookConfig {
shareConfig = c ?: configList.getOrNull(5) ?: Config() shareConfig = c ?: configList.getOrNull(5) ?: Config()
} }
fun upBg(width: Int, height: Int) { fun upBg(width: Int, height: Int): Drawable? {
val drawable = durConfig.curBgDrawable(width, height) val drawable = durConfig.curBgDrawable(width, height)
if (drawable is BitmapDrawable && drawable.bitmap != null) { if (drawable is BitmapDrawable && drawable.bitmap != null) {
bgMeanColor = drawable.bitmap.getMeanColor() bgMeanColor = drawable.bitmap.getMeanColor()
@@ -115,7 +115,9 @@ object ReadBookConfig {
} }
val tmp = bg val tmp = bg
bg = drawable bg = drawable
(tmp as? BitmapDrawable)?.bitmap?.recycle() // 返回旧 Drawable,由调用方在视图更新完成后再 recycle,
// 避免视图仍引用旧 BitmapDrawable 时 bitmap 已被 recycle 导致崩溃
return tmp
} }
fun save() { fun save() {
@@ -660,7 +660,7 @@ class BookInfoActivity :
duration = 400L duration = 400L
addUpdateListener { animation -> addUpdateListener { animation ->
val color = animation.animatedValue as Int val color = animation.animatedValue as Int
binding.btnRead.setBackgroundColor(color) binding.btnRead.backgroundTintList = ColorStateList.valueOf(color)
} }
} }
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.RectF import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.os.Build import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
@@ -616,10 +617,13 @@ class ReadView(context: Context, attrs: AttributeSet) :
* 更新背景 * 更新背景
*/ */
fun upBg() { fun upBg() {
ReadBookConfig.upBg(width, height) val oldBg = ReadBookConfig.upBg(width, height)
curPage.upBg() curPage.upBg()
prevPage.upBg() prevPage.upBg()
nextPage.upBg() nextPage.upBg()
// 所有视图背景更新完成后再 recycle 旧 bitmap,
// 防止视图仍持有旧 BitmapDrawable 引用时 bitmap 已被 recycle 导致崩溃
(oldBg as? BitmapDrawable)?.bitmap?.recycle()
} }
/** /**
@@ -110,6 +110,8 @@ import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -229,13 +231,22 @@ fun TocScreen(
var hasAutoScrolled by rememberSaveable { mutableStateOf(false) } var hasAutoScrolled by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(state.items) { LaunchedEffect(Unit) {
if (!hasAutoScrolled && state.items.isNotEmpty()) { if (!hasAutoScrolled) {
val targetIndex = state.items.indexOfFirst { it.isDur } // 等待列表包含当前阅读章节后自动定位,避免在无 isDur 项时提前退出
// 使用 animateScrollToItem 以便用户看到滚动动画,避免误操作
val items = snapshotFlow { state.items }
.filter { list -> list.any { it.isDur } }
.first()
val targetIndex = items.indexOfFirst { it.isDur }
if (targetIndex != -1) { if (targetIndex != -1) {
listState.scrollToItem( // 等待视口尺寸测量完成,确保 offset 计算正确
snapshotFlow { listState.layoutInfo.viewportEndOffset }
.filter { it > 0 }
.first()
listState.animateScrollToItem(
index = targetIndex, index = targetIndex,
scrollOffset = -offset scrollOffset = -(listState.layoutInfo.viewportEndOffset / 4)
) )
hasAutoScrolled = true hasAutoScrolled = true
} }
@@ -10,8 +10,10 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.graphics.ColorUtils
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.R import io.legado.app.R
@@ -22,6 +24,7 @@ import io.legado.app.databinding.FragmentBookshelf1Binding
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.book.group.GroupEditDialog import io.legado.app.ui.book.group.GroupEditDialog
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.themeColor import io.legado.app.utils.themeColor
@@ -49,6 +52,7 @@ class BookshelfFragment1() : BaseBookshelfFragment(R.layout.fragment_bookshelf1)
override val groupId: Long get() = selectedGroup?.groupId ?: 0 override val groupId: Long get() = selectedGroup?.groupId ?: 0
private lateinit var adapter: TabFragmentPageAdapter private lateinit var adapter: TabFragmentPageAdapter
private var tabFadeListener: AppBarLayout.OnOffsetChangedListener? = null
override val books: List<Book> override val books: List<Book>
get() { get() {
@@ -87,17 +91,20 @@ class BookshelfFragment1() : BaseBookshelfFragment(R.layout.fragment_bookshelf1)
} }
private fun updateTabFadeEffect() { private fun updateTabFadeEffect() {
binding.titleBar.addOnOffsetChangedListener({ appBarLayout, verticalOffset -> tabFadeListener?.let { binding.titleBar.removeOnOffsetChangedListener(it) }
if (-verticalOffset >= appBarLayout.totalScrollRange) { val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
val drawable = binding.tabRightFade.background as? GradientDrawable val drawable = binding.tabRightFade.background as? GradientDrawable
drawable?.colors = intArrayOf(Color.TRANSPARENT, val opacity = if (ThemeConfig.enableBlur) ThemeConfig.containerOpacity / 100f else 1f
requireContext().themeColor(com.google.android.material.R.attr.colorSurfaceContainer)) val baseColor = if (-verticalOffset >= appBarLayout.totalScrollRange) {
requireContext().themeColor(com.google.android.material.R.attr.colorSurfaceContainer)
} else { } else {
val drawable = binding.tabRightFade.background as? GradientDrawable requireContext().themeColor(com.google.android.material.R.attr.colorSurface)
drawable?.colors = intArrayOf(Color.TRANSPARENT,
requireContext().themeColor(com.google.android.material.R.attr.colorSurface))
} }
}) val newAlpha = (Color.alpha(baseColor) * opacity).toInt()
drawable?.colors = intArrayOf(Color.TRANSPARENT, ColorUtils.setAlphaComponent(baseColor, newAlpha))
}
tabFadeListener = listener
binding.titleBar.addOnOffsetChangedListener(listener)
} }
private fun showTabSelectionMenu() { private fun showTabSelectionMenu() {
@@ -27,6 +27,7 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
payloads: MutableList<Any> payloads: MutableList<Any>
) = binding.run { ) = binding.run {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
cdCover.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
upRefresh(binding, item) upRefresh(binding, item)
@@ -28,6 +28,7 @@ class BooksAdapterGridCompact(context: Context, private val callBack: CallBack)
payloads: MutableList<Any> payloads: MutableList<Any>
) = binding.run { ) = binding.run {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
cvContent.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
upRefresh(binding, item) upRefresh(binding, item)
@@ -28,6 +28,7 @@ class BooksAdapterGridCover(context: Context, private val callBack: CallBack) :
payloads: MutableList<Any> payloads: MutableList<Any>
) = binding.run { ) = binding.run {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
cvContent.transitionName = "book_${item.bookUrl}"
tvName.gone() tvName.gone()
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
upRefresh(binding, item) upRefresh(binding, item)
@@ -35,6 +35,7 @@ class BooksAdapterList(
payloads: MutableList<Any> payloads: MutableList<Any>
) = binding.run { ) = binding.run {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
cdCover.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
tvAuthor.text = item.author tvAuthor.text = item.author
tvRead.text = item.durChapterTitle tvRead.text = item.durChapterTitle
@@ -36,6 +36,7 @@ class BooksAdapterListCompact(
payloads: MutableList<Any> payloads: MutableList<Any>
) = binding.run { ) = binding.run {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
cdCover.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
tvAuthor.text = tvAuthor.text =
context.getString( context.getString(
@@ -64,6 +64,7 @@ class BooksAdapterGrid(context: Context, callBack: CallBack) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Book) = binding.run { fun onBind(item: Book) = binding.run {
cvContent.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
upRefresh(this, item) upRefresh(this, item)
@@ -71,6 +71,7 @@ class BooksAdapterGridCompact(context: Context, callBack: CallBack) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Book) = binding.run { fun onBind(item: Book) = binding.run {
cvContent.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
upRefresh(this, item) upRefresh(this, item)
@@ -71,6 +71,7 @@ class BooksAdapterGridCover(context: Context, callBack: CallBack) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Book) = binding.run { fun onBind(item: Book) = binding.run {
cvContent.transitionName = "book_${item.bookUrl}"
tvName.gone() tvName.gone()
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
upRefresh(this, item) upRefresh(this, item)
@@ -60,6 +60,7 @@ class BooksAdapterList(context: Context, callBack: CallBack) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Book) = binding.run { fun onBind(item: Book) = binding.run {
cdCover.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
tvAuthor.text = item.author tvAuthor.text = item.author
tvRead.text = item.durChapterTitle tvRead.text = item.durChapterTitle
@@ -60,6 +60,7 @@ class BooksAdapterListCompact(context: Context, callBack: CallBack) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Book) = binding.run { fun onBind(item: Book) = binding.run {
cdCover.transitionName = "book_${item.bookUrl}"
tvName.text = item.name tvName.text = item.name
tvAuthor.text = item.author tvAuthor.text = item.author
tvLast.text = item.latestChapterTitle tvLast.text = item.latestChapterTitle
@@ -17,7 +17,7 @@ internal object TransparentColorScheme : BaseColorScheme() {
secondaryContainer = Color(0xB0FFFFFF), secondaryContainer = Color(0xB0FFFFFF),
onSecondaryContainer = Color(0xFF000000), onSecondaryContainer = Color(0xFF000000),
tertiary = Color(0xFF000000), tertiary = Color(0xFF000000),
onTertiary = Color(0xFF000000), onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xB0FFFFFF), tertiaryContainer = Color(0xB0FFFFFF),
onTertiaryContainer = Color(0xFF000000), onTertiaryContainer = Color(0xFF000000),
error = Color(0xFFBA1A1A), error = Color(0xFFBA1A1A),
@@ -51,7 +51,7 @@ internal object TransparentColorScheme : BaseColorScheme() {
surfaceDim = Color(0x1AFFFFFF), surfaceDim = Color(0x1AFFFFFF),
surfaceBright = Color(0x33FFFFFF), surfaceBright = Color(0x33FFFFFF),
surfaceContainerLowest = Color(0x0DFFFFFF), surfaceContainerLowest = Color(0x0DFFFFFF),
surfaceContainerLow = Color(0x8FFFFFFF), surfaceContainerLow = Color(0x1AFFFFFF),
surfaceContainer = Color(0x33FFFFFF), surfaceContainer = Color(0x33FFFFFF),
surfaceContainerHigh = Color(0x4DFFFFFF), surfaceContainerHigh = Color(0x4DFFFFFF),
surfaceContainerHighest = Color(0x66FFFFFF), surfaceContainerHighest = Color(0x66FFFFFF),
@@ -67,7 +67,7 @@ internal object TransparentColorScheme : BaseColorScheme() {
secondaryContainer = Color(0xA0FFFFFF), secondaryContainer = Color(0xA0FFFFFF),
onSecondaryContainer = Color(0xFF000000), onSecondaryContainer = Color(0xFF000000),
tertiary = Color(0x80FFFFFF), tertiary = Color(0x80FFFFFF),
onTertiary = Color(0xFF000000), onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0x40FFFFFF), tertiaryContainer = Color(0x40FFFFFF),
onTertiaryContainer = Color(0xFF000000), onTertiaryContainer = Color(0xFF000000),
error = Color(0xFFCF6679), error = Color(0xFFCF6679),
@@ -101,7 +101,7 @@ internal object TransparentColorScheme : BaseColorScheme() {
surfaceDim = Color(0x1AFFFFFF), surfaceDim = Color(0x1AFFFFFF),
surfaceBright = Color(0x33FFFFFF), surfaceBright = Color(0x33FFFFFF),
surfaceContainerLowest = Color(0x0DFFFFFF), surfaceContainerLowest = Color(0x0DFFFFFF),
surfaceContainerLow = Color(0x8F000000), surfaceContainerLow = Color(0x1AFFFFFF),
surfaceContainer = Color(0x33FFFFFF), surfaceContainer = Color(0x33FFFFFF),
surfaceContainerHigh = Color(0x4DFFFFFF), surfaceContainerHigh = Color(0x4DFFFFFF),
surfaceContainerHighest = Color(0x66FFFFFF), surfaceContainerHighest = Color(0x66FFFFFF),
+1 -1
View File
@@ -524,7 +524,7 @@
<color name="transparent_theme_surfaceDim">#1AFFFFFF</color> <color name="transparent_theme_surfaceDim">#1AFFFFFF</color>
<color name="transparent_theme_surfaceBright">#33FFFFFF</color> <color name="transparent_theme_surfaceBright">#33FFFFFF</color>
<color name="transparent_theme_surfaceContainerLowest">#0DFFFFFF</color> <color name="transparent_theme_surfaceContainerLowest">#0DFFFFFF</color>
<color name="transparent_theme_surfaceContainerLow">#1AFFFFFF</color>
<color name="transparent_theme_surfaceContainer">#33FFFFFF</color> <color name="transparent_theme_surfaceContainer">#33FFFFFF</color>
<color name="transparent_theme_surfaceContainerHigh">#4DFFFFFF</color> <color name="transparent_theme_surfaceContainerHigh">#4DFFFFFF</color>
<color name="transparent_theme_surfaceContainerHighest">#66FFFFFF</color> <color name="transparent_theme_surfaceContainerHighest">#66FFFFFF</color>
+2 -2
View File
@@ -498,7 +498,7 @@
<color name="transparent_theme_secondaryContainer">#B0FFFFFF</color> <color name="transparent_theme_secondaryContainer">#B0FFFFFF</color>
<color name="transparent_theme_onSecondaryContainer">#FF000000</color> <color name="transparent_theme_onSecondaryContainer">#FF000000</color>
<color name="transparent_theme_tertiary">#000000</color> <color name="transparent_theme_tertiary">#000000</color>
<color name="transparent_theme_onTertiary">#FF000000</color> <color name="transparent_theme_onTertiary">#FFFFFFFF</color>
<color name="transparent_theme_tertiaryContainer">#B0FFFFFF</color> <color name="transparent_theme_tertiaryContainer">#B0FFFFFF</color>
<color name="transparent_theme_onTertiaryContainer">#FF000000</color> <color name="transparent_theme_onTertiaryContainer">#FF000000</color>
<color name="transparent_theme_error">#BA1A1A</color> <color name="transparent_theme_error">#BA1A1A</color>
@@ -532,7 +532,7 @@
<color name="transparent_theme_surfaceDim">#1AFFFFFF</color> <color name="transparent_theme_surfaceDim">#1AFFFFFF</color>
<color name="transparent_theme_surfaceBright">#33FFFFFF</color> <color name="transparent_theme_surfaceBright">#33FFFFFF</color>
<color name="transparent_theme_surfaceContainerLowest">#0DFFFFFF</color> <color name="transparent_theme_surfaceContainerLowest">#0DFFFFFF</color>
<color name="transparent_theme_surfaceContainerLow">#1AFFFFFF</color>
<color name="transparent_theme_surfaceContainer">#33FFFFFF</color> <color name="transparent_theme_surfaceContainer">#33FFFFFF</color>
<color name="transparent_theme_surfaceContainerHigh">#4DFFFFFF</color> <color name="transparent_theme_surfaceContainerHigh">#4DFFFFFF</color>
<color name="transparent_theme_surfaceContainerHighest">#66FFFFFF</color> <color name="transparent_theme_surfaceContainerHighest">#66FFFFFF</color>
+3
View File
@@ -607,7 +607,10 @@
<item name="colorSurfaceBright">@color/transparent_theme_surfaceBright</item> <item name="colorSurfaceBright">@color/transparent_theme_surfaceBright</item>
<item name="colorSurfaceContainerLowest">@color/transparent_theme_surfaceContainerLowest <item name="colorSurfaceContainerLowest">@color/transparent_theme_surfaceContainerLowest
</item> </item>
<item name="colorSurfaceContainerLow">@color/transparent_theme_surfaceContainerLow</item>
<item name="colorSurfaceContainer">@color/transparent_theme_surfaceContainer</item> <item name="colorSurfaceContainer">@color/transparent_theme_surfaceContainer</item>
<item name="colorSurfaceContainerHigh">@color/transparent_theme_surfaceContainerHigh</item>
<item name="colorSurfaceContainerHighest">@color/transparent_theme_surfaceContainerHighest</item>
</style> </style>
<style name="Theme.Base.Elink" parent="Base.AppTheme"> <style name="Theme.Base.Elink" parent="Base.AppTheme">