ViewDragHelper(一)— 介绍及使用(入门篇)

前言

随着入Android这个坑的时间越来越长,愈加觉得深入掌握原理以及技术输出的重要性,会使用轮子和造一个好轮子还是有天壤之别的。授人以鱼不如授人以渔,将一些经验分享出来,希望能够让更多的人更加深入地理解它,并帮助到有需要的朋友。本系列分为三篇,会由浅至深地对DrageHelper 进行详细讲解。

目录

ViewDragHelper 的介绍以及初步使用请阅读这篇:
ViewDragHelper (一)- 介绍及简单用例(入门篇)
ViewDragHelper 的源码以及Callback的详情介绍请阅读这篇:
ViewDragHelper (二)- 源码及原理解读(进阶篇)
利用DrageHelper 打造仿陌陌APP视频播放页的demo请阅读这篇:
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.

DrageHelper 它是Google官方推出的手势滑动辅助类,极大程度地简化了我们对控件的手势滑动跟踪及处理。让我们能够更加便捷地开发自定义ViewGroup控件,实现拖拽以及弹性滚动等功能。事实上,官方的SlidingPaneLayout和DrawerLayout都是利用ViewDragHelper实现的。掌握它,可以一定程度地减轻我们开发工作难度以及投入精力。

使用入门示例

接下来,我们主要通过一个简单的拖拽以及回弹的demo(类似于QQ空间视频播放页),来讲解如何利用DrageHelper 打造一个 ViewGroup 控件。

QQ空间视频播放页效果图:

QZone.gif

大致步骤如下:

第一步:

创建一个DraggableView类继承自ViewGroup(或者也可用 FrameLayout , RelativeLayout, LinearLayout等)。

package com.test.demo;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;

/**
 * Created by 小嵩 on 2017/9/10.
 */

public class MyDraggableView extends RelativeLayout{


    private ViewDragHelper viewDragHelper;

    public MyDraggableView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        viewDragHelper = ViewDragHelper.create(this, 1.0f, new DraggableViewCallback(this));
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return viewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
    }
}

在onInterceptTouchEvent方法中,通过viewDragHelper.shouldInterceptTouchEvent(event)来决定我们是否应该拦截当前的事件,如果返回的是True,则会触发onTouchEvent。
在onTouchEvent方法中,通过viewDragHelper.processTouchEvent(event)将事件分发给viewDragHelper。

对Android的事件分发机制若还不太理解的话,可自行查资料补一下相关知识。

第二步:

在init方法中用ViewDragHelper的静态方法实例化ViewDragHelper对象

viewDragHelper = ViewDragHelper.create(this, 1.0f, new VerticalDraggableViewCallback(this));

其中第一个参数指的当前的ViewGroup对象,第二个sensitivity参数指的是对滑动检测的灵敏度,越大越敏感,所需触发滑动的距离越小,默认传1.0f 即可。它的源码如下:

 /**
     * 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;
    }

由: helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); 可以显而易见地看出,其实就是mTouchSlop 除以我们传入的sensitivity然后重新赋值。而这个mTouchSlop 是怎么来的呢? 接着看源码,发现是这一段代码进行赋值的:

final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();

由此可见,mTouchSlop 它是获取的系统判定是否触发移动事件的阈值。即:单次移动大于这个值,才会判定是MOVE操作。

第三个参数为静态回调对象CallBack,我们接下来实现相关CallBack方法来操作拖拽的View。

第三步:

实现ViewDragHelper.Callback的相关方法。

/**
 * Created by 小嵩 on 2017/8/30.
 */

public class DraggableViewCallback extends ViewDragHelper.Callback {
   
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return true;
    }
}

其中,ViewDragHelper.Callback 是一个内部静态抽象类, tryCaptureView是必须实现的方法,其余是可选重写的方法,一般来说,我们重写:
clampViewPositionHorizontalView
clampViewPositionVertical
onViewReleased
onViewPositionChanged

这四个方法即可。更多方法的详情及含义,可阅读DrageHelper — 源码深入解析(第二篇)

tryCaptureView,可用于自由判定哪个子控件可被拖拽,返回true代表可拖拽,false则禁止。

第四步:

分别在 clampViewPositionVertical 和clampViewPositionHorizontal 方法中对它的可滑动边界进行控制。left , top 分别为即将移动到的位置,比如我希望只在的水平方向移动,则进行如下处理:

   /**
     * 子控件水平方向位置改变时触发
     */
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        //屏蔽掉水平方向
        return 0;
    }

同时,若我们只希望子控件向下平移,则做以下处理:

   /**
     * 子控件竖直方向位置改变时触发
     */
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        //不能滑出顶部
        return Math.max(top, 0);
    }

第五步:

在onViewReleased 方法中获取移动距离,判断拖拽距离是否超过阈值。若超过阈值,则执行关闭动画,否则处理回弹,Callback完整代码如下:

package com.test.demo;

import android.support.v4.widget.ViewDragHelper;
import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;

/**
 * ViewDragHelper.Callback 拖拽事件监听回调
 *
 * @author 小嵩
 */
class DraggableViewCallback extends ViewDragHelper.Callback {

    private static final String TAG = "DraggableViewCallback";

    private static float Y_MIN_VELOCITY = 300;//竖直方向关闭最小值 px

    private MyDraggableView mDraggableView;


    public DraggableViewCallback(MyDraggableView draggableView) {
        this.mDraggableView = draggableView;
        Y_MIN_VELOCITY = mDraggableView.getHeight() / 3;
    }

