View的事件体系


一、View基础知识


1、什么是View:


View是一种界面层的控件的一种抽象,它代表了一个控件。
ViewGroup为控件组。
View中也可以包含多个控件,形成控件树。

ImageButton是一个View,LinearLayout是一个View,也是一个ViewGroup。


2、View的位置参数:


(1)视图坐标系:(子视图在父视图中的位置关系)

以父视图的左上角为原点。
在触控事件MotionEvent中,通过getX()、getY()所获得的坐标就是视图坐标系中的坐标。

View的位置对应View的四个属性:top、left、right、button。
所以说View的宽高分别可以这样表示:
width = right - left
height = bottom - top

获取方式:getLeft(),getRight(),getTop(),getBottom()。



从Android3.0开始,另外还有几个参数:x、y、translationX 和 translationY。
x、y是VIew左上角的坐标,translationX 和 translationY 是View左上角相对于父容器的偏移量。默认为0。
且 x = left + translationX,y = top + translationY

是不是还是不清楚,就是说在View平移的过程中,top、left、right、button这四个值是不会变化的,是原始位置,变化的是x、y、translationX 和 translationY这四个参数。

(2)Android坐标系(以屏幕的左上角为原点)

系统提供了 getLocationOnScreen(intlocation[])这样的方法来获取Android坐标系中点的位置,即该视图左上角在Android坐标系中的坐标。
另外在触控事件MotionEvent中使用getRawX()、getRawY()方法所获得的坐标同样是An坐标系中的坐标。

(3)总结获取各种坐标值的办法:



属于MotionEvent的:
getX(),getY(),(相对父容器的)
getRawX(),getRawY();(相对屏幕的)

属于View自身的:
getTop(),getRight(),getTop(),getBottom()。(相对父容器的)




3、MotionEvent 手指触摸事件类型


ACTION_DOWN、ACTION_MOVE、ACTION_UP 就是这三个啦。

在这里我们将常会获取坐标:
getX/getY:返回相对于当前View左上角的 x 和 y 坐标。(视图坐标系)
getRawX/getRawY:返回相对于手机屏幕左上角的 x 和 y 坐标。(Android坐标系)

/**
 * 按下
 */
public static final int ACTION_DOWN             = 0;

/**
 * 抬起
 */
public static final int ACTION_UP               = 1;

/**
 * 移动
 */
public static final int ACTION_MOVE             = 2;

/**
 * 触摸动作取消
 */
public static final int ACTION_CANCEL           = 3;

/**
 * 触摸动作超出边界
 */
public static final int ACTION_OUTSIDE          = 4;

/**
 * 多点触摸按下动作
 */
public static final int ACTION_POINTER_DOWN     = 5;

/**
 * 多点离开动作
 */
public static final int ACTION_POINTER_UP       = 6;


4、TouchSlop 系统能识别的最小滑动距离


获取方式:ViewConfiguration.get(getContext()).getScaledTouchSlop() 。

我在程序中用的时候发现需要这样写才行:要用this才行:
    private int m = ViewConfiguration.get(DemoActivity_1.this).getScaledTouchSlop(); 
其中的get是为了获取一个ViewConfiguration类型的对象,然后这个对象再调用getScaledTouchSlop方法。


而它的默认值是定义在这里的:

里面的config.xml中:
    <!-- Base "touch slop" value used by ViewConfiguration as a
         movement threshold where scrolling should begin. -->
    <dimen name="config_viewConfigurationTouchSlop">8dp</dimen>


5、VelocityTracker速度追踪


用于追踪手指在滑动过程中的速度,包括水平和竖直。
它所谓的滑动速度指的是一段时间内手指划过的像素数,比如假设时间间隔为1s,速度就指的是手指在水平方向从左向右滑过100像素时,速度就是100,那如果向左滑动,速度就是负值啦。

    // 首先在View的onTouchEvent方法中获取追踪速度的对象
    VelocityTracker velocityTracker = VelocityTracker.obtain();
    // 将追踪速度对象与事件绑定起来:
    velocityTracker.addMovement(event);
    // 获取速度之前先计算速度,这一句不可缺:
    velocityTracker.computeCurrentVelocity(1000);
    // 分别计算水平和竖直速度:
    int xVelocity = (int) velocityTracker.getXVelocity();
    int yVelocity = (int) velocityTracker.getYVelocity();
    
    // 不需要使用它的时候,来重置和回收:
    velocityTracker.clear();
    velocityTracker.recycle();


6、GestureDetector手势检测


用于辅助检测用户的单击、滑动、长按、双击等行为。

一般情况下,我们知道View类有个View.OnTouchListener内部接口,通过重写他的onTouch(View v, MotionEvent event)方法,我们可以处理一些touch事件,但是这个方法太过简单,如果需要处理一些复杂的手势,用这个接口就会很麻烦(因为我们要自己根据用户触摸的轨迹去判断是什么手势)。

Android sdk给我们提供了GestureDetectorGesture:手势Detector:识别)类,通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别。虽然他能识别手势,但是不同的手势要怎么处理,应该是提供给程序员实现的。


GestureDetector这个类对外提供了两个接口:OnGestureListenerOnDoubleTapListener,还有一个内部类SimpleOnGestureListener


GestureDetector.OnDoubleTapListener接口用来通知DoubleTap事件,类似于鼠标的双击事件。

1、onDoubleTap(MotionEvent e)

双击的第二下,按下时触发 。它不能和 onSingleTapConfirmed 共存。


2、onDoubleTapEvent(MotionEvent e)

通知双击手势中的事件,包含downupmove事件。

(这里指的是在双击之间发生的事件,例如在同一个地方双击会产生双击手势,而在双击手势里面还会发生downup事件,这两个事件由该函数通知);

双击的第二下按下时,downup都会触发,可用e.getAction()区分。 


3onSingleTapConfirmed(MotionEvent e)

用来判定该次点击是单击而不是双击,如果连续点击两次就是双击手势,如果只点击一次,系统等待一段时间后没有收到第二次点击则判定该次点击为单击而不是双击,然后触发SingleTapConfirmed事件。这个方法不同于onSingleTapUp,他是在GestureDetector确信用户在第一次触摸屏幕后,没有紧跟着第二次触摸屏幕,也就是不是双击的时候触发  


GestureDetector.OnGestureListener接口用来通知普通的手势事件,该接口有如下六个回调函数:
1、
onDown(MotionEvent e)

down事件;


2、
onSingleTapUp(MotionEvente)

