OnDragListener
- 通过 startDrag() 来启动拖拽
- 用 setOnDragListener() 来监听
- OnDragListener 内部只有一个方法:onDrag()
- onDragEvent() 方法也会收到拖拽回调(界面中的每个 View 都会收到)
ViewDragHelper
- 需要创建一个 ViewDragHelper 和 Callback()
- 需要写在 ViewGroup 里面,重写 onIntercept() 和 onTouchevent()
为什么要这两个东⻄,而不是一个?
- OnDragListener
- API 11 加入的工具类,用于拖拽操作。
- 使用场景:用户的「拖起 -> 放下」操作,重在内容的移动。可以附加拖拽 数据
- 不需要写自定义 View,使用 startDrag() / startDragAndDrop() 手动开启拖拽
- 拖拽的原理是创造出一个图像在屏幕的最上层,用户的手指拖着图像移动
- ViewDragHelper
- 2015 年的 support v4 包中新增的工具类,用于拖拽操作。
- 使用场景:用户拖动 ViewGroup 中的某个子 View
- 需要应用在自定义 ViewGroup 中调用ViewDragHelper.shouldInterceptTouchEvent() 和 processTouchEvent(),程序会自动开启拖拽
- 拖拽的原理是实时修改被拖拽的子 View 的 mLeft, mTop, mRight,mBottom 值
拖拽的几种实现
1.DragHelperGridView,通过ViewDragHelper处理拖拽逻辑
- onMeasure中重新测量
- onLayout中摆放子view
- 通过ViewDragHelper处理各种事件逻辑
private const val COLUMNS = 2
private const val ROWS = 3
class DragHelperGridView(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
private var dragHelper = ViewDragHelper.create(this, DragCallback())
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val specWidth = MeasureSpec.getSize(widthMeasureSpec)
val specHeight = MeasureSpec.getSize(heightMeasureSpec)
val childWidth = specWidth / COLUMNS
val childHeight = specHeight / ROWS
measureChildren(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY))
setMeasuredDimension(specWidth, specHeight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var childLeft: Int
var childTop: Int
val childWidth = width / COLUMNS
val childHeight = height / ROWS
for ((index, child) in children.withIndex()) {
childLeft = index % 2 * childWidth
childTop = index / 2 * childHeight
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight)
}
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return dragHelper.shouldInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
dragHelper.processTouchEvent(event)
return true
}
override fun computeScroll() {
if (dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
private inner class DragCallback : ViewDragHelper.Callback() {
var capturedLeft = 0f
var capturedTop = 0f
override fun tryCaptureView(child: View, pointerId: Int): Boolean {
return true
}
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
return left
}
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
return top
}
override fun onViewCaptured(capturedChild: View, activePointerId: Int) {
capturedChild.elevation = elevation + 1
capturedLeft = capturedChild.left.toFloat()
capturedTop = capturedChild.top.toFloat()
}
override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
}
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
dragHelper.settleCapturedViewAt(capturedLeft.toInt(), capturedTop.toInt())
postInvalidateOnAnimation()
}
}
}
xml
<com.dsh.txlessons.viewtouchdrag.drag.view.DragHelperGridView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:id="@+id/draggedView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#EF5350" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#9C27B0" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#1E88E5" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00695C" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FDD835" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#546E7A" />
</com.dsh.txlessons.viewtouchdrag.drag.view.DragHelperGridView>
2.DragListenerGridView,通过OnDragListener处理拖拽逻辑
- 通过OnDragListener处理拖拽逻辑
- 拖拽后重新摆放子view
private const val COLUMNS = 2
private const val ROWS = 3
class DragListenerGridView(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
private var dragListener: OnDragListener = DshDragListener()
private var draggedView: View? = null
private var orderedChildren: MutableList<View> = ArrayList()
init {
isChildrenDrawingOrderEnabled = true
}
override fun onFinishInflate() {
super.onFinishInflate()
val count: Int = getChildCount()
for (index in 0 until count) {
val child = getChildAt(index)
orderedChildren.add(child) // 初始化位置
child.setOnLongClickListener { v ->
draggedView = v
v.startDrag(null, DragShadowBuilder(v), v, 0)
false
}
child.setOnDragListener(dragListener)
}
}
override fun onDragEvent(event: DragEvent?): Boolean {
return super.onDragEvent(event)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val specWidth = MeasureSpec.getSize(widthMeasureSpec)
val specHeight = MeasureSpec.getSize(heightMeasureSpec)
val childWidth = specWidth / COLUMNS
val childHeight = specHeight / ROWS
measureChildren(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY))
setMeasuredDimension(specWidth, specHeight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var childLeft: Int
var childTop: Int
val childWidth = width / COLUMNS
val childHeight = height / ROWS
val count: Int = getChildCount()
for (index in 0 until count) {
val child = getChildAt(index)
childLeft = index % 2 * childWidth
childTop = index / 2 * childHeight
child.layout(0, 0, childWidth, childHeight)
child.translationX = childLeft.toFloat()
child.translationY = childTop.toFloat()
}
}
private inner class DshDragListener : OnDragListener {
override fun onDrag(v: View, event: DragEvent): Boolean {
when (event.action) {
DragEvent.ACTION_DRAG_STARTED -> if (event.localState === v) {
v.visibility = View.INVISIBLE
}
DragEvent.ACTION_DRAG_ENTERED -> if (event.localState !== v) {
sort(v)
}
DragEvent.ACTION_DRAG_EXITED -> {
}
DragEvent.ACTION_DRAG_ENDED -> if (event.localState === v) {
v.visibility = View.VISIBLE
}
}
return true
}
}
private fun sort(targetView: View) {
var draggedIndex = -1
var targetIndex = -1
for ((index, child) in orderedChildren.withIndex()) {
if (targetView === child) {
targetIndex = index
} else if (draggedView === child) {
draggedIndex = index
}
}
orderedChildren.removeAt(draggedIndex)
orderedChildren.add(targetIndex, draggedView!!)
var childLeft: Int
var childTop: Int
val childWidth = width / COLUMNS
val childHeight = height / ROWS
for ((index, child) in orderedChildren.withIndex()) {
childLeft = index % 2 * childWidth
childTop = index / 2 * childHeight
child.animate()
.translationX(childLeft.toFloat())
.translationY(childTop.toFloat())
.setDuration(150)
}
}
}
3. DragToCollectLayout,拖动收藏/添加购物车效果
- onFinishInflate添加点击和拖拽事件
- 长按触发drag事件,调用ViewCompat.startDragAndDrop(...) 方法
- OnDragListener监听事件并向底部布局添加内容
class DragToCollectLayout(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) {
private var dragStarter = OnLongClickListener { v ->
val imageData = ClipData.newPlainText("name", v.contentDescription)
ViewCompat.startDragAndDrop(v, imageData, DragShadowBuilder(v), null, 0)
}
private var dragListener: OnDragListener = CollectListener()
override fun onFinishInflate() {
super.onFinishInflate()
avatarView.setOnLongClickListener(dragStarter)
logoView.setOnLongClickListener(dragStarter)
collectorLayout.setOnDragListener(dragListener)
}
inner class CollectListener : OnDragListener {
override fun onDrag(v: View, event: DragEvent): Boolean {
when (event.action) {
DragEvent.ACTION_DROP -> if (v is LinearLayout) {
val textView = TextView(context)
textView.textSize = 16f
textView.text = event.clipData.getItemAt(0).text
v.addView(textView)
}
}
return true
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<com.dsh.txlessons.viewtouchdrag.drag.view.DragToCollectLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/avatarView"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_weight="1"
android:contentDescription="Avatar"
android:src="@mipmap/slmh"
app:layout_constraintEnd_toStartOf="@id/logoView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/logoView"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_weight="1"
android:contentDescription="Logo"
android:src="@drawable/google_logo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarView"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/collectorLayout"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_alignParentBottom="true"
android:background="#78909C"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</com.dsh.txlessons.viewtouchdrag.drag.view.DragToCollectLayout>
4. DragUpDownLayout, 拖动自动居顶居底效果
- 主要是在ViewDragHelper.Callback()处理逻辑
class DragUpDownLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
private var dragListener: ViewDragHelper.Callback = DragCallback()
private var dragHelper: ViewDragHelper = ViewDragHelper.create(this, dragListener)
private var viewConfiguration: ViewConfiguration = ViewConfiguration.get(context)
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return dragHelper.shouldInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
dragHelper.processTouchEvent(event)
return true
}
override fun computeScroll() {
if (dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
internal inner class DragCallback : ViewDragHelper.Callback() {
override fun tryCaptureView(child: View, pointerId: Int): Boolean {
return child === draggedView
}
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
return top
}
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
if (Math.abs(yvel) > viewConfiguration.scaledMinimumFlingVelocity) {
if (yvel > 0) {
dragHelper.settleCapturedViewAt(0, height - releasedChild.height)
} else {
dragHelper.settleCapturedViewAt(0, 0)
}
} else {
if (releasedChild.top < height - releasedChild.bottom) {
dragHelper.settleCapturedViewAt(0, 0)
} else {
dragHelper.settleCapturedViewAt(0, height - releasedChild.height)
}
}
postInvalidateOnAnimation()
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<com.dsh.txlessons.viewtouchdrag.drag.view.DragUpDownLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/draggedView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#388E3C"
/>
</com.dsh.txlessons.viewtouchdrag.drag.view.DragUpDownLayout>