    /**
     * 子控件位置改变时触发(包括X和Y轴方向)
     *
     * @param left position.
     * @param top  position.
     * @param dx   change in X position from the last call.
     * @param dy   change in Y position from the last call.
     */
    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        mDraggableView.onViewPositionChanged(changedView, left, top, dx, dy);
    }

    /**
     * 子控件竖直方向位置改变时触发
     */
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        //不能滑出顶部
        return Math.max(top, 0);
    }

    /**
     * 子控件水平方向位置改变时触发
     */
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        //屏蔽掉水平方向
        return 0;
    }

    /**
     * 手指松开时触发
     *
     * @param releasedChild the captured child view now being released.
     * @param xVel          X velocity of the pointer as it left the screen in pixels per second.
     * @param yVel          Y velocity of the pointer as it left the screen in pixels per second.
     */
    @Override
    public void onViewReleased(View releasedChild, float xVel, float yVel) {
        super.onViewReleased(releasedChild, xVel, yVel);
        Log.d(TAG, "onViewReleased");
        int top = releasedChild.getTop(); //获取子控件Y值
        int left = releasedChild.getLeft(); //获取子控件X值

        if (Math.abs(left) <= Math.abs(top)) {//若为竖直滑动
            triggerOnReleaseActionsWhileVerticalDrag(top);
        }
    }

    @Override
    public boolean tryCaptureView(View view, int pointerId) {
        return true;
    }

    /**
     * 计算竖直方向的滑动
     */
    private void triggerOnReleaseActionsWhileVerticalDrag(float yVel) {
        if (yVel > 0 && yVel >= Y_MIN_VELOCITY) {
            mDraggableView.closedToBottom();
            Log.d(TAG, "ReleaseVerticalDrag" + ", closeToBottom");
        } else {
            mDraggableView.onReset();
            Log.d(TAG, "ReleaseVerticalDrag" + ", onReset");
        }
    }
}

第六步:

在自定义控件MyDraggableView中处理监听回调事件。手指松开时,会有两种情况:

1.当拖拽滑动距离未达到我们设定的值,则重置到原来位置:

    public void onReset() {
        Log.d(TAG, "onReset");
        viewDragHelper.settleCapturedViewAt(0, 0);
        ViewCompat.postInvalidateOnAnimation(this);
    }

2.拖拽滑动距离超过设定的值,滑向底部关闭:

    public void closedToBottom() {
        Log.d(TAG, "closedToBottom");
        if (viewDragHelper.smoothSlideViewTo(this, 0, getHeight())) {
            ViewCompat.postInvalidateOnAnimation(this);
            notifyClosedToBottomListener();
        }
    }

其中,我们重写了computeScroll 方法,以便在手指松开时,触发系统自动滑动。代码如下:

  @Override
    public void computeScroll() {
        if (viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

computeScroll的具体原理这里就不阐述了,不懂的话可以自行Google/百度 查找 View 的computeScroll 实现原理以及源码。

到这儿我们就已经实现了拖拽下拉关闭的功能了,效果演示如下:

下拉拖拽关闭.gif

类似地,如果我们需要实现向左或者向右拖拽回弹或者关闭的功能,只需要把ViewDragHelper.Callback里面clampViewPositionHorizontal以及clampViewPositionVertical方法稍加修改,然后在onViewReleased回调一下,执行viewDragHelper.smoothSlideViewTo()方法让View平顺移动到指定位置即可。具体实际情况可自行实践操作一波。

稍微修改一下代码,改成左右拖拽,代码如下:

    /**
     * 子控件竖直方向位置改变时触发
     */
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
//        return Math.max(top, 0);//不能滑出顶部
        return 0;
    }

    /**
     * 子控件水平方向位置改变时触发
     */
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
//        return 0;
        return left;
    }
    
    /**
     * 手指松开时触发
     */
      @Override
    public void onViewReleased(View releasedChild, float xVel, float yVel) {
        super.onViewReleased(releasedChild, xVel, yVel);
        Log.d(TAG, "onViewReleased");
        int top = releasedChild.getTop(); //获取子控件Y值
        int left = releasedChild.getLeft(); //获取子控件X值

        if (Math.abs(left) <= Math.abs(top)) {//若为竖直滑动
            triggerOnReleaseActionsWhileVerticalDrag(top);
        } else {
            triggerOnReleaseActionsWhileHorizontalDrag(left);
        }
    }
    
 /**
   * 计算水平方向
   */
 private void triggerOnReleaseActionsWhileHorizontalDrag(int xVel) {
        if (xVel > 0 && xVel >= X_MIN_VELOCITY) {
            mDraggableView.closedToRight();
            Log.d(TAG, "ReleaseVerticalDrag" + ", closedToRight");
        } else if (xVel < 0 && Math.abs(xVel) >= X_MIN_VELOCITY) {
            mDraggableView.closedToLeft();
            Log.d(TAG, "ReleaseVerticalDrag" + ", closedToLeft");
        } else {
            mDraggableView.onReset();
            Log.d(TAG, "ReleaseVerticalDrag" + ", onReset");
        }
    }

效果如下:

水平拖拽关闭.gif

结语:

读完这篇文章之后,若觉得有哪里写的不够详细或是有更多的建议,欢迎指出~ 也非常感谢各位的支持和收藏点赞。

下一篇将围绕源码进行解析它的运行原理以及所提供的方法,文章链接:ViewDragHelper (二)- 源码及原理解读(进阶篇)
这篇文章将会详细讲解ViewDragHelper它提供的方法所代表的含义,以及实现原理等。相信读完理解这篇文章的内容之后,对ViewDragHelper的基本操作会有一个更全面的理解。

(By the way,最近工作有点忙,一篇文章躺在草稿箱N久,零零散散抽时间总算出炉了。第二篇和第三篇后续会抽空抓紧赶时间写出来)

推荐阅读更多精彩内容