一次点击up事件;在touch down后又没有滑动onScroll),又没有长按(onLongPress),然后Touch up时触发。

点击一下非常快的(不滑动)Touchup

          onDown->onSingleTapUp->onSingleTapConfirmed 
点击一下稍微慢点的(不滑动)Touchup

          onDown->onShowPress->onSingleTapUp->onSingleTapConfirmed


3、onShowPress(MotionEvent e)

down事件发生而move或则up还没发生前触发该事件;按下了还没有滑动时触发(与onDownonLongPress)。

比较:onDown只要按下后一定立刻触发。而按下后停留一会儿且没有滑动,则先触发onShowPress再是onLongPress

所以按下后一直不滑动按照:onDown->onShowPress->onLongPress这个顺序触发。 


4、onLongPress(MotionEvent e)

长按事件;按下了不移动一直按着的时候触发。 


5、onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)

滑动手势事件;按下了滑动一点距离后,ACTION_UP时才会触发参数:

e1 1ACTION_DOWN 事件并且只有一个;

e2 最后一个ACTION_MOVE 事件 

velocityX X轴上的移动速度,像素/ 

velocityY Y轴上的移动速度,像素/.

触发条件:X轴的坐标位移大于FLING_MIN_DISTANCE且移动速度大于FLING_MIN_VELOCITY个像素/


6、onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)

在屏幕上拖动事件。无论是用手拖动view,或者是以抛的动作滚动,都会多次触发,这个方法在ACTION_MOVE动作发生时就会触发抛:

手指触动屏幕后,稍微滑动后立即松开:

          onDown-----onScroll----onScroll----onScroll----………----->onFling

拖动:

          onDown------onScroll----onScroll------onFiling


    // 首先需要创建GestureDetector对象,实现OnGestureListener接口:下面这个语句写的有点问题
    GestureDetector mGestureDetector = new GestureDetector(this);
    // 解决长按屏幕后无法拖动的现象:
    mGestureDetector.setIsLongpressEnabled(false);
    // 接管目标View 的 onTouchEvent 方法,在待监听View的onTouchEvent方法中添加如下实现:
    boolean consume = mGestureDetector.onTouchEvent(event);
    return consume;

三、View的滑动


要实现View的滑动,就必须监听用户的触摸事件,并根据事件传入的坐标,动态且不断地改变View的坐标,从而实现View跟随用户触摸的滑动而滑动。

不管采用哪一种方式,其实现的思想基本是一致的:
当触摸View时,系统记下当前触摸点的坐标,
当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对前一次坐标点的偏移量,并通过偏移量来修改View的坐标。
这样不断重复,从而实现滑动过程。

三种方法实现View的滑动:
(1)View本身提供的scrollTo/scrollBy。
(2)通过动画给View施加平移效果来实现滑动。
(3)通过改变View的LayoutParams使得View重新布局从而实现滑动。
(4)layout()方法。
(5)offsetLeftAndRight()与offsetTopAndBottom()。
(6)Scroller弹性滑动

1、使用scrollTo/scrollBy:(操作简单,适合对View内容的滑动)


scrollTo、scrollBy方法移动的是View的content内容,即让View的内容移动,
如果在ViewGroup中使用scrollBy、scrollTo方法,那么移动的将是所有的子View,
但如果在View中使用,那么移动的将是View的内容,
例如对TextView这个View而言,文本就是它的内容,对于ImageView而言,drawable就是它的内容,但TextView和ImageView本身的View却没有移动。

所以写的时候应该这样写:在View所在的ViewGroup中来使用:
((View)getParent()).scrollBy(offsetX, offsetY);

(1)源码位置在:sources\android\view\View.java
/**
 * Set the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the x position to scroll to
 * @param y the y position to scroll to
 */
/*
 * 实现的是基于所传递参数的绝对滑动,到x和y的地方
 * */
public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

/**
 * Move the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the amount of pixels to scroll by horizontally
 * @param y the amount of pixels to scroll by vertically
 */
/*
 * 实现的是基于所传递参数的相对滑动
 * */
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}


(2)其中的mScrollX和mScrollY,可以通过getScrollX和getScrollY来获得。

    /**
     * Return the scrolled top position of this view. This is the top edge of
     * the displayed part of your view. You do not need to draw any pixels above
     * it, since those are outside of the frame of your view on screen.
     *
     * @return The top edge of the displayed part of your view, in pixels.
     */
    public final int getScrollY() {
        return mScrollY;
    }
在滑动的过程中,mScrollX的值总是等于View的左边缘和View内容左边缘在水平方向的距离,

而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。

View边缘指的是View的位置,由四个顶点组成,

而View内容边缘指的是View中内容的边缘。

scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。假位移啦!!!!!!

这个图里面主要注意它们的那个正负值呐。




2、使用动画:(操作简单,适用于没有交互的View和复杂动画效果的View)


(1)使用动画来移动View,主要是操作View的translationX和translationY属性。
(2)动画包括传统的View动画和属性动画。
(3)注意点:同上面的scrollBy 和scrollTo一样,View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽高。
并且如果希望动画后的状态得以保留还必须将 fillAfter属性设置为true,否则动画完成后其动画结果会消失。
(4)属性动画并不存在(3)这样的情况。
(5)情况(3)会导致一个严重的后果,就是移动后不能带着它的点击事件一起移动。那怎么办呢?有两种方法:
第一种:使用属性动画。
第二种:我们可以在新的位置上预先创建一个和目标Button一模一样的Button,它们连onClick事件也一样。所以移动后,设置显示和隐藏,来间接达到移动的目的。
(6)复杂效果用动画。

3、改变布局参数LayoutParams:(操作稍微复杂,适用于有交互的View)


(1)改变Button的参数:主要改变的是margin属性。
MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.requestLayout();
// 或者mButton.setLayoutParams(params);
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)getLayoutParams();
用这个params的时候需要考虑父布局的类型,当然还可以使用ViewGroup.MarginLayoutParams来实现这个功能,效果是一样的,并且更加方便,不需要考虑父布局的类型:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();

(2)在Button的左边放置一个空的View,这个空View的默认宽度为0。当我们需要向右移动Button时,只需要重新设置空View的宽度即可。

4、layout方法:


