之前打算学习material design一些相关的东西,因为之前工作里也一直没有用到,所以不是很了解,而现在的app越来越多用了material design,学一学还是很有必要。然后又碰巧在github上看到这个开源控件,决定研究一番。
一些风格类似sticky header,flexibleSpace,fillGap等等,如果里面的view是可滑动(如scrollview,listview,recyclerview等),那么需要处理滑动事件来实现我们所需要的内容。那么作者就做出了这个滑动处理的framelayout。
因为处理比较多的是滑动事件,如果对滑动事件不是很熟悉的话,建议看一看这篇文章 Android Touch事件传递机制(一) -- onInterceptTouchEvent & onTouchEvent
那么下面开始。
监听器
首先定义了一个监听,用来传递一些滑动事件,主要是当前控件需要拦截子控件的滑动事件的时候会调用,已经注释了挺详细,注释并非原版翻译过来,添加了自己的理解。
public interface TouchInterceptionListener {
/**
* 确定布局是否应该拦截这个事件。
* @param ev Motion event.
* @param moving true如果当这个事件是移动的事件
* @param diffX 如果moving = true,那么之前的X和当前X的差值。
* @param diffY 如果moving = true,那么是之前的Y和当前的Y的差值
* @return True if the layout should intercept.
*/
//确定是否是要拦截事件,如果需要拦截,那么走onTouchEvent
boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY);
/**
* 当拦截的时候发出的一个down事件,第一次会调用到这里,
* 另外当从不需要拦截到需要拦截的move滑动事件也会调用到这个方法
* @param ev Motion event.
*/
void onDownMotionEvent(MotionEvent ev);
/**
* 如果拦截的时候走onTouchEvent,并会转发给已注册的监听
* @param ev Motion event.
* @param diffX Difference between previous X and current X.
* @param diffY Difference between previous Y and current Y.
*/
// 滑动过程中需要拦截或者从不需要突然变成需要拦截
void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY);
/**
* 当拦截的时候onTouchEvent转发的up或者cancel事件
* @param ev Motion event.
*/
void onUpOrCancelMotionEvent(MotionEvent ev);
}
字段属性
注释已经听明白了,都是个人理解,可能会有出入
//是否拦截
private boolean mIntercepting;
// 是否需要创建一个down滑动事件,当拦截状态要变成不需要的时候,这个标识变成false,创建一个down事件并传递给子view
private boolean mDownMotionEventPended;
// 是否从down事件开始,滑动从不需要拦截到需要拦截的时候,这个标识变成true,调用监听器的onDownMotionEvent方法
private boolean mBeganFromDownMotionEvent;
private boolean mChildrenEventsCanceled;
private PointF mInitialPoint;
//记录一个down事件,后面用于复制,但是会改变 x,y,效果应该和当时获取的一个事件将action 设置为down是一样的。
private MotionEvent mPendingDownMotionEvent;
private TouchInterceptionListener mTouchInterceptionListener;
重点一:onInterceptTouchEvent
如果拦截了那么会直接调用onTouchEvent,如果返回true,onTouchEvent也返回true,那么下一次不会在走这里,直接走onTouchEvent
如果返回false,子view消耗了事件,那么下一次会还会走这里,子view不消耗,那么以后的事件不会再给这个控件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mTouchInterceptionListener == null) {
return false;
}
// 在这里,我们必须初始化触摸状态变量
// 和确认我们是否应该拦截这个事件
// 我们是否应该拦截,保存在以后的事件处理中。
switch (ev.getActionMasked()) { //getActionMasked() 用来有多点触控?
case MotionEvent.ACTION_DOWN:
mInitialPoint = new PointF(ev.getX(), ev.getY());
// 复制一个action down的事件
mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);
// 赋值将要action down = true
mDownMotionEventPended = true;
//因为这里还没有开始滑动,所以传参为 false ,0 , 0
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);
// move的时候是不是从down开始的,如果不是的话,需要给子view发送一个down事件
mBeganFromDownMotionEvent = mIntercepting;
mChildrenEventsCanceled = false;
return mIntercepting;//这里拦截的话直接走onTouchEvent
case MotionEvent.ACTION_MOVE:
//(不拦截并且子控件消费了才会走这里)
// ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
//acion_move在down后會馬上調用,因此初始化避免異常
//子view消费了事件才会走这里
if (mInitialPoint == null) {
mInitialPoint = new PointF(ev.getX(), ev.getY());
}
// diffX and diffY are the origin of the motion, and should be difference
// from the position of the ACTION_DOWN event occurred.
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
Constant.Log(TAG+"onIntercept "+mIntercepting);
return mIntercepting;
}
return false;
}
这里需要注意的是如果这里返回false,子viewonTouchEvent返回了false,那么之后的事件会继续走过这里
如果这里返回true,自身的onTouchEvent也返回true,那么以后的事件不会在经过这个方法。
重重重点二:onTouchEvent
因为主要功能的实现都在这个方法里面,所以这个方法是最终要的。
我们需要知道的是,如果一开始拦截了,那么就会走这个方法,如果滑动的中途突然不想拦截了,因为后续的事件会一直经过这个方法(此时onIntercept方法已经不走了),所以需要手动将事件传递给子view。
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mTouchInterceptionListener != null) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (mIntercepting) {
//如果拦截了,那么通知监听down事件
mTouchInterceptionListener.onDownMotionEvent(ev);
//这里拦截了为什么还要重新传递事件??注释了一下好像也没有问题
duplicateTouchEventForChildren(ev);
return true;
}
break;
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
if (mInitialPoint == null) {
mInitialPoint = new PointF(ev.getX(), ev.getY());
}
// diffX and diffY are the origin of the motion, and should be difference
// from the position of the ACTION_DOWN event occurred.
float diffX = ev.getX() - mInitialPoint.x;
float diffY = ev.getY() - mInitialPoint.y;
mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
if (mIntercepting) {
// 我们应该根据现有的位置创建一个 aciont_down 事件
// 其实就是滑动中从不需要拦截到需要拦截的时候变换的时刻的通知
if (!mBeganFromDownMotionEvent) {
mBeganFromDownMotionEvent = true;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
//如果没有的话需要创建一个down事件传递给监听
mTouchInterceptionListener.onDownMotionEvent(event);
mInitialPoint = new PointF(ev.getX(), ev.getY());
diffX = diffY = 0;
}
// 因为拦截了,所以不再发事件给子控件(可能之前子view还在滚动,然后需要拦截,但这里不是已经收不到了吗)
if (!mChildrenEventsCanceled) {
mChildrenEventsCanceled = true;
duplicateTouchEventForChildren(obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL));
}
mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
// 如果下一个滑动事件不需要拦截了
// 那么我们应该创建一个假的 action_down 事件
// 因此我们设置一个pending标记 = true ,就是变成不需要拦截了,需要传递一个down事件给子view,不然会有bug
mDownMotionEventPended = true;
// 因为我们认定了这是已经消费了,所以返回true
return true;
} else {
// 如果到一半不需要拦截了,那么应该要将滑动事件转发给子view
if (mDownMotionEventPended) {
mDownMotionEventPended = false;
// 因为这是从move事件中突然不需要拦截的,因为从拦截到不拦截的过程,是不会在走OnIntercept的,所以还要重新传一次
//如果部分发down事件给字控件,那么子控件是不会有移动效果的,要有头有尾
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
duplicateTouchEventForChildren(ev,event);
//下面这两句话是自己加的,效果试了下是一样的
//ev.setAction(MotionEvent.ACTION_DOWN);
//duplicateTouchEventForChildren(ev);
} else {
//如果已经发送了down事件,那么就直接分发给子view
duplicateTouchEventForChildren(ev);
}
// 如果下一个事件变成需要拦截了
// 那么我们需要创建一个假的action_down事件
// 因此设置这个标记为false
// 好像我们没有收到一个down的动作事件
// 这是滑到一半的时候不需要拦截了,所以不是从down开始的
mBeganFromDownMotionEvent = false;
// 子控件触摸事件取消
mChildrenEventsCanceled = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mBeganFromDownMotionEvent = false;
if (mIntercepting) {
//滑动来滑动去,如果最后需要拦截了事件的话,那么直接通知。
mTouchInterceptionListener.onUpOrCancelMotionEvent(ev);
}
// 无论这个布局是否拦截了连续的滑动事件,都应该取消子view的滑动事件。
if (!mChildrenEventsCanceled) {
//需要给子view传递up或cancel事件,因为子view会在这up或cancel设置一些变量或逻辑
mChildrenEventsCanceled = true;
if (mDownMotionEventPended) {
mDownMotionEventPended = false;
MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
event.setLocation(ev.getX(), ev.getY());
//分发down事件,有头有尾
duplicateTouchEventForChildren(ev, event);
} else {
duplicateTouchEventForChildren(ev);
}
}
return true;
}
}
return super.onTouchEvent(ev);
}
这里我有一个疑问,在down里已经拦截了,为什么还要向子view传递down事件呢,而且马上又会传递一个cancel事件。希望有明白的同学可以给我指导一下~~