ViewDragHelper实现view拖拽

在了解View拖拽之前,应该了解android的事件传递机制
ViewDragHelper- 源码及原理解读(进阶篇)

1. 实现基本拖拽
ViewDragHelper 可以方便我们快速实现View拖拽功能。主要步骤如下

  1. 创建ViewDragHelper实例
  2. 实现ViewDragHelper的CallBack编写
  3. 处理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;
  }
}
  1. 创建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) {

  1. 实现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;
   }
}
  1. 处理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属性,又想可以正常移动那么该怎么办呢?仔细分析一下processTouchEventACTION_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的执行。从上述分析得只mDragStatetryCaptureViewForDrag内部置为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)。

  1. 记录当前需要操作的view(ViewGroup::onFinishInflate)。eg:R.id.text_3
  2. 记录该view的初始位置(ViewGroup::onLayout)
  3. 布局发生改变通知(ViewGroup::computeScroll
  4. 当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. 实现边缘拖拽

  1. 设置边缘可拖拽dragHelper.setEdgeTrackingEnabled
  2. 重写边缘拖拽回调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>
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值