ViewDragHelper使用笔记及侧滑菜单实践

ViewDragHelper

一个拖拽实现的帮助类,存在于v4包中,对于实现简单的拖拽简直不要太简单;再也不用去重写onTouch()了;

官网API https://developer.android.com/reference/android/support/v4/widget/ViewDragHelper.html

该类主要用于拖拽view的实现,例如侧滑菜单时候的左右拖拽或者上下拖拽

使用方法

创建 ViewDragHelper实例

ViewDragHelper create (ViewGroup forParent, 
               float sensitivity, 
               ViewDragHelper.Callback cb)
  • 参数1 要使用DragHelper的布局
  • 参数2 灵敏度,值越大越灵敏,1.0属于正常
  • 参数3 回调,这里是主要阵地

事件拦截于处理

重写布局的 onInterceptTouchEvent()方法 ,ViewDragHelper会自行判断是否需要拦截事件

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

重写布局的 onTouchEvent() 处理拦截的事件

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

如果需要滑动动画 重写 computeScroll()

/**
 * 因为要在 DragHelper的中使用动画
 */
@Override
public void computeScroll() {
    super.computeScroll();
    if (mDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

前面在创建ViewDragHelper的时候用到了一个Callback,其实主要是在这里添加自己的逻辑,主要介绍一下这个类 这是一个抽象类,我们必须要实现的方法只有一个tryCaptureView()

public abstract boolean tryCaptureView(View child, int pointerId);
  • 参数1 准备捕获的子view
  • 参数2 准备捕获的指针ID

该方法的返回值决定了ViewDragHelper是否要捕获这个view;如果返回false就不捕获;

说的一下我的思路吧,我在父布局中持有了需要拖拽处理的子view的引用,如果捕获的view是我持有的view就返回true,捕获这次事件

/**
 * 决定是否捕获此view
 * 这里自由决定
 * @param child 待捕获的子元素
 * @param pointerId
 * @return 是否捕获
 */
@Override
public boolean tryCaptureView(View child, int pointerId) {
    return child==mContentView;
}

如果你需要处理水平拖拽,重写 clampViewPositionHorizontal() 即可,该方法返回值就是view拖拽后的坐标值;默认是不处理的;

下面是我的实现,为了防止拖拽出屏幕做了简单处理

/**
 * 水平 拖动
 * @param child 拖动的元素
 * @param left 将要去往的位置
 * @param dx 拖动了的距离
 * @return 新位置
 */
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    //限制在容器内
    int leftBound = getPaddingLeft();
    int rightBound = getWidth() - mContentView.getWidth();
    int newLeft = Math.min(Math.max(left,leftBound),rightBound);
    return newLeft;
}

如果需要处理垂直拖拽,重写 clampViewPositionVertical() ;该方法和上面的那个方法一样,返回值就是view拖拽后的坐标值;默认不处理;

/**
 * 垂直拖动
 * @param child
 * @param top
 * @param dy
 * @return
 */
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
    int topBound = getPaddingTop();
    int bottomBound = getHeight() - mContentView.getHeight();
    int newTop = Math.min(Math.max(top,topBound),bottomBound);
    return newTop;
}

可以从onViewDragStateChanged()方法中得到ViewDragHelper的状态变化

@Override
public void onViewDragStateChanged(int state) {
    switch (state){
        case ViewDragHelper.STATE_IDLE:
            Log.e("onViewDragStateChanged","state-->STATE_IDLE"+state);
            break;
        case ViewDragHelper.STATE_DRAGGING:
            Log.e("onViewDragStateChanged","state-->STATE_DRAGGING"+state);
            break;
        case ViewDragHelper.STATE_SETTLING:
            Log.e("onViewDragStateChanged","state-->STATE_SETTLING"+state);
            break;
    }
}

当子view的位置发生变化会触发 onViewPositionChanged() 方法

/**
 * 当 view 的 position发生改变时触发
 * @param changedView 拖动的view
 * @param left 新位置 X轴
 * @param top 新位置 Y轴
 * @param dx 从上次位置 到这次位置移动的距离 X轴
 * @param dy 从上次位置 到这次位置移动的距离 Y轴
 */
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
}

拖动动作停止,可以从 onViewReleased() 中得到速度信息

/**
 * 
 * @param releasedChild
 * @param xvel x 轴速度  每秒移动的像素值
 * @param yvel Y 轴速度 每秒移动的像素值
 */
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    Log.e("onViewReleased","xvel-->"+xvel+";yvel-->"+yvel);
}

我用到的就这些,就介绍这些吧 , 实现了一个可以自由拖动的layout 看Demo中的DragLayout ; https://github.com/sky-mxc/AndroidDemo/tree/master/drag

侧滑菜单实现

