Android Jetpack:如何使用 PagingDataAdapter 在回收站视图中进行多选?

Android Jetpack: How to to multi selection in recycler view with PagingDataAdapter?

我想在我的 android 应用程序中进行多项选择。 我以前做过,但只用了一个 ArrayAdapter。

我有一个 Flow 作为我的数据集,我使用一个带有 ViewHolder 的 PagingDataAdapter。

我的问题是,如果数据集不仅仅是一个列表而且我无法真正轻松地访问它,如何进行多选。

如果您想查找代码,请查看此代码:

Fragment Adapter ViewHolder ViewModel

我已经实现了自定义的方式。

我在适配器中使用了一个可观察列表,并将方法公开给查看器 select 他们自己。

您当然可以为此创建一个基础 class。我最终也应该这样做:)

我的代码:

Adapter

class PhotoAdapter(
    private val context: Context,
    private val photoRepository: PhotoRepository,
    private val viewPhotoCallback: KFunction1<Int, Unit>,
    val lifecycleOwner: LifecycleOwner
) : PagingDataAdapter<Photo, PhotoItemViewHolder>(differCallback) {

    /**
     * Holds the layout positions of the selected items.
     */
    val selectedItems = ObservableArrayList<Int>()

    /**
     * Holds a Boolean indicating if multi selection is enabled. In a LiveData.
     */
    var isMultiSelectMode: MutableLiveData<Boolean> = MutableLiveData(false)

    override fun onBindViewHolder(holderItem: PhotoItemViewHolder, position: Int) {
        holderItem.bindTo(this, getItem(position))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoItemViewHolder =
        PhotoItemViewHolder(parent, context, photoRepository)

    /**
     * Called by ui. On Click.
     */
    fun viewPhoto(position: Int) {
        viewPhotoCallback.invoke(getItem(position)?.id!!)
    }

    /**
     * Disables multi selection.
     */
    fun disableSelection() {
        selectedItems.clear()
        isMultiSelectMode.postValue(false)
    }

    /**
     * Enables multi selection.
     */
    fun enableSelection() {
        isMultiSelectMode.postValue(true)
    }

    /**
     * Add an item it the selection.
     */
    fun addItemToSelection(position: Int): Boolean = selectedItems.add(position)

    /**
     * Remove an item to the selection.
     */
    fun removeItemFromSelection(position: Int) = selectedItems.remove(position)

    /**
     * Indicate if an item is already selected.
     */
    fun isItemSelected(position: Int) = selectedItems.contains(position)

    /**
     * Indicate if an item is the last selected.
     */
    fun isLastSelectedItem(position: Int) = isItemSelected(position) && selectedItems.size == 1

    /**
     * Select all items.
     */
    fun selectAll() {
        for (i in 0 until itemCount) {
            if (!isItemSelected(i)) {
                addItemToSelection(i)
            }
        }
    }

    /**
     * Get all items that are selected.
     */
    fun getAllSelected(): List<Photo> {
        val items = mutableListOf<Photo>()
        for(position in selectedItems) {
            val photo = getItem(position)
            if (photo != null) {
                items.add(photo)
            }
        }
        return items
    }

    companion object {
        private val differCallback = object : DiffUtil.ItemCallback<Photo>() {

            override fun areItemsTheSame(oldItem: Photo, newItem: Photo): Boolean =
                oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: Photo, newItem: Photo): Boolean =
                oldItem == newItem

        }
    }

}

ViewHolder

class PhotoItemViewHolder(
    parent: ViewGroup,
    private val context: Context,
    private val photoRepository: PhotoRepository
) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.photo_item, parent, false)
) {
    private val imageView: ImageView = itemView.findViewById(R.id.photoItemImageView)
    private val checkBox: CheckBox = itemView.findViewById(R.id.photoItemCheckBox)

    var photo: Photo? = null
    private lateinit var adapter: PhotoAdapter

    /**
     * Binds the parent adapter and the photo to the ViewHolder.
     */
    fun bindTo(adapter: PhotoAdapter, photo: Photo?) {
        this.photo = photo
        this.adapter = adapter
        imageView.setOnClickListener {
            if (adapter.isMultiSelectMode.value!!) {
                // If the item clicked is the last selected item
                if (adapter.isLastSelectedItem(layoutPosition)) {
                    adapter.disableSelection()
                    return@setOnClickListener
                }
                // Set checked if not already checked
                setItemChecked(!adapter.isItemSelected(layoutPosition))
            } else {
                adapter.viewPhoto(layoutPosition)
            }
        }

        imageView.setOnLongClickListener {
            if (!adapter.isMultiSelectMode.value!!) {
                adapter.enableSelection()
                setItemChecked(true)
            }
            true
        }

        adapter.isMultiSelectMode.observe(adapter.lifecycleOwner, {
            if (it) { // When selection gets enabled, show the checkbox
                checkBox.show()
            } else {
                checkBox.hide()
            }
        })

        adapter.selectedItems.addOnListChangedCallback(onSelectedItemsChanged)

        listChanged()
        loadThumbnail()
    }

    /**
     * Listener for changes in selected images.
     * Calls [listChanged] whatever happens.
     */
    private val onSelectedItemsChanged =
        object : ObservableList.OnListChangedCallback<ObservableList<Int>>() {

            override fun onChanged(sender: ObservableList<Int>?) {
                listChanged()
            }

            override fun onItemRangeChanged(
                sender: ObservableList<Int>?,
                positionStart: Int,
                itemCount: Int
            ) {
                listChanged()
            }

            override fun onItemRangeInserted(
                sender: ObservableList<Int>?,
                positionStart: Int,
                itemCount: Int
            ) {
                listChanged()
            }

            override fun onItemRangeMoved(
                sender: ObservableList<Int>?,
                fromPosition: Int,
                toPosition: Int,
                itemCount: Int
            ) {
                listChanged()
            }

            override fun onItemRangeRemoved(
                sender: ObservableList<Int>?,
                positionStart: Int,
                itemCount: Int
            ) {
                listChanged()
            }

        }

    private fun listChanged() {
        val isSelected = adapter.isItemSelected(layoutPosition)
        val padding = if (isSelected) 20 else 0

        checkBox.isChecked = isSelected
        imageView.setPadding(padding)
    }

    private fun setItemChecked(checked: Boolean) {
        layoutPosition.let {
            if (checked) {
                adapter.addItemToSelection(it)
            } else {
                adapter.removeItemFromSelection(it)
            }
        }
    }

    /**
     * Load the thumbnail for the [photo].
     */
    private fun loadThumbnail() {
        GlobalScope.launch(Dispatchers.IO) {
            val thumbnailBytes =
                photoRepository.readPhotoThumbnailFromInternal(context, photo?.id!!)
            if (thumbnailBytes == null) {
                Timber.d("Error loading thumbnail for photo: $photo.id")
                return@launch
            }
            val thumbnailBitmap =
                BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.size)
            runOnMain { // Set thumbnail in main thread
                imageView.setImageBitmap(thumbnailBitmap)
            }
        }
    }
}

