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:

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