Android拖拽API简析

1,510 阅读3分钟

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处理各种事件逻辑
55
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
56
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监听事件并向底部布局添加内容
57
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()处理逻辑
58
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>