I implement drag and drop for a RecyclerView
, it works well when have one View
type but reset the RecyclerView
when have multiple view type, I show the result in this gif:
and this is my code:
public class RecyclerListAdapter extends RecyclerView.Adapter<ItemViewHolder> {
private final Integer[] INVOICE_ITEMS_LIST = new Integer[]{
INVOICE_DESIGN_TITLE,
INVOICE_DESIGN_TITLE,
INVOICE_DESIGN_LOGO,
INVOICE_DESIGN_TITLE
};
public RecyclerListAdapter() {
mItems.addAll(Arrays.asList(INVOICE_ITEMS_LIST));
}
@Override
public int getItemViewType(int position) {
return INVOICE_ITEMS_LIST[position];
}
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
switch (viewType){
case INVOICE_DESIGN_TITLE:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.invoice_design_item_title, parent, false);
break;
case INVOICE_DESIGN_LOGO:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.invoice_design_item_logo, parent, false);
break;
default:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.invoice_design_item_title, parent, false);
}
return new ItemViewHolder(view);
}
@Override
public void onBindViewHolder(final ItemViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case INVOICE_DESIGN_TITLE:
break;
case INVOICE_DESIGN_LOGO:
// ... some code for setting the image source
break;
}
holder.dragIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (MotionEventCompat.getActionMasked(event) ==
MotionEvent.ACTION_DOWN) {
itemTouchHelper.startDrag(holder);
}
return false;
}
});
}
@Override
public int getItemCount() {
return mItems.size();
}
}
public class ItemViewHolder extends RecyclerView.ViewHolder {
final ImageView dragIcon;
final ImageView logo;
ItemViewHolder(View itemView) {
super(itemView);
dragIcon = (ImageView) itemView.findViewById(R.id.drag_ic);
logo = (ImageView) itemView.findViewById(R.id.logo);
}
}
public void initRecyclerSwipe(final RecyclerView recyclerView){
ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT ) {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.DOWN | ItemTouchHelper.UP;
int swipeFlags = ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
Collections.swap(mItems, fromPosition, toPosition);
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
return true;
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float width = (float) viewHolder.itemView.getWidth();
float alpha = 1.0f - Math.abs(dX) / width;
viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
actionState, isCurrentlyActive);
}
}
@Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
View itemView = viewHolder.itemView;
c.save();
c.clipRect(itemView.getLeft() + dX, itemView.getTop() + dY, itemView.getRight() + dX, itemView.getBottom() + dY);
c.translate(itemView.getLeft() + dX, itemView.getTop() + dY);
// draw the frame
c.drawColor(0x33000000);
c.restore();
}
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
mItems.remove(viewHolder.getAdapterPosition());
recyclerView.getAdapter().notifyItemRemoved(viewHolder.getAdapterPosition());
}
};
itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
itemTouchHelper.attachToRecyclerView(recyclerView);
}
How can I swap the children with different view type?
I also experienced the problem of the dragged view dropping immediately, when I moved it over some (but not all) other items.
The solution was to make sure the type of the items do not change while dragging.
So not an actual fix, rather the "source" of the problem and a workaround.
I created a RecyclerView
with an abstract ViewHolder
class and 3 different ViewHolder
types that extend it. I noticed that whenever I was dragging an item from one type over an item of another, it dropped automatically. After some debugging, logging, some Google searching (which yielded little result) and some experimenting I finally stumbled upon what caused it: overriding the getItemViewType
method to support multiple views. Once I removed that, the drag and drop started working normally again.
I haven't debugged the RecyclerView
and ItemTouchHelper
implementations to pinpoint the exact cause of the bug, because I had wasted enough time on this issue. So what I did was I made a common layout which had other 3 imported layouts which contained the specific Views
for the different types of data. Then I created 4 bind methods in the ViewHolder
(which was now reduced to just a single one). And then I made a when
statement where I determined the type of the data to be bound and called the appropriate bind
function.
internal class AnimalsAdapter() : RecyclerView.Adapter<AnimalViewHolder>() {
var animals by Delegates.observable<List<Animal>>(emptyList()) { _, _, _ ->
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimalViewHolder
= AnimalViewHolder(parent.inflate(R.layout.item_animal))
override fun onBindViewHolder(holder: AnimalViewHolder, position: Int) {
when (animals[position]) {
is Cat -> holder.bindCat(animal as Cat)
is Dog -> holder.bindDog(animal as Dog)
is Cuttlefish -> holder.bindCuttlefish(animal as Cuttlefish)
else -> throw IllegalArgumentException("Dafaq is this?")
}
............
}
internal class AnimalViewHolder(view: View) : RecyclerView.ViewHolder(view) {
// Common properties
private val animalType by lazy { view.findViewById<TextView>(R.id.animalTypeText) }
private val animalDesc by lazy { view.findViewById<TextView>(R.id.animalDescText) }
// Cat properties
private val catBreed by lazy { view.findViewById<TextView>(R.id.catBreedText) }
private val catYears by lazy { view.findViewById<TextView>(R.id.catYearsText) }
// Dog properties
private val dogBreed by lazy { view.findViewById<TextView>(R.id.dogBreedText) }
private val dogYears by lazy { view.findViewById<TextView>(R.id.dogYearsText) }
private val dogGender by lazy { view.findViewById<TextView>(R.id.dogGenderText) }
// Cuttlefish properties
private val cuttleCuddles by lazy { view.findViewById<TextView>(R.id.cuttleCuddlesText) }
fun bindCat(cat: Cat) {
// call the common bind
bindAnimal(cat)
catYears.text = cat.years
catBreed.text = cat.breed
catDetails.isVisible = true
}
fun bindDog(dog: Dog) {
// call the common bind
bindAnimal(dog)
dogYears.text = dog.years
dogBreed.text = dog.breed
dogGender.text = dog.gender
dogDetails.isVisible = true
}
fun bindCuttlefish(cuttle: Cuttlefish) {
// call the common bind
bindAnimal(cuttle)
cuttleCuddles.text = cuttle.cuddles
cuttleDetails.isVisible = true
}
private fun bindAnimal(animal: Animal) {
animalType.text = animal.type
animalDesc.text = animal.description
}
}
And here's an example of the base layout and one of the imported layouts.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/animalItem"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/animalTypeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/lorem" />
<TextView
android:id="@+id/animalDescText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
app:layout_constraintBaseline_toBaselineOf="@id/animalTypeText"
app:layout_constraintEnd_toEndOf="parent"
tools:text="@sample/lorem" />
<include
android:id="@+id/catDetails"
layout="@layout/partial_cat_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/animalTypeText" />
<include
android:id="@+id/dogDetails"
layout="@layout/partial_dog_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/animalTypeText" />
<include
android:id="@+id/cuttleDetails"
layout="@layout/partial_cuttleDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/animalTypeText" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/actionItem"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/dogYearsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
app:layout_constraintBottom_toTopOf="@id/dogBreedText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="3" />
<TextView
android:id="@+id/dogBreedText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toTopOf="@id/dogGenderText"
app:layout_constraintStart_toStartOf="@id/dogYearsText"
app:layout_constraintTop_toBottomOf="@id/dogYearsText"
tools:text="Shiba Inu" />
<TextView
android:id="@+id/dogGenderText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/dogYearsText"
app:layout_constraintTop_toBottomOf="@id/dogBreedText"
tools:text="Male" />
</androidx.constraintlayout.widget.ConstraintLayout>
As I said - it's a workaround, but I didn't have time to debug the actual problem in the framework. Hope this saves someone some time.
I made a mistake! the drag and drop mechanism works well. I swap the mItems
Collections.swap(mItems, fromPosition, toPosition);
but in getViewType
:
@Override
public int getItemViewType(int position) {
return INVOICE_ITEMS_LIST[position];
}
and this is the mistake. I should use this:
@Override
public int getItemViewType(int position) {
return mItems.get(position);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With