增加阅读底部按钮自定义功能

This commit is contained in:
HapeLee
2025-08-29 02:11:29 +08:00
parent b3e9040089
commit ed21dca146
12 changed files with 501 additions and 184 deletions
@@ -80,6 +80,7 @@ import io.legato.kazusa.ui.book.read.config.ReadAloudDialog
import io.legato.kazusa.ui.book.read.config.ReadStyleDialog
import io.legato.kazusa.ui.book.read.config.TipConfigDialog.Companion.TIP_COLOR
import io.legato.kazusa.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR
import io.legato.kazusa.ui.book.read.config.ToolButtonConfigDialog
import io.legato.kazusa.ui.book.read.page.ContentTextView
import io.legato.kazusa.ui.book.read.page.ReadView
import io.legato.kazusa.ui.book.read.page.entities.PageDirection
@@ -151,6 +152,7 @@ class ReadBookActivity : BaseReadBookActivity(),
ChangeChapterSourceDialog.CallBack,
ReadBook.CallBack,
AutoReadDialog.CallBack,
ToolButtonConfigDialog.CallBack,
TxtTocRuleDialog.CallBack,
ColorPickerDialogListener,
LayoutProgressListener {
@@ -533,6 +535,11 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadBook.loadContent(false)
}
R.id.menu_tool_button -> {
binding.readMenu.runMenuOut()
showDialogFragment(ToolButtonConfigDialog())
}
// R.id.menu_enable_review -> {
// AppConfig.enableReview = !AppConfig.enableReview
// item.isChecked = AppConfig.enableReview
@@ -1485,6 +1492,10 @@ class ReadBookActivity : BaseReadBookActivity(),
binding.readView.autoPager.resume()
}
override fun refresh() {
recreate()
}
override fun onLayoutPageCompleted(index: Int, page: TextPage) {
upSeekBarThrottle.invoke()
binding.readView.onLayoutPageCompleted(index, page)
@@ -2,6 +2,8 @@ package io.legato.kazusa.ui.book.read
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
@@ -13,6 +15,8 @@ import android.widget.SeekBar
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.google.android.material.button.MaterialButton
import com.google.android.material.button.MaterialButtonGroup
import com.google.android.material.slider.Slider
import io.legato.kazusa.R
import io.legato.kazusa.constant.PreferKey
@@ -41,7 +45,6 @@ import io.legato.kazusa.utils.startActivity
import io.legato.kazusa.utils.themeColor
import io.legato.kazusa.utils.visible
import splitties.views.onClick
import splitties.views.onLongClick
/**
* 阅读界面菜单
@@ -189,6 +192,10 @@ class ReadMenu @JvmOverloads constructor(
//// tvChapterName.setTextColor(textColor)
//// tvChapterUrl.setTextColor(textColor)
// }
val allButtons = getUserButtons()
renderButtons(binding.bottomView, allButtons)
val brightnessBackground = GradientDrawable()
brightnessBackground.cornerRadius = 5F.dpToPx()
//brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f))
@@ -231,7 +238,12 @@ class ReadMenu @JvmOverloads constructor(
/**
* 确保视图不被导航栏遮挡
*/
binding.bottomView.applyNavigationBarPadding()
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.bottomMenu.applyNavigationBarPadding()
} else {
binding.bottomView.applyNavigationBarPadding()
}
}
fun reset() {
@@ -459,20 +471,6 @@ class ReadMenu @JvmOverloads constructor(
}
})
//搜索
fabSearch.setOnClickListener {
runMenuOut {
callBack.openSearchActivity(null)
}
}
//自动翻页
fabAutoPage.setOnClickListener {
runMenuOut {
callBack.autoPage()
}
}
//替换
//fabReplaceRule.setOnClickListener { callBack.openReplaceRule() }
@@ -488,32 +486,130 @@ class ReadMenu @JvmOverloads constructor(
//下一章
tvNext.setOnClickListener { ReadBook.moveToNextChapter(true) }
//目录
ivCatalog.setOnClickListener {
runMenuOut {
callBack.openChapterList()
}
fun renderButtons(group: MaterialButtonGroup, buttons: List<ToolButton>) {
group.removeAllViews()
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
buttons.forEach { btn ->
val button = MaterialButton(
group.context,
null,
com.google.android.material.R.attr.materialIconButtonFilledTonalStyle
).apply {
id = btn.id.hashCode()
setIconResource(btn.iconRes)
contentDescription = btn.description
tooltipText = btn.description
setOnClickListener { btn.onClick() }
btn.onLongClick?.let { longAction ->
setOnLongClickListener {
longAction()
true
}
}
}
group.addView(button)
}
} else {
buttons.forEach { btn ->
val button = MaterialButton(
group.context,
null,
com.google.android.material.R.attr.materialIconButtonOutlinedStyle
).apply {
id = btn.id.hashCode()
setIconResource(btn.iconRes)
contentDescription = btn.description
tooltipText = btn.description
strokeWidth = 0
val primaryColor = context.themeColor(androidx.appcompat.R.attr.colorPrimary)
iconTint = ColorStateList.valueOf(primaryColor)
setOnClickListener { btn.onClick() }
btn.onLongClick?.let { longAction ->
setOnLongClickListener {
longAction()
true
}
}
}
group.addView(button)
}
}
}
private fun getAllButtons(): List<ToolButton> {
return listOf(
ToolButton(
id = "search",
iconRes = R.drawable.ic_search,
description = context.getString(R.string.search_content),
onClick = { callBack.openSearchActivity(null) }
),
ToolButton(
id = "auto_page",
iconRes = R.drawable.ic_auto_page,
description = context.getString(R.string.auto_next_page),
onClick = { callBack.autoPage() }
),
ToolButton(
id = "catalog",
iconRes = R.drawable.ic_toc,
description = context.getString(R.string.chapter_list),
onClick = { callBack.openChapterList() }
),
ToolButton(
id = "read_aloud",
iconRes = R.drawable.ic_read_aloud,
description = context.getString(R.string.read_aloud),
onClick = { callBack.onClickReadAloud() },
onLongClick = { callBack.showReadAloudDialog() }
),
ToolButton(
id = "setting",
iconRes = R.drawable.ic_settings,
description = context.getString(R.string.setting),
onClick = { callBack.showReadStyle() }
),
ToolButton(
id = "addBookmark",
iconRes = R.drawable.ic_bookmark,
description = context.getString(R.string.bookmark),
onClick = { callBack.addBookmark() }
)
)
}
private fun getUserButtons(): List<ToolButton> {
val prefs by lazy {
context.getSharedPreferences("tool_button_config", Context.MODE_PRIVATE)
}
val allButtons = getAllButtons().associateBy { it.id }
val str = prefs.getString("tool_buttons", null)
val savedList = str?.split(";")?.mapNotNull {
val parts = it.split(",")
if (parts.size == 2) parts[0] to parts[1].toBoolean() else null
} ?: emptyList()
val result = mutableListOf<ToolButton>()
savedList.forEach { (id, enabled) ->
if (enabled) allButtons[id]?.let { result.add(it) }
}
getAllButtons().forEach { btn ->
if (savedList.none { it.first == btn.id }) {
result.add(btn)
}
}
//朗读
ivReadAloud.setOnClickListener {
runMenuOut {
callBack.onClickReadAloud()
}
}
ivReadAloud.onLongClick {
runMenuOut {
callBack.showReadAloudDialog()
}
}
//设置
ivSetting.setOnClickListener {
runMenuOut {
callBack.showReadStyle()
}
}
return result
}
private fun initAnimation() {
@@ -522,8 +618,16 @@ class ReadMenu @JvmOverloads constructor(
}
fun upBookView() {
binding.titleBar.title = " "
binding.tvBookName.text = ReadBook.book?.name
val bookName = ReadBook.book?.name ?: ""
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.titleBar.title = bookName
binding.tvBookName.gone()
} else {
binding.titleBar.title = " "
binding.tvBookName.text = bookName
}
ReadBook.curTextChapter?.let {
binding.tvChapterName.text = it.title
if (!ReadBook.isLocalBook) {
@@ -598,16 +702,18 @@ class ReadMenu @JvmOverloads constructor(
}
fun setAutoPage(autoPage: Boolean) = binding.run {
if (autoPage) {
fabAutoPage.setIconResource(R.drawable.ic_auto_page_stop)
fabAutoPage.contentDescription = context.getString(R.string.auto_next_page_stop)
} else {
fabAutoPage.setIconResource(R.drawable.ic_auto_page)
fabAutoPage.contentDescription = context.getString(R.string.auto_next_page)
fabAutoPage?.let { fab ->
if (autoPage) {
fab.setIconResource(R.drawable.ic_auto_page_stop)
fab.contentDescription = context.getString(R.string.auto_next_page_stop)
} else {
fab.setIconResource(R.drawable.ic_auto_page)
fab.contentDescription = context.getString(R.string.auto_next_page)
}
}
//fabAutoPage.setColorFilter(textColor)
}
private fun upBrightnessVwPos() {
if (AppConfig.brightnessVwPos) {
binding.root.modifyBegin()
@@ -630,6 +736,7 @@ class ReadMenu @JvmOverloads constructor(
fun openSourceEditActivity()
fun openBookInfoActivity()
fun showReadStyle()
fun addBookmark()
fun showReadAloudDialog()
fun upSystemUiVisibility()
fun onClickReadAloud()
@@ -642,4 +749,11 @@ class ReadMenu @JvmOverloads constructor(
fun onMenuHide()
}
}
data class ToolButton(
val id: String, // 唯一标识
val iconRes: Int, // 图标资源
val description: String, // contentDescription / tooltipText
val onClick: () -> Unit, // 点击事件
val onLongClick: (() -> Unit)? = null // 可选长按
)
}
@@ -24,20 +24,6 @@ class AutoReadDialog : BaseBottomSheetDialogFragment(R.layout.dialog_auto_read)
private val binding by viewBinding(DialogAutoReadBinding::bind)
private val callBack: CallBack? get() = activity as? CallBack
override fun onStart() {
super.onStart()
// dialog?.window?.run {
// clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
// setBackgroundDrawableResource(R.color.background)
// decorView.setPadding(0, 0, 0, 0)
// val attr = attributes
// attr.dimAmount = 0.0f
// attr.gravity = Gravity.BOTTOM
// attributes = attr
// setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
// }
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
(activity as ReadBookActivity).bottomDialog--
@@ -49,20 +35,6 @@ class AutoReadDialog : BaseBottomSheetDialogFragment(R.layout.dialog_auto_read)
dismiss()
return
}
//val bg = requireContext().bottomBackground
//val isLight = ColorUtils.isColorLight(bg)
//val textColor = requireContext().getPrimaryTextColor(isLight)
//root.setBackgroundColor(bg)
// tvReadSpeedTitle.setTextColor(textColor)
// tvReadSpeed.setTextColor(textColor)
// ivCatalog.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
// tvCatalog.setTextColor(textColor)
// ivMainMenu.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
// tvMainMenu.setTextColor(textColor)
// ivAutoPageStop.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
// tvAutoPageStop.setTextColor(textColor)
// ivSetting.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
// tvSetting.setTextColor(textColor)
initOnChange()
initData()
initEvent()
@@ -31,14 +31,6 @@ import io.legato.kazusa.utils.showDialogFragment
class ReadAloudConfigDialog : BasePrefDialogFragment() {
private val readAloudPreferTag = "readAloudPreferTag"
override fun onStart() {
super.onStart()
dialog?.window?.run {
//setBackgroundDrawableResource(R.color.transparent)
//setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -0,0 +1,200 @@
package io.legato.kazusa.ui.book.read.config
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseBottomSheetDialogFragment
import io.legato.kazusa.databinding.DialogToolButtonConfigBinding
import io.legato.kazusa.databinding.ItemToolButtonBinding
import io.legato.kazusa.ui.book.read.ReadBookActivity
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
class ToolButtonConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tool_button_config) {
private val binding by viewBinding(DialogToolButtonConfigBinding::bind)
private val prefs by lazy {
requireContext().getSharedPreferences(
"tool_button_config",
Context.MODE_PRIVATE
)
}
private val callBack: CallBack? get() = activity as? CallBack
private lateinit var adapter: ToolButtonAdapter
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run {
val configList = loadButtonConfig().toMutableList()
adapter = ToolButtonAdapter(configList)
recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
val touchHelper = ItemTouchHelper(TouchHelperCallback(adapter))
touchHelper.attachToRecyclerView(recyclerView)
btnSave.setOnClickListener {
saveButtonConfig(adapter.items)
callBack?.refresh()
dismiss()
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
(activity as ReadBookActivity).bottomDialog--
}
data class ConfigEntry(val id: String, var enabled: Boolean)
inner class ToolButtonAdapter(val items: MutableList<ConfigEntry>) :
RecyclerView.Adapter<ToolButtonAdapter.VH>() {
inner class VH(val binding: ItemToolButtonBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val binding = ItemToolButtonBinding.inflate(layoutInflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int) {
val item = items[position]
val (iconRes, name) = getButtonInfo(item.id)
holder.binding.tvName.text = name
holder.binding.ivIcon.setIconResource(iconRes)
holder.binding.ivIcon.isEnabled = item.enabled
holder.binding.btnDisable.icon = ContextCompat.getDrawable(
holder.itemView.context,
if (item.enabled) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
)
holder.binding.btnDisable.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos == RecyclerView.NO_POSITION) return@setOnClickListener
item.enabled = !item.enabled
holder.binding.btnDisable.icon = ContextCompat.getDrawable(
holder.itemView.context,
if (item.enabled) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
)
if (!item.enabled) {
val removed = items.removeAt(pos)
items.add(removed)
notifyItemMoved(pos, items.size - 1)
} else {
notifyItemChanged(pos)
}
holder.binding.ivIcon.isEnabled = item.enabled
}
}
override fun getItemCount() = items.size
fun swap(from: Int, to: Int) {
if (from == RecyclerView.NO_POSITION || to == RecyclerView.NO_POSITION) return
val fromItem = items[from]
val toItem = items[to]
if (!fromItem.enabled || !toItem.enabled) return
items.add(to, items.removeAt(from))
notifyItemMoved(from, to)
}
}
private fun getAllButtonIds(): List<String> {
return listOf("search", "auto_page", "catalog", "read_aloud", "setting", "addBookmark")
}
private fun getButtonInfo(id: String): Pair<Int, String> {
return when (id) {
"search" -> R.drawable.ic_search to getString(R.string.search_content)
"auto_page" -> R.drawable.ic_auto_page to getString(R.string.auto_next_page)
"catalog" -> R.drawable.ic_toc to getString(R.string.chapter_list)
"read_aloud" -> R.drawable.ic_read_aloud to getString(R.string.read_aloud)
"setting" -> R.drawable.ic_settings to getString(R.string.setting)
"addBookmark" -> R.drawable.ic_bookmark to getString(R.string.bookmark)
else -> R.drawable.ic_help to id
}
}
private fun loadButtonConfig(): List<ConfigEntry> {
val str = prefs.getString("tool_buttons", null)
return if (str.isNullOrBlank()) {
getAllButtonIds().map { ConfigEntry(it, true) }
} else {
val saved = str.split(";").mapNotNull {
val parts = it.split(",")
if (parts.size == 2) ConfigEntry(parts[0], parts[1].toBoolean()) else null
}.toMutableList()
val allIds = getAllButtonIds()
for (id in allIds) {
if (saved.none { it.id == id }) {
saved.add(ConfigEntry(id, true))
}
}
saved
}
}
private fun saveButtonConfig(list: List<ConfigEntry>) {
val str = list.joinToString(";") { "${it.id},${it.enabled}" }
prefs.edit { putString("tool_buttons", str) }
}
class TouchHelperCallback(
private val adapter: ToolButtonAdapter
) : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val pos = viewHolder.bindingAdapterPosition
if (pos == RecyclerView.NO_POSITION) return 0
val item = adapter.items[pos]
return if (!item.enabled) {
0
} else {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
makeMovementFlags(dragFlags, 0)
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPos = viewHolder.bindingAdapterPosition
val toPos = target.bindingAdapterPosition
if (fromPos == RecyclerView.NO_POSITION || toPos == RecyclerView.NO_POSITION) return false
adapter.swap(fromPos, toPos)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun isLongPressDragEnabled() = true
}
interface CallBack {
fun refresh()
}
}