Android自定义控件系列 十:利用添加自定义布局来搞定触摸事件的

这个例子是比较有用的,基本上可以说,写完这一次,以后很多情况下,直接拿过来addView一下,然后再addInterceptorView一下,就可以轻轻松松的达到组合界面中特定控件来响应特定方向的触摸事件了。

在写Android应用的过程之中,经常会遇到这样的情况:界面包含了多个控件,我们希望触摸在界面上的不同滑动动作能被不同的控件所接收,或者在界面不同位置滑动的动作能被不同的控件所接收,换句话说,能否让特定子view响应特定方向的触摸事件?一个典型的例子就是ListView和Header的组合:

遇到的问题:

在上图的例子中,会发现一个问题,就是当手指在顶部轮播图上滑动的时候,如果我们想滑动轮播图,只能在手指非常水平的时候才能让轮播图翻动,而在手指滑动轨迹稍微有一点倾斜的时候,就发现触摸事件被ListView给响应了,变成了上下滑动ListView,这种体验显然不是很好。

假如说我们现在想要一种简单的实现:可能整个应用有很多页面,现在想在当前这个特定的界面,使得当手指在轮播图范围内滑动的时候,当手指轨迹角度<45度的时候(方向上较水平),那么让轮播图响应触摸事件,使得顶部图片能够水平滑动;让当手指手势轨迹角度>45度的时候(方向上较竖直),能够ListView来响应触摸事件,使得整个ListView能够上下滑动,这种效果要如何实现呢?

解决办法:

专栏的上一篇文章中,详细分析了Android的触摸事件的分发流程和ViewGroup的源代码(不熟悉的朋友可以看看:Android自定义控件系列九:从源码看Android触摸事件分发机制)。看过上一篇文章之后,应该了解到,Andrioid事件的分发是一层一层的进行的,最开始分发的时候总是从上层到下层,从活动的Activity开始,到DecorView,然后到我们写的布局,然后再是布局中的其他组件,那么本文的解决办法就是自定义一个ViewGroup,包裹在原来的ListView之外,放在这个特定的界面上。由于事件分发是一层层的进行的,所以我们重写这个外层的自定义ViewGroup的dispatchTouchEvent方法就可以实现控制所有子view的事件分发机制,从而在这个特定的界面实现我们想要的触摸事件的响应机制。

写一个自定的FrameLayout叫InterceptorFrameLayout,重写dispatchTouchEvent(MotionEvent ev)方法,主要解决几个问题:

1、在事件分发的时候,我们得到的是MotionEvent 事件,如何判断这个事件是否落在我们想要的控件区域上呢?

思路:可以在InterceptorFrameLayout中,使用一个Map集合,来存放我们想要控制触摸事件的View和对应的代表方向的参数,对外界暴露add和remove方法,来添加和移除拦截的view对象。然后拿到event事件之后,调用event.getRawX和event.getRawY可以拿到相对屏幕左上角的绝对坐标,然后遍历view的map集合对所有的判断触摸的绝对坐标是不是在View的范围内,且要拦截的方向参数是否符合。判断触摸是否在view上,可以使用view.getLocationOnScreen(int[])方法,得到的int数组,第一个元素表示view的左上角的x坐标,第二个元素表示view的右上角坐标,具体判断方法如下:

public static boolean isTouchInView(MotionEvent ev, View view) {//判断ev是否发生在view的范围内static int[] touchLocation = new int[2];view.getLocationOnScreen(touchLocation);//通过getLocationOnScreen方法,获取当前子view左上角的坐标float motionX = ev.getRawX();float motionY = ev.getRawY();// 返回是否在范围内,通过触摸事件的坐标和本子view的左上右下四边的坐标比较,来判断是不是落在view内return motionX >= touchLocation[0]&& motionX <= (touchLocation[0] + view.getWidth())&& motionY >= touchLocation[1]&& motionY <= (touchLocation[1] + view.getHeight());}

/** 在集合中查找对应event和方向参数的view,找到了则返回,没找到返回null */private View findTargetView(MotionEvent ev, int orientation) {// mViewAndOrientation为存放要监测触摸事件的子view和对应方向参数的集合Set<View> keySet = mViewAndOrientation.keySet();for (View view : keySet) {Integer ori = mViewAndOrientation.get(view);// 由于所有的方向参数都是二进制相互与运算为0的// 所以这里使用与运算来判断方向是否符合// 这里所有的判断条件是:// ①该子view在mViewAndOrientation集合内// ②方向一致// ③触摸事件落在该子view的范围内// ④该子view可以消费掉本次事件// 同时满足上面四个条件,则代表该子view是我们要找的子view,于是返回if ((ori & orientation) == orientation && isTouchInView(ev, view)&& view.dispatchTouchEvent(ev)) {return view;}}return null;}

2、重写dispatchTouchEvent方法:①如何处理Down事件和Move以及Cancel和Up事件的关系。

这个关系的纽带实际上就是mFirstTouchTarget,如果看完上一篇博文:Android自定义控件系列九:从源码看Android触摸事件分发机制还有印象的话,源码中mFirstTouchTarget会记录能够在Down事件时能够消费事件的子view,然后在Down事件之后的其他事件响应,都可以根据mFirstTouchTarget的状态来做进一步的判断后续动作。在这里我们也仿照源码的方式,定义一个mFirstTarget。在每一次进入到dispatchTouchEvent的时候,先需要判断一下mFirstTarget是否为空,如果mFirstTarget不为空,则代表之前有Down事件能够被某一个监测集合中的子view消费,于是我们可以继续调用boolean flag =mFirstTarget.dispatchTouchEvent()方法,将后续的事件(Move,Cancel,UP等)通过dispatchTouchEvent传递到这个对应的子view–即mFirstTarget上去;这个时候,如果flag返回true,则表示该子view(mFirstTarget)已经完全消费掉了事件,那么就应该将mFirstTarget重新置为空,方便下一次事件的分发;或者这个touch事件是Cancel或者Up,那么也表示本次事件的终止,于是也要将mFirstTarget置空。然后再将flag的值返回。

绚丽的民族风情,悠久的历史文化。抛开尘世的纷扰,

Android自定义控件系列 十:利用添加自定义布局来搞定触摸事件的

相关文章:

你感兴趣的文章:

标签云: