I have created a image slider using ViewPager2, I need a functionality that when I swipe up the image should be removed. I already worked with RecyclerView. So I remember I can use ItemTouchHelper.SimpleCallback for swipe to remove functionality. But attachToRecyclerView method requires a RecyclerView not a ViewPager2 even though viewpager2 uses RecyclerView adapter.
Kotlin:
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.UP) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}).attachToRecyclerView(binding.viewPager)
The RecyclerView of the ViewPager2 is not accessible by default, but you can enforce its accessibility using reflection.
In that you need to access the RecyclerView by reflecting its declared field name using getDeclaredField(), and for RecyclerView it is: mRecyclerView (you can check it in the ViewPager2 class)
Then use setAccessible() to make this field accessible in order to allow using it for the ItemTouchHelper.
Here is an extension function to return the ViewPager2 ReyclerView:
fun ViewPager2.getRecyclerView(): RecyclerView? {
try {
val field = ViewPager2::class.java.getDeclaredField("mRecyclerView")
field.isAccessible = true
return field.get(this) as RecyclerView
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
return null
}
And you can use it like:
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.UP) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}).attachToRecyclerView(binding.viewPager.getRecyclerView())
Preview:
UPDATE:
Thanks to #SimpleAndroid answer, there is a nice way for obtaining the RecyclerView:
viewPager.children.find { it is RecyclerView }?.let {
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.UP) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}).attachToRecyclerView(it as RecyclerView)
}
Here is another way to get RecyclerView from ViewPager2. I assume you have an adapter named is PictureItemAdapter:
class PictureItemAdapter(private val recyclerViewAttached: (RecyclerView) -> Unit) : RecyclerView.Adapter<PictureItemViewHolder>() {
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
recyclerViewAttached(recyclerView)
}
...
}
And you will receive the Recycler view from the recyclerViewAttached closure
val pictureItemAdapter = PictureItemAdapter{ recyclerView ->
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.UP) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}).attachToRecyclerView(recyclerView)
}
binding.viewPager.adapter = pictureItemAdapter
Note: Both RecyclerViewAdapter and FragmentStateAdapter have a method onAttachedToRecyclerView.
Related
I am building an app to be able to drag an item from one recycle view to another and I still have to keep the option to re-order inside a single recycler view.
So I have defined a Reorder Callback already
class ReorderHelperCallback(val adapter : ItemTouchHelperAdapter): ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
return makeMovementFlags( dragFlags, 0)//swipeFlags )
}
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
adapter.onItemMove(source.getAdapterPosition(),
target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
//Not use for Drag N Drop
}
}
also an interface:
interface OnStartDragListener {
fun onStartDrag(viewHolder: RecyclerView.ViewHolder?)
}
and a touchhelper:
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
fun onItemDismiss(position: Int)
}
to allow the reorder to work, I had to update the Recycler view adapter as below:
class Adapter(
private var context: Context?,
val dragStartListener : OnStartDragListener
): RecyclerView.Adapter<Adapter.ViewHolder>(), ItemTouchHelperAdapter {
var arrayItems : ArrayList<Data?> = ArrayList()
fun setData(array : MutableList<Data?>){
array.toCollection(arrayItems)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return arrayItems.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
val binding = DashboardTileLayoutBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding, dragStartListener)
}
override fun onBindViewHolder(holder: Adapter.ViewHolder, position: Int) {
holder.setData(arrayItems[position])
}
inner class ViewHolder(val binding: LayoutBinding,
val dragStartListener : OnStartDragListener? = null)
: RecyclerView.ViewHolder(binding.root) {
val tileLayout = binding.tileLayout
fun setData(data: Data?) {
....
tileLayout.setOnDragListener { view, dragEvent ->
when(dragEvent.action) {
ACTION_DRAG_STARTED -> {
dragStartListener?.onStartDrag(this)
true
}
else -> false
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
Collections.swap(arrayItems, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onItemDismiss(position: Int) {
TODO("Not yet implemented")
}
}
and the fragment which contain the rv, I have updated the adapter init:
list1adapter?.let { adapter ->
adapter.setData(list)
val callback: ItemTouchHelper.Callback = ReorderHelperCallback(adapter)
mItemTouchHelperSelected = ItemTouchHelper(callback)
mItemTouchHelperSelected?.attachToRecyclerView(selectedLayout)
}
---
override fun onStartDrag(viewHolder: RecyclerView.ViewHolder?) {
viewHolder?.let {
mItemTouchHelperSelected?.startDrag(it)
}
}
But my fragment contain 2 recycler views. list1 is working fine to user drag and drop to re-order the item but now, I would like to also be able to move an item from my rv list1 to the list2 and vice versa
Any idea, how to make it Kotlin ? I tried an sample code, byt I am losing the re-ordering.
Thanks
I have found an issue when dragging an item that doesn't trigger a move on a GridLayoutManager with 2 columns.
When dragging one item from the first column to the top of the list (which scrolls the list upwards) and then back to its original position, the recyclerView creates an empty space next to the original position, pushing the item that was next to the dragged item to the next row.
I have coded the simplest app to show you what I mean. On the first screenshot I start dragging one item towards the top of the list. On the second screenshot I reach the top of the screen. Then I move it back to its position and you can already see how the items have not been laid down properly.
Has anyone encountered this as well? I have not found any solution to this issue.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myAdapter = MyAdapter()
with(findViewById<RecyclerView>(R.id.recyclerView)) {
adapter = myAdapter
layoutManager = GridLayoutManager(this#MainActivity, 2)
ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, DOWN or UP or START or END)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun canDropOver(recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
TODO("Not yet implemented")
}
}).attachToRecyclerView(this)
}
myAdapter.submitList(data)
}
data class ListItem(val id: String, val name: String)
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
class MyAdapter : ListAdapter<ListItem, MyViewHolder>(MyAdapterDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
}
class MyAdapterDiffCallback : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem) = oldItem == newItem
}
}
So I've created an interface extending ItemToucHelper.Callback() and when I move my viewholder it changes the color of the stroke and when I drop it, it should return to the normal color.
The problem is that after dropping it doesn't change the color even though the Log shows it calls the clearview methodm then after "touching" the item sometimes it returns to normal.
Relevant code
Custom class for Callback
class NoteItemTouchHelperCallback(
private val itemTouchHelper: ItemTouchHelperAdapter
): ItemTouchHelper.Callback() {
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
Log.i(TAG, "Clear View")
viewHolder.itemView.itemCardView.apply {
changeStrokeColor(R.color.strokeCard)
changeStrokeWidth(2)
}
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if(actionState == ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder?.itemView?.itemCardView?.apply {
Log.i(TAG, "On Selected")
//I commented this and it's not the problem, but I know onSelectedChanged can be helpful here to solve it
//changeStrokeColor(R.color.strokeCardSelected)
//changeStrokeWidth(5)
}
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
itemTouchHelper.onItemMove(viewHolder.adapterPosition, target.adapterPosition, viewHolder)
return true
}
}
Interface that will be implemented in MainActivity
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int, viewHolder: RecyclerView.ViewHolder)
fun onItemSwiped(position: Int)
}
MainActivity
class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), ItemTouchHelperAdapter {
override fun onItemMove(fromPosition: Int, toPosition: Int, viewHolder: RecyclerView.ViewHolder) {
val noteList = mAdapter?.getNoteList()
val note = noteList?.get(fromPosition)
if(note!=null){
noteList.removeAt(fromPosition)
noteList.add(toPosition, note)
Log.i(TAG, "Change position")
viewHolder.itemView.itemCardView.apply {
changeStrokeColor(R.color.strokeCard)
changeStrokeWidth(5)
Log.i(TAG, "Change UI card")
}
}
mAdapter?.notifyItemMoved(fromPosition, toPosition)
}
}
I tried to put the relevants thing that matter when it comes to the action of moving the item.
Question: What am I missing in the code?
Edit: The clearview takes effect after locking and unlocking the phone. Why does this happen?
I've created drag and drop for my recycler view, but I want disable drag and drop option on pull of cells (it's a headers in my view). How to make them not available for drag and drop function?
Drag And drop helper
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled() = true
override fun isItemViewSwipeEnabled() = true
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
mAdapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
}
}
Adapter for Recyclerview
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
}
Code to pair adapter to rv
val drugAndDropHandler = SimpleItemTouchHelperCallback(adapter)
ItemTouchHelper(drugAndDropHandler).attachToRecyclerView(multiple_stores_list)
Just need to override getMovementFlags
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
if (viewHolder.adapterPosition in 0..NOT_DRAGABLE_ITEMS_MAX_POSITION) {
return 0
}
}
I have recyclerVIew in which I implement ItmeTouchHelper to drag and drop the items of RecyclerVIew but the positions of items are not saved
var itemTouch = ItemTouchHelper(object :
ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN,0){
override fun onMove(recyclerView: RecyclerView?, source: RecyclerView.ViewHolder?, target: RecyclerView.ViewHolder?): Boolean {
var source = source?.adapterPosition
var target = target?.adapterPosition
Collections.swap(database.DairyData(),source!!,target!!)
adapter.notifyItemMoved(source,target)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
}
})
itemTouch.attachToRecyclerView(Dairy_recyclerView)