From 38c90f35f2346b48557dea7c6efe1836d375c50e Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Sun, 16 Jul 2023 23:03:45 +0800 Subject: [PATCH] Move to recyclerview-selection, part 1/2 --- app/build.gradle.kts | 1 + .../hippo/ehviewer/ui/scene/FavoritesScene.kt | 108 +++++++++--------- .../hippo/ehviewer/ui/scene/GalleryAdapter.kt | 11 +- .../hippo/ehviewer/ui/scene/GalleryHolder.kt | 9 +- .../ehviewer/ui/scene/GalleryListScene.kt | 4 +- .../ui/scene/GallerySelectionTracker.kt | 77 +++++++++++++ app/src/main/res/layout/scene_favorites.xml | 2 +- 7 files changed, 146 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/scene/GallerySelectionTracker.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 45d094b0e..ca445b122 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -200,6 +200,7 @@ dependencies { implementation("androidx.paging:paging-compose:$paging") implementation("androidx.paging:paging-runtime-ktx:$paging") implementation("androidx.recyclerview:recyclerview:1.3.0") + implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01") // https://developer.android.com/jetpack/androidx/releases/room val room_version = "2.6.0-alpha02" diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt index 719cce7ed..8632fe48f 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt @@ -75,8 +75,6 @@ import com.hippo.ehviewer.ui.legacy.FabLayout import com.hippo.ehviewer.ui.legacy.FabLayout.OnClickFabListener import com.hippo.ehviewer.ui.legacy.FastScroller.OnDragHandlerListener import com.hippo.ehviewer.ui.legacy.HandlerDrawable -import com.hippo.ehviewer.ui.legacy.SelectableGalleryInfoRecyclerView -import com.hippo.ehviewer.ui.legacy.SelectableGalleryInfoRecyclerView.CustomChoiceListener import com.hippo.ehviewer.ui.legacy.ViewTransition import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper import com.hippo.ehviewer.ui.setMD3Content @@ -153,6 +151,7 @@ class FavoritesScene : SearchBarScene() { private var _binding: SceneFavoritesBinding? = null private val binding get() = _binding!! private var mAdapter: GalleryAdapter? = null + private val tracker get() = mAdapter!!.tracker!! private val showNormalFabsRunnable = Runnable { updateJumpFab() // index: 0, 2 binding.fabLayout.run { @@ -164,7 +163,7 @@ class FavoritesScene : SearchBarScene() { private fun onItemClick(position: Int) { if (isDrawerOpen(GravityCompat.END)) { // Skip if in search mode - if (!binding.recyclerView.isInCustomChoice) { + if (!tracker.isInCustomChoice) { switchFav(position - 2) updateJumpFab() closeDrawer(GravityCompat.END) @@ -202,6 +201,11 @@ class FavoritesScene : SearchBarScene() { Settings.recentFavCat = urlBuilder.favCat } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mAdapter?.let { tracker.saveSelection(outState) } + } + override fun onResume() { super.onResume() mAdapter?.type = Settings.listMode @@ -214,14 +218,13 @@ class FavoritesScene : SearchBarScene() { ): View { _binding = SceneFavoritesBinding.inflate(inflater, container!!) setOnApplySearch { - if (!binding.recyclerView.isInCustomChoice) { + if (!tracker.isInCustomChoice) { switchFav(urlBuilder.favCat, it) } } binding.fastScroller.attachToRecyclerView(binding.recyclerView) binding.fastScroller.setHandlerDrawable(HandlerDrawable().apply { setColor(inflater.context.theme.resolveColor(androidx.appcompat.R.attr.colorPrimary)) }) binding.fabLayout.run { - addOnExpandListener { if (!it && binding.recyclerView.isInCustomChoice) binding.recyclerView.outOfCustomChoiceMode() } ViewCompat.setWindowInsetsAnimationCallback(binding.root, WindowInsetsAnimationHelper(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP, this)) updateJumpFab() val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface) @@ -232,15 +235,15 @@ class FavoritesScene : SearchBarScene() { setHidePrimaryFab(false) setOnClickFabListener(object : OnClickFabListener { override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) { - if (binding.recyclerView.isInCustomChoice) { - binding.recyclerView.outOfCustomChoiceMode() + if (tracker.isInCustomChoice) { + tracker.clearSelection() } else { binding.fabLayout.toggle() } } override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) { - if (!binding.recyclerView.isInCustomChoice) { + if (!tracker.isInCustomChoice) { when (position) { 0 -> showGoToDialog() 1 -> switchFav(urlBuilder.favCat) @@ -254,7 +257,7 @@ class FavoritesScene : SearchBarScene() { } when (position) { // Check all - 3 -> binding.recyclerView.checkAll() + 3 -> tracker.selectAll() // Download 4 -> CommonOperations.startDownload(mainActivity!!, takeCheckedInfo(), false) // Delete @@ -283,6 +286,7 @@ class FavoritesScene : SearchBarScene() { addDelete.setDelete(ANIMATE_TIME) } else { addDelete.setAdd(ANIMATE_TIME) + if (tracker.isInCustomChoice) tracker.clearSelection() } } addAboveSnackView(this) @@ -293,7 +297,7 @@ class FavoritesScene : SearchBarScene() { } override fun onEndDragHandler() { - if (!binding.recyclerView.isInCustomChoice) { + if (!tracker.isInCustomChoice) { setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) } showSearchBar() @@ -303,25 +307,16 @@ class FavoritesScene : SearchBarScene() { mAdapter = GalleryAdapter( this@run, false, - { info, pos -> - if (binding.recyclerView.isInCustomChoice) { - binding.recyclerView.toggleItemChecked(pos) - } else { - navAnimated( - R.id.galleryDetailScene, - bundleOf( - GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO, - GalleryDetailScene.KEY_GALLERY_INFO to info, - ), - ) - } - }, - { _, pos -> - if (!binding.recyclerView.isInCustomChoice) { - binding.recyclerView.intoCustomChoiceMode() - } - binding.recyclerView.toggleItemChecked(pos) + { info -> + navAnimated( + R.id.galleryDetailScene, + bundleOf( + GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO, + GalleryDetailScene.KEY_GALLERY_INFO to info, + ), + ) }, + {}, ).also { adapter -> val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)!! drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) @@ -353,32 +348,34 @@ class FavoritesScene : SearchBarScene() { } } } - gidGetter = { adapter.peek(it)?.gid } - allGid = { adapter.snapshot().items.map { it.gid } } + adapter.tracker = GallerySelectionTracker( + "favorite-selection", + this, + { adapter.snapshot().items }, + { (this as GalleryHolder).galleryId }, + ).apply { + addCustomChoiceListener({ + showSelectionFab() + binding.fabLayout.setAutoCancel(false) + // Delay expanding action to make layout work fine + SimpleHandler.post { binding.fabLayout.isExpanded = true } + binding.refreshLayout.isEnabled = false + // Lock drawer + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START) + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) + }) { + showNormalFab() + binding.fabLayout.setAutoCancel(true) + binding.fabLayout.isExpanded = false + binding.refreshLayout.isEnabled = true + // Unlock drawer + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) + } + restoreSelection(savedInstanceState) + } } switchFav(Settings.recentFavCat) - setCustomCheckedListener(object : CustomChoiceListener { - override fun onIntoCustomChoice(view: SelectableGalleryInfoRecyclerView) { - showSelectionFab() - binding.fabLayout.setAutoCancel(false) - // Delay expanding action to make layout work fine - SimpleHandler.post { binding.fabLayout.isExpanded = true } - binding.refreshLayout.isEnabled = false - // Lock drawer - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START) - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) - } - - override fun onOutOfCustomChoice(view: SelectableGalleryInfoRecyclerView) { - showNormalFab() - binding.fabLayout.setAutoCancel(true) - binding.fabLayout.isExpanded = false - binding.refreshLayout.isEnabled = true - // Unlock drawer - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) - } - }) } setAllowEmptySearch(false) return binding.root @@ -463,12 +460,9 @@ class FavoritesScene : SearchBarScene() { } } - private fun takeCheckedInfo() = run { - val snapshot = mAdapter?.snapshot()?.items - binding.recyclerView.checkedItemGid.mapNotNull { gid -> snapshot?.find { it.gid == gid } } - }.also { binding.recyclerView.outOfCustomChoiceMode() } + private fun takeCheckedInfo() = tracker.getAndClearSelection() - private fun checkedSize() = binding.recyclerView.checkedItemCount + private fun checkedSize() = tracker.selectionSize private fun showNormalFab() { // Delay showing normal fab to avoid mutation diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt index 1f48ae0ea..6827ce31a 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt @@ -44,8 +44,8 @@ private val diffCallback = object : DiffUtil.ItemCallback() { class GalleryAdapter( private val recyclerView: RecyclerView, private val showFavorite: Boolean, - private val onItemClick: (GalleryInfo, Int) -> Unit, - private val onItemLongClick: (GalleryInfo, Int) -> Unit, + private val onItemClick: (GalleryInfo) -> Unit, + private val onItemLongClick: (GalleryInfo) -> Unit, ) : PagingDataAdapter(diffCallback) { private val resources = recyclerView.context.resources private val layoutManager: AutoStaggeredGridLayoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL) @@ -53,6 +53,7 @@ class GalleryAdapter( private var mListDecoration: ItemDecoration? = null private var mGirdDecoration: MarginItemDecoration? = null private var mType = TYPE_INVALID + var tracker: GallerySelectionTracker? = null var type: Int get() = mType @@ -161,10 +162,12 @@ class GalleryAdapter( override fun onBindViewHolder(holder: GalleryHolder, position: Int) { val gi = getItem(position) ?: return + holder.galleryId = gi.gid holder.bind( gi, - { onItemClick(gi, position) }, - { onItemLongClick(gi, position) }, + tracker?.isSelected(gi.gid) ?: false, + { onItemClick(gi) }, + { onItemLongClick(gi) }, ) } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.kt index 9be3f7848..f4d8f5ba9 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.kt @@ -41,8 +41,11 @@ import com.hippo.ehviewer.ui.main.GalleryInfoListItem import eu.kanade.tachiyomi.util.system.pxToDp abstract class GalleryHolder(composeView: CheckableComposeView) : RecyclerView.ViewHolder(composeView) { + var galleryId = 0L + abstract fun bind( galleryInfo: GalleryInfo, + isChecked: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, ) @@ -53,7 +56,8 @@ class ListGalleryHolder(private val composeView: CheckableComposeView, private v private val height = (3 * listThumbSize * 3).pxToDp.dp private val showPages = Settings.showGalleryPages - override fun bind(galleryInfo: GalleryInfo, onClick: () -> Unit, onLongClick: () -> Unit) { + override fun bind(galleryInfo: GalleryInfo, isChecked: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) { + composeView.isChecked = isChecked composeView.setMD3Content { GalleryInfoListItem( onClick = onClick, @@ -68,7 +72,8 @@ class ListGalleryHolder(private val composeView: CheckableComposeView, private v } class GridGalleryHolder(private val composeView: CheckableComposeView) : GalleryHolder(composeView) { - override fun bind(galleryInfo: GalleryInfo, onClick: () -> Unit, onLongClick: () -> Unit) { + override fun bind(galleryInfo: GalleryInfo, isChecked: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) { + composeView.isChecked = isChecked composeView.setMD3Content { GalleryInfoGridItem( onClick = onClick, diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt index 051e16093..c342abd0a 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt @@ -369,7 +369,7 @@ class GalleryListScene : SearchBarScene() { mAdapter = GalleryAdapter( binding.recyclerView, true, - { info, _ -> + { info -> navAnimated( R.id.galleryDetailScene, bundleOf( @@ -378,7 +378,7 @@ class GalleryListScene : SearchBarScene() { ), ) }, - { info, _ -> + { info -> lifecycleScope.launchIO { dialogState.doGalleryInfoAction(info, requireContext()) } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GallerySelectionTracker.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/GallerySelectionTracker.kt new file mode 100644 index 000000000..8cc90e67c --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GallerySelectionTracker.kt @@ -0,0 +1,77 @@ +package com.hippo.ehviewer.ui.scene + +import android.os.Bundle +import android.view.MotionEvent +import androidx.recyclerview.selection.ItemDetailsLookup +import androidx.recyclerview.selection.ItemKeyProvider +import androidx.recyclerview.selection.SelectionTracker +import androidx.recyclerview.selection.StorageStrategy +import androidx.recyclerview.widget.RecyclerView +import com.hippo.ehviewer.client.data.GalleryInfo + +class GallerySelectionTracker( + selectionId: String, + recyclerView: RecyclerView, + private val getItems: () -> List, + getItemKey: RecyclerView.ViewHolder.() -> Long, + builder: SelectionTracker.Builder.() -> Unit = {}, +) { + private val tracker: SelectionTracker + val selectionSize get() = tracker.selection.size() + var isInCustomChoice = false + private set + + init { + tracker = SelectionTracker.Builder( + selectionId, + recyclerView, + object : ItemKeyProvider(SCOPE_MAPPED) { + override fun getKey(position: Int) = getItems()[position].gid + override fun getPosition(key: Long) = getItems().indexOfFirst { it.gid == key } + }, + object : ItemDetailsLookup() { + override fun getItemDetails(e: MotionEvent) = + recyclerView.findChildViewUnder(e.x, e.y)?.let { + recyclerView.getChildViewHolder(it).run { + object : ItemDetails() { + override fun getPosition() = bindingAdapterPosition + override fun getSelectionKey() = getItemKey() + } + } + } + }, + StorageStrategy.createLongStorage(), + ).apply(builder).build() + } + + fun addCustomChoiceListener(onIntoCustomChoiceListener: () -> Unit, onOutOfCustomChoiceListener: () -> Unit) { + tracker.addObserver(object : SelectionTracker.SelectionObserver() { + override fun onSelectionChanged() { + if (tracker.selection.isEmpty) { + if (isInCustomChoice) { + isInCustomChoice = false + onOutOfCustomChoiceListener() + } + } else if (!isInCustomChoice) { + isInCustomChoice = true + onIntoCustomChoiceListener() + } + } + + override fun onSelectionRestored() { + if (!tracker.selection.isEmpty) { + isInCustomChoice = true + onIntoCustomChoiceListener() + } + } + }) + } + + fun isSelected(key: Long) = tracker.isSelected(key) + fun selectAll() = tracker.setItemsSelected(getItems().map(GalleryInfo::gid), true) + fun clearSelection() = tracker.clearSelection() + fun saveSelection(state: Bundle) = tracker.onSaveInstanceState(state) + fun restoreSelection(state: Bundle?) = tracker.onRestoreInstanceState(state) + fun getAndClearSelection() = getItems().run { tracker.selection.map { gid -> first { it.gid == gid } } } + .also { clearSelection() } +} diff --git a/app/src/main/res/layout/scene_favorites.xml b/app/src/main/res/layout/scene_favorites.xml index 28fe38907..220b20094 100644 --- a/app/src/main/res/layout/scene_favorites.xml +++ b/app/src/main/res/layout/scene_favorites.xml @@ -42,7 +42,7 @@ app:fitsSystemWindowsInsets="top" app:layout_behavior="@string/searchbar_scrolling_view_behavior"> -