Move to recyclerview-selection, part 1/2

This commit is contained in:
FooIbar authored and tarsin committed 2023-07-18 07:05:13 +08:00
1 parent bf3ee4cce2
commit 38c90f35f2
7 files changed
+146 -66

No files matched your search

@@ -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
@@ -44,8 +44,8 @@ private val diffCallback = object : DiffUtil.ItemCallback<GalleryInfo>() {
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<GalleryInfo, GalleryHolder>(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<GalleryInfo>? = 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) },
)
}
@@ -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,
@@ -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())
}
@@ -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<T : GalleryInfo>(
selectionId: String,
recyclerView: RecyclerView,
private val getItems: () -> List<T>,
getItemKey: RecyclerView.ViewHolder.() -> Long,
builder: SelectionTracker.Builder<Long>.() -> Unit = {},
) {
private val tracker: SelectionTracker<Long>
val selectionSize get() = tracker.selection.size()
var isInCustomChoice = false
private set
init {
tracker = SelectionTracker.Builder(
selectionId,
recyclerView,
object : ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int) = getItems()[position].gid
override fun getPosition(key: Long) = getItems().indexOfFirst { it.gid == key }
},
object : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent) =
recyclerView.findChildViewUnder(e.x, e.y)?.let {
recyclerView.getChildViewHolder(it).run {
object : ItemDetails<Long>() {
override fun getPosition() = bindingAdapterPosition
override fun getSelectionKey() = getItemKey()
}
}
}
},
StorageStrategy.createLongStorage(),
).apply(builder).build()
}
fun addCustomChoiceListener(onIntoCustomChoiceListener: () -> Unit, onOutOfCustomChoiceListener: () -> Unit) {
tracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
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() }
}