Procházet zdrojové kódy

增加书籍详情界面与有声书界面的根据封面生成主题色功能

HapeLee před 1 rokem
rodič
revize
f0b2d6cf21

+ 209 - 18
app/src/main/java/io/legato/kazusa/ui/book/audio/AudioPlayActivity.kt

@@ -1,8 +1,15 @@
 package io.legato.kazusa.ui.book.audio
 
+import android.animation.ValueAnimator
 import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
 import android.graphics.RenderEffect
 import android.graphics.Shader
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.TransitionDrawable
 import android.icu.text.SimpleDateFormat
 import android.os.Build
 import android.os.Bundle
@@ -11,10 +18,15 @@ import android.view.Menu
 import android.view.MenuItem
 import androidx.activity.viewModels
 import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.graphics.scale
 import androidx.core.view.isVisible
 import androidx.lifecycle.lifecycleScope
 import androidx.transition.TransitionManager
+import com.google.android.material.color.DynamicColors
+import com.google.android.material.color.DynamicColorsOptions
+import com.google.android.material.color.MaterialColors
 import com.google.android.material.slider.Slider
+import com.google.android.material.transition.platform.MaterialContainerTransform
 import io.legato.kazusa.R
 import io.legato.kazusa.base.VMBaseActivity
 import io.legato.kazusa.constant.BookType
@@ -50,7 +62,10 @@ import io.legato.kazusa.utils.startActivityForBook
 import io.legato.kazusa.utils.startAnimation
 import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
 import io.legato.kazusa.utils.visible
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import splitties.views.onLongClick
@@ -70,6 +85,13 @@ class AudioPlayActivity :
     private var adjustProgress = false
     private var playMode = AudioPlay.PlayMode.LIST_END_STOP
     private var playSpeed = 1f
+    private var primaryFinalColor: Int = 0
+    private var surfaceFinalColor: Int = 0
+    private var secondaryFinalColor: Int = 0
+    private var onSurfaceFinalColor: Int = 0
+    private var secondaryContainerFinalColor: Int = 0
+    private var currentJob: Job? = null
+    private var wrappedContext: Context? = null
 
     private val progressTimeFormat by lazy {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -95,7 +117,27 @@ class AudioPlayActivity :
         }
 
     override fun onCreate(savedInstanceState: Bundle?) {
+        val transform = MaterialContainerTransform().apply {
+            addTarget(binding.root)
+            scrimColor = Color.TRANSPARENT
+        }
+        window.sharedElementEnterTransition = transform
+        window.sharedElementReturnTransition = transform
         super.onCreate(savedInstanceState)
+        binding.root.transitionName = intent.getStringExtra("transitionName")
+        primaryFinalColor =
+            MaterialColors.getColor(this, androidx.appcompat.R.attr.colorPrimary, -1)
+        surfaceFinalColor =
+            MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, -1)
+        secondaryFinalColor =
+            MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondary, -1)
+        onSurfaceFinalColor =
+            MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, -1)
+        secondaryContainerFinalColor = MaterialColors.getColor(
+            this,
+            com.google.android.material.R.attr.colorSecondaryContainer,
+            -1
+        )
         binding.titleBar.setBackgroundResource(R.color.transparent)
         binding.titleBar.title = ""
         AudioPlay.register(this)
@@ -318,25 +360,29 @@ class AudioPlayActivity :
     }
 
     private fun upCover(path: String?) {
-        BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) {
-            if (!AppConfig.isEInkMode) {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-                    BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl)
-                        .into(binding.ivBg)
-                    val blurEffect =
-                        RenderEffect.createBlurEffect(120f, 120f, Shader.TileMode.CLAMP)
-                    binding.ivBg.setRenderEffect(blurEffect)
-                } else {
-                    // 低版本直接加载模糊图
-                    BookCover.loadBlur(
-                        this,
-                        path,
-                        sourceOrigin = AudioPlay.bookSource?.bookSourceUrl
-                    )
-                        .into(binding.ivBg)
+        BookCover.load(
+            this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl,
+            onLoadFinish = {
+                binding.ivCover.post {
+                    val drawable = binding.ivCover.drawable
+                    if (drawable != null) {
+                        addColorScheme(drawable)
+                    }
                 }
+            }).into(binding.ivCover)
+        if (!AppConfig.isEInkMode) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl)
+                    .into(binding.ivBg)
+                val blurEffect = RenderEffect.createBlurEffect(120f, 120f, Shader.TileMode.CLAMP)
+                binding.ivBg.setRenderEffect(blurEffect)
+            } else {
+                // 低版本直接加载模糊图
+                BookCover.loadBlur(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl)
+                    .into(binding.ivBg)
             }
-        }.into(binding.ivCover)
+        }
+        addColorScheme(binding.ivCover.drawable)
     }
 
     private fun playButton() {
@@ -347,6 +393,145 @@ class AudioPlayActivity :
         }
     }
 
