具有可拖动芯片的芯片组
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.OnDragListener
和 chip.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"
动画。
选择要移动的视图是基本的,可以改进。进一步测试可能还会出现其他问题。
在我的 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.OnDragListener
和 chip.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"
动画。
选择要移动的视图是基本的,可以改进。进一步测试可能还会出现其他问题。