通过调用View的layout方法,给layout设置新的值:(使用相对父容器的坐标)
@Override
public boolean onTouchEvent(MotionEvent event) {

	// 每次进来获取以下现在的坐标值:
    int x = (int) event.getX();
    int y = (int) event.getY();
    
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        // 记录触摸点坐标:
    	lastX = x;
    	lastY = y;
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int offsetX = x - lastX;
        int offsetY = y - lastY;
        // 在当前left、top、right、bottom的基础上加上偏移量(注意这个左上右下的顺序)
        layout(getLeft() + offsetX,
        		getTop() + offsetY,
        		getRight() + offsetX,
        		getBottom() + offsetY,
        		);
        break;
    }
    case MotionEvent.ACTION_UP: {
        break;
    }
    default:
        break;
    }

    return true;
}

下面使用(绝对坐标,相对屏幕的坐标):
@Override
public boolean onTouchEvent(MotionEvent event) {

	// 每次进来获取以下现在的坐标值:
    int rawX = (int) event.getRawX();
    int rawY = (int) event.getRawY();
    
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        // 记录触摸点坐标:
    	lastX = rawX;
    	lastY = rawY;
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int offsetX = rawX - lastX;
        int offsetY = rawY - lastY;
        // 在当前left、top、right、bottom的基础上加上偏移量(注意这个左上右下的顺序)
        layout(getLeft() + offsetX,
        		getTop() + offsetY,
        		getRight() + offsetX,
        		getBottom() + offsetY,
        		);
        /*
         * 这里是必须要注意的一点
         * 使用绝对坐标系,在每次执行完ACTION_DOWN的逻辑后,
         * 一定要重新设置初始坐标,这样才能准确的获取偏移量。
         * */
        lastX = rawX;
        lastY = rawY;
        break;
    }
    case MotionEvent.ACTION_UP: {
        break;
    }
    default:
        break;
    }

    return true;
}

5、offsetLeftAndRight()与offsetTopAndBottom()


这个方法相当于系统提供了一个对左右、上下移动的API的封装。
当计算出偏移量后,只需要使用如下代码就可以完成View的重新布局,
效果与使用Layout方法一样,
代码如下所示:
// 同时对left和right进行偏移:
offsetLeftAndRight(offsetX);
// 同时对top和bottom进行偏移:
offsetTopAndBottom(offsetY);


6、Scroller弹性滑动


Scroller的工作机制:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTp方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。
使用Scroller类通常需要三个步骤:
(1)初始化Scroller:
Scroller scroller = new Scroller(mContext);
(2)重写computeScroll()方法,实现模拟滑动:
它是scroller的核心,系统在绘制View的时候会在draw()方法中调用该方法。
这个方法实际上就是使用的scrollTo方法,再结合Scroller对象,帮助获取到当前的滚动值。
我们可以通过不断地瞬间移动一个小的距离来实现整体上的平滑移动效果:
@Override
public void computeScroll(){
	super.computeScroll();
	// 判断Scroller是否执行完毕
	if(mScroller.computeScrollOffest()){
		((View)getParent()).scrollTo(
				mScroller.getCurrX(), 
				mScroller.getCurrY()
				);
		// 通过重绘来不断调用computeScroll:
		invalidate();
	}
}

Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了 getCurrX()、getCurrY()方法来获得当前的滑动坐标。

还有一个注意点就是invalidate()方法,因为只能在computeScroll()方法中获取模拟过程中的scrollX和scrollY坐标。
但computeScroll()方法是不会自动调用的,只能通过invalidate()-> draw()    -> computeScroll()来间接调用computeScroll()方法,
所以需要在上面的代码中调用invalidate()方法,实现循环获取scrollX和scrollY的目的。
而当模拟过程结束以后,scroller.computeScrollOffset()方法会返回false,从而中断循环,完成整个平滑移动过程。

(3)startScroll开启模拟过程:
有两个重载方法:
public void startScroll(int startX, int startY, int dx, int dy, int duration)

public void startScroll(int startX, int startY, int dx, int dy)
在获取坐标时,通常可以使用 getScrollX()和getScrollY()方法来获取父视图中content所滑动到的点的坐标,不过还是要注意正负值,和scrollTo、scrollBy一样正负值相反。

(4)具体的使用:
就是在ACTION_UP的时候,写上面的(1)(2)就可以啦,但是要记得再次调用invalidate()来通知View进行重绘。

(5)注意点:
因为scrollTo/scrollBy的滑动过程是瞬间完成的,所以为了用户体验,需要设置弹性滑动。
Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。
Scroller scroller = new Scroller(mContext);

// 缓慢滚动到指定位置:
private void smoothScrollTo(int destX, int destY){
	int scrollX = getScrollX();
	int delta = destX - scrollX;
	// 1000ms内滑向destX,效果就是慢慢滑动
	mScroller.startScroll(scrollX, 0, delta, 0, 1000);
	invalidate();
}

@Override
public void computeScroll(){
	if(mScroller.computeScrollOffest()){
		scrollTo(mScroller.getCurrX(), mScroller.getCurrY();
		postInvalidate();
	}
}
所以呢,结合上面的(1)(2)(3)(4),我们规整为(5),就是说第一行scroller是必须创建的,下面的computeScroll方法是需要重写的,中间的smoothScrollTo方法是我们自己写的,里面主要是要调用startScroll方法,随后又调用了invalidate方法,这样就可以循环往复的一直调用了:computeScroll()方法是不会自动调用的,只能通过invalidate()-> draw()    -> computeScroll()来间接调用computeScroll()方法。


四、弹性滑动


三种方法:

(1)使用Scoller。

(2)通过动画。

(3)使用延时策略


1、使用Scoller:


(1)刚刚已经写过了下面的这两个函数:这两个方法写在活动中:

Scroller scroller = new Scroller(mContext);

//缓慢滚动到指定位置:
private void smoothScrollTo(int destX, int destY){
	int scrollX = getScrollX();
	int delta = destX - scrollX;
	// 1000ms内滑向destX,效果就是慢慢滑动
	mScroller.startScroll(scrollX, 0, delta, 0, 1000);
	invalidate();//通知View进行重绘
}

@Override
public void computeScroll(){
	if(mScroller.computeScrollOffest()){
		scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
		postInvalidate();
	}
}
但其实现的重点在于startCroll方法和computeScrollOffest方法的实现。

(2)源码地址:sources\android\widget\Scoller.java

(3)其实在startScroll中什么都没有做,它只是保存了我们传递的几个参数:

/**
 * Start scrolling by providing a starting point, the distance to travel,
 * and the duration of the scroll.
 * 
 * @param startX Starting horizontal scroll offset in pixels. Positive
 *        numbers will scroll the content to the left.
 * @param startY Starting vertical scroll offset in pixels. Positive numbers
 *        will scroll the content up.
 * @param dx Horizontal distance to travel. Positive numbers will scroll the
 *        content to the left.
 * @param dy Vertical distance to travel. Positive numbers will scroll the
 *        content up.
 * @param duration Duration of the scroll in milliseconds.
 */
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    // 滑动起点
    mStartX = startX;
    mStartY = startY;
    // 滑动终点
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    // 滑动时间,内容的滑动而不是位置的滑动
    mDurationReciprocal = 1.0f / (float) mDuration;
}
(4)真正的滑动实现是在startScroll下面的 invalidate()方法中。

 invalidate方法会导致View重绘,View重绘会调用draw方法,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在View中是一个空实现(它在View.java中),因此需要我们自己去重写实现。