+    private fun addColorScheme(drawable: Drawable?) {
+        currentJob?.cancel()
+        currentJob = CoroutineScope(Dispatchers.Default).launch {
+            val bitmap = when (drawable) {
+                is BitmapDrawable -> drawable.bitmap
+                is TransitionDrawable -> (drawable.getDrawable(1) as? BitmapDrawable)?.bitmap
+                else -> null
+            } ?: return@launch
+
+            val colorAccuracy = true
+            val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
+            val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
+            val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
+
+            val options = DynamicColorsOptions.Builder()
+                .setContentBasedSource(scaledBitmap)
+                .build()
+
+            wrappedContext = DynamicColors.wrapContextIfAvailable(
+                this@AudioPlayActivity,
+                options
+            ).apply {
+                resources.configuration.uiMode =
+                    this@AudioPlayActivity.resources.configuration.uiMode
+            }
+
+            withContext(Dispatchers.Main) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                    applyColorScheme()
+                }
+            }
+        }
+    }
+
+    private suspend fun applyColorScheme() {
+        val ctx = wrappedContext ?: this
+
+        val colorPrimary = MaterialColors.getColor(ctx, androidx.appcompat.R.attr.colorPrimary, -1)
+        val colorSecondary =
+            MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondary, -1)
+        val colorOnSurface =
+            MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSurface, -1)
+        val colorSurface =
+            MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSurface, -1)
+        val colorSurfaceContainer = MaterialColors.getColor(
+            ctx,
+            com.google.android.material.R.attr.colorSurfaceContainer,
+            -1
+        )
+        val colorSecondaryContainer = MaterialColors.getColor(
+            ctx,
+            com.google.android.material.R.attr.colorSecondaryContainer,
+            -1
+        )
+
+        val primaryTransition = ValueAnimator.ofArgb(primaryFinalColor, colorPrimary).apply {
+            duration = 400L
+            addUpdateListener { animation ->
+                val color = animation.animatedValue as Int
+                binding.settingSlider.thumbTintList = ColorStateList.valueOf(color)
+                binding.settingSlider.trackActiveTintList = ColorStateList.valueOf(color)
+                binding.playerProgress.thumbTintList = ColorStateList.valueOf(color)
+                binding.playerProgress.trackActiveTintList = ColorStateList.valueOf(color)
+            }
+        }
+
+        val secondaryContainerTransition =
+            ValueAnimator.ofArgb(secondaryContainerFinalColor, colorSecondaryContainer).apply {
+                duration = 400L
+                addUpdateListener { animation ->
+                    val color = animation.animatedValue as Int
+                    binding.fabPlayStop.backgroundTintList = ColorStateList.valueOf(color)
+                    binding.btnReset.backgroundTintList = ColorStateList.valueOf(color)
+                    binding.settingSlider.trackInactiveTintList = ColorStateList.valueOf(color)
+                    binding.playerProgress.trackInactiveTintList = ColorStateList.valueOf(color)
+                }
+            }
+
+        val surfaceTransition =
+            ValueAnimator.ofArgb(surfaceFinalColor, colorSurfaceContainer).apply {
+                duration = 400L
+                addUpdateListener { animation ->
+                    val color = animation.animatedValue as Int
+                    binding.vwBg2.setBackgroundColor(color)
+                    binding.cdTimer.setBackgroundColor(color)
+                    binding.cdSpeed.setBackgroundColor(color)
+                }
+            }
+
+        val textTransition = ValueAnimator.ofArgb(onSurfaceFinalColor, colorOnSurface).apply {
+            duration = 400L
+            addUpdateListener { animation ->
+                val color = animation.animatedValue as Int
+                binding.tvSpeed.setTextColor(color)
+                binding.tvTimer.setTextColor(color)
+                binding.tvTitle.setTextColor(color)
+                binding.tvDurTime.setTextColor(color)
+                binding.tvAllTime.setTextColor(color)
+                binding.tvSubTitle.setTextColor(color)
+            }
+        }
+
+        withContext(Dispatchers.Main) {
+            surfaceTransition.start()
+            textTransition.start()
+            primaryTransition.start()
+            secondaryContainerTransition.start()
+        }
+
+        surfaceFinalColor = colorSurface
+        secondaryFinalColor = colorSecondary
+        onSurfaceFinalColor = colorOnSurface
+        primaryFinalColor = colorPrimary
+        secondaryContainerFinalColor = colorSecondaryContainer
+
+        binding.progressLoading.setIndicatorColor(colorPrimary)
+
+        val states = arrayOf(
+            intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked),
+            intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked),
+            intArrayOf(-android.R.attr.state_enabled)
+        )
+
+        val colors = intArrayOf(
+            colorSurface,
+            colorOnSurface,
+            colorOnSurface.copy(alpha = 0.3f)
+        )
+
+        val stateList = ColorStateList(states, colors)
+        listOf(
+            binding.ivSkipNext, binding.ivSkipPrevious, binding.ivPlayMode, binding.ivTimer,
+            binding.ivChapter, binding.ivFastForward, binding.btnReset, binding.fabPlayStop
+        ).forEach { btn ->
+            btn.setTextColor(stateList)
+            btn.iconTint = stateList
+        }
+    }
+
     override val oldBook: Book?
         get() = AudioPlay.book
 
@@ -487,4 +672,10 @@ class AudioPlayActivity :
         }
     }
 
-}
+}
+
+private fun Int.copy(alpha: Float): Int {
+    val alpha = (alpha * 255).toInt()
+    return (this and 0x00FFFFFF) or (alpha shl 24)
+}
+

+ 181 - 4
app/src/main/java/io/legato/kazusa/ui/book/info/BookInfoActivity.kt

@@ -1,10 +1,17 @@
 package io.legato.kazusa.ui.book.info
 
+import android.animation.ValueAnimator
 import android.annotation.SuppressLint
+import android.content.Context
 import android.content.Intent
+import android.content.res.ColorStateList
 import android.graphics.Color
 import android.graphics.RenderEffect
 import android.graphics.Shader
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.TransitionDrawable
 import android.net.Uri
 import android.os.Build
 import android.os.Bundle
@@ -15,8 +22,12 @@ import android.widget.LinearLayout
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.viewModels
 import androidx.core.content.ContextCompat
+import androidx.core.graphics.scale
 import androidx.core.view.doOnPreDraw
 import androidx.lifecycle.lifecycleScope
+import com.google.android.material.color.DynamicColors
+import com.google.android.material.color.DynamicColorsOptions
+import com.google.android.material.color.MaterialColors
 import com.google.android.material.transition.platform.MaterialContainerTransform
 import io.legato.kazusa.R
 import io.legato.kazusa.base.VMBaseActivity
@@ -75,7 +86,10 @@ import io.legato.kazusa.utils.startActivity
 import io.legato.kazusa.utils.toastOnUi
 import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
 import io.legato.kazusa.utils.visible
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -144,6 +158,13 @@ class BookInfoActivity :
             viewModel.refreshBook(book)
         }
     }
+
+    private var surfaceFinalColor: Int = 0
+    private var secondaryFinalColor: Int = 0
+    private var onSurfaceFinalColor: Int = 0
+    private var secondaryContainerFinalColor: Int = 0
+    private var currentJob: Job? = null
+    private var wrappedContext: Context? = null
     private var chapterChanged = false
     private val waitDialog by lazy { WaitDialog(this) }
     private var editMenuItem: MenuItem? = null
@@ -156,15 +177,26 @@ class BookInfoActivity :
     override fun onCreate(savedInstanceState: Bundle?) {
         postponeEnterTransition()
         val transform = MaterialContainerTransform().apply {
-            addTarget(binding.ivCover)
+            addTarget(binding.cdCov)
             scrimColor = Color.TRANSPARENT
         }
         window.sharedElementEnterTransition = transform
         window.sharedElementReturnTransition = transform
         window.sharedElementsUseOverlay = false
         super.onCreate(savedInstanceState)
-        binding.ivCover.transitionName = intent.getStringExtra("transitionName")
-        binding.ivCover.doOnPreDraw {
+        surfaceFinalColor =
+            MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, -1)
+        secondaryFinalColor =
+            MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondary, -1)
+        onSurfaceFinalColor =
+            MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, -1)
+        secondaryContainerFinalColor = MaterialColors.getColor(
+            this,
+            com.google.android.material.R.attr.colorSecondaryContainer,
+            -1
+        )
+        binding.cdCov.transitionName = intent.getStringExtra("transitionName")
+        binding.cdCov.doOnPreDraw {
             startPostponedEnterTransition()
         }
         setSupportActionBar(binding.topBar)
