自定义控件-拖拽实现开关切换

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义控件-拖拽实现开关切换相关的知识,希望对你有一定的参考价值。

 

在这里,我们的主要工作就是在原有代码的基础上,增加一个重写的onTouchEvent方法,刚添加上来的时候是这个样子的:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

对于触摸事件来说,一般返回值为true的话,那么就代表在这里消费掉本次触摸,而返回false的话,就在当前位置对本次触摸不做处理或者不能完全处理,还需要继续将本次事件分发给后续view或者viewgroup响应,对于这里,我们定义的开关按钮已经是子view了,所以这里返回true就可以,现在看起来我们直接将return super.onTouchEvent(event);这一句删掉,然后加上return true;就可以了,会不会产生什么问题呢?我们暂时先这样处理。

之后的工作就比较简单了,我们需要根据event.getAction()的类型来做相应的处理,即我们基本上每天都在使用的:MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP

 

由于在ACTION_MOVE的时候,我们想让滑块随着我们手指的位置的移动而移动,这里由于只是水平移动,所以我们只需要记录x方向的位置,需要两个值:int firstXint secondXfirstX负责记录上一次ACTION_MOVE时候的x值,secondX负责记录本次ACTION_MOVE时候的x值,然后这两个值相减,则可以得到手指在两次ACTION_MOVE触发的时间里移动的距离,得到这个距离之后还需要将firstX调整为当前的值,以便下次使用。然后将这个距离差值与我们滑块的位置进行求和,这样就可以调整我们滑块的位置随手指移动了,当然滑块的位置是有一个范围的,这里应该是[0,MAX_LEFT_DISTANCE]MAX_LEFT_DISTANCE = backgroundBitmap.getWidth()- slideButton.getWidth();,所以我们需要做一个判断,来限制滑块,不让其划出边界,好了,基本逻辑到这里,看看代码:

/**
     * 获取屏幕点击事件
     * 类似于ios中touchBegan方法
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        /**
         * 处理触摸手势
         * recognizer.state
         * UIGestureRecognizerStateBegan
         * UIGestureRecognizerStateChanged
         * UIGestureRecognizerStateEnd
         */
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            /**
             * 获取触摸点的x坐标
             * 类似于ios中的CGPoint point = [recognizer locationInView:recogznier.view];
             */
            firstTouchX = secondTouchX = (int)event.getX(); 
            isDrag = false;
            
            Log.i("test", "ACTION_DOWN..........");
            break;
        case MotionEvent.ACTION_MOVE:
            // 当移动的时候,计算手指在屏幕上移动的距离
            int distanceX = (int) (event.getX() - secondTouchX);
            if (Math.abs(distanceX) > 5) {
                isDrag = true;
            }
            
            secondTouchX = (int) event.getX();
            
            // 将滑动的距离累加到滑动按钮的X值
            switchBtnX = switchBtnX+distanceX;
            
            // 做一个判断,防止滑块超出边界,滑块的范围应该在[0,MAX_LEFT_DISTANCE]
            if (switchBtnX < 0) {
                switchBtnX = 0;
            } else {
                if (switchBtnX > switchBtnMaxSlideDistance) { // 如果按钮的X坐标超过了按钮可以移动的最大距离
                    switchBtnX = switchBtnMaxSlideDistance;
                }
            }
            Log.i("test", "ACTION_MOVE..........");
            break;
        case MotionEvent.ACTION_UP:
            // 当抬起的时候,判断松开的位置是哪里,由此决定开关的的状态
            if (switchBtnX < switchBtnMaxSlideDistance / 2) {
                switchBtnX = 0;
            } else if (switchBtnX >= switchBtnMaxSlideDistance /2 ) {
                switchBtnX = switchBtnMaxSlideDistance;
            }
            break;
        default:
            break;
        }
        invalidate(); // 刷新当前状态,类似于ios中的setNeedDisplay方法
        return true;
    }

写成现在这样会导致只有拖动效果,而点击效果不起作用了。。。

 

问题在哪里呢,实际上对于一个view来说,当点击事件传入的时候是先会调用onTouchEvent方法,然后才是调用onClickonLongClick等方法;但是我们也没有看到这个方法里面能够如何调用onClick方法啊?其实,秘密就在我们之前删掉的

return super.onTouchEvent(event);这一句里面。

 

