尊重他人劳动成果,转载请说明出处:http://blog.csdn.net/bingospunky
需求
我们自定义一个可以滑动的MyView,把这个MyView放在ViewPager(ScrollView同理)里面,滑动该MyView。如果MyView可滑动,那么MyView响应滑动;如果MyView不能滑动,那么让外层的ViewPager滑动。
几个不可行的方法
1.在MyView的onTouchEvent方法中,MotionEvent.ACTION_DOWN事件,返回true。
弊端:不能在适当的时候让ViewPager响应滑动。该方案不可取。
由于我们需要根据情况决定是否让父view响应,那么我们需要在滑动一定距离,类似于android认为滑动超过8像素才是滑动。
2.在MyView的onTouchEvent方法中,MotionEvent.ACTION_MOVE事件,如果移动距离>8像素,那么返回true,并且调用android.view.ViewParent.requestDisallowInterceptTouchEvent(boolean disallowIntercept) 方法,不允许父View截断。
优势:有8像素的距离来决定MyView是否响应滑动
弊端:当移动距离>8像素的时候,先执行ViewPager的onInterceptTouchEvent方法,被ViewPager截断,当移动距离>8像素时,不会执行MyView的onTouchEvent方法。该方案不可取。
3..在MyView的onTouchEvent方法中,MotionEvent.ACTION_MOVE事件,如果移动距离>4像素,那么返回true,并且调用android.view.ViewParent.requestDisallowInterceptTouchEvent(boolean disallowIntercept) 方法,不允许父View截断。
优势:有4像素的距离来决定MyView是否响应滑动
弊端:当滑动距离在4--8像素之间,能达到我们的需求,MyView有权利先决定是否响应滑动。但是4--8像素是很小的一段距离,只有在我们很慢很慢滑动的时候才会出现滑动距离在4--8像素之间。当我们随手一滑,滑动距离超过8像素,那么MyView的onTouchEvent方法也不会出现4--8像素。该方案不可取。
思路
ViewPager的onInterceptTouchEvent方法里,如果滑动超过8像素,那么就截断该事件,如果我们能在ViewPager截断前,先让它的子view进行判断,并有权处理该滚动事件,那么就可满足需求。可是,从touch事件的传递规则,我们知道:touch从父view传递给子view,父view的onInterceptTouchEvent方法必然是先执行的。那么应该如何解决呢?
灵感
如果两个ViewPager嵌套在一起,你用手滑动内层的ViewPager。如果内层的可滑动,那么内层的响应滑动;如果内层的不可滑动,那么外层的响应滑动。
如果两个ScrollView嵌套在一起,你用手滑动内层的ScrollView。内层的Scroll不会响应滑动,滑动只会被外层的ScrollView响应。
可行的方法:
那么就是说ViewPager已经提供了这种功能,内层的可滑动View有滑动时间的优先响应权。
那么它是怎么做到的呢?就让源码来告诉我们答案。
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
if (DEBUG) Log.v(TAG, "Intercept done!");
mIsBeingDragged = false;
mIsUnableToDrag = false;
mActivePointerId = INVALID_POINTER;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
return false;
}
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) {
break;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = MotionEventCompat.getY(ev, 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)) {
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) {
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsUnableToDrag = false;
mScroller.computeScrollOffset();
if (mScrollState == SCROLL_STATE_SETTLING &&
Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
} else {
completeScroll(false);
mIsBeingDragged = false;
}
if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+ " mIsBeingDragged=" + mIsBeingDragged
+ "mIsUnableToDrag=" + mIsUnableToDrag);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
return mIsBeingDragged;
}
第43~49行的代码的作用就是在ViewPager截断前,把分发的权利转移给子view。
那么我们来看第44行的boolean android.support.v4.view.ViewPager.canScroll(View v, boolean checkV, int dx, int x, int y)方法
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--) {
// TODO: Add versioned support here for transformed views.
// This will not work for transformed views in Honeycomb+
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);
}
这个方法作用是:递归的找到touch事件对应的最内层的子view,由最内层的子view来决定是否响应水平滑动事件。
在32行用到了一个处理兼容的方法,跟踪一下,我们会发现,子view决定是否响应水平滑动事件是通过boolean android.view.View.canScrollHorizontally(int direction)这个方法。ScrollView:
在ViewPager的onInterceptTouchEvent方法里,自己截断之前,先让子view来决定自己是否响应。然而在ScrollView里却没有这个功能,我们可以模仿ViewPager,给ScrollView添加这个功能。
/**
* 支持嵌套的scrollview,事件先被内部可滑动view决定响应者。
* 在srcollview截断前,先测试它的子view是否需要先消化滚动事件。
*
* @author mbb
* @blog http://blog.csdn.net/bingospunky
*/
public class NestedScrollView extends ScrollView {
public NestedScrollView(Context context) {
super(context);
}
public NestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NestedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private int yLast = 0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction()&MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
yLast = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int xCur = (int) ev.getX();
int yCur = (int) ev.getY();
int yDiff = yCur - yLast;
if(canScroll(this, false, yDiff, xCur, yCur)){
return false;
}
break;
default:
yLast = 0;
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 递归检测一个view通过dy是否可滑动
*
* @param v
* @param checkV 是否检测
* @param dy
* @param x
* @param y
* @return
*/
protected boolean canScroll(View v, boolean checkV, int dy, 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();
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, dy, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollVertically(v, -dy);
}
}
事例:
/**
* 可滚动view demo
*
* @author qingtian 2015年6月29日 14:22:18
* @blog http://blog.csdn.net/bingospunky
*/
@SuppressLint("DrawAllocation")
public class CanScrollView extends View {
protected int needWidth;
protected int needHeight;
private Bitmap bitmap1;
private Canvas canvas1;
private int xOffset = 0;
private int yOffset = 0;
@SuppressWarnings("deprecation")
private int mTouchSlop = ViewConfiguration.getTouchSlop();
public CanScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public CanScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CanScrollView(Context context) {
super(context);
init();
}
private void init() {
}
Paint p = new Paint();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
needWidth = getMeasuredWidth() * 2;
needHeight = getMeasuredHeight() * 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.img1);
bitmap1 = Bitmap.createBitmap(needWidth, needHeight, Config.RGB_565);
canvas1 = new Canvas(bitmap1);
canvas1.drawBitmap(bitmap,
new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()),
new Rect(0, 0, needWidth, needHeight), p);
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
if (getWidth() > 0 && getHeight() > 0) {
canvas.drawBitmap(bitmap1, new Rect(xOffset, yOffset, xOffset
+ getWidth(), yOffset + getHeight()), new Rect(0, 0,
getWidth(), getHeight()), p);
}
}
private float xDownLocation;
private float yDownLocation;
private float xLastLocation;
private float yLastLocation;
/**
* 该view是否可左右滑动
*
* @param dx
* x方向偏移量 dx=前一点x值-后一点x值
* @return true 可以滚动
*/
@Override
public boolean canScrollHorizontally(int dx) {
int canvasWidth = getWidth();
if (dx < 0) {// 手指向右移动
if (dx < (-mTouchSlop) && xOffset > 0) {
return true;
} else {
return false;
}
} else {// 手指向左移动
int needXOffset = needWidth - canvasWidth;
if (dx > mTouchSlop && xOffset < needXOffset) {
return true;
} else {
return false;
}
}
}
/**
* 该view是否可上下滑动
*
* @param dy
* y方向偏移量 dy=前一点y值-后一点y值
* @return true 可以滚动
*/
@Override
public boolean canScrollVertically(int dy) {
int canvasHeight = getHeight();
if (dy < 0) {// 手指向下移动
if (dy < (-mTouchSlop) && yOffset > 0) {
return true;
} else {
return false;
}
} else {// 手指向上移动
int needYOffset = needHeight - canvasHeight;
if (dy > mTouchSlop && yOffset < needYOffset) {
return true;
} else {
return false;
}
}
}
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xDownLocation = event.getX();
yDownLocation = event.getY();
xLastLocation = event.getX();
yLastLocation = event.getY();
int canvasWidth = getWidth();
int canvasHeight = getHeight();
if (needWidth <= canvasWidth && needHeight <= canvasHeight) {
return false;
}
break;
case MotionEvent.ACTION_MOVE:
if (getParent() != null
&& (Math.abs(xDownLocation - xLastLocation) > mTouchSlop)
&& (Math.abs(yDownLocation - yLastLocation) > mTouchSlop)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
float xCurrentLocation = event.getX();
float yCurrentLocation = event.getY();
float xMove = xCurrentLocation - xLastLocation;
float yMove = yCurrentLocation - yLastLocation;
xLastLocation = xCurrentLocation;
yLastLocation = yCurrentLocation;
// 修改偏移量
// 向右移动 xMove>0 xOffset减小
xOffset = (int) (xOffset - xMove);
yOffset = (int) (yOffset - yMove);
if (xOffset > needWidth - getWidth())
xOffset = needWidth - getWidth();
if (xOffset < 0)
xOffset = 0;
if (yOffset > needHeight - getHeight())
yOffset = needHeight - getHeight();
if (yOffset < 0)
yOffset = 0;
break;
case MotionEvent.ACTION_UP:
xDownLocation = 0;
yDownLocation = 0;
xLastLocation = 0;
yLastLocation = 0;
break;
}
invalidate();
return true;
}
}
源码下载