@@ -342,6 +374,7 @@ class BookInfoActivity :
 
     private fun showBook(book: Book) = binding.run {
         showCover(book)
+        addColorScheme(binding.ivCover.drawable)
         tvName.text = book.name
         tvAuthor.text = getString(R.string.author_show, book.getRealAuthor())
         tvOrigin.text = getString(R.string.origin_show, book.originName)
@@ -377,7 +410,21 @@ class BookInfoActivity :
     }
 
     private fun showCover(book: Book) {
-        binding.ivCover.load(book.getDisplayCover(), book.name, book.author, false, book.origin) {
+        binding.ivCover.load(
+            book.getDisplayCover(),
+            book.name,
+            book.author,
+            false,
+            book.origin,
+            onLoadFinish = {
+                binding.ivCover.post {
+                    val drawable = binding.ivCover.drawable
+                    if (drawable != null) {
+                        addColorScheme(drawable)
+                    }
+                }
+            }
+        )
             if (!AppConfig.isEInkMode) {
                 //高版本使用RenderEffect
                 BookCover.load(this, book.getDisplayCover(), false, book.origin)
@@ -392,7 +439,137 @@ class BookInfoActivity :
                         .into(binding.bgBook)
                 }
             }
+        addColorScheme(binding.ivCover.drawable)
+
+    }
+
+    private fun addColorScheme(drawable: Drawable?) {
+        currentJob?.cancel()
+        currentJob = CoroutineScope(Dispatchers.Default).launch {
+            val bitmap = when (drawable) {
+                is BitmapDrawable -> drawable.bitmap
+                is TransitionDrawable -> (drawable.getDrawable(1) as? BitmapDrawable)?.bitmap
+                else -> null
+            } ?: return@launch
+
+            val colorAccuracy = true
+            val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
+            val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
+            val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
+
+            val options = DynamicColorsOptions.Builder()
+                .setContentBasedSource(scaledBitmap)
+                .build()
+
+            wrappedContext = DynamicColors.wrapContextIfAvailable(
+                this@BookInfoActivity,
+                options
+            ).apply {
+                resources.configuration.uiMode =
+                    this@BookInfoActivity.resources.configuration.uiMode
+            }
+
+            withContext(Dispatchers.Main) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                    applyColorScheme()
+                }
+            }
+        }
+    }
+
+    private suspend fun applyColorScheme() {
+        val ctx = wrappedContext ?: this
+
+        val colorPrimary = MaterialColors.getColor(ctx, androidx.appcompat.R.attr.colorPrimary, -1)
+        val colorSecondary =
+            MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondary, -1)
+        val colorTertiary =
+            MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorTertiary, -1)
+        val colorOnSurface =
+            MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSurface, -1)
+        val colorSurface =
+            MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSurface, -1)
+        val colorSurfaceContainer = MaterialColors.getColor(
+            ctx,
+            com.google.android.material.R.attr.colorSurfaceContainer,
+            -1
+        )
+        val colorSecondaryContainer = MaterialColors.getColor(
+            ctx,
+            com.google.android.material.R.attr.colorSecondaryContainer,
+            -1
+        )
+
+        val surfaceTransition = ValueAnimator.ofArgb(surfaceFinalColor, colorSurface).apply {
+            duration = 400L
+            addUpdateListener { animation ->
+                val color = animation.animatedValue as Int
+                binding.cdInfo?.setCardBackgroundColor(color)
+                binding.llInfo.setBackgroundColor(color)
+            }
+        }
+
+        val buttonTransition = ValueAnimator.ofArgb(secondaryFinalColor, colorSecondary).apply {
+            duration = 400L
+            addUpdateListener { animation ->
+                val color = animation.animatedValue as Int
+                val tint = ColorStateList.valueOf(color)
+                listOf(
+                    binding.btnShelf,
+                    binding.tvTocView,
+                    binding.tvChangeGroup,
+                    binding.btnChangeSource
+                ).forEach { btn ->
+                    btn.setTextColor(color)
+                    btn.iconTint = tint
+                }
+            }
         }
+
+        val backTransition =
+            ValueAnimator.ofArgb(secondaryContainerFinalColor, colorSecondaryContainer).apply {
+                duration = 400L
+                addUpdateListener { animation ->
+                    val color = animation.animatedValue as Int
+                    binding.bgBookMask.setBackgroundColor(color)
+                }
+            }
+
+        val textTransition = ValueAnimator.ofArgb(onSurfaceFinalColor, colorOnSurface).apply {
+            duration = 400L
+            addUpdateListener { animation ->
+                val color = animation.animatedValue as Int
+                binding.tvName.setTextColor(color)
+                binding.tvAuthor.setTextColor(color)
+                binding.tvOrigin.setTextColor(color)
+                binding.tvDetail.setTextColor(color)
+                binding.ivName.imageTintList = ColorStateList.valueOf(color)
+                binding.ivWeb.imageTintList = ColorStateList.valueOf(color)
+            }
+        }
+
+        withContext(Dispatchers.Main) {
+            surfaceTransition.start()
+            buttonTransition.start()
+            textTransition.start()
+            backTransition.start()
+        }
+
+        surfaceFinalColor = colorSurface
+        secondaryFinalColor = colorSecondary
+        onSurfaceFinalColor = colorOnSurface
+
+        (binding.llCover.background as? GradientDrawable)?.colors =
+            intArrayOf(Color.TRANSPARENT, colorSurface)
+
+        binding.collTopBar.setContentScrimColor(colorSurfaceContainer)
+        binding.lbKind.applyColorScheme(colorSurfaceContainer, colorOnSurface)
+        binding.tvChapter.setTextColor(colorPrimary)
+        binding.div.setTextColor(colorSecondary)
+        binding.tvChapterIndex.setTextColor(colorSecondary)
+        binding.btnRead.setBackgroundColor(colorTertiary)
+        binding.tvToc.setTextColor(colorOnSurface)
+        binding.tvLasted.setTextColor(colorOnSurface)
     }
 
     private fun upLoading(isLoading: Boolean, chapterList: List<BookChapter>? = null) {

+ 12 - 1
app/src/main/java/io/legato/kazusa/ui/widget/LabelsBar.kt

@@ -47,9 +47,20 @@ class LabelsBar @JvmOverloads constructor(
                 usedChips.add(it)
             }
         }
-        style(chip) // 在这里应用外部传入的样式
+        style(chip)
         chip.text = label
         addView(chip)
     }
 
+    fun applyColorScheme(colorPrimaryContainer: Int, colorOnPrimaryContainer: Int) {
+        usedChips.forEach { chip ->
+            chip.chipBackgroundColor = ColorStateList.valueOf(colorPrimaryContainer)
+            chip.setTextColor(colorOnPrimaryContainer)
+        }
+        unUsedChips.forEach { chip ->
+            chip.chipBackgroundColor = ColorStateList.valueOf(colorPrimaryContainer)
+            chip.setTextColor(colorOnPrimaryContainer)
+        }
+    }
+
 }

+ 218 - 213
app/src/main/res/layout-land/activity_book_info.xml

@@ -10,9 +10,16 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:contentDescription="@string/bg_image"
-        android:foreground="@drawable/bg_book_mask_land"
         android:scaleType="centerCrop" />
 
+    <ImageView
+        android:id="@+id/bg_book_mask"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:alpha="0.80"
+        android:background="@drawable/bg_gradient_mask"
+        android:contentDescription="@string/bg_image" />
+
     <com.google.android.material.appbar.AppBarLayout
         android:id="@+id/app_bar"
         android:layout_width="match_parent"
