目录卷快速跳转与展开 #230、#212、#195

This commit is contained in:
HapeLee
2025-11-05 22:08:14 +08:00
parent 27ce9452d6
commit f2d0e149ce
7 changed files with 226 additions and 5 deletions
@@ -229,6 +229,70 @@ class ChapterListAdapter(
}
}
fun expandAllVolumes() {
val all = getItems()
var offset = 0
collapsedVolumes.forEach { volumeIndex ->
val volume = all.firstOrNull { it.index == volumeIndex } ?: return@forEach
val startIndex = all.indexOf(volume)
val currentPos = visibleItems.indexOf(volume)
if (currentPos == -1) return@forEach
val toAdd = mutableListOf<BookChapter>()
for (i in startIndex + 1 until all.size) {
val next = all[i]
if (next.isVolume) break
toAdd.add(next)
}
if (toAdd.isNotEmpty()) {
visibleItems.addAll(currentPos + 1, toAdd)
notifyItemRangeInserted(currentPos + 1, toAdd.size)
notifyItemChanged(currentPos)
offset += toAdd.size
}
}
collapsedVolumes.clear()
}
fun collapseAllVolumes() {
val all = getItems()
all.filter { it.isVolume }.forEach { volume ->
val startIndex = visibleItems.indexOf(volume)
if (startIndex == -1) return@forEach
val toRemove = mutableListOf<BookChapter>()
for (i in startIndex + 1 until visibleItems.size) {
val next = visibleItems[i]
if (next.isVolume) break
toRemove.add(next)
}
if (toRemove.isNotEmpty()) {
visibleItems.removeAll(toRemove)
notifyItemRangeRemoved(startIndex + 1, toRemove.size)
notifyItemChanged(startIndex)
}
collapsedVolumes.add(volume.index)
}
}
fun getAllVolumes(): List<String> {
return getItems().filter { it.isVolume }.map { it.title }
}
fun getVolumeStartPosition(volumeName: String): Int {
return getItems().indexOfFirst { it.isVolume && it.title == volumeName }
}
fun areAllVolumesExpanded(): Boolean {
return getItems().filter { it.isVolume }.all { !collapsedVolumes.contains(it.index) }
}
private fun getDisplayTitle(chapter: BookChapter): String =
displayTitleMap[chapter.url] ?: chapter.title
@@ -18,6 +18,7 @@ import androidx.fragment.app.activityViewModels
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.transition.platform.MaterialFadeThrough
import io.legado.app.R
@@ -259,11 +260,47 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
}
}
// 可选:清空选择状态并提示
adapter.clearSelection()
}
}
fun areAllVolumesExpanded(): Boolean = adapter.areAllVolumesExpanded()
fun toggleAllVolumes() {
val allExpanded = adapter.areAllVolumesExpanded()
if (allExpanded) {
adapter.collapseAllVolumes()
toastOnUi("已收起所有卷")
} else {
adapter.expandAllVolumes()
toastOnUi("已展开所有卷")
}
}
fun isInSelectionMode(): Boolean = adapter.isInSelectionMode()
fun toggleChapterSelection() {
if (adapter.isInSelectionMode()) {
adapter.clearSelection()
toastOnUi("已取消选择")
} else {
adapter.selectAll()
toastOnUi("已全选 ${adapter.itemCount} 个章节")
}
}
fun getAllVolumes(): List<String> {
return adapter.getAllVolumes()
}
fun scrollToVolume(volumeName: String) {
val position = adapter.getVolumeStartPosition(volumeName)
if (position >= 0) {
(binding.recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, 0)
toastOnUi("已跳转到 $volumeName")
}
}
fun scrollToChapter(chapterIndex: Int) {
val pos = adapter.getItems().indexOfFirst { it.index == chapterIndex }
if (pos != -1) {
@@ -4,13 +4,24 @@ package io.legado.app.ui.book.toc
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import com.google.android.material.tabs.TabLayout
@@ -78,6 +89,88 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
intent.getStringExtra("bookUrl")?.let {
viewModel.initBook(it)
}
setupTabListener()
setupMoreMenuButton()
}
private fun setupTabListener() {
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
updateButtonVisibility(tab.position)
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {
updateButtonVisibility(tab.position)
}
})
}
private fun updateButtonVisibility(tabPosition: Int) {
binding.btnChapterMenu.isEnabled = tabPosition == 0
}
private fun setupMoreMenuButton() {
val tag = "android:switcher:${binding.viewPager.id}:0"
binding.viewPager.postDelayed({
val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment
val volumes = chapterFragment?.getAllVolumes().orEmpty()
binding.btnChapterMenu.isVisible = volumes.isNotEmpty()
}, 200)
binding.btnChapterMenu.setOnClickListener { view ->
showChapterActionsMenu(view)
}
}
private fun showChapterActionsMenu(anchor: View) {
val popup = PopupMenu(this, anchor)
popup.menuInflater.inflate(R.menu.menu_chapter_actions, popup.menu)
val tag = "android:switcher:${binding.viewPager.id}:0"
val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment
val volumes = chapterFragment?.getAllVolumes().orEmpty()
volumes.forEachIndexed { index, volumeName ->
popup.menu.add(R.id.action_jump_to_volume_section, index, Menu.NONE, volumeName)
}
val titleItem = popup.menu.findItem(R.id.action_jump_to_volume_section)
val s = SpannableString(titleItem.title)
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
s.setSpan(ForegroundColorSpan(Color.GRAY), 0, s.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
s.setSpan(AbsoluteSizeSpan(12, true), 0, s.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
titleItem.title = s
val toggleSelectionItem = popup.menu.findItem(R.id.action_toggle_selection)
toggleSelectionItem.title = if (chapterFragment?.isInSelectionMode() == true) {
getString(R.string.cancel_select)
} else {
getString(R.string.select_all)
}
val toggleVolumesItem = popup.menu.findItem(R.id.action_toggle_volumes)
toggleVolumesItem.title = if (chapterFragment?.areAllVolumesExpanded() == true) {
getString(R.string.coll_volume)
} else {
getString(R.string.expand_volume)
}
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_toggle_volumes -> {
chapterFragment?.toggleAllVolumes()
true
}
R.id.action_toggle_selection -> {
chapterFragment?.toggleChapterSelection()
true
}
else -> {
if (item.groupId == R.id.action_jump_to_volume_section && item.itemId in volumes.indices) {
chapterFragment?.scrollToVolume(volumes[item.itemId])
true
} else false
}
}
}
popup.setOnDismissListener {
binding.btnChapterMenu.isChecked = false
}
popup.show()
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
@@ -25,7 +25,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp">
android:layout_marginStart="16dp"
android:layout_marginEnd="12dp">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
@@ -37,12 +38,17 @@
app:tabMode="fixed" />
<com.google.android.material.button.MaterialButton
android:layout_width="32dp"
android:layout_height="32dp"
android:id="@+id/btn_chapterMenu"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:visibility="gone"
app:iconSize="16dp"
android:clickable="true"
android:checkable="true"
android:layout_gravity="center_vertical"
app:icon="@drawable/ic_toc"
style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"/>
style="@style/Widget.Material3Expressive.Button.IconButton.Outlined"/>
</LinearLayout>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_toggle_volumes"
android:title="" />
<item
android:id="@+id/action_toggle_selection"
android:title="" />
<item
android:id="@+id/action_jump_to_volume_section"
android:title="@string/jump_to_volume"
android:enabled="false" />
</menu>
+4
View File
@@ -1326,4 +1326,8 @@
<string name="sort_name_asc">名称升序</string>
<string name="sort_name_desc">名称降序</string>
<string name="text_dottedline">文字下划虚线</string>
<string name="jump_to_volume">跳转到卷</string>
<string name="cancel_select">取消选择</string>
<string name="coll_volume">折叠所有卷</string>
<string name="expand_volume">展开所有卷</string>
</resources>
+4
View File
@@ -1327,4 +1327,8 @@
<string name="sort_name_asc">名称升序</string>
<string name="sort_name_desc">名称降序</string>
<string name="text_dottedline">文字下划虚线</string>
<string name="jump_to_volume">跳转到卷</string>
<string name="cancel_select">取消选择</string>
<string name="coll_volume">折叠所有卷</string>
<string name="expand_volume">展开所有卷</string>
</resources>