前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ViewDragHelper使用笔记及侧滑菜单实践

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

作者头像
佛系编码
发布2018-05-22 11:17:52
1.3K0
发布2018-05-22 11:17:52
举报

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


}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ViewDragHelper
    • 使用方法
    • 侧滑菜单实现
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档