@@ -21,6 +28,7 @@
         android:fitsSystemWindows="true">
 
         <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/coll_top_bar"
             android:layout_width="match_parent"
             android:layout_height="120dp"
             style="?attr/collapsingToolbarLayoutLargeSize"
@@ -58,302 +66,301 @@
             android:scrollbars="vertical">
 
             <LinearLayout
+                android:id="@+id/ll_cover"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:baselineAligned="false"
                 android:orientation="horizontal">
 
-                <LinearLayout
+                <androidx.constraintlayout.widget.ConstraintLayout
                     android:layout_width="0dp"
                     android:layout_height="match_parent"
-                    android:layout_weight="1"
+                    android:layout_weight="3"
                     android:orientation="vertical"
                     android:paddingHorizontal="32dp"
                     tools:ignore="RtlSymmetry">
 
-                    <androidx.cardview.widget.CardView
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
+                    <com.google.android.material.card.MaterialCardView
+                        android:id="@+id/cd_cov"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
                         android:layout_marginTop="16dp"
-                        android:layout_marginStart="32dp"
-                        app:cardCornerRadius="16dp">
+                        app:cardCornerRadius="8dp"
+                        app:layout_constraintDimensionRatio="5:7"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintHorizontal_bias="0.3"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent"
+                        app:layout_constraintWidth_percent="0.5"
+                        app:strokeWidth="0dp">
 
                         <io.legato.kazusa.ui.widget.image.CoverImageView
                             android:id="@+id/iv_cover"
-                            android:layout_width="150dp"
-                            android:layout_height="210dp"
+                            android:layout_width="match_parent"
+                            android:layout_height="match_parent"
                             android:contentDescription="@string/img_cover"
                             android:scaleType="centerCrop"
                             android:src="@drawable/image_cover_default" />
 
-                    </androidx.cardview.widget.CardView>
+                    </com.google.android.material.card.MaterialCardView>
 
-                    <LinearLayout
-                        android:layout_width="match_parent"
-                        android:layout_height="match_parent"
-                        android:layout_gravity="center"
-                        android:orientation="vertical"
-                        android:paddingTop="16dp"
-                        android:paddingHorizontal="30dp">
+                    <TextView
+                        android:id="@+id/tv_name"
+                        style="@style/TextAppearance.Material3.ActionBar.Title"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="32dp"
+                        android:text="@string/book_name"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="@id/cd_cov"
+                        app:layout_constraintTop_toBottomOf="@id/cd_cov" />
+
+                    <ImageView
+                        android:id="@+id/iv_name"
+                        android:layout_width="18sp"
+                        android:layout_height="18sp"
+                        android:layout_marginTop="8dp"
+                        android:contentDescription="@string/author"
+                        android:src="@drawable/ic_author"
+                        app:layout_constraintStart_toStartOf="@id/cd_cov"
+                        app:layout_constraintTop_toBottomOf="@id/tv_name"
+                        tools:ignore="RtlHardcoded,RtlSymmetry" />
 
-                        <TextView
-                            android:id="@+id/tv_name"
-                            android:layout_width="wrap_content"
-                            android:layout_height="wrap_content"
-                            android:text="@string/book_name"
-                            style="@style/TextAppearance.Material3.ActionBar.Title"
-                            tools:ignore="RtlHardcoded" />
+                    <TextView
+                        android:id="@+id/tv_author"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="4dp"
+                        android:ellipsize="end"
+                        android:singleLine="true"
+                        android:text="@string/author"
+                        android:textSize="14sp"
+                        app:layout_constraintBottom_toBottomOf="@id/iv_name"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toEndOf="@id/iv_name"
+                        app:layout_constraintTop_toTopOf="@id/iv_name"
+                        tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" />
 
-                        <LinearLayout
-                            android:layout_width="match_parent"
-                            android:layout_height="wrap_content"
-                            android:gravity="center"
-                            android:orientation="horizontal"
-                            android:paddingTop="6dp"
-                            android:paddingBottom="2dp"
-                            tools:ignore="UseCompoundDrawables">
-
-                            <ImageView
-                                android:layout_width="18sp"
-                                android:layout_height="18sp"
-                                android:contentDescription="@string/author"
-                                android:src="@drawable/ic_author"
-                                tools:ignore="RtlHardcoded,RtlSymmetry" />
-
-                            <TextView
-                                android:id="@+id/tv_author"
-                                android:layout_width="0dp"
-                                android:layout_height="wrap_content"
-                                android:layout_weight="1"
-                                android:layout_marginStart="2dp"
-                                android:ellipsize="end"
-                                android:singleLine="true"
-                                android:text="@string/author"
-                                android:textSize="14sp"
-                                tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" />
 
-                        </LinearLayout>
+                    <ImageView
+                        android:id="@+id/iv_web"
+                        android:layout_width="18sp"
+                        android:layout_height="18sp"
+                        android:layout_marginTop="8dp"
+                        android:contentDescription="@string/origin_format"
+                        android:src="@drawable/ic_web_outline"
+                        app:layout_constraintStart_toStartOf="@id/cd_cov"
+                        app:layout_constraintTop_toBottomOf="@id/iv_name"
+                        tools:ignore="RtlHardcoded,RtlSymmetry" />
 
-                        <LinearLayout
-                            android:layout_width="match_parent"
-                            android:layout_height="wrap_content"
-                            android:gravity="center_vertical"
-                            android:orientation="horizontal"
-                            tools:ignore="UseCompoundDrawables">
-
-                            <ImageView
-                                android:id="@+id/iv_web"
-                                android:layout_width="18sp"
-                                android:layout_height="18sp"
-                                android:contentDescription="@string/origin_format"
-                                android:src="@drawable/ic_web_outline"
-                                tools:ignore="RtlHardcoded,RtlSymmetry" />
-
-                            <TextView
-                                android:id="@+id/tv_origin"
-                                android:layout_width="0dp"
-                                android:layout_height="wrap_content"
-                                android:layout_weight="1"
-                                android:layout_marginStart="2dp"
-                                android:ellipsize="end"
-                                android:paddingRight="6dp"
-                                android:singleLine="true"
-                                android:textSize="14sp"
-                                tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
-                                tools:text="@string/origin_format" />
-
-                        </LinearLayout>
-                    </LinearLayout>
-                </LinearLayout>
-
-                <LinearLayout
-                    android:id="@+id/ll_info"
+                    <TextView
+                        android:id="@+id/tv_origin"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="4dp"
+                        android:layout_weight="1"
+                        android:ellipsize="end"
+                        android:singleLine="true"
+                        android:textSize="14sp"
+                        app:layout_constraintBottom_toBottomOf="@id/iv_web"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toEndOf="@id/iv_web"
+                        app:layout_constraintTop_toTopOf="@id/iv_web"
+                        tools:text="@string/origin_format" />
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <com.google.android.material.card.MaterialCardView
+                    android:id="@+id/cd_info"
                     android:layout_width="0dp"
                     android:layout_height="match_parent"
