android如何实现两层可滚动view,内层优先响应

尊重他人劳动成果,转载请说明出处: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;
	}

}

源码下载

源码下载





评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值