增加阅读漫画自定义点击区域功能

增加页脚配置预览
更新界面
This commit is contained in:
HapeLee
2025-07-22 02:24:52 +08:00
parent 47274f13d4
commit 82a2aa349e
13 changed files with 361 additions and 71 deletions
@@ -192,4 +192,15 @@ object PreferKey {
const val showBottomView = "showBottomView"
const val mangaScrollMode = "mangaScrollMode"
const val webtoonSidePaddingDp = "webtoonSidePaddingDp"
const val mangaClickActionTL = "mangaClickActionTopLeft"
const val mangaClickActionTC = "mangaClickActionTopCenter"
const val mangaClickActionTR = "mangaClickActionTopRight"
const val mangaClickActionML = "mangaClickActionMiddleLeft"
const val mangaClickActionMC = "mangaClickActionMiddleCenter"
const val mangaClickActionMR = "mangaClickActionMiddleRight"
const val mangaClickActionBL = "mangaClickActionBottomLeft"
const val mangaClickActionBC = "mangaClickActionBottomCenter"
const val mangaClickActionBR = "mangaClickActionBottomRight"
}
@@ -36,6 +36,18 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
var clickActionBL = appCtx.getPrefInt(PreferKey.clickActionBL, 2)
var clickActionBC = appCtx.getPrefInt(PreferKey.clickActionBC, 1)
var clickActionBR = appCtx.getPrefInt(PreferKey.clickActionBR, 1)
// -1无操作 1下一页 2上一页 0显示菜单
var mangaClickActionTL = appCtx.getPrefInt(PreferKey.mangaClickActionTL, -1)
var mangaClickActionTC = appCtx.getPrefInt(PreferKey.mangaClickActionTC, -1)
var mangaClickActionTR = appCtx.getPrefInt(PreferKey.mangaClickActionTR, 1)
var mangaClickActionML = appCtx.getPrefInt(PreferKey.mangaClickActionML, 2)
var mangaClickActionMC = appCtx.getPrefInt(PreferKey.mangaClickActionMC, 0)
var mangaClickActionMR = appCtx.getPrefInt(PreferKey.mangaClickActionMR, 1)
var mangaClickActionBL = appCtx.getPrefInt(PreferKey.mangaClickActionBL, 2)
var mangaClickActionBC = appCtx.getPrefInt(PreferKey.mangaClickActionBC, 1)
var mangaClickActionBR = appCtx.getPrefInt(PreferKey.mangaClickActionBR, 1)
var themeMode = appCtx.getPrefString(PreferKey.themeMode, "0")
var useDefaultCover = appCtx.getPrefBoolean(PreferKey.useDefaultCover, false)
var optimizeRender = CanvasRecorderFactory.isSupport
@@ -76,6 +88,33 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
PreferKey.clickActionBR -> clickActionBR =
appCtx.getPrefInt(PreferKey.clickActionBR, 1)
PreferKey.mangaClickActionTL -> mangaClickActionTL =
appCtx.getPrefInt(PreferKey.mangaClickActionTL, -1)
PreferKey.mangaClickActionTC -> mangaClickActionTC =
appCtx.getPrefInt(PreferKey.mangaClickActionTC, -1)
PreferKey.mangaClickActionTR -> mangaClickActionTR =
appCtx.getPrefInt(PreferKey.mangaClickActionTR, 1)
PreferKey.mangaClickActionML -> mangaClickActionML =
appCtx.getPrefInt(PreferKey.mangaClickActionML, 2)
PreferKey.mangaClickActionMC -> mangaClickActionMC =
appCtx.getPrefInt(PreferKey.mangaClickActionMC, 0)
PreferKey.mangaClickActionMR -> mangaClickActionMR =
appCtx.getPrefInt(PreferKey.mangaClickActionMR, 1)
PreferKey.mangaClickActionBL -> mangaClickActionBL =
appCtx.getPrefInt(PreferKey.mangaClickActionBL, 2)
PreferKey.mangaClickActionBC -> mangaClickActionBC =
appCtx.getPrefInt(PreferKey.mangaClickActionBC, 1)
PreferKey.mangaClickActionBR -> mangaClickActionBR =
appCtx.getPrefInt(PreferKey.mangaClickActionBR, 1)
PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh =
appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
@@ -619,6 +658,17 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
}
}
fun detectMangaClickArea() {
if (mangaClickActionTL * mangaClickActionTC * mangaClickActionTR
* mangaClickActionML * mangaClickActionMC * mangaClickActionMR
* mangaClickActionBL * mangaClickActionBC * mangaClickActionBR != 0
) {
appCtx.putPrefInt(PreferKey.mangaClickActionMC, 0)
appCtx.toastOnUi("当前没有配置菜单区域,自动恢复中间区域为菜单.")
}
}
//跳转到漫画界面不使用富文本模式
val showMangaUi: Boolean
get() = appCtx.getPrefBoolean(PreferKey.showMangaUi, true)
@@ -42,6 +42,7 @@ import io.legato.kazusa.model.ReadManga
import io.legato.kazusa.receiver.NetworkChangedListener
import io.legato.kazusa.ui.book.changesource.ChangeBookSourceDialog
import io.legato.kazusa.ui.book.info.BookInfoActivity
import io.legato.kazusa.ui.book.manga.config.MangaClickActionConfigDialog
import io.legato.kazusa.ui.book.manga.config.MangaColorFilterConfig
import io.legato.kazusa.ui.book.manga.config.MangaColorFilterDialog
import io.legato.kazusa.ui.book.manga.config.MangaFooterConfig
@@ -53,6 +54,7 @@ import io.legato.kazusa.ui.book.manga.entities.MangaPage
import io.legato.kazusa.ui.book.manga.recyclerview.MangaAdapter
import io.legato.kazusa.ui.book.manga.recyclerview.MangaLayoutManager
import io.legato.kazusa.ui.book.manga.recyclerview.ScrollTimer
import io.legato.kazusa.ui.book.manga.recyclerview.WebtoonFrame
import io.legato.kazusa.ui.book.read.MangaMenu
import io.legato.kazusa.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED
import io.legato.kazusa.ui.book.toc.TocActivityResult
@@ -121,6 +123,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
}
private var enableAutoScrollPage = false
private var enableAutoScroll = false
private var enableScroll = false
private val mLinearInterpolator by lazy {
LinearInterpolator()
}
@@ -230,19 +233,31 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
}
}
}
binding.webtoonFrame.run {
onTouchMiddle {
binding.webtoonFrame.actionHandler = object : WebtoonFrame.ClickActionHandler {
override fun showMenu() {
if (!binding.mangaMenu.isVisible && !loadingViewVisible) {
binding.mangaMenu.runMenuIn()
}
}
onNextPage {
override fun nextPage() {
scrollToNext()
}
onPrevPage {
override fun prevPage() {
scrollToPrev()
}
override fun nextChapter() {
ReadManga.moveToNextChapter()
}
override fun prevChapter() {
ReadManga.moveToPrevChapter()
}
}
}
override fun onNewIntent(intent: Intent) {
@@ -532,7 +547,6 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
override fun showFooterConfig() {
showDialogFragment(MangaFooterSettingDialog().apply {
initialAutoPageEnabled = enableAutoScrollPage
callback = this@ReadMangaActivity
})
}
@@ -542,8 +556,14 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
showDialogFragment(MangaColorFilterDialog())
}
override fun showClickConfig() {
binding.mangaMenu.runMenuOut()
showDialogFragment(MangaClickActionConfigDialog())
}
override fun showScrollModeDialog() {
showDialogFragment(MangaScrollModeDialog().apply {
initialAutoPageEnabled = enableScroll
callback = this@ReadMangaActivity
})
}
@@ -591,7 +611,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
//自动翻页
override fun onAutoPageToggle(enabled: Boolean) {
enableAutoScroll = enabled
enableScroll = enabled
setAutoReadEnabled(enabled)
}
@@ -622,6 +642,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
//1 自动翻页 2 自动滚动 0 关闭
fun setAutoReadEnabled(enabled: Boolean) {
enableScroll = enabled
val isWebtoonMode =
scrollMode == MangaScrollMode.WEBTOON || scrollMode == MangaScrollMode.WEBTOON_WITH_GAP
if (enabled) {
@@ -0,0 +1,145 @@
package io.legato.kazusa.ui.book.manga.config
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseDialogFragment
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.databinding.DialogClickActionConfigBinding
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.utils.getCompatColor
import io.legato.kazusa.utils.putPrefInt
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
class MangaClickActionConfigDialog : BaseDialogFragment(R.layout.dialog_click_action_config) {
private val binding by viewBinding(DialogClickActionConfigBinding::bind)
private val actions by lazy {
linkedMapOf(
Pair(-1, getString(R.string.non_action)),
Pair(0, getString(R.string.menu)),
Pair(1, getString(R.string.next_page)),
Pair(2, getString(R.string.prev_page)),
Pair(3, getString(R.string.next_chapter)),
Pair(4, getString(R.string.previous_chapter)),
)
}
override fun onStart() {
super.onStart()
dialog?.window?.run {
setBackgroundDrawableResource(R.color.transparent)
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
view.setBackgroundColor(getCompatColor(R.color.translucent))
initData()
initViewEvent()
}
private fun initData() = binding.run {
tvTopLeft.text = actions[AppConfig.mangaClickActionTL]
tvTopCenter.text = actions[AppConfig.mangaClickActionTC]
tvTopRight.text = actions[AppConfig.mangaClickActionTR]
tvMiddleLeft.text = actions[AppConfig.mangaClickActionML]
tvMiddleCenter.text = actions[AppConfig.mangaClickActionMC]
tvMiddleRight.text = actions[AppConfig.mangaClickActionMR]
tvBottomLeft.text = actions[AppConfig.mangaClickActionBL]
tvBottomCenter.text = actions[AppConfig.mangaClickActionBC]
tvBottomRight.text = actions[AppConfig.mangaClickActionBR]
}
private fun initViewEvent() {
binding.ivClose.setOnClickListener {
dismissAllowingStateLoss()
}
binding.tvTopLeft.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionTL, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvTopCenter.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionTC, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvTopRight.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionTR, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvMiddleLeft.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionML, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvMiddleCenter.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionMC, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvMiddleRight.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionMR, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvBottomLeft.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionBL, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvBottomCenter.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionBC, action)
(it as? TextView)?.text = actions[action]
}
}
binding.tvBottomRight.setOnClickListener {
selectAction { action ->
putPrefInt(PreferKey.mangaClickActionBR, action)
(it as? TextView)?.text = actions[action]
}
}
}
private fun selectAction(success: (action: Int) -> Unit) {
context?.selector(
getString(R.string.select_action),
actions.values.toList()
) { _, index ->
success.invoke(actions.keys.toList()[index])
}
}
override fun onDestroy() {
super.onDestroy()
AppConfig.detectMangaClickArea()
}
}
@@ -23,8 +23,6 @@ class MangaFooterSettingDialog :
var callback: Callback? = null
var initialAutoPageEnabled: Boolean = false
private val binding by viewBinding(DialogMangaFooterSettingBinding::bind)
override fun onStart() {
@@ -134,6 +132,11 @@ class MangaFooterSettingDialog :
dismiss()
}
binding.btnClickSet.setOnClickListener {
callback?.showClickConfig()
dismiss()
}
updateChapterText()
}
@@ -183,6 +186,7 @@ class MangaFooterSettingDialog :
fun onAutoPageToggle(enable: Boolean)
fun onAutoPageSpeedChanged(speed: Int)
fun showColorFilterConfig()
fun showClickConfig()
}
}
@@ -15,6 +15,7 @@ class ScrollTimer(
) : RecyclerView.OnScrollListener() {
private var distance = 1
private var mScrollPageJob: Job? = null
var isEnabled: Boolean = false
set(value) {
if (field != value) {
@@ -8,6 +8,7 @@ import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.FrameLayout
import io.legato.kazusa.help.config.AppConfig
class WebtoonFrame : FrameLayout {
@@ -21,6 +22,7 @@ class WebtoonFrame : FrameLayout {
private val scaleDetector = ScaleGestureDetector(context, ScaleListener())
private val flingDetector = GestureDetector(context, FlingListener())
private val regionRects = mutableMapOf<String, RectF>()
var doubleTapZoom = true
set(value) {
@@ -34,45 +36,55 @@ class WebtoonFrame : FrameLayout {
private val recycler: WebtoonRecyclerView?
get() = getChildAt(0) as? WebtoonRecyclerView
private val mcRect = RectF()
private val blRect = RectF()
private val brRect = RectF()
private var mTouchMiddle: (() -> Unit)? = null
fun onTouchMiddle(init: () -> Unit) = apply { this.mTouchMiddle = init }
private var mNextPage: (() -> Unit)? = null
fun onNextPage(init: () -> Unit) = apply { this.mNextPage = init }
private var mPrevPage: (() -> Unit)? = null
fun onPrevPage(init: () -> Unit) = apply { this.mPrevPage = init }
var disabledClickScroll = false
var actionHandler: ClickActionHandler? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
recycler?.tapListener = { ev ->
when {
mcRect.contains(ev.rawX, ev.rawY) -> {
mTouchMiddle?.invoke()
}
recycler?.tapListener = tapListener@{ ev ->
val x = ev.rawX
val y = ev.rawY
val key =
regionRects.entries.find { it.value.contains(x, y) }?.key ?: return@tapListener
blRect.contains(ev.rawX, ev.rawY) && !disabledClickScroll -> {
mPrevPage?.invoke()
}
brRect.contains(ev.rawX, ev.rawY) && !disabledClickScroll -> {
mNextPage?.invoke()
}
val action = when (key) {
"TL" -> AppConfig.mangaClickActionTL
"TC" -> AppConfig.mangaClickActionTC
"TR" -> AppConfig.mangaClickActionTR
"ML" -> AppConfig.mangaClickActionML
"MC" -> AppConfig.mangaClickActionMC
"MR" -> AppConfig.mangaClickActionMR
"BL" -> AppConfig.mangaClickActionBL
"BC" -> AppConfig.mangaClickActionBC
"BR" -> AppConfig.mangaClickActionBR
else -> -1
}
performClickAction(action)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mcRect.set(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f)
blRect.set(0f, height * 0.66f, width * 0.33f, height.toFloat())
brRect.set(width * 0.66f, height * 0.66f, width.toFloat(), height.toFloat())
val cellWidth = w / 3f
val cellHeight = h / 3f
val keys = listOf("TL", "TC", "TR", "ML", "MC", "MR", "BL", "BC", "BR")
keys.forEachIndexed { index, key ->
val row = index / 3
val col = index % 3
regionRects[key] = RectF(
col * cellWidth,
row * cellHeight,
(col + 1) * cellWidth,
(row + 1) * cellHeight
)
}
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (!disableMangaScale) {
scaleDetector.onTouchEvent(ev)
@@ -93,6 +105,18 @@ class WebtoonFrame : FrameLayout {
return super.dispatchTouchEvent(ev)
}
private fun performClickAction(action: Int) {
when (action) {
-1 -> return
0 -> actionHandler?.showMenu()
1 -> actionHandler?.nextPage()
2 -> actionHandler?.prevPage()
3 -> actionHandler?.nextChapter()
4 -> actionHandler?.prevChapter()
else -> {}
}
}
inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
recycler?.onScaleBegin()
@@ -123,4 +147,13 @@ class WebtoonFrame : FrameLayout {
return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
}
}
interface ClickActionHandler {
fun showMenu()
fun nextPage()
fun prevPage()
fun nextChapter()
fun prevChapter()
}
}
@@ -3,24 +3,22 @@ package io.legato.kazusa.ui.dict
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import com.google.android.material.tabs.TabLayout
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseDialogFragment
import io.legato.kazusa.base.BaseBottomSheetDialogFragment
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.databinding.DialogDictBinding
//import io.legado.app.lib.theme.accentColor
//import io.legado.app.lib.theme.backgroundColor
import io.legato.kazusa.utils.invisible
import io.legato.kazusa.utils.setHtml
import io.legato.kazusa.utils.setLayout
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import io.legato.kazusa.utils.visible
/**
* 词典
*/
class DictDialog() : BaseDialogFragment(R.layout.dialog_dict) {
class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
constructor(word: String) : this() {
arguments = Bundle().apply {
@@ -33,11 +31,6 @@ class DictDialog() : BaseDialogFragment(R.layout.dialog_dict) {
private var word: String? = null
override fun onStart() {
super.onStart()
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.tvDict.movementMethod = LinkMovementMethod()
word = arguments?.getString("word")
@@ -46,8 +39,7 @@ class DictDialog() : BaseDialogFragment(R.layout.dialog_dict) {
dismiss()
return
}
// binding.tabLayout.setBackgroundColor(backgroundColor)
// binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab) {
@@ -61,7 +53,7 @@ class DictDialog() : BaseDialogFragment(R.layout.dialog_dict) {
val dictRule = tab.tag as DictRule
binding.rotateLoading.visible()
viewModel.dict(dictRule, word!!) {
binding.rotateLoading.inVisible()
binding.rotateLoading.invisible()
binding.tvDict.setHtml(it)
}
}