-                    android:layout_weight="2"
-                    android:background="@drawable/bg_gradient_mask_land"
-                    android:orientation="vertical"
-                    android:padding="8dp">
+                    android:layout_marginHorizontal="32dp"
+                    android:layout_marginVertical="16dp"
+                    android:layout_weight="5"
+                    app:strokeWidth="0dp">
 
-                    <HorizontalScrollView
+                    <androidx.constraintlayout.widget.ConstraintLayout
+                        android:id="@+id/ll_info"
                         android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:layout_marginStart="8dp"
-                        android:scrollbars="none">
+                        android:layout_height="match_parent"
+                        android:layout_margin="8dp"
+                        android:background="@drawable/bg_gradient_mask_land"
+                        android:padding="8dp">
 
-                        <io.legato.kazusa.ui.widget.LabelsBar
-                            android:id="@+id/lb_kind"
-                            android:layout_width="wrap_content"
+                        <HorizontalScrollView
+                            android:id="@+id/scroll_labels"
+                            android:layout_width="0dp"
                             android:layout_height="wrap_content"
-                            android:visibility="visible" />
+                            android:scrollbars="none"
+                            app:layout_constraintEnd_toEndOf="parent"
+                            app:layout_constraintStart_toStartOf="parent"
+                            app:layout_constraintTop_toTopOf="parent">
 
-                    </HorizontalScrollView>
+                            <io.legato.kazusa.ui.widget.LabelsBar
+                                android:id="@+id/lb_kind"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:visibility="visible" />
 
-                    <LinearLayout
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:orientation="vertical"
-                        android:paddingHorizontal="8dp"
-                        android:paddingBottom="8dp">
+                        </HorizontalScrollView>
 
-                        <LinearLayout
+                        <com.google.android.material.button.MaterialButtonGroup
+                            android:id="@+id/cl_buttons"
                             android:layout_width="match_parent"
                             android:layout_height="wrap_content"
-                            android:orientation="horizontal">
-
+                            app:layout_constraintStart_toStartOf="parent"
+                            app:layout_constraintTop_toBottomOf="@id/scroll_labels">
                             <com.google.android.material.button.MaterialButton
                                 android:id="@+id/btn_shelf"
-                                style="@style/Widget.Material3Expressive.Button.TonalButton"
-                                android:backgroundTint="?attr/colorSurfaceContainer"
+                                style="@style/Widget.Material3Expressive.Button.TextButton"
                                 app:cornerRadius="16dp"
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
-                                android:layout_marginEnd="6dp"
                                 android:padding="8dp"
                                 android:text="@string/remove_from_bookshelf"
                                 android:textSize="13sp"
                                 app:icon="@drawable/ic_clear_all"
                                 app:iconGravity="top"
                                 app:iconPadding="4dp"
-                                app:iconSize="20dp" />
+                                app:iconSize="20dp"
+                                app:layout_constraintStart_toStartOf="parent"
+                                app:layout_constraintTop_toTopOf="parent" />
 
-                            <!-- 查看目录按钮 -->
                             <com.google.android.material.button.MaterialButton
                                 android:id="@+id/tv_toc_view"
-                                style="@style/Widget.Material3Expressive.Button.TonalButton"
-                                android:backgroundTint="?attr/colorSurfaceContainer"
+                                style="@style/Widget.Material3Expressive.Button.TextButton"
                                 app:cornerRadius="16dp"
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
-                                android:layout_marginHorizontal="8dp"
+                                android:layout_marginStart="8dp"
                                 android:padding="8dp"
                                 android:text="@string/view_toc"
                                 android:textSize="13sp"
                                 app:icon="@drawable/ic_toc"
                                 app:iconGravity="top"
                                 app:iconPadding="4dp"
-                                app:iconSize="20dp" />
+                                app:iconSize="20dp"
+                                app:layout_constraintStart_toEndOf="@id/btn_shelf"
+                                app:layout_constraintTop_toTopOf="@id/btn_shelf" />
 
-                            <!-- 更换分组按钮 -->
                             <com.google.android.material.button.MaterialButton
                                 android:id="@+id/tv_change_group"
-                                style="@style/Widget.Material3Expressive.Button.TonalButton"
-                                android:backgroundTint="?attr/colorSurfaceContainer"
+                                style="@style/Widget.Material3Expressive.Button.TextButton"
                                 app:cornerRadius="16dp"
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
-                                android:layout_marginHorizontal="8dp"
+                                android:layout_marginStart="8dp"
                                 android:padding="8dp"
                                 android:text="@string/change_group"
                                 android:textSize="13sp"
                                 app:icon="@drawable/ic_groups"
                                 app:iconGravity="top"
                                 app:iconPadding="4dp"
-                                app:iconSize="20dp" />
+                                app:iconSize="20dp"
+                                app:layout_constraintStart_toEndOf="@id/tv_toc_view"
+                                app:layout_constraintTop_toTopOf="@id/tv_toc_view" />
 
-                            <!-- 更换来源按钮(保持原样) -->
                             <com.google.android.material.button.MaterialButton
                                 android:id="@+id/btn_change_source"
-                                style="@style/Widget.Material3Expressive.Button.TonalButton"
-                                android:backgroundTint="?attr/colorSurfaceContainer"
+                                style="@style/Widget.Material3Expressive.Button.TextButton"
                                 app:cornerRadius="16dp"
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:layout_marginStart="8dp"
-                                android:text="@string/book_source"
                                 android:padding="8dp"
-                                app:icon="@drawable/ic_change_source"
+                                android:text="@string/book_source"
                                 android:textSize="13sp"
+                                app:icon="@drawable/ic_change_source"
                                 app:iconGravity="top"
                                 app:iconPadding="4dp"
                                 app:iconSize="20dp"
+                                app:layout_constraintStart_toEndOf="@id/tv_change_group"
+                                app:layout_constraintTop_toTopOf="@id/tv_change_group"
                                 tools:contentDescription="@string/change_origin" />
+                        </com.google.android.material.button.MaterialButtonGroup>
 
-                        </LinearLayout>
+                        <TextView
+                            android:id="@+id/tv_toc"
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="8dp"
+                            android:textSize="16sp"
+                            android:textStyle="bold"
+                            app:layout_constraintEnd_toEndOf="parent"
+                            app:layout_constraintStart_toStartOf="parent"
+                            app:layout_constraintTop_toBottomOf="@id/cl_buttons"
+                            tools:text="@string/toc_s" />
 
-                        <LinearLayout
-                            android:layout_width="match_parent"
+                        <TextView
+                            android:id="@+id/tv_lasted"
+                            android:layout_width="0dp"
                             android:layout_height="wrap_content"