具体过程:当View重绘后会在draw方法中调用computeScroll方法,而computeScroll方法又会去向Scroller获取当前的scrollX 和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,这一次重绘和上一次重绘过程一样的,还是会导致computeScroll方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,如此反复,直到整个滑动过程结束。

/**
 * Call this when you want to know the new location.  If it returns true,
 * the animation is not yet finished.
 */ 
/*
 * 这个方法会根据事件的流逝来计算出当前scrollX和scrollY的值。
 * 根据时间流逝的百分比来算出scrollX和scrollY改变的百分比并计算出当前的值
 * 它返回true表示滑动还没有结束,false表示滑动已经结束。
 * */
public boolean computeScrollOffset() {
    if (mFinished) {
        return false;
    }

    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    if (timePassed < mDuration) {
        switch (mMode) {
        case SCROLL_MODE:
            float x = timePassed * mDurationReciprocal;

            if (mInterpolator == null)
                x = viscousFluid(x); 
            else
                x = mInterpolator.getInterpolation(x);

            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        case FLING_MODE:
            final float t = (float) timePassed / mDuration;
            final int index = (int) (NB_SAMPLES * t);
            float distanceCoef = 1.f;
            float velocityCoef = 0.f;
            if (index < NB_SAMPLES) {
                final float t_inf = (float) index / NB_SAMPLES;
                final float t_sup = (float) (index + 1) / NB_SAMPLES;
                final float d_inf = SPLINE_POSITION[index];
                final float d_sup = SPLINE_POSITION[index + 1];
                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                distanceCoef = d_inf + (t - t_inf) * velocityCoef;
            }

            mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
            
            mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
            // Pin to mMinX <= mCurrX <= mMaxX
            mCurrX = Math.min(mCurrX, mMaxX);
            mCurrX = Math.max(mCurrX, mMinX);
            
            mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
            // Pin to mMinY <= mCurrY <= mMaxY
            mCurrY = Math.min(mCurrY, mMaxY);
            mCurrY = Math.max(mCurrY, mMinY);

            if (mCurrX == mFinalX && mCurrY == mFinalY) {
                mFinished = true;
            }

            break;
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

是不是很神奇?切


2、通过动画:


动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果。

(1)下面的代码可以让一个View的内容在100ms内向左移动100像素:

ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).satrt();

(2)我们可以利用动画的特性来实现一些动画不能实现的效果。我们可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离。

final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
	@Override
	public void onAnimationUpdate(ValueAnimator animator){
		float fraction = animator.getAnimatedFraction();
		mButton1.scrollTo(startX + (int)(deltaX * fraction), 0);
	}
});
animator.start();
在这里例子中,我们并没有在ofInt方法中直接对目标对象进行移动,而是在下面的scrollTo中进行实际的移动,这个思想和上面的Scroller滑动思想是相同的。


3、使用延时策略:


延时策略的工作机制:通过发送一系列延时消息从而达到一种渐近式的效果。

(1)两种方式:Handler或View的postDelayed方法,或使用线程的sleep方法。
(2)其实还是要计算滑动过程中的百分比的。这里有一个小例子:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;

private int mCount = 0;

private Handler mHandler = new Handler(){
	public void handleMessage(Message msg){
		switch(msg.what){
		case MESSAGE_SCROLL_TO:
			mCount++;
			if(mCount <= FRAME_COUNT){
				float fraction = mCount / (float) FRAME_COUNT;
				int scrollX = (int) (fraction * 100);
				mButton.scrollTo(scrollX, 0);
				mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
			}
			break;
		
		}
		
		default:
			break;
	    }
	};
};


五、View的事件分发机制


点击事件的事件分发,就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View。


1、点击事件的分发过程由三个很重要的方法共同完成:


public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发,dispatch派分。如果事件能够传递给当前View,这个方法就一定会被调用,

返回结果受当前View的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。


public boolean onInterceptTouchEvent(MotionEvent event)

在 dispatchTouchEvent 方法的内部调用,用来判断是否拦截某个事件,如果当前View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,

返回结果表示是否拦截当前事件。true表示拦截。


public boolean onTouchEvent(MotionEvent event)

在 dispatchTouchEvent 方法中调用,用来处理点击事件,

返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View再也无法接收到该事件的后续事件。

就是说如果消耗了down,还可以消耗move,up等,如果不消耗,后续的就都没有了。

但如果不消耗,该事件序列就要交由上一层来处理。


它们三者的关系用伪代码表示的话就是这样的:是不是很简单的样子?

	public boolean dispatchTouchEvent(MotionEvent ev) {
		
		boolean consume = false;
		
		if (onInterceptTouchEvent(ev)) {
			consume = onTouchEvent(ev);
		} else {
			consume = child.dispatchTouchEvent(ev);
		}
		
		return consume;
	}


2、OnTouchListener 和 OnTouchEvent 的关系


如果一个View并没有给它设置OnTouchListener,也就不存在什么问题了,就按上面的程序走。

但如果有设置了OnTouchListener,那么它里面的 onTouch 方法就会被调用。

这时候问题就来了,如果 onTouch 方法返回的是 false,则当前View的onTouchEvent 方法会被调用;

但如果onTouch 方法返回的是true,那么当前View的 onTouchEvent 方法就不会被调用了!

所以说呢,给View设置的 OnTouchListener 的优先级要比 onTouchEvent 要高,

两者只会有一个返回true,先询问 OnTouchListener,不行再去看看 OnTouchEvent。


在onTouchEvent方法中,还可能设置有 OnClickListener,那么它的 onClick 方法会被调用,

也就是说,只有 onTouchEvent被调用了,onClick 才有调用的机会,所以说onClick的优先级是最低的。


3、事件传递顺序


Activity -> Window -> View


虽然是从上往下的传递,但是当一个View 的 onTouchEvent 返回false(参考下面Tips中的(4)),那么它的父容器的 onTouchEvent 将会被调用,如果还返回false就以此类推的往上推,直到Activity。


