6

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.

drmrbrewer
  • 11,491
  • 21
  • 85
  • 181
  • Instead of _ChipGroup_ have you consider using _RecyclerView_? The layout manager can be [FlexboxLayoutManager](https://github.com/google/flexbox-layout#flexboxlayoutmanager-within-recyclerview) with an [ItemTouchHelper](https://developer.android.com/reference/androidx/recyclerview/widget/ItemTouchHelper). I think you should be able to mimic any _ChipGroup_-specific functionality that you may need. I have not tried this, but it seems to me that it would be workable. – Cheticamp Feb 25 '22 at 13:42

2 Answers2

4

But I can't see any guidance about how [chip reordering within a ChipGroup] is done, or find any examples out there.

It is surprising that there doesn't seem to be an "out-of-the-box" way to reorder chips in a ChipGroup - at least not one that I have found.

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

The following doesn't really fully answer your question since the answer involves a RecyclerView and not a ChipGroup, but the effect is the same. This solution is based up the ItemTouchHelper Demo by Paul Burke. I have converted the Java to Kotlin and made some modifications to the code. I have posted a demo repo at ChipReorder The layout manager I use for the RecyclerView is FlexboxLayoutManager.

The demo app relies upon ItemTouchHelper which is a utility class that adds swipe to dismiss and drag & drop support to RecyclerView. If you look at the actual code of ItemTouchHelper, you will get an idea of the underlying complexity of the animation that appears on the screen for a simple drag.

Here is a quick video of chips being dragged around using the demo app.

enter image description here

I believe that any functionality that you may need from ChipGroup can be implemented through RecyclerView or its adapter.

Update: I have added a module to the demo repo called "chipgroupreorder" which reorders chips within a ChipGroup with animation. Although this looks much the same as the RecyclerView solution, it uses a ChipGroup and not a RecyclerView.

The demo uses a View.OnDragListener and relies upon android:animateLayoutChanges="true" that is set for the ChipGroup for the animations.

The selection of which view to shift is rudimentary and can be improved. There are probably other issues that may arise upon further testing.

enter image description here

Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • Fantastic answer, thanks. Not only to provide a couple of different solutions, one of which is still based on `ChipGroup`, but you've also taught how relatively easy it is to add drag/drop/swipe functionality to a `RecyclerView` using `ItemTouchHelper`, which I am now implementing in other parts of my app! – drmrbrewer Mar 02 '22 at 10:45
2

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

Mayur Gajra
  • 8,285
  • 6
  • 25
  • 41