-                            android:paddingVertical="16dp"
-                            android:paddingHorizontal="2dp"
-                            android:orientation="vertical">
+                            android:layout_marginTop="8dp"
+                            android:singleLine="true"
+                            android:textSize="14sp"
+                            app:layout_constraintEnd_toEndOf="parent"
+                            app:layout_constraintStart_toStartOf="parent"
+                            app:layout_constraintTop_toBottomOf="@id/tv_toc"
+                            tools:text="@string/read_dur_progress" />
 
-                            <TextView
-                                android:id="@+id/tv_toc"
-                                android:layout_width="match_parent"
-                                android:layout_height="wrap_content"
-                                android:paddingBottom="6dp"
-                                android:textSize="16sp"
-                                android:textStyle="bold"
-                                tools:text="@string/toc_s" />
 
-                            <TextView
-                                android:id="@+id/tv_lasted"
-                                android:layout_width="wrap_content"
-                                android:layout_height="wrap_content"
-                                android:paddingBottom="6dp"
-                                android:singleLine="true"
-                                android:textSize="14sp"
-                                tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
-                                tools:text="@string/read_dur_progress" />
-
-                            <LinearLayout
-                                android:layout_width="match_parent"
-                                android:layout_height="wrap_content"
-                                android:orientation="horizontal">
-
-                                <TextView
-                                    android:id="@+id/tv_chapter"
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="wrap_content"
-                                    android:textColor="?attr/colorPrimary"
-                                    android:singleLine="true"
-                                    android:paddingEnd="8dp"
-                                    android:textSize="14sp"
-                                    android:textStyle="bold"
-                                    tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
-                                    tools:text="@string/read_chapter_total" />
-
-                                <TextView
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="match_parent"
-                                    android:text="|"
-                                    android:textColor="?attr/colorSecondary"
-                                    android:layout_marginEnd="8dp"/>
-
-                                <TextView
-                                    android:id="@+id/tv_chapter_index"
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="wrap_content"
-                                    android:textColor="?attr/colorSecondary"
-                                    android:singleLine="true"
-                                    android:textSize="14sp"
-                                    tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
-                                    tools:text="@string/read_chapter_index"/>
-                            </LinearLayout>
-
-                        </LinearLayout>
-
-                    </LinearLayout>
+                        <TextView
+                            android:id="@+id/tv_chapter"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="8dp"
+                            android:singleLine="true"
+                            android:textColor="?attr/colorPrimary"
+                            android:textSize="14sp"
+                            android:textStyle="bold"
+                            app:layout_constraintStart_toStartOf="parent"
+                            app:layout_constraintTop_toBottomOf="@id/tv_lasted"
+                            tools:text="@string/read_chapter_total" />
 
-                    <TextView
-                        android:id="@+id/tv_detail"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:layout_marginTop="8dp"
-                        android:layout_marginHorizontal="8dp"
-                        android:clickable="true"
-                        android:focusable="true"
-                        android:minHeight="48dp"
-                        android:paddingBottom="80dp"
-                        android:lineSpacingExtra="4dp"
-                        android:text="@string/book_intro"
-                        android:textIsSelectable="true"
-                        android:textSize="14sp"
-                        android:visibility="visible"
-                        tools:ignore="RtlHardcoded,RtlSymmetry" />
+                        <TextView
+                            android:id="@+id/div"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_marginStart="8dp"
+                            android:text="|"
+                            android:textColor="?attr/colorSecondary"
+                            app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
+                            app:layout_constraintStart_toEndOf="@id/tv_chapter" />
+
+                        <TextView
+                            android:id="@+id/tv_chapter_index"
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_marginStart="8dp"
+                            android:singleLine="true"
+                            android:textColor="?attr/colorSecondary"
+                            android:textSize="14sp"
+                            app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
+                            app:layout_constraintEnd_toEndOf="parent"
+                            app:layout_constraintStart_toEndOf="@id/div"
+                            tools:text="@string/read_chapter_index" />
 
-                </LinearLayout>
+                        <TextView
+                            android:id="@+id/tv_detail"
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="8dp"
+                            android:clickable="true"
+                            android:focusable="true"
+                            android:lineSpacingExtra="4dp"
+                            android:minHeight="48dp"
+                            android:paddingBottom="80dp"
+                            android:text="@string/book_intro"
+                            android:textIsSelectable="true"
+                            android:textSize="14sp"
+                            android:visibility="visible"
+                            app:layout_constraintEnd_toEndOf="parent"
+                            app:layout_constraintStart_toStartOf="parent"
+                            app:layout_constraintTop_toBottomOf="@id/tv_chapter"
+                            tools:text="@string/book_intro" />
+
+                    </androidx.constraintlayout.widget.ConstraintLayout>
+                </com.google.android.material.card.MaterialCardView>
             </LinearLayout>
         </androidx.core.widget.NestedScrollView>
     </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -363,15 +370,13 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="bottom|end"
-        android:layout_marginEnd="40dp"
-        android:minWidth="0dp"
-        android:layout_marginBottom="24dp"
+        android:layout_margin="48dp"
+        android:contentDescription="@string/reading"
+        android:minWidth="56dp"
         android:text="@string/reading"
         android:textColor="?attr/colorOnPrimary"
-        android:contentDescription="@string/reading"
-        app:iconGravity="textStart"
+        app:backgroundTint="?attr/colorTertiary"
         app:icon="@drawable/ic_bottom_books"
-        app:backgroundTint="?attr/colorSecondary"
         app:iconTint="?attr/colorOnPrimary" />
 
 </androidx.coordinatorlayout.widget.CoordinatorLayout>

+ 1 - 8
app/src/main/res/layout/activity_audio_play.xml

@@ -14,16 +14,11 @@
         android:scaleType="centerCrop"
         tools:ignore="ContentDescription,ImageContrastCheck" />
 
-    <View
-        android:id="@+id/vw_bg"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
     <View
         android:id="@+id/vw_bg2"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:alpha="0.80"
+        android:alpha="0.70"
         android:background="?attr/colorSurfaceContainer" />
 
     <io.legato.kazusa.ui.widget.TitleBar
@@ -194,8 +189,6 @@
         android:paddingHorizontal="6dp"
         app:layout_constraintBottom_toTopOf="@id/ll_sub_menu">
 
-
-
         <View
             android:layout_width="0dp"
             android:layout_height="1dp"

+ 197 - 162
app/src/main/res/layout/activity_book_info.xml

@@ -10,9 +10,16 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:contentDescription="@string/bg_image"
-        android:foreground="@drawable/bg_book_mask"
         android:scaleType="centerCrop" />
 
+    <ImageView
+        android:id="@+id/bg_book_mask"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:alpha="0.6"
+        android:background="?attr/colorSecondaryContainer"
+        android:contentDescription="@string/bg_image" />
+
     <com.google.android.material.appbar.AppBarLayout
         android:id="@+id/app_bar"
         android:layout_width="match_parent"
@@ -21,6 +28,7 @@
         android:fitsSystemWindows="true">
 
         <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/coll_top_bar"
             android:layout_width="match_parent"
             android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
             style="?attr/collapsingToolbarLayoutMediumStyle"
