Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android RecyclerView: drag and drop over multiple ViewType

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:
screen recorder

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?

like image 659
Mneckoee Avatar asked Apr 01 '17 07:04

Mneckoee


3 Answers

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.

like image 187
Eerko Avatar answered Nov 17 '22 20:11

Eerko


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.

like image 24
Schadenfreude Avatar answered Oct 11 '22 08:10

Schadenfreude


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);
        }
like image 1
Mneckoee Avatar answered Nov 17 '22 19:11

Mneckoee