欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用ViewDragHelper实现可移动可点击控件

程序员文章站 2022-05-12 16:12:42
...

概述

上一篇文章WindowManager实现可移动可点击(可只在应用中显示)悬浮窗写了利用WindowManager实现可移动可点击的悬浮窗,这一篇主要讲的是在单个界面中利用ViewDragHelper实现可移动可点击控件。

(一)普通方法实现

在利用ViewDragHelper实现之前,我们先用普通方法实现看看,通过自定义一个父布局,在父布局中添加一个可移动的控件。
这里我创建了一个类DragLayout2继承FrameLayout:

public class DragLayout2 extends FrameLayout {
    private Context mContext;
    private View mDragView; //可移动控件
    private float downX; //手指落下时的x坐标值
    private float downY; //手指落下时的y坐标值
    private OnClickListener mOnClickListener; //给可移动控件添加点击事件的回调接口
    private boolean isMove = false; //判断可移动控件是否移动了
    public DragLayout2(Context context) {
        this(context,null);
    }

    public DragLayout2(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
    }
}

添加可移动控件,可以在构造函数中进行:

        //通过布局填充器添加
        mDragView = LayoutInflater.from(mContext).inflate(R.layout.kefu, null);
        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(DensityUtil.dip2px(mContext,52), DensityUtil.dip2px(mContext,52)); //设置控件宽高
        this.addView(mDragView,params);

如果想要通过获取布局中子控件的方式得到mDragView,需要重写onFinishInflate(),在布局完成后得到:

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(1); //1是当前布局下的位置,代表第2个直接子控件
    }

我这里就直接使用第一种方法了,不用再在相应的布局文件中添加。用第一种方法,我这里要让控件出来的时候就直接显示在右下角,如下图所示:
使用ViewDragHelper实现可移动可点击控件
可以通过调用layout(int l, int t, int r, int b)方法设置布局:

    mDragView.layout(getWidth()-mDragView.getWidth(),
                    getHeight()-mDragView.getHeight(),
                    getWidth(),
                    getHeight());

layout四个参数分别代表控件左边距离整个布局左边的距离,控件右边距离整个布局左边的距离,控件顶部距离整个布局顶部的距离,控件底部距离整个布局顶部的距离,参考下图:
使用ViewDragHelper实现可移动可点击控件
这步设置需要放在DragLayout2执行onLayout方法之后,因为在执行该方法后才能获取到整个布局的宽高——getWidth( )和getHeight( )获得的值才不会为0。

然后最重要的就是给控件设置触摸事件了:

    mDragView.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        downX = event.getX();
                        downY = event.getY();
                        isMove = false;
                        break;
                    case MotionEvent.ACTION_MOVE:
                        float xDistance = event.getX() - downX; //x轴移动距离
                        float yDistance = event.getY() - downY; //y轴移动距离
                        if (xDistance != 0 && yDistance != 0) {
                            int l = (int) (v.getLeft() + xDistance);
                            int r = (int) (v.getRight() + xDistance);
                            int t = (int) (v.getTop() + yDistance);
                            int b = (int) (v.getBottom() + yDistance);
                            if (l < 0){ //使移动不能超过左边界
                                l = 0;
                                r = mDragView.getWidth();
                            }
                            if (r > getWidth()){ //使移动不能超过右边界
                                r = getWidth();
                                l = r - mDragView.getWidth();
                            }
                            if (t < 0){ //使移动不能超过上边界
                                t = 0;
                                b = mDragView.getHeight();
                            }
                            if (b > getHeight()){ //使移动不能超过下边界
                                b = getHeight();
                                t = b - mDragView.getHeight();
                            }
                            v.layout(l, t, r, b); //设置位置
                            isMove = true;
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        v.layout(getWidth()-
                                mDragView.getWidth(),
                                v.getTop(),getWidth(),
                                v.getBottom()); //设置手指抬起时控件移到右边
                        if (isMove){
                            return true;
                        }
                        break;
                }
                return false;
            }
        });

触摸事件返回false时事件没被执行,点击事件可以被执行;返回true时说明消费了事件,点击事件就不会再被执行了。这里我定义了一个布尔变量isMove,当它移动了isMove就为true,onTouch方法就返回true,就不会触发点击事件。
然后设置一个setOnClickListener方法,让Activity可以通过调用该方法触发可移动控件的点击事件:

    public void setOnClickListener(OnClickListener onClickListener) {
        mOnClickListener = onClickListener;
    }
        mDragView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mOnClickListener.onClick(v);
            }
        });

最后效果如下:
使用ViewDragHelper实现可移动可点击控件

(二)使用ViewDragHelper实现

使用ViewDragHelper来实现就不用自己写触摸事件了,通过在ViewDragHelper.Callback回调中重写方法就行了。
一、创建实例:

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

通过调用create(ViewGroup forParent, float sensitivity, Callback cb)方法创建实例,该方法有三个参数,第一个参数传入要监视的布局,传入this代表该布局本身;第二个参数是敏感度,1代表正常的敏感度,值越大越敏感;第三个参数是提供信息和接收事件的回调。

private ViewDragHelper mViewDragHelper; //定义一个ViewDragHelper全局变量
mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return child == mDragView;
            }//可以判断是否要捕获当前的view,返回true时表示可以捕获

            @Override  //控制child在水平方向上的位置,不限制边界返回left即可
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                if(left <0){
                    //限制左边界
                    left = 0;
                }else if (left > (getMeasuredWidth() - child.getMeasuredWidth())){
                    //限制右边界
                    left = getMeasuredWidth() - child.getMeasuredWidth();
                }
                return left;
            }

            @Override  //控制child在竖直方向上的位置,不限制边界返回top即可
            public int clampViewPositionVertical(View child, int top, int dy) {
                if(top <0){
                    //限制上边界
                    top = 0;
                }else if (top > (getMeasuredHeight() - child.getMeasuredHeight())){
                    //限制下边界
                    top = getMeasuredHeight() - child.getMeasuredHeight();
                }
                return top;
            }

            @Override  //不重写该方法默认返回0,返回0时若还设置了点击事件则水平方向不能移动
            public int getViewHorizontalDragRange(View child) {
                return getMeasuredWidth() - child.getMeasuredWidth();
            }

            @Override  //不重写该方法默认返回0,返回0时若还设置了点击事件则竖直方向不能移动
            public int getViewVerticalDragRange(View child) {
                return getMeasuredHeight() - child.getMeasuredHeight();
            }

            @Override   //处理手指释放的事件
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                if (releasedChild == mDragView){
                    mViewDragHelper.smoothSlideViewTo(releasedChild,getMeasuredWidth() - releasedChild.getMeasuredWidth(),releasedChild.getTop()); //平滑移动到最右边
                    ViewCompat.postInvalidateOnAnimation(DragLayout.this); //刷新布局
                }
            }
        });

二、触摸、拦截事件:
callback中需要重写的方法弄好之后,需要对事件监听做相应的处理,重写onInterceptTouchEvent和onTouchEvent方法:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);  //由viewDragHelper来判断是否应该拦截此事件
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true; //消费掉事件,让ViewDragHelper来处理
    }

三、处理computeScroll():

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mViewDragHelper.continueSettling(true)){ //判断是否结束
            ViewCompat.postInvalidateOnAnimation(DragLayout.this);
        }
    }

该方法由父级调用,以请求孩子在需要时更新mScrollX和mScrollY的值。不重写该方法,之前手指释放时的动画效果就不会执行。

以上就完成了,其中布局添加和点击事件跟用普通方法实现一样,最终效果图也差不多。
GitHub链接地址