@@ -56,32 +64,52 @@
             android:scrollbarStyle="outsideOverlay"
             android:scrollbars="vertical">
 
-            <LinearLayout
+            <androidx.constraintlayout.widget.ConstraintLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical">
 
-                <LinearLayout
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/ll_cover"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:background="@drawable/bg_gradient_mask"
                     android:paddingHorizontal="16dp"
-                    android:paddingVertical="8dp">
+                    android:paddingVertical="8dp"
+                    app:layout_constraintTop_toTopOf="parent">
+
+                    <com.google.android.material.card.MaterialCardView
+                        android:id="@+id/cd_cov"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        app:cardCornerRadius="8dp"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintDimensionRatio="5:7"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent"
+                        app:layout_constraintWidth_percent="0.32"
+                        app:strokeWidth="0dp">
 
                         <io.legato.kazusa.ui.widget.image.CoverImageView
                             android:id="@+id/iv_cover"
-                            android:layout_width="100dp"
-                            android:layout_height="140dp"
+                            android:layout_width="match_parent"
+                            android:layout_height="match_parent"
                             android:contentDescription="@string/img_cover"
                             android:scaleType="centerCrop"
                             android:src="@drawable/image_cover_default" />
 
+                    </com.google.android.material.card.MaterialCardView>
+
                     <LinearLayout
-                        android:layout_width="match_parent"
+                        android:layout_width="0dp"
                         android:layout_height="wrap_content"
                         android:layout_gravity="center"
                         android:orientation="vertical"
-                        android:padding="12dp">
+                        android:padding="12dp"
+                        app:layout_constraintBottom_toBottomOf="@id/cd_cov"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toEndOf="@id/cd_cov"
+                        app:layout_constraintTop_toTopOf="@id/cd_cov">
 
                         <TextView
                             android:id="@+id/tv_name"
@@ -102,6 +130,7 @@
                             tools:ignore="UseCompoundDrawables">
 
                             <ImageView
+                                android:id="@+id/iv_name"
                                 android:layout_width="18sp"
                                 android:layout_height="18sp"
                                 android:contentDescription="@string/author"
@@ -152,189 +181,195 @@
 
                         </LinearLayout>
                     </LinearLayout>
-                </LinearLayout>
 
-                <LinearLayout
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <androidx.constraintlayout.widget.ConstraintLayout
                     android:id="@+id/ll_info"
                     android:layout_width="match_parent"
-                    android:layout_height="match_parent"
+                    android:layout_height="0dp"
                     android:background="?attr/colorSurface"
-                    android:orientation="vertical">
+                    android:orientation="vertical"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintTop_toBottomOf="@id/ll_cover">
 
-                        <HorizontalScrollView
-                            android:layout_width="match_parent"
-                            android:layout_height="wrap_content"
-                            android:layout_marginStart="16dp"
-                            android:scrollbars="none">
+                    <HorizontalScrollView
+                        android:id="@+id/scroll_labels"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:paddingStart="16dp"
+                        android:scrollbars="none"
+                        app:layout_constraintTop_toTopOf="parent">
 
-                            <io.legato.kazusa.ui.widget.LabelsBar
-                                android:id="@+id/lb_kind"
-                                android:layout_width="wrap_content"
-                                android:layout_height="wrap_content"
-                                android:visibility="gone" />
+                        <io.legato.kazusa.ui.widget.LabelsBar
+                            android:id="@+id/lb_kind"
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:visibility="gone" />
 
-                        </HorizontalScrollView>
+                    </HorizontalScrollView>
 
                     <LinearLayout
+                        android:id="@+id/cl_buttons"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
-                        android:orientation="vertical"
-                        android:paddingBottom="8dp">
-
-                        <LinearLayout
-                            android:layout_width="match_parent"
+                        android:layout_marginHorizontal="8dp"
+                        android:gravity="center_vertical"
+                        android:orientation="horizontal"
+                        android:weightSum="4"
+                        app:layout_constraintTop_toBottomOf="@id/scroll_labels">
+
+                        <com.google.android.material.button.MaterialButton
+                            android:id="@+id/btn_shelf"
+                            style="@style/Widget.Material3Expressive.Button.TextButton"
+                            android:layout_width="0dp"
                             android:layout_height="wrap_content"
-                            android:orientation="horizontal"
-                            android:paddingHorizontal="8dp"
-                            android:weightSum="4"
-                            android:gravity="center_vertical">
-
-                            <com.google.android.material.button.MaterialButton
-                                android:id="@+id/btn_shelf"
-                                style="@style/Widget.Material3Expressive.Button.TextButton"
-                                android:layout_width="0dp"
-                                android:layout_height="wrap_content"
-                                android:layout_weight="1"
-                                android:padding="8dp"
-                                android:text="@string/remove_from_bookshelf"
-                                android:textSize="12sp"
-                                app:icon="@drawable/ic_clear_all"
-                                app:iconGravity="top"
-                                app:iconPadding="4dp"
-                                app:iconSize="20dp" />
-
-                            <!-- 查看目录按钮 -->
-                            <com.google.android.material.button.MaterialButton
-                                android:id="@+id/tv_toc_view"
-                                style="@style/Widget.Material3Expressive.Button.TextButton"
-                                android:layout_width="0dp"
-                                android:layout_height="wrap_content"
-                                android:layout_weight="1"
-                                android:layout_marginHorizontal="4dp"
-                                android:padding="8dp"
-                                android:text="@string/view_toc"
-                                android:textSize="12sp"
-                                app:icon="@drawable/ic_toc"
-                                app:iconGravity="top"
-                                app:iconPadding="4dp"
-                                app:iconSize="20dp" />
-
-                            <!-- 更换分组按钮 -->
-                            <com.google.android.material.button.MaterialButton
-                                android:id="@+id/tv_change_group"
-                                style="@style/Widget.Material3Expressive.Button.TextButton"
-                                android:layout_width="0dp"
-                                android:layout_height="wrap_content"
-                                android:layout_marginEnd="4dp"
-                                android:layout_weight="1"
-                                android:padding="8dp"
-                                android:text="@string/change_group"
-                                android:textSize="12sp"
-                                app:icon="@drawable/ic_groups"
-                                app:iconGravity="top"
-                                app:iconPadding="4dp"
-                                app:iconSize="20dp" />
-
-                            <!-- 更换来源按钮(保持原样) -->
-                            <com.google.android.material.button.MaterialButton
-                                android:id="@+id/btn_change_source"
-                                style="@style/Widget.Material3Expressive.Button.TextButton"
-                                android:layout_width="0dp"
-                                android:layout_height="wrap_content"
-                                android:layout_weight="1"
-                                android:text="@string/book_source"
-                                android:padding="8dp"
-                                app:icon="@drawable/ic_change_source"
-                                android:textSize="12sp"
-                                app:iconGravity="top"
-                                app:iconPadding="4dp"
-                                app:iconSize="20dp"
-                                tools:contentDescription="@string/change_origin" />
-                        </LinearLayout>
-
-                        <LinearLayout
-                            android:layout_width="match_parent"
+                            android:layout_weight="1"
+                            android:padding="8dp"
+                            android:text="@string/remove_from_bookshelf"
+                            android:textSize="12sp"
+                            app:icon="@drawable/ic_clear_all"
+                            app:iconGravity="top"
+                            app:iconPadding="4dp"
+                            app:iconSize="20dp" />
+
+                        <!-- 查看目录按钮 -->
+                        <com.google.android.material.button.MaterialButton
+                            android:id="@+id/tv_toc_view"
+                            style="@style/Widget.Material3Expressive.Button.TextButton"
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_marginHorizontal="4dp"
+                            android:layout_weight="1"
+                            android:padding="8dp"
+                            android:text="@string/view_toc"
+                            android:textSize="12sp"
+                            app:icon="@drawable/ic_toc"
+                            app:iconGravity="top"
+                            app:iconPadding="4dp"
+                            app:iconSize="20dp" />
+
+                        <!-- 更换分组按钮 -->
+                        <com.google.android.material.button.MaterialButton
+                            android:id="@+id/tv_change_group"
+                            style="@style/Widget.Material3Expressive.Button.TextButton"
+                            android:layout_width="0dp"
+                            android:layout_height="wrap_content"
+                            android:layout_marginEnd="4dp"
+                            android:layout_weight="1"
+                            android:padding="8dp"
+                            android:text="@string/change_group"
+                            android:textSize="12sp"
+                            app:icon="@drawable/ic_groups"
+                            app:iconGravity="top"
+                            app:iconPadding="4dp"
+                            app:iconSize="20dp" />
+
+                        <!-- 更换来源按钮(保持原样) -->
+                        <com.google.android.material.button.MaterialButton
+                            android:id="@+id/btn_change_source"
+                            style="@style/Widget.Material3Expressive.Button.TextButton"
+                            android:layout_width="0dp"
                             android:layout_height="wrap_content"
