前言
最近项目中有一个视频小窗口,为什么不是悬浮窗,是因为在固定的父窗口可以随意拖动,可以点击进入房间或者点击关闭按钮关闭小窗口。效果图如下:
看到上方的效果图,大概也知道需要自定义ViewGroup,既需要处理ViewGroup的移动事件又同时兼容子类的点击事件。其实之前也写过一篇关于事件分发的文章,但是觉得网上太多这类文章了就没有发表,网上有大把关于事件分发的文章,我刚开始参加工作那会,由于感觉事件分发一直模糊不清,就去网上看到各种文章解析,看的时候觉得还好,就跟着文章看一下源码,顺着流程走一遍,但是发现遇到实际情况还是感觉有心无力。但是现在再看网上关于事件分发的文章,我发现了一些问题,有部分人对于事件分发了解并不是很清晰,还有一部分是只对分发、拦截、回传、反拦截等源码说一遍,但是事件下面细分的ACTION_DOWN、ACTION_MOVE、ACTION_UP并不是很清楚,不过最多的都是只是说理论,可能正在遇到实际问题还会感觉束手无策。接下来几篇文章我会根据实际项目遇到的例子来说明事件分发。
功能实现
首先要分析整个功能,需要自定义ViewGroup继承现有的各种ViewGroup,剩下的就是关于自定义VieGroup功能点的实现,首先滑动事件分配给ViewGroup,让之在父窗口之内随意滑动,其次是ViewGroup中的其他子控件的点击事件要分发下去,讲到这里大家也明白了,肯定涉及到事件分发冲突问题,需要判断当前事件是滑动还是点击事件,如果是滑动需要拦截,如果是点击分发给子类就行。
分析功能点
- 整个ViewGroup可以在父窗口随意滑动
- 点击整个条目又可以跳转
- 点击关闭按钮可以关闭当前的ViewGroup
判断是否是滑动
判断是否是滑动分为两部分,一是系统能检测到的最小滑动距离(常量为8dp),系统提供一个API,所以我们可以直接利用此API来判断是否是滑动,。二是滑动过程要拦截ACTION_MOVE,响应自己的onTouchEvent方法处理ViewGroup的滑动。
//获取系统检测最小滑动距离
ViewConfiguration.get(mContext).getScaledTouchSlop();
//判断是否是滑动
if (Math.abs(dx) > minTouchSlop || Math.abs(dy) > minTouchSlop) {
interceptd = true;
} else {
interceptd = false;
}
拦截事件
先看拦截事件的处理,我下面分析,拦截事件处理如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean interceptd = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
interceptd = false;
break;
case MotionEvent.ACTION_MOVE:
//计算移动距离 判定是否滑动
float dx = event.getX() - mDownX;
float dy = event.getY() - mDownY;
if (Math.abs(dx) > minTouchSlop || Math.abs(dy) > minTouchSlop) {
interceptd = true;
} else {
interceptd = false;
}
break;
case MotionEvent.ACTION_UP:
interceptd = false;
break;
}
return interceptd;
}
关于拦截事件的分析:
首先,只有判定为滑动的时候才需要拦截事件,由于拦截事件是在分发事件里面调用的,并且由于dispatchTouchEvent内部及其复杂,所以处理滑动冲突,只需要处理onInterceptTouchEvent和onTouchEvent即可,除了特别情况,一般情况无需dispatchTouchEvent事件。拦截事件onInterceptTouchEvent只要返回true就会把事件拦截掉,这样ACTION_DOWN、ACTION_UP直接返回false就行,ACTION_MOVE事件的时候,如果X、Y轴其中之一的移动距离大于系统能检测的最小滑动距离就判定为滑动。
注意事项:
1、系统对于一个事件里面的三个子事件的优先级不同,ACTION_DOWN优先级是最高的。如果拦截事件里面拦截了ACTION_DOWN事件,其他的事件都会被拦截
2、requestDisallowInterceptTouchEvent(true)会告诉父类不要拦截事件,除了ACTION_DOWN事件以外,因为ACTION_DOWN在分发的过程中,会重置requestDisallowInterceptTouchEvent方法的标志位,因此如果父类设置了拦截ACTION_DOWN,即使子类调用requestDisallowInterceptTouchEvent(true)强制请求事件也是无法响应的。
消费事件
拦截事件已经处理了在滑动的时候拦截,所以在onTouchEvent事件里就可以直接处理滑动事件了
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
if (mDownX >= 0
&& mDownY >= mRootTopY
&& mDownX <= mRootMeasuredWidth
&& mDownY <= (mRootMeasuredHeight + mRootTopY)) {
float dx = event.getX() - mDownX;
float dy = event.getY() - mDownY;
float ownX = getX();
//获取手指按下的距离与控件本身Y轴的距离
float ownY = getY();
//理论中X轴拖动的距离
float endX = ownX + dx;
//理论中Y轴拖动的距离
float endY = ownY + dy;
//X轴可以拖动的最大距离
float maxX = mRootMeasuredWidth - getWidth();
//Y轴可以拖动的最大距离
float maxY = mRootMeasuredHeight - getHeight();
//X轴边界限制
endX = endX < 0 ? 0 : endX > maxX ? maxX : endX;
//Y轴边界限制
endY = endY < 0 ? 0 : endY > maxY ? maxY : endY;
//开始移动
setX(endX);
setY(endY);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
关于消费滑动事件的分析:
首先要判断需要在父窗口内随意滑动,所以需要下面这一判断条件,具体父窗口的位置在接下来会说明。
if (mDownX >= 0
&& mDownY >= mRootTopY
&& mDownX <= mRootMeasuredWidth
&& mDownY <= (mRootMeasuredHeight + mRootTopY))
接着要判断最后的位置不能超过父类所在的区域
//X轴边界限制
endX = endX < 0 ? 0 : endX > maxX ? maxX : endX;
//Y轴边界限制
endY = endY < 0 ? 0 : endY > maxY ? maxY : endY;
最后通过使用setX(endX)来设置位置,为什么不使用其他几种方式设置位置呢?,可能有人使用过layout(int l, int t, int r, int b)这个方法来移动View,那肯定会遇到翻页或者刷新,原本移动的View又恢复到原来位置了,那是因为只要父类调用requestLayout();方法,子类就会重新布局,这涉及到关于view的测量、布局、绘制过程了,还有一种方式是通过属性动画也可以改变位置,属性动画通过反射属性改变view的真实值。
肯定还有疑问就是为什么onTouchEvent方法直接返回true,而不像onInterceptTouchEvent需要各个方法判断返回值的方式,因为滑动事件的处理要交给父类处理,需要返回true才能去消费事件而不至于分发给子类消费。
注意事项:
1、切记要使用event.getX()来或者坐标,相对于父窗口的位置,而不是使用event.getRawX(),event.getRawX()是相对于整个屏幕坐标
2、在ACTION_UP事件里也可以添加贴边动画,增强体验
确定父窗口
在onInterceptTouchEvent方法和dispatchTouchEvent方法里面都可以获取到父窗口的大小,dispatchTouchEvent是必定要走的方法,所以放在onInterceptTouchEvent方法中有利于了解事件的拦截。下面是onInterceptTouchEvent方法中的ACTION_DOWN事件
case MotionEvent.ACTION_DOWN:
interceptd = false;
//测量按下位置
mDownX = event.getX();
mDownY = event.getY();
//测量父类的位置和宽高
if (!mHasMeasuredParent) {
ViewGroup mViewGroup = (ViewGroup) getParent();
if (mViewGroup != null) {
//获取父布局的高度
mRootMeasuredHeight = mViewGroup.getMeasuredHeight();
mRootMeasuredWidth = mViewGroup.getMeasuredWidth();
int top = mViewGroup.getTop();
//获取父布局顶点的坐标
mRootTopY = mViewGroup.getTop();;
mHasMeasuredParent = true;
}
}
break;
说明:mHasMeasuredParent参数为了防止每次都去获取,但是如果你的父窗口也是动态改变的,去掉此判定条件
写在最后
至此,关于在父窗口可以随意拖动的ViewGroup功能已经完成,通过实际的例子也发现了,事件分发并不简单的关于一个方法的处理,有时候会涉及到好几个方法结合起来使用。网上的文章虽然多,但是大部分都是讲述事件主干线,对于单个事件分析不透彻,当然如果本人中结论如果有误,请及时下方评论纠正,在此感谢。由于接下来我会根据实际项目事件分发一系列的文章,所以准备把demo上传到GitHub上,本文的demo如下。