具有可拖动芯片的芯片组

ChipGroup with draggable Chips

在我的 XML 中,我只是声明一个 ChipGroup 如下:

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

然后动态添加每个 Chip(其中 CustomChipFilterStyle 将它们设置为 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);
}

guidance中(参见“可移动”下的视频片段)提示“输入芯片可以重新排序或移动到其他字段”:

但我看不到有关如何完成此操作的任何指导,也找不到任何示例。它是完全定制的东西(通过 View.OnDragListenerchip.setOnDragListener()),还是作为 Chip 框架的一部分有实用方法?我真正需要做的就是在同一个 ChipGroup 中重新排序 Chip。我确实从 chip.setOnDragListener() 开始,但很快意识到我对如何创建必要的动画来推动和重新排序其他 Chip 没有足够的知识,因为 Chip 本身正在被拖动(并区分点击 - 过滤 - 和拖动)......我希望可能有一些开箱即用的方法可以用 ChipGroup 来做到这一点在上面的指导中。

如您所言,没有 out-of-the-box 解决方案。所以我做了一个示例项目来展示 setOnDragListener 的用法以及如何为自己创建这样的东西。

注意:这远非您可能期望的完美解决方案,但我相信它可以将您推向正确的方向。

完整代码: https://github.com/mayurgajra/ChipsDragAndDrop

输出:

在这里粘贴代码以及内联注释:

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>

用于详细了解拖动的工作原理。我建议您阅读:https://www.raywenderlich.com/24508555-android-drag-and-drop-tutorial-moving-views-and-data

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

令人惊讶的是,在 ChipGroup 中似乎没有一种“out-of-the-box”方式来重新排序筹码 - 至少我没有找到.

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

以下内容并未真正完全回答您的问题,因为答案涉及 RecyclerView 而不是 ChipGroup,但效果是相同。此解决方案基于 ItemTouchHelper 演示 作者:保罗·伯克。我已将 Java 转换为 Kotlin 并对代码进行了一些修改。我在 ChipReorder The layout manager I use for the RecyclerView is FlexboxLayoutManager.

发布了一个演示回购

演示应用程序依赖于 ItemTouchHelper,它是一个实用程序 class,它向 RecyclerView 添加了滑动关闭和拖放支持。如果您查看 ItemTouchHelper 的实际代码,您将了解屏幕上出现的简单拖动动画的潜在复杂性。

这是使用演示应用程序拖动筹码的快速视频。

我相信您可能需要 ChipGroup 的任何功能都可以通过 RecyclerView 或其适配器实现。

更新: 我在演示库中添加了一个名为“chipgroupreorder”的模块,它用动画重新排序 ChipGroup 中的芯片。尽管这看起来与 RecyclerView 解决方案非常相似,但它使用的是 ChipGroup 而不是 RecyclerView

该演示使用 View.OnDragListener 并依赖于为 ChipGroup 设置的 android:animateLayoutChanges="true" 动画。

选择要移动的视图是基本的,可以改进。进一步测试可能还会出现其他问题。