Android自定义控件系列三:自定义开关按钮(二)

接上一篇自定义开关按钮(一)的内容继续。上一次实现了一个开关按钮的基本功能,即自定义了一个控件,开关按钮,实现了点击切换开关状态的功能。今天我们想在此基础之上,进一步实现触摸拖拽开关滑块来实现开关的功能,还是一样先来看看效果,这里由于要显示拖拽,我打开了开发者选项中的显示触摸操作,会在屏幕上显示一个圆圈表示触摸位置:



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


[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public boolean onTouchEvent(MotionEvent event) {  
  3.         return super.onTouchEvent(event);  
  4.     }  


对于触摸事件来说,一般返回值为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();,所以我们需要做一个判断,来限制滑块,不让其划出边界,好了,基本逻辑到这里,看看代码:


[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public boolean onTouchEvent(MotionEvent event) {  
  3.   
  4.         switch (event.getAction()) {  
  5.   
  6.         case MotionEvent.ACTION_DOWN:  
  7.             // 当按下的时候  
  8.             firstX = secondX = (int) event.getX();  
  9.             break;  
  10.         case MotionEvent.ACTION_MOVE:  
  11.             // 当移动的时候  
  12.             // 计算手指在屏幕上移动的距离  
  13.             int disX = (int) (event.getX() - secondX);  
  14.             secondX = (int) event.getX();  
  15.   
  16.             // 将本次的位置,设置给lastX  
  17.             slideBtn_left = slideBtn_left + disX;  
  18.   
  19.             // 做一个判断,防止滑块划出边界,滑块的范围应该是在[0,MAX_LEFT_DISTANCE];  
  20.             if (slideBtn_left < 0) {  
  21.                 slideBtn_left = 0;  
  22.             } else {  
  23.                 if (slideBtn_left > MAX_LEFT_DISTANCE) {  
  24.                     slideBtn_left = MAX_LEFT_DISTANCE;  
  25.                 }  
  26.             }  
  27.   
  28.             break;  
  29.         case MotionEvent.ACTION_UP:  
  30.             // 当抬起的时候  
  31.   
  32.             // 抬起的时候,判断松开的位置是哪里,来由此来决定开关的状态是打开还是关闭  
  33.             if (slideBtn_left < MAX_LEFT_DISTANCE / 2) {  
  34.                 currentState = false;  
  35.             } else if (slideBtn_left >= MAX_LEFT_DISTANCE / 2) {  
  36.                 currentState = true;  
  37.             }  
  38.   
  39.             // 由开关的状态标志,确定应该是打开还是关闭状态  
  40.             flushState();  
  41.             break;  
  42.         }  
  43.   
  44.         // 根据状态标志来刷新对应的滑块停止位置,从而实现打开或者关闭效果  
  45.         flushView();  
  46.   
  47.         // 返回true意味着消费掉本次事件,不让其他控件还可以接收到这个事件  
  48.         return true;  
  49.     }  



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


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


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


[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.  if (((viewFlags & CLICKABLE) == CLICKABLE ||  
  2.                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
  3.             switch (event.getAction()) {  
  4.           case MotionEvent.ACTION_UP:  
  5.             ...  
  6.              if (!post(mPerformClick)) {  
  7.                                     performClick();  
  8.                                 }  
  9.             ...  
  10.         break;  
  11.         case MotionEvent.ACTION_DOWN:  
  12.             ...  
  13.             else {  
  14.                             // Not inside a scrolling container, so show the feedback right away  
  15.                             setPressed(true);  
  16.                             checkForLongClick(0);  
  17.                         }  
  18.             ...  
  19.         break;  
  20.         ...  
  21.     }  
  22.     ...  
  23. }  


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

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public boolean performClick() {  
  2.     ...  
  3.     li.mOnClickListener.onClick(this);  
  4.     ...  
  5. }  


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


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

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public boolean onTouchEvent(MotionEvent event) {  
  3.   
  4.   
  5.         super.onTouchEvent(event);// 这一句不能少,否则无法触发onclick事件  
  6.   
  7.   
  8.         switch (event.getAction()) {  
  9.   
  10.   
  11.         case MotionEvent.ACTION_DOWN:  
  12.             // 当按下的时候  
  13.             isDrag = false;  
  14.             firstX = secondX = (int) event.getX();  
  15.             break;  
  16.         case MotionEvent.ACTION_MOVE:  
  17.             // 当移动的时候  
  18.             isDrag = true;  
  19.             // 计算手指在屏幕上移动的距离  
  20.             int disX = (int) (event.getX() - secondX);  
  21.             secondX = (int) event.getX();  
  22.   
  23.   
  24.             // 更新slideBtn_left的大小  
  25.             slideBtn_left = slideBtn_left + disX;  
  26.   
  27.   
  28.             // 做一个判断,防止滑块划出边界,滑块的范围应该是在[0,MAX_LEFT_DISTANCE];  
  29.             if (slideBtn_left < 0) {  
  30.                 slideBtn_left = 0;  
  31.             } else {  
  32.                 if (slideBtn_left > MAX_LEFT_DISTANCE) {  
  33.                     slideBtn_left = MAX_LEFT_DISTANCE;  
  34.                 }  
  35.             }  
  36.   
  37.   
  38.             break;  
  39.         case MotionEvent.ACTION_UP:  
  40.             // 当抬起的时候  
  41.   
  42.   
  43.             // 抬起的时候,判断松开的位置是哪里,来由此来决定开关的状态是打开还是关闭  
  44.             if (slideBtn_left < MAX_LEFT_DISTANCE / 2) {  
  45.                 currentState = false;  
  46.             } else if (slideBtn_left >= MAX_LEFT_DISTANCE / 2) {  
  47.                 currentState = true;  
  48.             }  
  49.   
  50.   
  51.             // 由开关的状态标志,确定应该是打开还是关闭状态  
  52.             flushState();  
  53.             break;  
  54.         }  
  55.   
  56.   
  57.         // 根据状态标志来刷新对应的滑块停止位置,从而实现打开或者关闭效果  
  58.         flushView();  
  59.   
  60.   
  61.         // 返回true意味着消费掉本次事件,不让其他控件还可以接收到这个事件  
  62.         return true;  
  63.     }  



[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public void onClick(View v) {  
  3.         if (!isDrag) {  
  4.             // 如果不是拖动事件,才进行点击事件的响应操作  
  5.             currentState = !currentState;  
  6.             flushState();  
  7.             flushView();  
  8.         } else {  
  9.             // 如果是拖动事件,则不进行点击事件的响应操作  
  10.         }  
  11.     }  



至此,关于自定义开关的触摸事件就基本完成了,其实关于触摸点击的机制,Android是有一整套的流程的。。找时间会写一篇关于此的博文吧。下一篇会讲解如何自己定义控件的自定义属性,谢谢关注!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值