Android Jetpack:如何使用 PagingDataAdapter 在回收站视图中进行多选?
Android Jetpack: How to to multi selection in recycler view with PagingDataAdapter?
我想在我的 android 应用程序中进行多项选择。
我以前做过,但只用了一个 ArrayAdapter。
我有一个 Flow 作为我的数据集,我使用一个带有 ViewHolder 的 PagingDataAdapter。
我的问题是,如果数据集不仅仅是一个列表而且我无法真正轻松地访问它,如何进行多选。
如果您想查找代码,请查看此代码:
我已经实现了自定义的方式。
我在适配器中使用了一个可观察列表,并将方法公开给查看器 select 他们自己。
您当然可以为此创建一个基础 class。我最终也应该这样做:)
我的代码:
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
}
}
}
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()
}
}
}
我想在我的 android 应用程序中进行多项选择。 我以前做过,但只用了一个 ArrayAdapter。
我有一个 Flow 作为我的数据集,我使用一个带有 ViewHolder 的 PagingDataAdapter。
我的问题是,如果数据集不仅仅是一个列表而且我无法真正轻松地访问它,如何进行多选。
如果您想查找代码,请查看此代码:
我已经实现了自定义的方式。
我在适配器中使用了一个可观察列表,并将方法公开给查看器 select 他们自己。
您当然可以为此创建一个基础 class。我最终也应该这样做:)
我的代码:
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
}
}
}
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()
}
}
}