Search code examples
androidandroid-fragmentsandroid-recyclerviewselectionmultipleselection

RecyclerView Selection library - clicking too fast lets the click pass through to my OnClickListener


I'm using the RecyclerView selection library but my ViewHolders also have their own OnClickListeners. When I select items quickly after another, or double-tap an item, instead of selecting/unselecting the item, the click is passed through to my own OnClickListener. This behavior is really annoying. Is this a bug or a problem with my setup?

This is my code (I removed the not interesting parts).

Adapter:

class UserListAdapter : ListAdapter<User, UserListAdapter.UserViewHolder>(USER_COMPARATOR) {

    private var listener: ((User) -> Unit)? = null

    [...]

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val user = getItem(position)
        val tracker = tracker

        if (user != null && tracker != null) {
            holder.bind(user, tracker.isSelected(user.uid))
        }
    }

    inner class UserViewHolder(private val binding: ItemUserListBinding) :
        RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = bindingAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val item = getItem(position)
                    listener?.invoke(item)
                }
            }
        }

        fun bind(user: User, isSelected: Boolean) {
            binding.apply {
                [...]
                root.isActivated = isSelected
            }
        }

        fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<String>() {
            override fun getPosition() = bindingAdapterPosition

            override fun getSelectionKey() = getItem(bindingAdapterPosition).uid
        }
    }

    class KeyProvider(private val adapter: UserListAdapter) :
        ItemKeyProvider<String>(SCOPE_CACHED) {
        override fun getKey(position: Int) = adapter.currentList[position].uid

        override fun getPosition(key: String) =
            adapter.currentList.indexOfFirst { it.uid == key }
    }

    class DetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<String>() {
        override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
            val view = recyclerView.findChildViewUnder(e.x, e.y)
            if (view != null) {
                return (recyclerView.getChildViewHolder(view) as UserListAdapter.UserViewHolder).getItemDetails()
            }
            return null
        }
    }

    var tracker: SelectionTracker<String>? = null

    fun setOnClickListener(listener: (User) -> Unit) {
        this.listener = listener
    }

    [...]
}

Fragment:

@AndroidEntryPoint
class UserListFragment : Fragment(R.layout.fragment_user_list) {
    [...]

    private lateinit var selectionTracker: SelectionTracker<String>
    private var actionMode: ActionMode? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        [...]

        val userAdapter = UserListAdapter()

        binding.apply {
            recyclerView.apply {
                adapter = userAdapter
                layoutManager = LinearLayoutManager(requireContext())
                setHasFixedSize(true)
                addItemDecoration(
                    DividerItemDecoration(
                        requireContext(),
                        DividerItemDecoration.VERTICAL
                    )
                )
            }

            selectionTracker = SelectionTracker.Builder(
                "user selection",
                recyclerView,
                UserListAdapter.KeyProvider(userAdapter),
                UserListAdapter.DetailsLookup(recyclerView),
                StorageStrategy.createStringStorage()
            ).withSelectionPredicate(
                SelectionPredicates.createSelectAnything()
            ).build()

            userAdapter.tracker = selectionTracker

            selectionTracker.addObserver(selectionObserver)
        }

        if (savedInstanceState != null) {
            selectionTracker.onRestoreInstanceState(savedInstanceState)
            if (selectionTracker.hasSelection()) {
                selectionObserver.onSelectionChanged()
            }
        }

        userAdapter.setOnClickListener { user ->
            viewModel.onUserClick(user)
        }

        viewModel.userList.observe(viewLifecycleOwner) { userList ->
            userAdapter.submitList(userList)
        }

        [...]
    }

    private val selectionObserver = object : SelectionTracker.SelectionObserver<String>() {
        override fun onSelectionChanged() {
            if (selectionTracker.selection.size() > 0) {
                if (actionMode == null) {
                    actionMode =
                        (requireActivity() as AppCompatActivity).startSupportActionMode(
                            actionModeCallback
                        )
                }
                actionMode?.title = selectionTracker.selection.size().toString()
            } else {
                actionMode?.finish()
            }
            selectionTracker.copySelection(viewModel.selection)
        }
    }

    private val actionModeCallback = object : ActionMode.Callback {
        override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
            mode?.menuInflater?.inflate(R.menu.menu_action_mode_user_list_fragment, menu)
            return true
        }

        override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
            return false
        }

        override fun onActionItemClicked(
            mode: ActionMode?,
            item: MenuItem?
        ): Boolean {
            return when (item?.itemId) {
                R.id.action_new_group -> {
                    viewModel.onNewGroupClick()
                    actionMode?.finish()
                    true
                }
                else -> false
            }
        }

        override fun onDestroyActionMode(mode: ActionMode?) {
            actionMode = null
            selectionTracker.clearSelection()
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        if (::selectionTracker.isInitialized) {
            selectionTracker.onSaveInstanceState(outState)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        actionMode?.finish()
    }
}

Solution

  • It is a bug in recyclerview-selection library

    I suggest you increment recyclerview-selection library to latest:

    'androidx.recyclerview:recyclerview-selection:1.1.0-rc03'
    

    Here is the list of changes