'ChipGroup with draggable Chips

In my XML I'm just declaring a ChipGroup as follows:

<com.google.android.material.chip.ChipGroup
    android:id="@+id/chipGroup" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

And then adding each Chip dynamically (where CustomChipFilterStyle styles them as a "filter" type of Chip):

ChipGroup chipGroup = findViewById(R.id.chipGroup);
for (String name : names) {
    Chip chip = new Chip(this, null, R.attr.CustomChipFilterStyle);
    chip.setText(name);
    chipGroup.addView(chip);
}

In the guidance (see the video clip under "Movable") it suggests that "Input chips can be reordered or moved into other fields":

enter image description here

But I can't see any guidance about how this is done, or find any examples out there. Is it a completely bespoke thing (via View.OnDragListener and chip.setOnDragListener()), or are there utility methods for this as part of the Chip framework? All I really need to be able to do is to reorder Chips within the same ChipGroup. I did start with chip.setOnDragListener() but soon realised I didn't have sufficient knowledge about how to create the necessary animations to nudge and re-order other Chips as the Chip itself is being dragged (and to distinguish between a tap -- to filter -- and a drag)... and I hoped that there might be some out-of-the-box way of doing this with a ChipGroup like is maybe alluded to in the above guidance.



Solution 1:[1]

As you suggested there's no out-of-the-box solution for this. So I've made a sample project to show usage of setOnDragListener & how you can create something like this for yourself.

Note: This is far from being the perfect polished solution that you might expect but I believe it can nudge you in the right direction.

Complete code: https://github.com/mayurgajra/ChipsDragAndDrop

Output:

drag chips

Pasting code here as well with inline comments:

MainActivity

class MainActivity : AppCompatActivity() {

    private val dragMessage = "Chip Added"

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val names = mutableListOf("Name 1", "Name 2", "Name 3")

        for (name in names) {
            val chip = Chip(this, null, 0)
            chip.text = name
            binding.chipGroup1.addView(chip)
        }

        attachChipDragListener()

        binding.chipGroup1.setOnDragListener(chipDragListener)
    }

    private val chipDragListener = View.OnDragListener { view, dragEvent ->
        val draggableItem = dragEvent.localState as Chip

        when (dragEvent.action) {

            DragEvent.ACTION_DRAG_STARTED -> {
                true
            }

            DragEvent.ACTION_DRAG_ENTERED -> {
                true
            }

            DragEvent.ACTION_DRAG_LOCATION -> {
                true
            }

            DragEvent.ACTION_DRAG_EXITED -> {
                //when view exits drop-area without dropping set view visibility to VISIBLE
                draggableItem.visibility = View.VISIBLE
                view.invalidate()
                true
            }

            DragEvent.ACTION_DROP -> {

                //on drop event in the target drop area, read the data and
                // re-position the view in it's new location
                if (dragEvent.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                    val draggedData = dragEvent.clipData.getItemAt(0).text
                    println("draggedData $draggedData")
                }


                //on drop event remove the view from parent viewGroup
                if (draggableItem.parent != null) {
                    val parent = draggableItem.parent as ChipGroup
                    parent.removeView(draggableItem)
                }

                // get the position to insert at
                var pos = -1

                for (i in 0 until binding.chipGroup1.childCount) {
                    val chip = binding.chipGroup1[i] as Chip
                    val start = chip.x
                    val end = (chip.x + (chip.width / 2))
                    if (dragEvent.x in start..end) {
                        pos = i
                        break
                    }
                }


                //add the view view to a new viewGroup where the view was dropped
                if (pos >= 0) {
                    val dropArea = view as ChipGroup
                    dropArea.addView(draggableItem, pos)
                } else {
                    val dropArea = view as ChipGroup
                    dropArea.addView(draggableItem)
                }


                true
            }

            DragEvent.ACTION_DRAG_ENDED -> {
                draggableItem.visibility = View.VISIBLE
                view.invalidate()
                true
            }

            else -> {
                false
            }

        }
    }

    private fun attachChipDragListener() {
        for (i in 0 until binding.chipGroup1.childCount) {
            val chip = binding.chipGroup1[i]
            if (chip !is Chip)
                continue

            chip.setOnLongClickListener { view: View ->

                // Create a new ClipData.Item with custom text data
                val item = ClipData.Item(dragMessage)

                // Create a new ClipData using a predefined label, the plain text MIME type, and
                // the already-created item. This will create a new ClipDescription object within the
                // ClipData, and set its MIME type entry to "text/plain"
                val dataToDrag = ClipData(
                    dragMessage,
                    arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                    item
                )

                // Instantiates the drag shadow builder.
                val chipShadow = ChipDragShadowBuilder(view)

                // Starts the drag
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                    //support pre-Nougat versions
                    @Suppress("DEPRECATION")
                    view.startDrag(dataToDrag, chipShadow, view, 0)
                } else {
                    //supports Nougat and beyond
                    view.startDragAndDrop(dataToDrag, chipShadow, view, 0)
                }

                view.visibility = View.INVISIBLE
                true
            }
        }

    }


}

ChipDragShadowBuilder:

class ChipDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {

    //set shadow to be the drawable
    private val shadow = ResourcesCompat.getDrawable(
        view.context.resources,
        R.drawable.shadow_bg,
        view.context.theme
    )

    // Defines a callback that sends the drag shadow dimensions and touch point back to the
    // system.
    override fun onProvideShadowMetrics(size: Point, touch: Point) {
        // Sets the width of the shadow to full width of the original View
        val width: Int = view.width

        // Sets the height of the shadow to full height of the original View
        val height: Int = view.height

        // The drag shadow is a Drawable. This sets its dimensions to be the same as the
        // Canvas that the system will provide. As a result, the drag shadow will fill the
        // Canvas.
        shadow?.setBounds(0, 0, width, height)

        // Sets the size parameter's width and height values. These get back to the system
        // through the size parameter.
        size.set(width, height)

        // Sets the touch point's position to be in the middle of the drag shadow
        touch.set(width / 2, height / 2)
    }

    // Defines a callback that draws the drag shadow in a Canvas that the system constructs
    // from the dimensions passed in onProvideShadowMetrics().
    override fun onDrawShadow(canvas: Canvas) {
        // Draws the Drawable in the Canvas passed in from the system.
        shadow?.draw(canvas)
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.google.android.material.chip.ChipGroup
        android:id="@+id/chipGroup1"
        android:layout_width="match_parent"
        android:layout_height="56dp"
        app:singleSelection="true">


    </com.google.android.material.chip.ChipGroup>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#555" />


</LinearLayout>

For understanding how drag works in detail. I would suggest you read: https://www.raywenderlich.com/24508555-android-drag-and-drop-tutorial-moving-views-and-data

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mayur Gajra