以前写过一个侧滑菜单,思路是重写 ListView或者RecycleView 的onTouch事件,判断根据坐标点判断找到子view,然后让子view滑动,从而实现的侧滑。感觉比较麻烦。今天说一下另外一个思路,

写一个通用的布局,例如一个LineaLayout,里面定义两个Group,一个是item内容,另一个是Item 菜单;在LineaLayout内部定义一个ViewDragHelper来处理拖动事件。ViewDragHelper会将拖动事件处理好,我们只需要在callback中处理简单的逻辑就好。 写一个SwipeLayout 继承自 LineaLayout; 在构造时就创建好 DragHelper

public SwipeItemLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDragHelper = ViewDragHelper.create(this, 1.0f, new SwipeItemDragHelper());
}

加载完毕布局之后,拿到两个item,一个内容,一个菜单

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mContentView = getChildAt(0);
    mActionView = getChildAt(1);
}

事件交由 DragHelper处理

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

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

要在callback中使用动画

/**
 * 因为要在 DragHelper的中使用动画
 */
@Override
public void computeScroll() {
    super.computeScroll();
    if (mDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

主要逻辑就在callback中处理 tryCaptureView() 如果拖动的时内容或者菜单就捕获此次多动

@Override
public boolean tryCaptureView(View child, int pointerId) {
    return child == mContentView || child == mActionView;
}

因为实现的是侧滑菜单,这里只处理 水平拖动就好,注释写的很清楚了

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    dragDx+=dx;
    if (child == mContentView) {
        /**
         * 这个位置 的范围应该是在 0和 -dragDistance之间;最大是0;最小是 -dragDistance
         */
        int leftBound = getPaddingLeft();
        int minLeft = -leftBound - mDragDistance;
        int newLeft = Math.min(Math.max(minLeft, left), 0);
        return newLeft;
    } else {
        /**
         * 这个view的位置范围应该是在 父布局的宽度-actionView的宽和父布局的宽度之间;
         */
        int leftBound = getPaddingLeft();
        int minLeft = getWidth() - leftBound - mActionView.getWidth();
        int newLeft = Math.min(Math.max(minLeft, left), getWidth());
        return newLeft;
    }
}

当view 被拖动的时候,另一个view跟随被拖动的view一起移动

@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    //同时移动
    if (changedView == mContentView) {
        mActionView.offsetLeftAndRight(dx);
    } else {
        mContentView.offsetLeftAndRight(dx);
    }
    invalidate();

}

当滑动结束后,可以根据滑动的速度或者滑动的距离来决定是否要打开或者关闭菜单;具体思路 注释已经很清楚了

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    /**
     * 这里的速度 是这样计算的  每秒的拖动的像素 值
     * 速度判断
     *  如果向→滑动 速度肯定是 正数;
     *  如果向←滑动 速度肯定是 负数
     * 如果 拖动距离 是 actionView的 ¼ 就允许打开或关闭
     */
    //根据速度决定是否打开
    boolean settleToOpen = false;
    float realVel = Math.abs(xvel);
    int realDragX = Math.abs(dragDx);
    if (realVel > AUTO_OPEN_SPEED_LIMIT) { //根据速度判断
        if (xvel > 0) { //右滑
            settleToOpen = false;
        } else {  //左滑
            settleToOpen = true;
        }
    }else if(realDragX> mDragDistance/4){  //根据拖动距离判断
        if (dragDx>0){ //右滑
            settleToOpen = false;
        }else{
            settleToOpen = true;
        }
    }
    isOpen = settleToOpen;
    int settleDestX = isOpen ? -mDragDistance : 0;
    Log.e("onViewReleased", "settleToOpen->" + settleToOpen + ";destX->" + settleDestX + ";xvel->" + xvel + ";dragDx-->" + dragDx);
    mDragHelper.smoothSlideViewTo(mContentView, settleDestX, 0);
    ViewCompat.postInvalidateOnAnimation(SwipeItemLayout.this);
    dragDx = 0;
}
}

为了滑动更加灵敏,在左右滑动item时,禁止父布局的上下滑动

在onTouch中 判断滑动距离,超过一定范围就不让父布局处理;getParent().requestDisallowInterceptTouchEvent(true);

@Override
public boolean onTouchEvent(MotionEvent event) {
    mDragHelper.processTouchEvent(event);
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            x = event.getRawX();
            break;
        case MotionEvent.ACTION_MOVE:
            float gap = event.getRawX() - x;
            int sl = ViewConfiguration.get(getContext()).getScaledTouchSlop();
            if (Math.abs(gap) > sl) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            x = 0;
            break;
    }
    return true;
}

贴一下代码,完整Demo看GitHub https://github.com/sky-mxc/AndroidDemo/tree/master/drag

/**
 * Created by mxc on 2017/7/23.
 * description:
 */

