上一篇介绍了些简单View,ViewGroup及滑动控件的定义,也提到android4.0以后有解决了多层滑动控件的嵌套问题,但是如果要自己再定义最上层的滑动控件,再去嵌套google提供的类似ViewPager,ListView,ScrollView,HorizontalScrollView,RecyclerView这些即有横向又有纵向的滑动控件时,就要自己去处理滑动冲突了,这时候就要深入了解android的滑动嵌套时如何解决滑动冲突的原理了
google提供的ViewPager类就是个很强大的控件,在4.0以后ViewPager不管是嵌套ScrollView. HorizontalScrollView,RecyclerView还是再嵌套ViewPager都不会有冲突,那么我们定义最上层控件的思路就是从ViewPager的源码着手
1:requestDisallowInterceptTouchEvent(boolean disallowIntercept)
请求上一层控件是否拦截事件 true代表上一层不拦截并且不执行onInterceptTouchEvent方法, false相反,且ViewGroup方法默认都会一层层向上请求
2:onInterceptTouchEvent(MotionEvent ev):事件拦截,上一篇有讲述在此方法中如何真正的产生滑动而非MotionEvent.ACTION_MOVE事件产生就等于产生滑动
3:onTouchEvent(MotionEvent ev) 拦截子控件的事件后会调用自身的此方法
在滑动控件中的调用时机:不管是纵向还是横向滑动都不会在action_down的时候就requestDisallowInterceptTouchEvent(true)来请求上层不拦截,google提供的滑动控件源码中都可以看到,而是在产生滑动的时候才会请求
首先看下ScrollView源码中的事件拦截方法中的MotionEvent.ACTION_MOVE:事件
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
这里再重复一遍,google产生滑动的标准是横向或纵向在MotionEvent.ACTION_MOVE事件时的横向或纵向坐标值与按下时的坐标值的差值的绝对值大于mTouchSlop(最小滑动单位)才算是产生滑动
从 if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
这断逻辑可以看出,ScrollView比较霸道,一旦自身产生滑动,就将自身是否正在滑动标记true并且返回,HorizontalScrollView也一样,只不过是水平滑动一产生就拦截了事件,并且请求上层控件不拦截事件,而如果ScrollView中嵌套了ListView,RecyclerView这样的滑动控件的时候,子控件还没在产生滑动前事件就被拦截了,parent.requestDisallowInterceptTouchEvent(true)也没来的及调用,所以就会有滑动冲动,这也是为什么ScrollView不建议嵌套ListView,RecyclerView的原因
直入主题,开头有讲述ViewPager就能兼容这些滑动冲突,所以还是直接看ViewPager的源码中onInterceptTouchEvent方法事件处理
if (action != MotionEvent.ACTION_DOWN) {
if (mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "Intercept returning true!");
return true;
}
if (mIsUnableToDrag) {
if (DEBUG) Log.v(TAG, "Intercept returning false!");
return false;
}
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = ev.getY(pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (dx != 0 && !isGutterDrag(mLastMotionX, dx)
&& canScroll(this, false, (int) dx, (int) x, (int) y)) {
// Nested view has scrollable area under this point. Let it be handled there.
mLastMotionX = x;
mLastMotionY = y;
mIsUnableToDrag = true;
return false;
}
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
mLastMotionX = dx > 0
? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
原来ViewPager中加了一断逻辑
再看看canScroll方法
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
//递归的查看子控件是否处在当前触摸点范围内,如果不在范围内不考虑
//在触摸范围内则需要考虑是否可以滑动
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
&& y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
&& canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollHorizontally(v, -dx);
}
可以看到递归的查看子控件是否可以滑动,但是递归方法在嵌套复杂些的布局中是非常耗时的,而MotionEvent.ACTION_MOVE事件在滑动的时候是调用的非常频繁的,此时又用到一个变量mIsBeingDragged,如果子控件可以滑动,这个变量会标记true,如果子控件不可滑动,自身产生了滑动就直接拦截掉事件,也加上标记,再回到事件拦截方法中看另外一断逻辑
原来只要在产生滑动的时候判断子控件是否能滑动,如果子控件可以滑动,就标记自己不可滑动,在MotionEvent.ACTION_MOVE事件再次调用时就直接判断自已是否正在滑动或是否不能滑动来判断是否需要拦截,也就不会再执行复杂的递归方法
索德斯勒~~原理就是这么简单
所以当你在定义最上层控件需要嵌套各种水平或纵向滑动控件时,可以采用此方式来解决喽,子控件是否优先滑动就根据你的需求定喽
/**
* 子控件能否滑动
* @param v 当前递归到的子控件
* @param x x轴坐标点
* @param y Y轴坐标点
* @param diffY y轴滑动差值
* @param diffX x轴滑动差值
* @return
*/
protected boolean isChildCanScroll(View v, int x, int y, int diffY, int diffX) {
if (v instanceof HorizontalScrollView) {
return isHorizontalScrollViewCanScroll((HorizontalScrollView) v, diffx);
} else if (v instanceof RecyclerView) {
return isRecyclerViewCanScroll((RecyclerView) v, diffY);
} else if (v instanceof ScrollView) {
return isScrollViewCanScroll((ScrollView) v, diffY);
} else if (v instanceof ViewGroup) {
ViewGroup group = (ViewGroup) v;
int count = group.getChildCount();
int scrollX = v.getScrollX();
int scrollY = v.getScrollY();
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
if (isChildViewOnTouch(child, x, y, scrollX, scrollY) && isChildCanScroll(child, x, y, diffY)) {
return true;
}
}
}
return false;
}
这里贴出一个示例代码,具体可以根据需求而定
/**
* 判断ScrollView能否滑动
*/
private boolean isScrollViewCanScroll(ScrollView v, int dy) {
int scrollY = v.getScrollY();
if (dy > 0) { //向下滑子控件优先
return scrollY > 0;
} else { //向上滑,父控件优先
return this.getScrollY() >= scrollHeight;
}
}
多点触摸最常见就图片放大缩小,但是在滑动中也是有用到,而且原生的滑动控件都有支持,这里就只介绍在滑动时如何使用多点触摸
滑动的过程都是在ACTION_MOVE事件的时候根据当前MotionEvent的x,或y坐标点与上一次纪录的坐标点的差值来滑动(在滑动时如何不考虑多点触摸,在多个手指操作滑动的时候就会出现滑动的错乱,而且后面按下的手指在第一次按下的手指离开前不可滑动)
ACTION_POINTER_DOWN:非第一个手指按下时的事件
ACTION_POINTER_UP:非最后一个手指抬起时的事件
在滑动中多点触摸滑动几乎写法都是一样,只是纵向与横向不同方向的滑动纪录上次坐标不一样而已,直接看ViewPager代码
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
final float x = ev.getX(index);
//这里一定要将上一次的坐标点附值,如果没有附值,试想如果第二个手指
//按下的坐标与上一个手指按下距离较远时,如果不附值就会出现界面瞬间滑动到第二个手指与上次滑动坐标的差值距离
mLastMotionX = x;
//记录当前操作滑动的触摸点的id,根据最后一个按下的手指来操作滑动
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
break;
//弹起时将当前的触摸点交给第一个按下的手指
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = ev.getX(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
注意在ACTION_MOVE获取x,y轴的坐标时就不能直接event.getX(),event.getY(),这种写法默认都是获取第0个触摸点的坐标
case MotionEvent.ACTION_MOVE:
int index = MotionEventCompat.findPointerIndex(event, mPointerId);
if (index == -1)
index = 0;
float y = MotionEventCompat.getY(event, index);
int diffY = (int) (y - mLastMotionY);
if (isChildNeedScroll(diffY)) {
scrollBy(0, -diffY);
}
mLastMotionY = y;
break;
下拉刷新也算是个上层控件吧,里面也常见的嵌套ListView或,RecyclerView,而里面又再会嵌套ViewPager轮播
侧滑关闭界面的控件肯定就是最上层啦,里面嵌套的就可能更多了,你在定义最上层控件时是否有碰到滑动的冲突呢,是否有支持多点触摸呢,如果没有现在能解决吗?