-                            android:paddingHorizontal="16dp"
-                            android:paddingVertical="8dp"
-                            android:orientation="vertical">
+                            android:layout_weight="1"
+                            android:padding="8dp"
+                            android:text="@string/book_source"
+                            android:textSize="12sp"
+                            app:icon="@drawable/ic_change_source"
+                            app:iconGravity="top"
+                            app:iconPadding="4dp"
+                            app:iconSize="20dp"
+                            tools:contentDescription="@string/change_origin" />
+                    </LinearLayout>
 
-                            <TextView
-                                android:id="@+id/tv_toc"
-                                android:layout_width="match_parent"
-                                android:layout_height="wrap_content"
-                                android:paddingBottom="6dp"
-                                android:textSize="16sp"
-                                android:textStyle="bold"
-                                tools:text="@string/toc_s" />
+                    <TextView
+                        android:id="@+id/tv_toc"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginHorizontal="16dp"
+                        android:layout_marginTop="8dp"
+                        android:textSize="16sp"
+                        android:textStyle="bold"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toBottomOf="@id/cl_buttons"
+                        tools:text="@string/toc_s" />
 
-                            <TextView
-                                android:id="@+id/tv_lasted"
-                                android:layout_width="wrap_content"
-                                android:layout_height="wrap_content"
-                                android:paddingBottom="6dp"
-                                android:textSize="14sp"
-                                tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
-                                tools:text="@string/read_dur_progress" />
+                    <TextView
+                        android:id="@+id/tv_lasted"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginHorizontal="16dp"
+                        android:layout_marginTop="8dp"
+                        android:singleLine="true"
+                        android:textSize="14sp"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toBottomOf="@id/tv_toc"
+                        tools:text="@string/read_dur_progress" />
 
-                            <LinearLayout
-                                android:layout_width="match_parent"
-                                android:layout_height="wrap_content"
-                                android:orientation="horizontal">
-
-                                <TextView
-                                    android:id="@+id/tv_chapter"
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="wrap_content"
-                                    android:textColor="?attr/colorPrimaryVariant"
-                                    android:singleLine="true"
-                                    android:paddingEnd="8dp"
-                                    android:textSize="14sp"
-                                    android:textStyle="bold"
-                                    tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
-                                    tools:text="@string/read_chapter_total" />
-
-                                <TextView
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="match_parent"
-                                    android:text="|"
-                                    android:textColor="?attr/colorSecondary"
-                                    android:layout_marginEnd="8dp"/>
-
-                                <TextView
-                                    android:id="@+id/tv_chapter_index"
-                                    android:layout_width="wrap_content"
-                                    android:layout_height="wrap_content"
-                                    android:textColor="?attr/colorSecondary"
-                                    android:singleLine="true"
-                                    android:textSize="14sp"
-                                    tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
-                                    tools:text="@string/read_chapter_index"/>
-                            </LinearLayout>
 
-                        </LinearLayout>
+                    <TextView
+                        android:id="@+id/tv_chapter"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="16dp"
+                        android:layout_marginTop="8dp"
+                        android:singleLine="true"
+                        android:textColor="?attr/colorPrimary"
+                        android:textSize="14sp"
+                        android:textStyle="bold"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toBottomOf="@id/tv_lasted"
+                        tools:text="@string/read_chapter_total" />
 
-                    </LinearLayout>
+                    <TextView
+                        android:id="@+id/div"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="8dp"
+                        android:text="|"
+                        android:textColor="?attr/colorSecondary"
+                        app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
+                        app:layout_constraintStart_toEndOf="@id/tv_chapter" />
 
                     <TextView
-                        android:id="@+id/tv_detail"
-                        android:layout_width="match_parent"
+                        android:id="@+id/tv_chapter_index"
+                        android:layout_width="0dp"
                         android:layout_height="wrap_content"
+                        android:layout_marginStart="8dp"
+                        android:layout_marginEnd="16dp"
+                        android:singleLine="true"
+                        android:textColor="?attr/colorSecondary"
+                        android:textSize="14sp"
+                        app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toEndOf="@id/div"
+                        tools:text="@string/read_chapter_index" />
+
+                    <TextView
+                        android:id="@+id/tv_detail"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="16dp"
                         android:layout_marginTop="8dp"
                         android:clickable="true"
                         android:focusable="true"
-                        android:minHeight="48dp"
-                        android:paddingHorizontal="16dp"
-                        android:paddingBottom="80dp"
                         android:lineSpacingExtra="4dp"
+                        android:minHeight="48dp"
+                        android:paddingBottom="120dp"
                         android:text="@string/book_intro"
                         android:textIsSelectable="true"
                         android:textSize="14sp"
                         android:visibility="visible"
-                        tools:ignore="RtlHardcoded,RtlSymmetry" />
-
-                </LinearLayout>
-            </LinearLayout>
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toBottomOf="@id/tv_chapter"
+                        tools:text="@string/book_intro" />
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
+            </androidx.constraintlayout.widget.ConstraintLayout>
         </androidx.core.widget.NestedScrollView>
     </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>