ViewDragHelper用法介绍,快捷的拖动效果设计

来源:互联网 发布:c语言如何打开doc文件 编辑:程序博客网 时间:2024/06/06 05:24

许多朋友在开发中,经常会碰到许多的拖放,滑动效果处理。手势的判断让人烦不胜烦,还经常出现这样那样的问题,真是心累!

然而,在Android中,Google在其中support库的v4包中,为我们提供类一个强大的类——ViewDragHelper。通过这个类,我们基本可以实现各种各样的滑动与拖放效果。下面我们就来初步的了解一下这个类的用法。


如上图,我们通过ViewDragHelper来实现。

我们自定义一个DragViewLayoutOne,它继承了一个LinearLayout,代码如下

public class DragViewLayoutOne extends LinearLayout {    private ViewDragHelper mViewDragHelper;    public DragViewLayoutOne(Context context) {        super(context);        initView();    }    public DragViewLayoutOne(Context context, AttributeSet attrs) {        super(context, attrs);        initView();    }    public DragViewLayoutOne(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    private void initView() {        mViewDragHelper = ViewDragHelper.create(this, callback);//通过其静态方法创建ViewDragHelper对象    }    @Override    public boolean onInterceptTouchEvent(MotionEvent event) { //重写事件拦截方法        return mViewDragHelper.shouldInterceptTouchEvent(event); //是否拦截,交由ViewDragHelper进行判断    }    @Override    public boolean onTouchEvent(MotionEvent event) {        mViewDragHelper.processTouchEvent(event);//将事件传递给ViewDragHelper处理        return true; //消费事件,使事件处理不在向上移交    }    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { //ViewDragHelper的回调内部类        @Override        public boolean tryCaptureView(View child, int pointerId) { //指定可以被移动的子View            return true;        }        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) { //水平方向上的移动回调            return left;        }        @Override        public int clampViewPositionVertical(View child, int top, int dy) { //垂直方向上的移动回调            return top;        }    };}

创建ViewDragHelper实例

其实DragViewLayoutOne就是一个自定义的ViewGroup,或者说是一个自定义的LinearLayout,我们通过ViewDragHelper的静态方法creat来创建它的对象,下面为creat的源代码:

    public static ViewDragHelper create(ViewGroup forParent, Callback cb) {        return new ViewDragHelper(forParent.getContext(), forParent, cb);    }    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;    }
我们可以看到,ViewDragHelper实际上拥有两个不同参数的creat方法可以进行ViewDragHelper的实例化,不过,可以看到,三个参数的creat中,也是通过调用两个参数的creat方法获得其对象的。

参数介绍:

1、第一个参数forParent就是被监控的父视图,我们监控的是当前自定义的ViewGroup内部子View的移动,所以用this。

2、第二个参数sensitivity是用来设置mTouchSlop的,通过上面源码可以发现,sensitivity值越小,mTouchSlop越大。至于mTouchSlop是什么,可以理解为,被认为是滑动状态的最小距离。简单来说就是,我们手指从ACTION_DOWN到ACTION_MOVE时,两个点之间的距离超过mTouchSlop这个最小临界值时,才会被认为是滑动状态。用来优化用户的交互体验。

3、第三个参数cb就是ViewDragHelper的内部回调对象Callback。

处理触摸事件

    @Override    public boolean onInterceptTouchEvent(MotionEvent event) { //重写事件拦截方法        return mViewDragHelper.shouldInterceptTouchEvent(event); //是否拦截,交由ViewDragHelper进行判断    }    @Override    public boolean onTouchEvent(MotionEvent event) {        mViewDragHelper.processTouchEvent(event);//将事件传递给ViewDragHelper处理        return true; //消费事件,使事件处理不在向上移交    }
通常,我们或重写这个自定义ViewGroup的事件拦截与处理方法,在其拦截方法onInterceptTouchEvent中,通过ViewDragHelper的shouldInterceptTouchEvent(event)
来确定是否进行事件来接。然后在onTouchEvent中,通过ViewDragHelper的processTouchEvent(event),将事件交由ViewDragHelper处理。

对Callback的方法进行实现

 private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { //ViewDragHelper的回调内部类        @Override        public boolean tryCaptureView(View child, int pointerId) { //指定可以被移动的子View            return true;        }        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) { //水平方向上的移动回调            return left;        }        @Override        public int clampViewPositionVertical(View child, int top, int dy) { //垂直方向上的移动回调            return top;        }    };

IDE会帮我们自动重写一个tryCaptureView的方法,并默认返回false,这个方法指定了哪个view是可以被拖动的,我们返回true,所以说有这个自定义VIewGroup下的view都可以被拖动。当然,你也可以指定其中某几个view才能被拖动,通过其传给我们的参数child,他就是被捕获到子view,我们通过child==xxView的判断方式,就可以指定了。

clampViewPositionHorizontal和clampViewPositionVertical方法分别是设置水平和垂直方向上可移动的距离。这两个方法IDE不会为我们自动重写,但是我们根据滑动需要,必须重写其中某个或者全部重写,因为他们默认返回0,代表可滑动距离为0,不重写会发现无法实现滑动效果。

其中,left和top就是对应放下上的滑动距离,我们直接返回,就是可以滑动任意的距离。当然,这样就会是活动可以超出屏幕边界,如果想要在屏幕内左右滑动,还不超出屏幕边界,代码如下:

        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) { //水平方向上的移动回调            final int leftBound = getPaddingLeft(); //左边界数值            final int rightBound = getWidth() - getChildAt(0).getWidth();//右边界数值            final int newLeft = Math.min(Math.max(left, leftBound), rightBound); //可滑动到边界数值            return newLeft;        }

最后,就是布局文件了:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <com.example.caifengyao.drawerlayout.DragViewLayoutOne        android:layout_width="match_parent"        android:layout_height="match_parent"        android:orientation="vertical">        <TextView            android:layout_width="100dp"            android:layout_height="100dp"            android:layout_gravity="center_horizontal"            android:gravity="center"            android:background="@color/colorAccent"            android:text="Drag View One" />        <TextView            android:layout_width="100dp"            android:layout_height="100dp"            android:layout_gravity="center_horizontal"            android:gravity="center"            android:layout_marginTop="10dp"            android:background="@color/colorPrimary"            android:text="Drag View Two" />        <TextView            android:layout_width="100dp"            android:layout_height="100dp"            android:layout_gravity="center_horizontal"            android:gravity="center"            android:layout_marginTop="10dp"            android:background="@color/colorPrimaryDark"            android:text="Drag View Three" />    </com.example.caifengyao.drawerlayout.DragViewLayoutOne></LinearLayout>

到此,我们就实现了图中的滑动效果了。

然而,在ViewDragHelper.Callback中,还为我们提供了许多方便的回调方法,让我们快捷的做出更加多样的滑动拖放效果。


依然是三个view,第一个和之前一样,可以随意拖动。第二个在释放手指的时候,回到初始位置。第三个是通过滑动屏幕边缘,实现对其的拖动。

下面,我们来看看是怎么实现的。

public class DragViewLayoutTwo extends LinearLayout {    private ViewDragHelper mViewDragHelper;    private TextView dragViewOne;    private TextView dragViewTwo;    private TextView dragViewThree;    private int startLeft, startTop; //记录dragViewTwo的初始位置    public DragViewLayoutTwo(Context context) {        super(context);        initView();    }    public DragViewLayoutTwo(Context context, AttributeSet attrs) {        super(context, attrs);        initView();    }    public DragViewLayoutTwo(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    private void initView() {        mViewDragHelper = ViewDragHelper.create(this, callback);        mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);//设置左边缘拖动模式    }    @Override    protected void onFinishInflate() {        super.onFinishInflate();        //获取其中的三个子View        dragViewOne = (TextView) getChildAt(0);        dragViewTwo = (TextView) getChildAt(1);        dragViewThree = (TextView) getChildAt(2);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        //获取dragViewTwo的初始位置        startLeft = dragViewTwo.getLeft();        startTop = dragViewTwo.getTop();    }    @Override    public boolean onInterceptTouchEvent(MotionEvent event) {        return mViewDragHelper.shouldInterceptTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        mViewDragHelper.processTouchEvent(event);        return true;    }    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {        @Override        public boolean tryCaptureView(View child, int pointerId) {            return child == dragViewOne || child == dragViewTwo; //当捕获的child是dragViewOne或dragViewTwo时,可以拖动        }        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) {            return left;        }        @Override        public int clampViewPositionVertical(View child, int top, int dy) {            return top;        }        @Override        public void onViewReleased(View releasedChild, float xvel, float yvel) { //释放捕获的view时执行            super.onViewReleased(releasedChild, xvel, yvel);            if (releasedChild == dragViewTwo) {                mViewDragHelper.smoothSlideViewTo(dragViewTwo, startLeft, startTop);//使dragViewTwo滑动到初始位置                ViewCompat.postInvalidateOnAnimation(DragViewLayoutTwo.this);//刷新页面显示            }        }        @Override        public void onEdgeDragStarted(int edgeFlags, int pointerId) { //当设置了边缘滑动后,会触发此回调方法            //滑动边缘时,捕获dragViewThree为滑动对象。此方法能够越过tryCaptureView指定滑动的View            mViewDragHelper.captureChildView(dragViewThree, pointerId);        }    };    @Override    public void computeScroll() {        if (mViewDragHelper.continueSettling(true)) {            ViewCompat.postInvalidateOnAnimation(this);        }    }}
代码部分注释已经阐述了方法的作用,我们来逐条说明一下:

首先,在实例化ViewDragHelper后,我进行了边缘拖动设置setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)。当然,它还有包扩TOP,RIGHT,BOTTOM,ALL等方向的设置,看单词就可以理解其意思了,大家可以试试。值得一提的时,这个设置时候Callback中的回调onEdgeDragStarted一起使用的,当设置了边缘拖动后,当我触发边缘拖动的时候,就会回调这个方法。我们就是在这个方法中,通过ViewDragHelper的captureChildView方法,确定哪一个View被捕获,作为实现拖动效果的目标View。captureChildView这个方法有个特点,就是它所捕获的View,无需在tryCaptureView回调中指定,也可实现拖动。

然后,就是onViewReleased回调方法了,这个方法在我们释放捕获的View时,就会调用。由于我们例子中是释放dragViewTwo的时候,其回到初始位置,所以我们在回调中判断了被释放的View是dragViewTwo这个条件,releasedChild这个参数就是回调给我们的,指的就是被释放的子View。

我们通过ViewDragHelper的smoothSlideViewTo方法,指定某个子View滑动到指定位置,这个方法实际上是通过Scroller的startScroll方法实现的,因此,我们需要重写computeScroll方法。ViewCompat.postInvalidateOnAnimation是用来刷新界面的,其参数是要刷新的View,注意的是,其指的是父容器,而不是子View本身。而这个方法与invalidate()和postInvalidate()的区别就是,google进行了适配,保证调用刷新不会出现崩溃的现象。还有一点,是个人感觉,它的刷新更加的平滑。

至于mViewDragHelper.continueSettling(true),我们只需知道这是ViewDragHelper提供给我们,用于判断回滚是否结束的就可以了,返回true说明还没有结束,至于参数为什么设置为true。

Set this to true if you are calling this method from{@linkandroid.view.View#computeScroll()}

这是源码的参数解释,告诉我们,如果在android.view.View下的computerScroll中调用,就设置为true。其实,看源码我们会发现,continueSettling内部也是通过Scroller的computeScrollOffset进行回滚结束状态的判断的。

OK,到这,我们就将第二套效果中用到的回调介绍完毕了。


下面,我们对一些比较容易碰到的问题进行一下说明。

有些时候,我们不仅仅要求某个View可以被拖动,它还会拥有自己的点击事件,当我们给一个dragView设置点击事件的时候,会发现,这个dragView无法被拖动了。

看过setOnclickListener源码的同学会知道,它会先将view的setClickable设置为true,其实问题就出在了这里,当一个view可以被点击的时候,ViewDragHelper将不会进行事件的拦截处理,因此最终事件会被这个可点击的View消费。

解决办法:Callback中有两个回调,他们的默认返回值为0,ViewDragHelper会对其进行验证,当捕获的view可点击,并且这两个回调大于0,才可以被拖动,因此我们需要重新这两个回调,并返回大于0的值。

        @Override        public int getViewHorizontalDragRange(View child) {//水平方向的拖动            return 1;        }        @Override        public int getViewVerticalDragRange(View child) {//垂直方向的拖动            return 1;        }

下面我们来说说ViewDragHelper的三个参数的creat方法中,第二个参数的作用,上面说了,这个sensitivity是用来设置mTouchSlop的,sensitivity值越小,mTouchSlop越大。而sensitivity的默认值是1。下面我们将值设置为0.1f。

  mViewDragHelper = ViewDragHelper.create(this, 0.1f, callback);
通过mViewDragHelper.getTouchSlop()我们可以知道mTouchSlop的值。下面我们看看效果:


对于第一个view,貌似没什么特别影响,拖动很顺畅。但是第二个可点击的view,我们发现,在拖动了一段距离之后,才被认定为是拖动状态,之后才有了拖动效果。

对于第三个边界拖动,也是在拖动了一段距离之后,才被认定为拖动状态,出现拖动效果。这就是所谓的被认定为拖动状态的最小滑动临界值。

Demo下载地址:点击打开链接

1 0
原创粉丝点击