4、结论Tips


(1)同一个事件序列:是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束。

其中会有一个down,多个move,一个up事件。

(2)正常情况下,一个事件序列只能被一个VIew拦截且消耗,因为一旦拦截,剩下的就都交给拦截的那个View了。不过可以采取特殊手段,比如说一个View将本该自己处理的事件通过onTouchEvent 强行传递给其他View处理。

(3)一个View一旦决定拦截,那剩下的事件序列都会给它,而且它的 onInterceptTouchEvent 就不会再被调用了。

(4)某个VIew一旦开始处理事件,也就是到了onTouchEvent的地步,如果它不消耗 ACTION_DOWN事件,也就是说onTouchEvent返回了false,那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理。就是说事件一旦交给一个VIew处理,他就必须消耗掉,不然同一事件序列中剩下的事件就不再交给它来处理了。

(5)(没看懂这条)如果VIew不消耗除 ACTION_DOWN 以外的其他事件(就是说消耗了ACTION_DOWN,却没消耗其他的),那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前View可以持续接到后续的事件,最终这些消失的点击事件会传递给Activity处理。

(6)ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent 方法默认返回 false。

(7)View没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。

(8)View 的onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable 和 longClickable 同时为 false)。

View的longClickable 属性默认都为false,clickable 属性要看具体的控件,比如Button为true,TextView为false。

(9)View的enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个View是disable状态的,只要它的clickable 或者 longClickable 有一个为true,那么它的onTouchEvent 就返回ture。

(10)onClick会发生的前提是当前的View 是可点击的,并且它收到了 down 和 up的事件。

(11)事件传递过程是由外向内的,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,就是下面的那个FLAG_DISALLOW_INTERCEPT标志位,但是ACTION_DOWN事件除外。


5、事件分发的源码解析


1、Activity对点击事件的分发过程:


(1)源码位置在:sources\android\app\Activity.java

(2)这里是一个事件发生时最先到达的地方。

(3)Activity调用它的dispatchTouchEvent来进行事件派发。

(4)看第二个if语句,首先Activity将事件交给Windows,然后Windows会调用它的superDispatchTouchEvent,如果成功了,说明Activity下面的子处理了该事件,返回true,否则就会由下级传回来来调用Activity最下面的那个onTouchEvent来处理事件。

/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 * 
 * @param ev The touch screen event.
 * 
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // 交给下级Window去处理
    if (getWindow().superDispatchTouchEvent(ev)) {
    	// 下级处理成功,返回false。
        return true;
    }
    // 下级处理失败,退回来自己在onTouchEvent中处理。
    return onTouchEvent(ev);
}


2、Window对点击事件的分发过程:


(1)源码位置在:sources\android\view\Window.java
(2)由Activity传递到Window,然后Window要将事件传递给ViewGroup。
(3)Window是一个抽象类,其中的方法都是抽象方法,所以superDispatchTouchEvent 也是一个抽象方法。
    /**
     * Used by custom windows, such as Dialog, to pass the touch screen event
     * further down the view hierarchy. Application developers should
     * not need to implement or call this.
     *
     */
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
(4)Window的唯一实现类是 PhoneWindow。唯一的!!!
/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.policy.PhoneWindow, which you should instantiate when needing a
 * Window.  Eventually that class will be refactored and a factory method
 * added for creating Window instances without knowing about a particular
 * implementation.
 */

(5)关于PhoneWindow,我并没有找到它的源码,呵呵。位置应该在:sources\android\policy\PhoneWindow.java 。看吧,PhoneWindow 又把事件分发的任务给了DecorView。
public boolean superDispatchTouchEvent(MotionEvent event){
	return mDecor.superDispatchTouchEvent(event);
}


3、DecorView对点击事件的分发过程:


反正最后是从DecorView传给了View了,过程先略。


4、顶级View对点击事件的分发过程:


我这里需要加一下我一直没搞懂的陈述:

(1)就是在ViewGroup中的 dispatchTouchEvent 方法,其实所有的事件序列中的事件包括ACTION_DOWN、ACTION_MOVE和ACTION_UP都会进入到这里来进行事件的分配。

(2)对于ACTION_DOWN,如果ViewGroup拦截了ACTION_DOWN,就会导致 mFirstTouchTarget == null,interception ==  true;这样的话当其他的后续的事件到来时,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 语句无法进入,所以ViewGroup拦截了ACTION_DOWN以后,这个事件的后续只能由ViewGroup来处理。

(3)如果ACTION_DOWN是由子元素拦截的,那么 mFirstTouchTarget != null,interception ==  false,这样呢当其他后续的事件到来时,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 语句还是可以进入的。然后可以执行到onInterceptTouchEvent 方法,默认的ViewGroup的这个方法是都返回false的,也就是ViewGroup不拦截任何的事件,并且这也意味着,一旦某个子元素拦截了ACTION_DOWN,那后续的事件序列也都交给这个子元素来处理了。但是我们在写自己的ViewGroup例如LinearLayout时,就可以重写onInterceptTouchEvent方法,然后让它可以在例如ACTION_MOVE的时候返回ture,这样ViewGroup就可以实现没有拦截ACTION_DOWN并交给了子元素,但是却拦截了后面的ACTION_MOVE,这也就是下面在滑动冲突中讲到的外部拦截法。

是不是很神奇呀!


顶级View一般是一个ViewGroup,所以我们去看ViewGroup。

(1)源码位置在:sources\android\view\ViewGroup.java

(2)下面我们将一直在ViewGroup的超长dispatchTouchEvent函数中讲解!