学习了material-components-android

原代码:CardSelectionModeActivity.Java SelectableCardsAdapter.java

我的代码:

myfragment.kt

  binding.rvApp.adapter = adapter
  selectionTracker = SelectionTracker.Builder<Long>(
            "selection",
            binding.rvApp,
            ListItemAdapter.KeyProvider(),
            ListItemAdapter.DetailsLookup(binding.rvApp),
            StorageStrategy.createLongStorage()
        )
            .withSelectionPredicate(SelectionPredicates.createSelectAnything()).build()
  adapter.setSelectionTracker(selectionTracker)
  selectionTracker.addObserver(
            object : SelectionTracker.SelectionObserver<Long>() {
                override fun onSelectionChanged() {
                    if (selectionTracker.selection.size() > 0) {
                        if (actionMode == null) {
                            actionMode =
                                (activity as AppCompatActivity).startSupportActionMode(this@AppListFragment)
                        }
                        actionMode?.title = selectionTracker.selection.size().toString()
                    } else {
                        actionMode?.finish()
                    }
                }
            })
  binding.rvApp.layoutManager = LinearLayoutManager(context)

ListItemAdapter.kt

open class ListItemAdapter @Inject constructor(
    private val appItemDao: AppItemDao
) :PagingDataAdapter<AppItem, ListItemAdapter.ItemViewHolder>(diffCallback) {


    private lateinit var selectionTracker: SelectionTracker<Long>

    private lateinit var binding: ItemFragmentAppListBinding

    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<AppItem>() {
            override fun areItemsTheSame(oldItem: AppItem, newItem: AppItem): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: AppItem, newItem: AppItem): Boolean {
                return oldItem == newItem
            }
        }
    }

    open fun setSelectionTracker(selectionTracker: SelectionTracker<Long>) {
        this.selectionTracker = selectionTracker
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        binding =
            ItemFragmentAppListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ItemViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = getItem(position)
            if (item != null){
                holder.bind(item,position)
            }
    }


    fun getSelectItems(): List<AppItem> {
        val list = mutableListOf<AppItem>()
        for (position in selectionTracker.selection){
            val item = getItem(position.toInt())
            list.add(item!!)
        }
        return list
    }

    inner class ItemViewHolder(private val holderBinding: ItemFragmentAppListBinding) : RecyclerView.ViewHolder(binding.root) {
        private val details = ItemDetails()

        @SuppressLint("SetTextI18n")
        fun bind(item: AppItem, position: Int) {
            details.position = position.toLong()
            holderBinding.tvAppId.text = item.id
            /**
            *something to change ui
            */
            bindSelectedState()
        }

        private fun bindSelectedState() {
            //cvAppItem is MaterialCardView 
            holderBinding.cvAppItem.isChecked =
                this@ListItemAdapter.selectionTracker.isSelected(details.selectionKey)
        }

        fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> {
            return details
        }

    }

    class ItemDetails : ItemDetailsLookup.ItemDetails<Long>() {
        var position: Long = 0
        override fun getPosition(): Int {
            return position.toInt()
        }

        override fun getSelectionKey(): Long {
            return position
        }

        override fun inSelectionHotspot(e: MotionEvent): Boolean {
            return false
        }

        override fun inDragRegion(e: MotionEvent): Boolean {
            return true
        }
    }


    class DetailsLookup(private val recyclerView: RecyclerView) :
        ItemDetailsLookup<Long>() {
        override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
            val view = recyclerView.findChildViewUnder(e.x, e.y)
            val viewHolder = view?.let { recyclerView.getChildViewHolder(it) }
            if (viewHolder is ListItemAdapter.ItemViewHolder) {
                return viewHolder.getItemDetails()
            }
            return null
        }
    }

    class KeyProvider : ItemKeyProvider<Long?>(SCOPE_MAPPED) {
        override fun getKey(position: Int): Long {
            return position.toLong()
        }

        override fun getPosition(@NonNull key: Long): Int {
            return key.toInt()
        }
    }

}