没来的日子里在忙着写项目然后看书,习惯了把笔记整理到typora里面,今天突然发现好久没写博客了,打算痛改前非,从今日做起!把每天的笔记在博客上也贴一份,以此来记录每天的学习进度并且督促自己。学的有点慢但在爬行中…
最近在学习Android开发艺术探索,整理笔记。
界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突
3.5.1常见的滑动冲突场景
常见的滑动一般有三个方面:
- 外部滑动方向和内部滑动方向不一致;
- 外部滑动方向和内部滑动方向一致;
- 上面两种情况的嵌套。
场景一:ViewPager+Fragment配合使用组成的页面滑动效果;左右滑动来切换页面,而每个页面内部往往又是一个Listview;ViewPager内部处理了这种滑动冲突。如果我们采用的不是ViewPager而是ScrollView等,那就必须手动处理滑动冲突了,否则造成的后果就是内外两层只能有一层能够滑动。除了这种情况,还有外部上下滑动、内部左右滑动等。
场景三:场景3是场景1和场景2两种情况的嵌套。比如外部有一个SlidingMenu效果,然后内部有一个ViewPager,ViewPager的每一个页面中又是一个Listview。只需要分别处理内层和中层、中层和外层之间的滑动冲突即可。
3.5.2滑动冲突的处理规则
对于场景1,它的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。具体来说:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,可以通过水平和竖直方向的距离差来判断。也可以依据滑动路径和水平方向做形成的夹角。
对于场景2、3来说,根据业务进行判断和拦截。
3.5.3滑动冲突的解决方式
1.外部拦截法
外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要这个事件就给他,比较类似事件的分发机制,需要重写父容器的onInterceptTouchEvent:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要当前的点击事件){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = x;
return intercepted;
}
这里对上述代码再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;ACTION_UP必须返回false,假设事件交给子元素处理,如果父容器这里返回true,那么子元素就无法接收到ACTION_UP,onClick就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它处理,而ACTION_UP也必定可以传递给父容器,即使他返回了false。
2.内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素要消耗此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,需要重写子元素的dispatchTouchEvent方法。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
if(父容器需要拦截事件){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除了子元素需要处理之外,父元素默认也要拦截除ACTION_DOWN之外的其他事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(true)方法时,父元素才能继续拦截所需要的事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
场景一示例
考虑到一种情况,如果此时用户正在水平滑动,但是水平滑动停止之前如果用户再迅速的进行竖直滑动,就会导致界面在水平滑动无法滑动到终点,而处于一种中间状态,为了避免这种不友好的体验,我们水平正在滑动的时候,下一个序列的点击仍然交给父容器。
//场景一的外部拦截法
//重点是父容器的onInterceptTouchEvent()
public class HorizontalScrollViewEx extends ViewGroup {
private int mChindrensize;
private int mChindrenWidth;
private int mChindrenIndex;
//分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
//分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
....
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = true;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltax = x - mLastXIntercept;
int deltaY = y = mLastYIntercept;
if (Math.abs(deltax) > Math.abs(deltaY)) { //判断水平方向和垂直方向移动距离
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
//根据用户的滑动速度和位置来决定滚动到哪个子视图,并在用户停止触摸时平滑的滚动到那个位置
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
int scrollToChildIndex = scrollX / mChindrenWidth;
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChindrenIndex = xVelocity > 0 ? mChindrenIndex - 1 : mChindrenIndex + 1;
} else {
mChindrenIndex = (scrollX + mChindrenWidth / 2) / mChindrenWidth;
}
mChindrenIndex = Math.max(0, Math.min(mChindrenIndex, mChindrensize - 1));
int dx = mChindrenIndex * mChindrenWidth - scrollX;
ssmoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastX = x;
mLastY = y;
return true;
}
private void ssmoothScrollBy(int dx, int i) {
mScroller.startScroll(getScrollX(),0,dx,500);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
}
内部拦截:只需要修改ListView的dispatchTouchEvent方法中的父容器的拦截逻辑,同时让父拦截MOVE和Up事件即可。为了重写Listview的dispatchTouchfvent方法,我们必须自定义一个ListView,称为ListViewEx,然后对内部拦截法的模板代码进行修改。
public class ListViewEx extends ListView {
public static final String TAG = "ListViewEx";
private HorizontalScrollViewEx mHorizontalScrollViewEx;
//记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int delatX = x - mLastX;
int delatY = y - mLastY;
if (Math.abs(delatX) > Math.abs(delatY)) {
mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(false);
}
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}
//除了对子容器的修改,我们还需要修改父容器的onInterceptTouchEvent()
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
mLastX = x;
mLastY = y;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
return true;
}
return false;
}else {
return true;
}
}
场景二示例
提供一个可以上下滑动的父容器,这里就叫 StickyLayout,它看起来就像是可以上下滑动的竖直的LinearLayout,然后在它的内部中的滑动冲突了。当然这个StickyLayout是有滑动规则的:当Header显示时或者ListView 滑动到顶部时,由StickyLayout拦截事件:当Header隐藏时,这要分情况,如果Listview已经滑动到顶部并且当前手势是向下滑动的话,这个时候还是StickyLayout拦截事件,其他情况则由ListView拦截事件。
//我们主要看它onInterceptTouchEvent中对move的处理
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
intercepted = 0;
} else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
intercepted = 0;
} else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
intercepted = 1;
} else if (mGiveUpTouchEventListener != null) {
if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
intercepted = 1;
}
}
当事件落在Header上面时,就不会拦截该事件;接着,如果竖直距离差小于水平距离差,那么父容器也不会拦截该事件;当Header是展开状态并且是向上滑动时,父容器拦截事件;当ListView滑动到顶部并且向下滑动的时候,父容器也会拦截事件。