前言: 好记性真的不如烂笔头啊,以前用到DrawerLayout的时候,就有自己去看就过ViewDragHelper,这几天又用到的时候,发现我是如此的陌生了,于是还是打算写一篇博客,就当学习笔记了,大牛勿喷(^__^) 嘻嘻……有童鞋说我学习的一些东西都是一些好老的东西了,我当时也是有点苦闷啊,毕竟Android现在都到7.0了,但是我老的东西都还不会啊,新的东西也是建立在老的东西的基础之上的,我觉得搞技术的切勿好高骛远,要脚踏实地,加油吧,骚年!!!!!
那么什么是ViewDragHelper?
/**
* ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
* of useful operations and state tracking for allowing a user to drag and reposition
* views within their parent ViewGroup.
*/
英文不是太好哈,大致翻译一下,“VDH是用于自定义ViewGroup拖动的一个工具类”也号称“自定义ViewGroup的一个神器”,比如(DrawerLayout中的一系列的拖动事件的处理都是利用VDH完成的)既然那么牛掰,我们就从源码的角度一步一步地揭开她的神秘面纱。
定义 | 注释 |
---|---|
int clampViewPositionHorizontal (View child, int left, int dx) | 此方法返回一个值,告诉Helper,这个view能滑动的最大(或者负向最大)的横向坐标 |
int clampViewPositionVertical (View child, int top, int dy) | 此方法返回一个值,告诉Helper,这个view能滑动的最大(或者负向最大)的纵向坐标 |
int getOrderedChildIndex (int index) | 返回这个索引所指向的子视图的Z轴坐标 |
int getViewHorizontalDragRange (View child) | 返回指定View在横向上能滑动的最大距离 |
int getViewVerticalDragRange (View child) | 返回指定View在纵向上能滑动的最大距离 |
void onEdgeDragStarted (int edgeFlags, int pointerId) | 当边缘开始拖动的时候,会调用这个回调 |
boolean onEdgeLock (int edgeFlags) | 返回指定的边是否被锁定 |
void onEdgeTouched (int edgeFlags, int pointerId) | 当边缘被触摸时,系统会回调这个函数 |
void onViewCaptured (View capturedChild, int activePointerId) | 当有一个子视图被指定为可拖动时,系统会回调这个函数 |
void onViewDragStateChanged (int state) | 拖动状态改变时,会回调这个函数 |
void onViewPositionChanged (View changedView, int left, int top, int dx, int dy) | 当子视图位置变化时,会回调这个函数 |
void onViewReleased (View releasedChild, float xvel, float yvel) | 当手指从子视图松开时,会调用这个函数,同时返回在x轴和y轴上当前的速度 |
boolean tryCaptureView (View child, int pointerId) | 系统会依次列出这个父容器的子视图,你需要指定当前传入的这个视图是否可拖动,如果可拖动则返回true 否则为false |
还有一些api我就不一一说明了,还是要多按一下“Ctrl+左键”(^__^) 或者利用科学上网查看官方文档。
下面我们来结合例子说说ViewDragHelper的用法,既然是定义ViewGroup的神器,我们就自己创建一个View叫DragView,为了方便我就直接继承LinearLayout了:
/**
* author:yinqingy
* date:2016-11-06 13:49
* blog:http://blog.csdn.net/vv_bug
* desc:
*/
public class DragView extends LinearLayout{
public DragView(Context context) {
super(context);
init();
}
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
代码很简单,节奏我们创建一个ViewDragHelper,为了打断点调试方便,我直接copy了一份v4的VDH源码,我们不能直接new一个VDH对象,因为它的构造方法法私有化了:
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
既然不允许我们直接new,那么内部一定有方法可以获取到其实例对象,我们看到有这么一个方法:
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
1、需要传入两个参数,传入一个ViewGroup跟一个cb,ViewGroup就是我们当前定义个ViewGroup,cb需要我们自己创建一个,经过我前面介绍的一些方法,想必大家都知道cb的作用了,也就是用于各类事件触发后的回调处理。
2、传入ViewGroup、sensitivity、cb,下面重点说一下sensitivity,这个是敏感度的意思,(int)(helper.mTouchSlop*(1/sensitivity)),一般我们传入1.0f,传入的值越大,敏感度越高。
补充一下吧:
我们看到有一个mTouchSlop,这个值最初在什么地方初始化的呢?
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
mTouchSlop = vc.getScaledTouchSlop();
mTouchSlop到底是什么东西呢?
/**
* @return Distance in pixels a touch can wander before we think the user is scrolling
*/
public int getScaledTouchSlop() {
return mTouchSlop;
}
意思就是应该触发移动事件的最短距离,如果小于这个距离就不触发移动控件,比如viewpager就是利用这个距离来判断是不是要翻页。
不了解也没关系,接下来我们创建一个VDH:
private void init() {
mDragger=ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//当id为edgeview的时候,不允许其滑动
return false
}
传递一个cb,然后重写其抽象方法tryCaptureView,我们上面介绍过tryCaptureView了,“系统会依次列出这个父容器的子视图,你需要指定当前传入的这个视图是否可以被拖动,如果可以拖动就返回true,否则就返回false”返回两个参数,child是正在被捕捉的view,pointerid是当前按下点的id。
我们试试返回true,看看我们的子view可以不可以拖动?
ids.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--可以被拖动的View-->
<item name="draggedview" type="id"/>
<!--拖动后手指一抬起返回初始位置-->
<item name="autobackview" type="id"/>
<!--滑动边缘开始滑动的view-->
<item name="edgeview" type="id"/>
</resources>
test.layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.cisetech.demo.MainActivity">
<com.cisetech.demo.DragView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true">
<TextView
android:layout_margin="10dp"
android:gravity="center"
android:layout_gravity="center"
android:background="#44ff00ff"
android:text="我可以被拖动"
android:layout_width="100dp"
android:layout_height="100dp"/>
<TextView
android:layout_margin="10dp"
android:layout_gravity="center"
android:gravity="center"
android:background="#44ff"
android:text="我不可以被拖动"
android:layout_width="100dp"
android:layout_height="100dp"/>
</com.cisetech.demo.DragView>
</RelativeLayout>
布局很简单,里面就放了两个TextView,一个可以被拖动,一个不可以被拖动。
然后我们改改代码:
private void init() {
mDragger=ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//当id为edgeview的时候,不允许其滑动
return child.getId()==R.id.draggedview;
}
然后再DragView的onInterceptTouchEvent把事件的拦截交给VDH,然后在onTouchEvent中把事件的处理给VDH:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragger.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}
我们运行代码:
我们拖动id为drawggedview是,它直接回到了坐标为(0,0)了,然后怎么拖都不会移动了,为什么会这样呢?是不是我们少写了什么东西呢?我们看看VDH的源码到底干了什么。
当我们点击屏幕的时候,我们最先走的方法是
mDragger.shouldInterceptTouchEvent(ev);
然后我们在VDH的mDragger.shouldInterceptTouchEvent(ev);方法中发现,当ActionEvent为ACTION_DOWN的时候:
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = ev.getPointerId(0);
saveInitialMotion(x, y, pointerId);
final View toCapture = findTopChildUnder((int) x, (int) y);
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
.......
代码有点多,我们直接看重点,首先根据手指按下的位置找到当前子View:
final View toCapture = findTopChildUnder((int) x, (int) y);
接着走了一个判断:
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
然后我们看看tryCaptureViewForDrag干了什么?
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
我们看到一行代码mCallback.tryCaptureView(toCapture, pointerId)是不是很熟悉呢?就是我们在cb里面重写的方法:
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child.getId()==R.id.draggedview
}
那我们一开始不是说了嘛,只要tryCaptureView返回true,这个子view就可以拖动了额,这不是骗人吗?先别激动哈,我们接下来拖动的处理代码,既然IntercepterTouchEvent是拦截事件用的,那事件的处理肯定都在mDragger.processTouchEvent(event);方法中了。
照样processTouchEvent的代码太多,我们捡重点的来说,我们找到这个一段代码:
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(index);
final float y = ev.getY(index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;
final float x = ev.getX(i);
final float y = ev.getY(i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy)
&& tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
我们可以看到一个叫dragTo的方法,传递进入的参数为:滑动过后的left、滑动过后的top、x轴上的距离,y轴上的距离:
private void dragTo(int left, int top, int dx, int dy) {
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
我们在dragTo方法中找到了这个一段代码:
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
}
促使view可以被拖动的代码就是ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);跟ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
我们看到只有当dx跟dy不等于0的时候才会进拖动的代码:
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
我们看到进了判断之后,clampedX就是滑动过后的并且经过cb的clampViewPositionHorizontal方法处理后的结果,我们看看clampViewPositionHorizontal的返回值:
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
可见,如果我们不重写cb的clampViewPositionHorizontal的时候,默认就返回0了,所以(0-oldLeft)=oldLeft,而这个oldLeft就是子view的初始left,移动-oldLeft也就是回到了(0,0)点了,所以也证明了我们一开始滑动我们的子view直接回到原点。
既然知道了原因了,我们就改改代码,重写一下cb的
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
修改后:
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
我们对滑动过后的left不做任何处理,原样返回。
我们再次运行代码:
gif录制的有点问题额,要过会才会有效果…
我们终于实现了我们想要的效果,可见有了VDH的帮助,我们很容易的就实现的一个可以随手指移动而移动的View了,是不是soeasy呢??
VDH暂时就分析到这里了,感兴趣的小伙伴可以继续关注VDH这个系列文章:
为何说Android ViewDragHelper是神器 (二)
为何说Android ViewDragHelper是神器 (实战)
Android 神器ViewDragHelper(实战二)
最后附上demo的git链接:
https://github.com/913453448/SwipeBackLayout
3Q啦!!!!!!!!!!!!!!!!!