美文网首页androidAndroid 开发相关文章收集Android开发
ViewDragHelper(一)— 介绍及使用(入门篇)

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

作者: 一枕黄粱终成梦 | 来源:发表于2017-09-20 21:23 被阅读127次

    前言

    随着入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久,零零散散抽时间总算出炉了。第二篇和第三篇后续会抽空抓紧赶时间写出来)

    相关文章

      网友评论

      本文标题:ViewDragHelper(一)— 介绍及使用(入门篇)

      本文链接:https://www.haomeiwen.com/subject/ccbxjxtx.html