第三章-View事件体系(事件分发机制、滑动冲突解决)

一、View的事件分发
1、点击事件的传递规则

在介绍点击事件的传递规则之前,首先明白分析的对象就是MotionEvent,即点击事件。所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MoonEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的分发来完成dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面我们先介绍一下这几个方法。

第三章-View事件体系(事件分发机制、滑动冲突解决)_第1张图片
上述三个方法到底有什么区别呢?它们是什么关系呢?其实它们的关系可以用如下伪代码可以了解

public boolean dispatchTouchEvent(MotionEvent ev) {

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

}

上述的伪代码已经将三者的区别说明了,我们可以大致的了解传递的规则就是,对于一个根ViewGroup来说,点击事件产生以后,首先传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onIntereptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,则他的onTouchEvent方法就会被调用;如果这个ViewGroup的onIntereptTouchEvent方法返回false就表示不需要拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的onIntereptTouchEvent方法就会被调用,如此反复直到事件被最终处理。

关于事件传递的机制,这里给出一些结论,根据这些结论可以更好地理解整个传递机制,如下所示。

(1)同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏慕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最后以up结束。

(2)正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个Vew将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

(3)某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceprTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。

(4)**某个View一旦开始处理事件,如果它不消耗ACTON_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。**意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了,二者是类似的道理。

(5)如果View不消耗除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的clickable属性默认为true,而TextView 的clickable属性默认为false

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

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

(11) 事件传递过程是由外到内的,理解就是事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN除外。

二、事件分发的源码解析
1.Activity对点击事件的分发过程
第三章-View事件体系(事件分发机制、滑动冲突解决)_第2张图片

//Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
	if (ev.getAction() == MotionEvent.ACTION_DOWN) {
		onUserInteraction();
	}
	if (getWindow().superDispatchTouchEvent(ev)) {
		return true;
	}
	return onTouchEvent(ev);
}

第三章-View事件体系(事件分发机制、滑动冲突解决)_第3张图片
第三章-View事件体系(事件分发机制、滑动冲突解决)_第4张图片
第三章-View事件体系(事件分发机制、滑动冲突解决)_第5张图片
第三章-View事件体系(事件分发机制、滑动冲突解决)_第6张图片

2.顶级View对事件的分发过程
第三章-View事件体系(事件分发机制、滑动冲突解决)_第7张图片

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
		|| mFirstTouchTarget != null) {
	final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
	if (!disallowIntercept) {
		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.
	intercepted = true;
}

第三章-View事件体系(事件分发机制、滑动冲突解决)_第8张图片

ViewGroup不拦截事件的时候,事件会向下分发由他的子View进行处理

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
	final int childIndex = getAndVerifyPreorderedIndex(
			childrenCount, i, customOrder);
	final View child = getAndVerifyPreorderedView(
			preorderedList, children, childIndex);

	// If there is a view that has accessibility focus we want it
	// to get the event first and if not handled we will perform a
	// normal dispatch. We may do a double iteration but this is
	// safer given the timeframe.
	if (childWithAccessibilityFocus != null) {
		if (childWithAccessibilityFocus != child) {
			continue;
		}
		childWithAccessibilityFocus = null;
		i = childrenCount - 1;
	}

	if (!canViewReceivePointerEvents(child)
			|| !isTransformedTouchPointInView(x, y, child, null)) {
		ev.setTargetAccessibilityFocus(false);
		continue;
	}

	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);
	if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
		// Child wants to receive touch within its bounds.
		mLastTouchDownTime = ev.getDownTime();
		if (preorderedList != null) {
			// childIndex points into presorted list, find original index
			for (int j = 0; j < childrenCount; j++) {
				if (children[childIndex] == mChildren[j]) {
					mLastTouchDownIndex = j;
					break;
				}
			}
		} else {
			mLastTouchDownIndex = childIndex;
		}
		mLastTouchDownX = ev.getX();
		mLastTouchDownY = ev.getY();
		newTouchTarget = addTouchTarget(child, idBitsToAssign);
		alreadyDispatchedToNewTouchTarget = true;
		break;
	}

3.View对点击事件的处理

View对点击事件的处理稍微有点简单, 这里注意,这里的View不包含ViewGroup,先看他的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {
	boolean result = false;
	...
	if (onFilterTouchEventForSecurity(event)) {
		if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
			result = true;
		}
		//noinspection SimplifiableIfStatement
		ListenerInfo li = mListenerInfo;
		if (li != null && li.mOnTouchListener != null
				&& (mViewFlags & ENABLED_MASK) == ENABLED
				&& li.mOnTouchListener.onTouch(this, event)) {
			result = true;
		}

		if (!result && onTouchEvent(event)) {
			result = true;
		}
	}
	...	
	return result;
}