我们不妨进入到super.onTouchEvent(event);里面一探究竟,找到switch(event.getAction()){}这一块核心代码:

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
          case MotionEvent.ACTION_UP:
            ...
             if (!post(mPerformClick)) {
                                    performClick();
                                }
            ...
        break;
        case MotionEvent.ACTION_DOWN:
            ...
             else {
                            // Not inside a scrolling container, so show the feedback right away
                            setPressed(true);
                            checkForLongClick(0);
                        }
            ...
        break;
        ...
    }
    ...
}

而在performClick()之中我们可以发现:

public boolean performClick() {
    ...
    li.mOnClickListener.onClick(this);
    ...
}

到这里基本上就清楚了,onClickonLongClick是在super.onTouchEvent方法里被调用的,onClick是在ACTION_UP的时候可能被调用,而onLongClick是在ACTION_DOWN的时候可能被调用。所以在这里我们虽然去掉了 return super.onTouchEvent(event);这一句,但是super.onTouchEvent(event);是需要保留的。

 

做完这一步之后我们会发现又可以响应到onClick方法了,但是还是有些不完美;我们希望在拖动之后就不要有点击操作,点击也不要有拖动效果(点击当然不会有拖动效果...),由于onClick点击的判断只是单纯的检测ACTION_DOWN之后是否有一个ACTION_UP,如果有,那么就判断为一次点击事件,至于中间的过程是否滑动了,它却不管。所以在这里我们需要再加上一个boolean类型的判断标志isDrag,在ACTION_DOWN的时候将其设置为false,如果触发了ACTION_MOVE了,则将其置为true。然后在onClick方法里面,加上一个条件,如果isDrag为假才执行点击的对应操作,否则就跳过。这样就比较完美了。。。来看看最终的onTouchEventonClick的写法:

private boolean isDrag = false;
    
    /**
     * 获取屏幕点击事件
     * 类似于ios中touchBegan方法
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        /**
         * 处理触摸手势
         * recognizer.state
         * UIGestureRecognizerStateBegan
         * UIGestureRecognizerStateChanged
         * UIGestureRecognizerStateEnd
         */
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            /**
             * 获取触摸点的x坐标
             * 类似于ios中的CGPoint point = [recognizer locationInView:recogznier.view];
             */
            firstTouchX = secondTouchX = (int)event.getX(); 
            isDrag = false;
            
            Log.i("test", "ACTION_DOWN..........");
            break;
        case MotionEvent.ACTION_MOVE:
            // 当移动的时候,计算手指在屏幕上移动的距离
            int distanceX = (int) (event.getX() - secondTouchX);
            if (Math.abs(distanceX) > 5) {
                isDrag = true;
            }
            
            secondTouchX = (int) event.getX();
            
            // 将滑动的距离累加到滑动按钮的X值
            switchBtnX = switchBtnX+distanceX;
            
            // 做一个判断,防止滑块超出边界,滑块的范围应该在[0,MAX_LEFT_DISTANCE]
            if (switchBtnX < 0) {
                switchBtnX = 0;
            } else {
                if (switchBtnX > switchBtnMaxSlideDistance) { // 如果按钮的X坐标超过了按钮可以移动的最大距离
                    switchBtnX = switchBtnMaxSlideDistance;
                }
            }
            Log.i("test", "ACTION_MOVE..........");
            break;
        case MotionEvent.ACTION_UP:
            // 当抬起的时候,判断松开的位置是哪里,由此决定开关的的状态
            if (switchBtnX < switchBtnMaxSlideDistance / 2) {
                switchBtnX = 0;
            } else if (switchBtnX >= switchBtnMaxSlideDistance /2 ) {
                switchBtnX = switchBtnMaxSlideDistance;
            }
            break;
        default:
            break;
        }
        invalidate(); // 刷新当前状态,类似于ios中的setNeedDisplay方法
        return super.onTouchEvent(event);
    }
/** 当点击时调用此方法 */
    private void switchBtnClick() {
        if (switchBtnX == 0) {
            switchBtnX = switchBtnMaxSlideDistance;
        } else {
            switchBtnX = 0;
        }
        
        invalidate(); // 类似于ios中[self setNeedDisplay]方法
    }

最终效果:

技术分享

 



以上是关于自定义控件-拖拽实现开关切换的主要内容,如果未能解决你的问题,请参考以下文章

Ext 6.5.3 classic版本,自定义实现togglefield开关控件

Android高级控件——自定义ListView高仿一个QQ可拖拽列表的实现

Android中的自定义控件

自定义三档半圆开关控件

Android自定义View之可拖拽悬浮控件 代码赏析

自定义video控件