/**
 * {@inheritDoc}
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;


        /*
         * 当新的一轮点击到来的时候,从ACTION_DOWN开始的,做一些初始化的工作:
         * */
        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Throw away all previous state when starting a new touch gesture.
            // The framework may have dropped the up or cancel event for the previous gesture
            // due to an app switch, ANR, or some other state change.
        /*
         * 至少我知道在这个函数中最终将mFirstTouchTarget设为null。
         * mFirstTouchTarget代表的就是一个事件序列中第一个拦截的对象,
         * 所以这里需要重置。
         * */
            cancelAndClearTouchTargets(ev);
            /*
             * 如果事件是ACTION_DOWN,
             * ViewGroup就会在resetTouchState中重置下面的FLAG_DISALLOW_INTERCEPT标志位。
             * 重置的方式是这样的:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
             * */
            resetTouchState();
        }


        // Check for interception.
        /*
         * 这个标识很重要,因为它一旦被标志位true,意味着下面的各种if语句都进不去了,
         * 意味着本ViewGroup拦截了该事件,并且后续的事件序列直接由该ViewGroup处理,
         * 而不是进入各种if中判断是否需要拦截。
         * */
        final boolean intercepted;// 拦截标识
        /*
         * 这个if中需要满足两个条件:
         * (1)actionMasked == MotionEvent.ACTION_DOWN:
         * 该事件是否为点击下按事件时成立,就是说新的一轮事件到来
         * (2)mFirstTouchTarget != null:
         * 当ViewGroup不拦截事件并将事件交给子元素处理时,成立,mFirstTouchTarget指向这个子元素。
         * 而且在ViewGroup中,默认onInterceptTouchEvent返回false,它是不拦截任何事件的,
         * 但是在LinearLayout中可能就会拦截啊,可以改写啊。
         * 而且,当第二个条件成立时,此时发生的事件序列就是ACTION_MOVE或者ACTION_UP,都会进入到这个if语句中。
         * */
        /*
         * 所以说呢,当子元素成功拦截了事件或者下按事件发生的时候就会进入if语句。
         * 所以说呢,如果子元素没有处理,并且是move和up发生的时候就无法进入该if语句。
         * 但为什么这样设定呢,因为如果子元素没有处理的话,事件序列中的其他事件就会直接由ViewGroup来处理了,
         * 不需要来这里来判断一下到底要不要拦截事件了。那如果是move和up也是同样的,不需要来这里来判断要不要拦截事件。
         * */
        /*
         * 也就相当于说,一个事件,第一次因为ACTION_DOWN进入这里,然后ViewGroup判断是否来拦截。
         * 之后在子元素成功处理后,因为子元素是可以通过FLAG_DISALLOW_INTERCEPT标志位来干预父元素的事件分发过程,所以又来这里来要看是否拦截。
         * */
        /*
         * 为什么总说一旦父元素拦截ACTION_DOWN以后其他的事件序列就只能由父元素来处理呢?
         * 是因为如果父元素拦截了ACTION_DOWN,那么mFirstTouchTarget == null
         * 当ACTION_MOVE和ACTION_UP到来的时候,这条if语句就不会进入了,
         * 然后intercepted = true;表示事件序列由父元素全拦截了。
         * */
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
        /*
         * 通常事件传递过程是由外向内的,
         * 但是通过 requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,
         * 不过ACTION_DOWN事件除外。
         * 干预表现在子元素已经拦截了事件,
         * 但是可以通过requestDisallowInterceptTouchEvent来控制
         * ACTION_MOVE和ACTION_UP能不能够进入到这里来。
         * */
        /*
         * FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截处理ACTION_DOWN以外的其他点击事件了。
         * 因为在事件分发时,ACTION_DOWN会重置FLAG_DISALLOW_INTERCEPT标志位,表示另一次事件开始。
         * */
        /*
         * 子View干涉ViewGroup的过程:
         * 初始化:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
         * 在子View中FLAG_DISALLOW_INTERCEPT被重置,也就是要去干扰,
         * 然后mGroupFlags & FLAG_DISALLOW_INTERCEPT为1
         * 然后(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 为true
         * 然后disallowIntercept为true
         * 然后导致if (!disallowIntercept)无法进入。
         * */
        /*
         * FLAG_DISALLOW_INTERCEPT标志位有什么用呢?
         * 当面对滑动冲突时,我们可以考虑用这种方法去解决问题。
         * */
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
            /*
             * 所以说onInterceptTouchEvent并不是每次事件都会被调用的。
             * 而dispatchTouchEvent却会在每次都调用。
             * 对于原始的ViewGroup,onInterceptTouchEvent会返回false,
             * 但是对于你自己写的LinearLayout,则可以修改这个函数,
             * 让它对ACTION_DOWN、ACTION_MOVE、ACTION_UP做出不同的选择。
             * */
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
        /*
         * 就是说没有子元素mFirstTouchTarget,而且事件也不是ACTION_DOWN,
         * 没人管那就只能自己拦截了。
         * */
            intercepted = true;
        }


        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;


        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        
        /*
         * 当ViewGroup不拦截事件的时候,intercepted=false,事件会向下分发由它的子View进行处理
         * 所以说一旦ViewGroup拦截了事件,intercepted=true,
         * 意味着事件序列中的任何事件都不再会传给子元素了,由父元素全权处理。
         * 所以intercepted=true一定要谨慎设置。
         * */
        if (!canceled && !intercepted) {
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;


                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final View[] children = mChildren;


                    final boolean customOrder = isChildrenDrawingOrderEnabled();
                    
                    /*
                     * 遍历ViewGroup的所有子元素,判断子元素是否能够接收到点击事件。
                     * */
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder ?
                                getChildDrawingOrder(childrenCount, i) : i;
                        final View child = children[childIndex];
                        /*
                         * 判断子元素是否能够接收到点击事件:
                         * (1)canViewReceivePointerEvents:子元素是否在播动画。
                         * (2)isTransformedTouchPointInView:点击事件的坐标是否落在子元素的区域内。
                         * */
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }


                        /*
                         * 如果上面那个if语句没有成立,说明这个子元素是可以拦截事件的,
                         * 所以新的TouchTarget出现了,就是这个子元素。
                         * */
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }


                        resetCancelNextUpFlag(child);
                        /*
                         * 这个子元素已经拦截该事件了,现在要子元素传递给它自己的子元素去分派这个事件了:
                         * dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法。
                         * 下面的第三个参数中child一定不为null,所以child的dispatchTouchEvent一定会被调用。
                         * 子元素的dispatchTouchEvent返回true,
                         * 意味着dispatchTransformedTouchEvent也返回ture,
                         * 表示事件被子元素分发成功,并break跳出循环。
                         * */
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            mLastTouchDownIndex = childIndex;
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            /*
                             * 分发成功后,在addTouchTarget会对mFirstTouchTarget进行赋值
                             * */
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            /*
                             * 分发成功,跳出循环
                             * */
                            break;
                        }
                    }
                }


                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }


        /*
         * 有两种情况遍历所有的子元素后事件也没有处理:
         * (1)ViewGroup根本没有子元素
         * (2)子元素的dispatchTouchEvent都返回了false。
         * 这种情况下只能ViewGroup自己来处理事件了。
         * */
        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
       /*
        * 注意第三个参数:null,在上面变量子元素的时候这里放的是child。
        * 如果是null,dispatchTransformedTouchEvent内部就会调用:
         * super.dispatchTouchEvent(event);
         * 很显然,这里就转到了View的dispatchTouchEvent(event)方法,即点击事件开始交由View来处理。在View中有onTouchEvent。
         * 其实父元素ViewGroup的onTouchEvent就是指的是View中的onTouchEvent方法,它自己这里是没有的。因为ViewGroup是继承View的!!!!
         * */
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }


        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }


    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}


