目录卷快速跳转与展开 #230、#212、#195
This commit is contained in:
@@ -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 =
|
private fun getDisplayTitle(chapter: BookChapter): String =
|
||||||
displayTitleMap[chapter.url] ?: chapter.title
|
displayTitleMap[chapter.url] ?: chapter.title
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.transition.platform.MaterialFadeThrough
|
import com.google.android.material.transition.platform.MaterialFadeThrough
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
@@ -259,11 +260,47 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选:清空选择状态并提示
|
|
||||||
adapter.clearSelection()
|
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) {
|
fun scrollToChapter(chapterIndex: Int) {
|
||||||
val pos = adapter.getItems().indexOfFirst { it.index == chapterIndex }
|
val pos = adapter.getItems().indexOfFirst { it.index == chapterIndex }
|
||||||
if (pos != -1) {
|
if (pos != -1) {
|
||||||
|
|||||||
@@ -4,13 +4,24 @@ package io.legado.app.ui.book.toc
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
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.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
@@ -78,6 +89,88 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
|
|||||||
intent.getStringExtra("bookUrl")?.let {
|
intent.getStringExtra("bookUrl")?.let {
|
||||||
viewModel.initBook(it)
|
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 {
|
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp">
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="12dp">
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/tab_layout"
|
android:id="@+id/tab_layout"
|
||||||
@@ -37,12 +38,17 @@
|
|||||||
app:tabMode="fixed" />
|
app:tabMode="fixed" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:layout_width="32dp"
|
android:id="@+id/btn_chapterMenu"
|
||||||
android:layout_height="32dp"
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:iconSize="16dp"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:checkable="true"
|
android:checkable="true"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
app:icon="@drawable/ic_toc"
|
app:icon="@drawable/ic_toc"
|
||||||
style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"/>
|
style="@style/Widget.Material3Expressive.Button.IconButton.Outlined"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</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>
|
||||||
@@ -1326,4 +1326,8 @@
|
|||||||
<string name="sort_name_asc">名称升序</string>
|
<string name="sort_name_asc">名称升序</string>
|
||||||
<string name="sort_name_desc">名称降序</string>
|
<string name="sort_name_desc">名称降序</string>
|
||||||
<string name="text_dottedline">文字下划虚线</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>
|
</resources>
|
||||||
|
|||||||
@@ -1327,4 +1327,8 @@
|
|||||||
<string name="sort_name_asc">名称升序</string>
|
<string name="sort_name_asc">名称升序</string>
|
||||||
<string name="sort_name_desc">名称降序</string>
|
<string name="sort_name_desc">名称降序</string>
|
||||||
<string name="text_dottedline">文字下划虚线</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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user