因为他只是一个View,他没有子元素所以无法向下传递,所以只能自己处理点击事件,从上门的源码可以看出View对点击事件的处理过程,首选会判断你有没有设置onTouchListener**,如果onTouchListener中的onTouch为true,那么onTouchEvent就不会被调用,可见onTouchListener的优先级高于onTouchEvent,这样做到好处就是方便在外界处理点击事件**;
接着我们再来分析下onTouchEvent的实现,先看当View处于不可用的状态下点击事件的处理过程,如下,很显然,不可用状态下的View照样会消耗点击事件,尽管他看起来不可用。

下面再看一下onTouchEvent中点击事件的具体处理,如下所示:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
		(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
		(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
	switch (action) {
		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, x, y);
			   }

				if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
					// 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)) {
							performClick();
						}
					}
				}
		 black;
	 }
....
return true;
}

第三章-View事件体系(事件分发机制、滑动冲突解决)_第9张图片

public boolean performClick() {
	final boolean result;
	final ListenerInfo li = mListenerInfo;
	if (li != null && li.mOnClickListener != null) {
		playSoundEffect(SoundEffectConstants.CLICK);
		li.mOnClickListener.onClick(this);
		result = true;
	} else {
		result = false;
	}

	sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
	return result;
}

第三章-View事件体系(事件分发机制、滑动冲突解决)_第10张图片

public void setOnClickListener(@Nullable OnClickListener l) {
	if (!isClickable()) {
		setClickable(true);
	}
	getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
	if (!isLongClickable()) {
		setLongClickable(true);
	}
	getListenerInfo().mOnLongClickListener = l;
}

二、View的滑动冲突

滑动冲突时怎样产生的?其实在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突,如何解决滑动冲突?这既是一键困难的事情又是一件简单的事情,说困难时因为许多开发者面对滑动冲突都会显得束手无策,说简单是因为滑动冲突的解决办法有固定的套路。

1.常见的滑动冲突场景

第三章-View事件体系(事件分发机制、滑动冲突解决)_第11张图片

先说场景1,主要是将ViewPager和Fragment配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果。在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个Listview。本来这种情况下是有滑动冲突的,但是viewPager内部处理了这种滑动冲突,因此采用ViewPager时我们无须关注这个问题,如果我们采用的不是ViewPager而是ScrollView等,那就必须手动处理滑动冲突了,否则造成的后果就是内外两层只能有一层能够滑动,这是因为两者之间的滑动事件有冲突。除了这种典型情况外,还存在其他情况,比如外部上下滑动、内部左右滑动等,但是它们属于同一类滑动冲突。

再说场景2,这种情况就稍微复杂一些,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题。因为当手指开始滑动的时候,系统无法知道用户到底是想让哪一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动得很卡顿。在实际的开发中,这种场景主要是指内外两层同时能上下滑动或者内外两层,同时能左右滑动。

最后说下场景3,场景3是场景1和场景2两种情况的嵌套,因此场景3的滑动冲突看起来就更加复杂了。比如在许多应用中会有这么一个效果:内层有一个场景1中的滑动效果,然后外层又有一个场景2中的滑动效果。具体说就是,外部有一个SlidingMenu效果,然后内部有一个ViewPager,ViewPager的每一个页面中又是一个Listview。虽然说场景3的滑动冲突看起来更复杂,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中层、中层和外层之间的滑动冲突即可,而具体的处理方法其实是和场景1、场景2相同的。
从本质上来说,这三种滑动冲突场景的复杂度其实是相同的,因为它们的区别仅仅是滑动策略的不同,至于解决滑动冲突的方法,它们几个是通用的。

2.滑动冲突的处理规则

第三章-View事件体系(事件分发机制、滑动冲突解决)_第12张图片
第三章-View事件体系(事件分发机制、滑动冲突解决)_第13张图片
3.滑动冲突的解决方式

主要有2种:外部拦截法(推荐使用)、内部拦截法。

a.外部拦截法
第三章-View事件体系(事件分发机制、滑动冲突解决)_第14张图片

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
	boolean intercepted = false;
	int x = (int) ev.getX();
	int y = (int) ev.getY();
	switch (ev.getAction()){
		case MotionEvent.ACTION_DOWN:
			intercepted = false;//必须不能拦截,如果拦截了,后续move和up都由父元素处理了
			break;
		case MotionEvent.ACTION_MOVE:
			if("父容器需要当前点击事件"){
				intercepted = true;
			}else {
				intercepted = false;
			}
			break;
		case MotionEvent.ACTION_UP:
			intercepted = false;//必须不能拦截,如果拦截了,子元素的onClick点击事件就无法触发
			break;
	}
	mLastXIntercept = x;
	mLastYIntercept = x;
	return intercepted;
}