5、View对点击事件的处理过程


(1)源码位置在:sources\android\view\View.java

(2)在上面的顶级View中,如果顶级View没有处理事件,而顶级View的子元素也没有人处理这个事件,那就会到这里来由View来处理事件。

(3)View对点击事件的处理过程稍微简单一些。

(4)注意,这里的View不包含ViewGroup。只是简单的单个View的处理,因为他没有子元素因此不能向下传递事件,所以它只能自己处理事件。

(5)下面对View源码中的dispatchTouchEvent进行分析:


/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
    	/*
    	 * 首先会判断有没有设置OnTouchListener。
    	 * 如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会调用,
    	 * 这样做的好处是方便外界处理点击事件。
    	 * */
        ListenerInfo li = mListenerInfo;
        if (li != null 
        		&& li.mOnTouchListener != null 
        		&& (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        /*
         * 优先级低于OnTouchListener
         * */
        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

(6)下面对View源码中的onTouchEvent方法进行分析:

/**
 * Implement this method to handle touch screen motion events.
 * <p>
 * If this method is used to detect click actions, it is recommended that
 * the actions be performed by implementing and calling
 * {@link #performClick()}. This will ensure consistent system behavior,
 * including:
 * <ul>
 * <li>obeying click sound preferences
 * <li>dispatching OnClickListener calls
 * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
 * accessibility features are enabled
 * </ul>
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    /*
     * 当View处于不可用状态下时,View照样会消耗点击事,
     * 但它并不对事件做出任何的反映
     * */
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    /*
     * 如果View设置有代理,那么还会执行mTouchDelegate的onTouchEvent方法,
     * 这个onTouchEvent的工作机制看起来和OnTouchListener类似,这里我们不做研究
     * */
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    /*
     * 这里是对点击事件的具体处理。
     * 可以发现的是View的CLICKABLE和LONG_CLICKABLE只要有一个为true,
     * 那么这个View就消耗这个事件,即onTouchEvent返回ture,不管他是不是DISABLE状态。
     * 这个证明了前面(8)(9)(10)的结论。
     * */
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
        /*
         * 当up事件发生时,就会触发performClick()方法。
         * */
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        setPressed(true);
                   }

                    if (!mHasPerformedLongPress) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                            	/*
                            	 * 如果View设置了OnClickListener,
                            	 * 那么performClick()方法内部会调用它的onClick方法
                            	 * */
                                performClick();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }
                    removeTapCallback();
                }
                break;

            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true);
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }
        return true;
    }

    return false;
}


六、View的滑动冲突


1、常见的滑动冲突场景:


(1)外部滑动方向和内部滑动方向不一致。

(2)外部滑动方向和内部滑动方向一致。

(3)上面两种情况的嵌套。


2、滑动冲突的处理规则:


具体来说:就是根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。

也就是说可以根据滑动过程中两个点之间的坐标就可以得出到底是水平滑动还是竖直滑动。


对于场景(1)如下解决办法:(一左一右,或者一上一下)

(1)可以根据滑动路径和水平方向的夹角。

(2)可以根据水平方向和竖直方向的距离差。

(3)可以根据水平方向和竖直方向的速度差。


对于场景(2)如下解决办法:(同上同下,或者同左同右)

一般需要在业务上寻找突破点。

比如业务上有规定:当初与某种状态时需要外部View相应用户的滑动,而处于另一种状态时需要内部View来响应View的滑动。


对于场景(3)也只能从业务上寻找突破点。



3、滑动冲突的解决方式:


(1)外部拦截法:


所谓外部拦截法就是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。

外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,在内部做出相应的拦截即可。

下面给出伪代码:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
	
	boolean intercepted = false;
	
    int x = (int) event.getX();
    int y = (int) event.getY();
    
    int action = event.getAction();
    switch (action) {
    
    case MotionEvent.ACTION_DOWN:
    	// 必须为fasle,不然父类容器拦截ACTION_DOWN以后,
    	// 后续的事件序列就都由父容器处理了。
    	intercepted = false;
    	break;
    	
    case MotionEvent.ACTION_MOVE:
    	if(父类容器需要当前点击事件){
    		intercepted = true;
    	}   		
    	else{
    		intercepted = false;
    	}
    	break;
    	
    case MotionEvent.ACTION_UP:
    	intercepted = false;//这里也是必须的。
    	break;
    	
    default:
    	break;
    }
    
    mLastXIntercept = x;
    mLastYIntercept = y;
    
    return intercepted
}


(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: {
    	// 这里的意思是设置让父容器无法拦截ACTION_DOWN事件:
        parent.requestDisallowInterceptTouchEvent(true);
        break;
    }
    
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        if(父容器需要此类点击事件){
        	// 如果需要的话就让父类容器可以接收ACTION_MOVE事件。
            parent.requestDisallowInterceptTouchEvent(false);
        }
        break;
    }
    case MotionEvent.ACTION_UP: {

        break;
    }
    default:
        break;
    }

    mLastX = x;
    mLastY = y;
    return true;
}
当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不同有改动。
除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,
(这里ACTION_DOWN不能让父元素默认拦截,因为一旦父元素拦截,剩下的指令序列就都由父元素来处理了)
这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false); 方法时,父元素才能继续拦截所需的事件。
父元素去哪里改呢,当然是 onInterceptTouchEvent 方法啦。
public boolean onInterceptTouchEvent(MotionEvent event) {
	int action = event.getAction();
	if(action == MotionEvent.ACTION_DOWN) {
		return false;
	} else {
		return true;
	}
}


我们去源码中看一下ViewGroup中的 requestDisallowInterceptTouchEvent 这个方法吧:是通过操控disallowIntercept 来达到目的的。
    /**
     * {@inheritDoc}
     */
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

(3)举例:在一个水平布局的LinearLayout中添加三个并列的ListView,所以父容器左右移动,子容器上下移动,造成冲突。

首先看一下Activity中的初始化代码:

package com.ryg.chapter_3;

import java.util.ArrayList;
import com.ryg.chapter_3.R;
import com.ryg.chapter_3.ui.HorizontalScrollViewEx;
import com.ryg.chapter_3.utils.MyUtils;

