在了解View拖拽之前,应该了解android的事件传递机制
ViewDragHelper- 源码及原理解读(进阶篇)
1. 实现基本拖拽
ViewDragHelper 可以方便我们快速实现View拖拽功能。主要步骤如下
- 创建ViewDragHelper实例
- 实现ViewDragHelper的CallBack编写
- 处理ViewGroup触摸事件
下面就是一个可以实现View拖拽实现的ViewGroup
public class VDHLinearLayout extends LinearLayout {
ViewDragHelper dragHelper;
public VDHLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return dragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
}
- 创建ViewDragHelper实例
dragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {});
/*
函数原型
@param forParent 当前的ViewGroup
@param sensitivity 设置拖拽灵敏度,数字越大越灵敏。默认给1就行(即系统的默认值)
@param cb 触摸过程的回调加函数
*/
public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity,
@NonNull Callback cb) {
- 实现ViewDragHelper.Callback相关方法
new ViewDragHelper.Callback() {
/*
返回true表示捕获当前view,如果不想使某个view移动,可以在内部判断当前View饭后返回false
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
/*
child垂直移动的距离,top 表示y轴坐标,相对于ViewGroup而言。dy表示偏移的距离
返回值确定view最终的y轴坐标,如果不想垂直移动return 0
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
/*
child垂直移动的距离,left表示x轴坐标,相对于ViewGroup而言。dx表示偏移的距离
返回值确定view最终的x轴坐标,如果不想水平移动return 0
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
}
- 处理ViewGroup触摸事件(原理参考)android的事件传递机制
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return dragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.qwe.VDHLinearLayout 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:orientation="vertical"
tools:context=".SecoundActivity">
<TextView
android:id="@+id/text_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="not clickable"
android:background="@mipmap/ic_launcher_round"
/>
<TextView
android:id="@+id/text_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="clickable"
android:clickable="true"
android:background="@mipmap/ic_launcher_round"
/>
</com.example.qwe.VDHLinearLayout>
此时把text_2 指定clickable属性,发现控件没办法移动了。这是因为text_2 控件消耗了ACTION_DOWN事件,导致ViewGroup没有调用onTouchEvent。
下面具体分析一个非clickable控件的移动过程。从点击到移动的整个过程。因为非clickable,所以控件并不会处理ACTION_DOWN事件,转而投递到上层ViewGroup处理。即:ViewGroup::onTouchEvent
,然后ViewGroup将调用dragHelper.processTouchEvent(event);
public void processTouchEvent(@NonNull MotionEvent ev) {
...
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
...
tryCaptureViewForDrag(toCapture, pointerId);
...
}
...
}
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
...
//这里设置窗口状态为STATE_DRAGGING
captureChildView(toCapture, pointerId);
...
}
public void captureChildView(@NonNull View childView, int activePointerId) {
...
setDragState(STATE_DRAGGING);
...
}
从调用过程可以看出ViewGroup在onTouchEvent事件里将状态设置为STATE_DRAGGING,此时移动窗口(产生ACTION_MOVE事件),dragHelper.shouldInterceptTouchEvent将返回true,拦截Move事件,触发ViewGroup::onTouchEvent
。
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
...
return mDragState == STATE_DRAGGING;
}
如果view设置clickable属性,view将消耗ACTION_DOWN事件,导致ViewGroup::onTouchEvent
无法被触发 ,从而导致tryCaptureViewForDrag
没有调用,mDragState
状态不为STATE_DRAGGING
。所以后面当触发ACTION_MOVE
的时候返回false(mDragState == STATE_DRAGGING)
故无法移动。
那么如果我们既想使用clickable属性,又想可以正常移动那么该怎么办呢?仔细分析一下processTouchEvent
的ACTION_MOVE
事件。
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
...
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;
...
final int hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
&& (vDragRange == 0 || (vDragRange > 0 && newTop == oldTop))) {
break;
}
}
...
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
...
}
从上述代码我们发现如果mCallback.getViewHorizontalDragRange = 0 && mCallback.getViewVerticalDragRange = 0
那么程序将Break,过滤掉下面tryCaptureViewForDrag
的执行。从上述分析得只mDragState
由tryCaptureViewForDrag
内部置为STATE_DRAGGING。所以此时(mDragState == STATE_DRAGGING)将为false。所以为了使tryCaptureViewForDrag执行,那么我们需要重写mCallback.getViewHorizontalDragRange 或者 mCallback.getViewVerticalDragRange 返回非0就可以了。
2. 支持Clickable view拖动
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
return getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
return getMeasuredHeight();
}
此时在拖拽具有clickable 的view就OK了。
3. 支持松开后回弹
支持松开后回到原始位置dragHelper.settleCapturedViewAt
实现这个需要重写ViewGroup::computeScroll 进行ViewDragHelper内部刷新布局(布局改变之后记得Invalidate)。
- 记录当前需要操作的view(
ViewGroup::onFinishInflate
)。eg:R.id.text_3
- 记录该view的初始位置(
ViewGroup::onLayout
) - 布局发生改变通知(
ViewGroup::computeScroll
) - 当view(
ViewDragHelper.callBack::onViewReleased
)松开时候,设置view位置
ViewGroup class
/*
布局资源加载完,记录view
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mText3 = findViewById(R.id.text_3);
}
/*
布局完成之后,记录view位置
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
nText3Left = mText3.getLeft();
nText3Top = mText3.getTop();
}
/*
位置发生改变通知dragHelper
*/
@Override
public void computeScroll() {
super.computeScroll();
if(dragHelper.continueSettling(true)){
invalidate();
}
}
ViewDragHelper.Callback class
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
Log.i(TAG, String.format("onViewReleased: %d",releasedChild.getId() ));
if(releasedChild == mText3){
dragHelper.settleCapturedViewAt(nText3Left,nText3Top);
invalidate();
}
}
4. 实现边缘拖拽
- 设置边缘可拖拽
dragHelper.setEdgeTrackingEnabled
- 重写边缘拖拽回调
ViewDragHelper.Callback::onEdgeDragStarted
ViewGroup class
// 设置左边缘可以被Drag
dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
ViewDragHelper.Callback class
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
dragHelper.captureChildView(mText4,pointerId);
}
如果我们只允许从左边拖拽,那么我们应该在tryCaptureView
里面过滤掉对应的窗口
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
if(child != mText4){
return true;
}
return false;
}
5. 完整的代码和布局文件
public class VDHLinearLayout extends LinearLayout {
static final String TAG="VDHLinearLayout";
private ViewDragHelper dragHelper;
private TextView mText3;
private TextView mText4;
private int nText3Left = 0,nText3Top = 0;
public VDHLinearLayout(Context context, AttributeSet attrs){
super(context,attrs);
dragHelper = ViewDragHelper.create(this,1.0f,new ViewDragHelper.Callback(){
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
if(child != mText4){
return true;
}
return false;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return top;
}
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
return getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
return getMeasuredHeight();
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
Log.i(TAG, String.format("onViewReleased: %d",releasedChild.getId() ));
if(releasedChild == mText3){
dragHelper.settleCapturedViewAt(nText3Left,nText3Top);
invalidate();
}
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
dragHelper.captureChildView(mText4,pointerId);
}
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
super.onEdgeTouched(edgeFlags, pointerId);
}
});
dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return dragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mText3 = findViewById(R.id.text_3);
mText4 = findViewById(R.id.text_4);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
nText3Left = mText3.getLeft();
nText3Top = mText3.getTop();
}
@Override
public void computeScroll() {
super.computeScroll();
if(dragHelper.continueSettling(true)){
invalidate();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<com.example.qwe.VDHLinearLayout 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:orientation="vertical"
tools:context=".SecoundActivity">
<TextView
android:id="@+id/text_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="not clickable"
android:background="@mipmap/ic_launcher_round"
/>
<TextView
android:id="@+id/text_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="clickable"
android:clickable="true"
android:background="@mipmap/ic_launcher_round"
/>
<TextView
android:id="@+id/text_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="松开后回到初始位置"
android:background="@mipmap/ic_launcher_round"
/>
<TextView
android:id="@+id/text_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="left edge view"
android:background="@mipmap/ic_launcher_round"
/>
</com.example.qwe.VDHLinearLayout>