public class SwipeItemLayout extends LinearLayout {
    private final double AUTO_OPEN_SPEED_LIMIT = 500.0;
    private View mActionView;
    private View mContentView;
    private int mDragDistance;
    private ViewDragHelper mDragHelper;
    private boolean isOpen;


    public SwipeItemLayout(Context context) {
        this(context, null);
    }

    public SwipeItemLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeItemLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDragHelper = ViewDragHelper.create(this, 1.0f, new SwipeItemDragHelper());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mContentView = getChildAt(0);
        mActionView = getChildAt(1);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mDragDistance = mActionView.getMeasuredWidth();
    }

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


    float x = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                x = event.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                float gap = event.getRawX() - x;
                int sl = ViewConfiguration.get(getContext()).getScaledTouchSlop();
                if (Math.abs(gap) > sl) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                x = 0;
                break;
        }
        return true;
    }


    /**
     * 因为要在 DragHelper的中使用动画
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    class SwipeItemDragHelper extends ViewDragHelper.Callback {

        private int dragDx;

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == mContentView || child == mActionView;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            dragDx+=dx;
            if (child == mContentView) {
                /**
                 * 这个位置 的范围应该是在 0和 -dragDistance之间;最大是0;最小是 -dragDistance
                 */
                int leftBound = getPaddingLeft();
                int minLeft = -leftBound - mDragDistance;
                int newLeft = Math.min(Math.max(minLeft, left), 0);
                return newLeft;
            } else {
                /**
                 * 这个view的位置范围应该是在 父布局的宽度-actionView的宽和父布局的宽度之间;
                 */
                int leftBound = getPaddingLeft();
                int minLeft = getWidth() - leftBound - mActionView.getWidth();
                int newLeft = Math.min(Math.max(minLeft, left), getWidth());
                return newLeft;
            }
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            //同时移动
            if (changedView == mContentView) {
                mActionView.offsetLeftAndRight(dx);
            } else {
                mContentView.offsetLeftAndRight(dx);
            }
            invalidate();
//            Log.e("onViewPosition", "dx-->" + dx);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            /**
             * 这里的速度 是这样计算的  每秒的拖动的像素 值
             * 速度判断
             *  如果向→滑动 速度肯定是 正数;
             *  如果向←滑动 速度肯定是 负数
             * 如果 拖动距离 是 actionView的 ¼ 就允许打开或关闭
             */
            //根据速度决定是否打开
            boolean settleToOpen = false;
            float realVel = Math.abs(xvel);
            int realDragX = Math.abs(dragDx);
            if (realVel > AUTO_OPEN_SPEED_LIMIT) { //根据速度判断
                if (xvel > 0) { //右滑
                    settleToOpen = false;
                } else {  //左滑
                    settleToOpen = true;
                }
            }else if(realDragX> mDragDistance/4){  //根据拖动距离判断
                if (dragDx>0){ //右滑
                    settleToOpen = false;
                }else{
                    settleToOpen = true;
                }
            }
            isOpen = settleToOpen;
            int settleDestX = isOpen ? -mDragDistance : 0;
            Log.e("onViewReleased", "settleToOpen->" + settleToOpen + ";destX->" + settleDestX + ";xvel->" + xvel + ";dragDx-->" + dragDx);
            mDragHelper.smoothSlideViewTo(mContentView, settleDestX, 0);
            ViewCompat.postInvalidateOnAnimation(SwipeItemLayout.this);
            dragDx = 0;
        }
    }


}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android开发指南

2.ui

50290
来自专栏Jack的Android之旅

淘宝开源库VLayout实践

最近淘宝出了vlayout,刚开始看淘宝的文档的时候还是有点懵,后来自己也总结规划了一下,写了一个比较好看的demo,顺便在这里总结一下。

24720
来自专栏xingoo, 一个梦想做发明家的程序员

【插件开发】—— 7 SWT布局详解,不能再详细了!

前文回顾: 1 插件学习篇 2 简单的建立插件工程以及模型文件分析 3 利用扩展点,开发透视图 4 SWT编程须知 5 SWT简单控件的使用与布局搭...

244100
来自专栏三好码农的三亩自留地

打造Android微信朋友圈下拉刷新控件

我们初步分析下,界面上主要有二个控件,一个彩虹状的圆形LoadingView,一个是ListView,那么我大致可以有下面三个步骤:

35120
来自专栏Android开发与分享

【Android】造轮子:轮播图

44250
来自专栏Android相关

RecyclerView的NestedScroll实现

16920
来自专栏Android干货

Android项目实战(十四):TextView显示html样式的文字

47080
来自专栏androidBlog

自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/details/...

13610
来自专栏项勇

笔记20 | 学习整理开源APP(BaseAnimation)程序源码“中的通讯录效果(一)

17550
来自专栏androidBlog

仿网易新闻的顶部导航指示器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/details...

16310

扫码关注云+社区

领取腾讯云代金券