import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

public class DemoActivity_1 extends Activity {
	private static final String TAG = "DemoActivity_1";

	/*
	 * HorizontalScrollViewEx mListContainer是父容器,是自定义View。
	 * 下面的代码中向父容器中添加了三个ListView。
	 * */
	private HorizontalScrollViewEx mListContainer;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.demo_1);
		Log.d(TAG, "onCreate");
		initView();
	}

	private void initView() {
		LayoutInflater inflater = getLayoutInflater();
		mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
		final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
		final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
		for (int i = 0; i < 3; i++) {
			/*
			 * 这个layout就是一个TextView和ListView的组合布局,它的父容器是mListContainer
			 * */
			ViewGroup layout = (ViewGroup) inflater.inflate(
					R.layout.content_layout, mListContainer, false);
			layout.getLayoutParams().width = screenWidth;
			TextView textView = (TextView) layout.findViewById(R.id.title);
			textView.setText("page " + (i + 1));
			layout.setBackgroundColor(Color
					.rgb(255 / (i + 1), 255 / (i + 1), 0));
			/*
			 * 为layout中的TextView和ListView添加内容:
			 * */
			createList(layout);
			mListContainer.addView(layout);
		}
	}

	/*
	 * 为layout中的TextView和ListView添加内容,
	 * ListView中的每个Item又是一个TextView文本。
	 * */
	private void createList(ViewGroup layout) {
		ListView listView = (ListView) layout.findViewById(R.id.list);
		ArrayList<String> datas = new ArrayList<String>();
		for (int i = 0; i < 50; i++) {
			datas.add("name " + i);
		}

		ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
				R.layout.content_list_item, R.id.name, datas);
		listView.setAdapter(adapter);
		listView.setOnItemClickListener(new OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView<?> parent, View view,
					int position, long id) {
				Toast.makeText(DemoActivity_1.this, "click item",
						Toast.LENGTH_SHORT).show();

			}
		});
	}

}


下面采用外部拦截法来解决滑动冲突问题:

我们只需要修改父容器需要拦截事件的条件即可。对于本例来说,父容器的拦截条件就是滑动过程中水平距离差比竖直距离差大,在这种情况下,父容器就拦截当前点击事件,根据这一条件进行相应修改(正常情况下父容器的onInterceptTouchEvent都是默认返回false不拦截的),修改后的HorizontalScrollViewEx父容器的onInterceptTouchEvent方法如下所示:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
        	/*
        	 * 这里必须是false,不然父容器拦截了ACTION_DOWN以后,子容器再也接收不到任何事件了
        	 * */
            intercepted = false;
            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: {
        	/*
        	 * 这里也必须是false,不然如果是子容器拦截了上面的事件,子容器将接收不到up事件,就无法结束了
        	 * */
            intercepted = false;
            break;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }
下面给出HorizontalScrollViewEx的完整代码:

package com.ryg.chapter_3.ui;


import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;


public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";


    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;


    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    // 分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;


    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;


    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }


    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }


    public HorizontalScrollViewEx(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
        init();
    }


    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();


        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
        <span style="white-space:pre">	</span>/*
        <span style="white-space:pre">	</span> * 这里必须是false,不然父容器拦截了ACTION_DOWN以后,子容器再也接收不到任何事件了
        <span style="white-space:pre">	</span> * */
            intercepted = false;
            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: {
        <span style="white-space:pre">	</span>/*
        <span style="white-space:pre">	</span> * 这里也必须是false,不然如果是子容器拦截了上面的事件,子容器将接收不到up事件,就无法结束了
        <span style="white-space:pre">	</span> * */
            intercepted = false;
            break;
        }
        default:
            break;
        }


        Log.d(TAG, "intercepted=" + intercepted);
        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;
        }
        /*
         * 要根据移动的距离来判断当前显示哪个ListView。
         * */
        case MotionEvent.ACTION_UP: {
            int scrollX = getScrollX();
            int scrollToChildIndex = scrollX / mChildWidth;
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.getXVelocity();
            if (Math.abs(xVelocity) >= 50) {
                mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
            } else {
                mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
            }
            mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
            int dx = mChildIndex * mChildWidth - scrollX;
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            break;
        }
        default:
            break;
        }


        mLastX = x;
        mLastY = y;
        return true;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);


        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        } else {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        }
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;


        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }


    /*
     * 弹性滑动
     * */
    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }


    /*
     * 弹性滑动
     * */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }


    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}


下面采用内部拦截法来解决滑动冲突问题:

我们只需要修改ListView的dispatchTouchEvent方法中的父容器的拦截逻辑,同时让父容器拦截ACTION_MOVE和ACTION_UP事件即可。为了重写ListView的dispatchTouchEvent方法,我们必须自定义一个ListView,称为ListViewEx,然后对内部拦截法的模板代码进行修改:

package com.ryg.chapter_3.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ListView;

public class ListViewEx extends ListView {
    private static final String TAG = "ListViewEx";

    private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;

    // 分别记录上次滑动的坐标:
    private int mLastX = 0;
    private int mLastY = 0;

    public ListViewEx(Context context) {
        super(context);
    }

    public ListViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ListViewEx(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public void setHorizontalScrollViewEx2(
            HorizontalScrollViewEx2 horizontalScrollViewEx2) {
        mHorizontalScrollViewEx2 = horizontalScrollViewEx2;
    }

    /*
     * 
     * */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        /*
         * 不允许父容器拦截ACTION_DOWN:
         * */
        case MotionEvent.ACTION_DOWN: {
            mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
            /*
             * 判断逻辑,根据逻辑来决定是否允许父容器拦截ACTION_MOVE:
             * */
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

}
我们还需要修改HorizontalScrollViewEx父容器的onInterceptTouchEvent方法:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();
        /*
         * 设置父容器不拦截ACTION_DOWN,交由子容器处理。
         * 子容器在接收到ACTION_DOWN以后在dispatchTouchEvent中对父容器设置不允许拦截事件。
         * 然后在子容器的dispatchTouchEvent中的ACTION_MOVE中判断子容器是否要拦截此事件,
         * 如果子容器不拦截,就解开对父容器不允许拦截事件的条件,
         * 这样事件又从子容器推回到父容器中。
         * */
        if (action == MotionEvent.ACTION_DOWN) {
            mLastX = x;
            mLastY = y;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                return true;
            }
            return false;
        } else {
            return true;
        }
    }


推荐采用外部拦截法来解决常见的滑动冲突!!!





  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值