为何说Android ViewDragHelper是神器 (一)

前言: 好记性真的不如烂笔头啊,以前用到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啦!!!!!!!!!!!!!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值