第三章-View事件体系(事件分发机制、滑动冲突解决)_第15张图片

b.内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
	int x = (int) event.getX();
	int y = (int) event.getY();
	switch (event.getAction()){
		case MotionEvent.ACTION_DOWN:
			getParent().requestDisallowInterceptTouchEvent(true);//设置父容器不允许拦截
			break;
		case MotionEvent.ACTION_MOVE:
			int deltaX =  x - mLastX;
			int deltaY =  x - mLastY;
			if("父容器需要此类点击事件"){
				getParent().requestDisallowInterceptTouchEvent(false);//设置拦截
			}
			break;
		case MotionEvent.ACTION_UP:

			break;
	}
	mLastX = x;
	mLastY = y;
	return super.dispatchTouchEvent(event);//这个值取决于子view的处理结果,
	//例如这个view如果继承ListView,就需要看listview的这个方法处理结果。并不是再调用了父容器的这个方法。
}

第三章-View事件体系(事件分发机制、滑动冲突解决)_第16张图片

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
	int action = ev.getAction();
	if(action == MotionEvent.ACTION_DOWN){
		return false;//不拦截
	}else {
		return true;
	}
}

总结下:使用外部拦截只需要处理父容器的OnInterceptTouchEvent方法。使用内部拦截需要处理子容器的dispatchTouchEvent和重写父元素的OnInterceptTouchEvent方法。所以建议使用外部拦截法会比较方便。

下面演示一个外部拦截法的实例:

自定义一个HorizontalScrollViewEx

package com.example.viewsample;


import android.content.Context;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewGroup;
import android.widget.Scroller;

public class HorizontalScrollViewEx extends ViewGroup {

    public static final String TAG = "HorizontalScrollViewEx";

    private int mChindrensize;
    private int mChindrenWidth;
    private int mChindrenIndex;
    //分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    //分别记录上次滑动的坐标
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }

    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = true;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltax = x - mLastXIntercept;
                int deltaY = y = mLastYIntercept;
                if (Math.abs(deltax) > Math.abs(deltaY)) {//父容器的拦截逻辑,水平滑动就拦截
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);//拦截后处理逻辑
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                int scrollToChildIndex = scrollX / mChindrenWidth;
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChindrenIndex = xVelocity > 0 ? mChindrenIndex - 1 : mChindrenIndex + 1;
                } else {
                    mChindrenIndex = (scrollX + mChindrenWidth / 2) / mChindrenWidth;
                }
                mChindrenIndex = Math.max(0, Math.min(mChindrenIndex, mChindrensize - 1));
                int dx = mChindrenIndex * mChindrenWidth - scrollX;
                ssmoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

	//这个方法上面一章讲解过,Scroller的弹性滑动原理
    private void ssmoothScrollBy(int dx, int i) {
        mScroller.startScroll(getScrollX(),0,dx,500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }
}


测试:

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "MainActivity";
    private HorizontalScrollViewEx mListContainer;
    private int w,h;

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

    private void initView() {
        LayoutInflater inflater = getLayoutInflater();
        mListContainer = findViewById(R.id.container);
        //屏幕宽高
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        w = wm.getDefaultDisplay().getWidth();
        h = wm.getDefaultDisplay().getHeight();
        for (int i = 0; i < 3; i++) {
            ViewGroup layout = inflater.inflate(R.layout.content_layout,mListContainer,false);
            layout.getLayoutParams().width = w;
            TextView textview = (TextView) layout.findViewById(R.id.title);
            textview.setText("page"  + (i+1));
            layout.setBackgroundColor(Color.rgb(255/(i+1),255/(i+1),0));
            createList(layout);
            mListContainer.addView(layout);
        }
    }

    private void createList(ViewGroup layout) {
        ListView listview = (ListView) layout.findViewById(R.id.list);
        ArrayList<String>datas= new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            datas.add("names" + i);
        }
        ArrayAdapter<String>adapter = new ArrayAdapter<String>(this,R.layout.content_list_item,R.id.name,datas);
        listview.setAdapter(adapter);
    }
}

内部拦截参考上面的伪代码自行研究下。滑动冲突解决基本就掌握了。

你可能感兴趣的:(Android艺术开发阅读)