android事件分发
示例代码地址https://github.com/kinglong123/androiddistribution
基础知识
事件主要有 down (MotionEvent.ACTION_DOWN),move(MotionEvent.ACTION_MOVE),up(MotionEvent.ACTION_UP)。
基本上的手势均由 down 事件为起点,up 事件为终点,中间可能会有一定数量的 move 事件。这三种事件是大部分手势动作的基础。
先来分析View的分发
结合下面的布局
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="touch.touchdemo.MainActivity">
<Button
android:text="Hello World!"
android:id="@+id/bt"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:text="Hello World!"
android:id="@+id/iv"
android:layout_below="@+id/bt"
android:background="@mipmap/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
首先为button设置点击事件和OnTouch事件并返回false。
btn = (Button) findViewById(R.id.bt);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.v("TAG","onClick execute!");
}
});
btn.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View view, MotionEvent event) {
// TODO Auto-generated method stub
Log.v("TAG","onTouch execute,"+"action is "+ ViewTool.actionToString(event.getAction()));
return false;
}
});
这时点击button的打印信息
09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onClick execute!
再把button的OnTouch事件的返回值设为true。
btn = (Button) findViewById(R.id.bt);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.v("TAG","onClick execute!");
}
});
btn.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View view, MotionEvent event) {
// TODO Auto-generated method stub
Log.v("TAG","onTouch execute,"+"action is "+ ViewTool.actionToString(event.getAction()));
return true;
}
});
这时点击button的打印信息
09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP
发现点击事件没有执行
onClick()方法没有被执行,这里我们把这种现象叫做点击事件被onTouch()消费掉了,事件不会在继续向onClick()方法传递了
onTouch中返回了true时底层到底发生了什么?为什么在onTouch中返回了true,事件便不会继续向下传递了?onTouch和onTouchEvent的区别到底在哪里?为了解决我们心中的疑惑,我们必须去深入分析相关的源代码了。
补充知识点:Android中所有的事件都必须经过disPatchTouchEvent(MotionEvent ev)这个方法的分发。<br />
然后决定是自身消费当前事件还是继续往下分发给子控件处理。<br />
那么我们看看这个view里面的disPatchTouchEvent(MotionEvent ev)方法<br />
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
代码有点多,我们一步步来看:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
最前面这一段就是判断当前事件是否能获得焦点,如果不能获得焦点或者不存在一个View那我们就直接返回False跳出循环,接下来:
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
设置一些标记和处理input与手势等传递,不用管,到这里:
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
这里if (onFilterTouchEventForSecurity(event))是用来判断View是否被遮住等,ListenerInfo是View的静态内部类,专门用来定义一些XXXListener等方法的,到了重点:
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
设置一些标记和处理input与手势等传递,不用管,到这里:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
很长的一个判断,一个个来解释:第一个li肯定不为空,因为在这个If判断语句之前就new了一个li,第二个条件li.mOnTouchListener != null,怎么确定这个mOnTouchListener不为空呢?我们在View类里面发现了如下方法:
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
意味着只要给控件注册了onTouch事件这个mOnTouchListener就一定会被赋值,接下来(mViewFlags & ENABLED_MASK) == ENABLED是通过位与运算来判断这个View是否是ENABLED的,我们默认控件都是ENABLED的,所以这一条也成立;最后一条li.mOnTouchListener.onTouch(this, event)是判断onTouch()的返回值是否为True,我们后面把默认为False的返回值改成了True,所以这一整系列的判断都是True,那么这个disPatchTouchEvent(MotionEvent ev)方法直接就返回了True,那么接下来的代码都不会被执行。<br />
这就解释了上面为什么setOnTouchListener的毁掉onTouch返回true时,onClick不执行了。<br />
结合上面的代码可以得到结论:<br />
<br />
1 . OnTouchListener的优先级比onTouchEvent要高,联想到刚才的小Demo也可以得出OnTouchListener 中的onTouch方法优先于onClick()方法执行(onClick()是在onTouchEvent(event)方法中被执行的这个待会会说到)
<br />
2 . 如果控件(View)的onTouch返回False或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是ENABLE的情况下会调用onTouchEvent方法,此时dispatchTouchEvent方法的返回值与onTouchEvent的返回值一样。
继续分析dispatchTouchEvent方法里面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 float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
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();
}
}
}
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();
}
mIgnoreNextUpEvent = false;
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();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// 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;
}
代码还是很多,我们依然一段一段来分析,最前面的一段代码:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
根据前面的分析我们知道这一段代码是对当前View处于不可用状态的情况下的分析,通过注释我们知道即使是一个不可用状态下的View依然会消耗点击事件,只是不会对这个点击事件作出响应罢了,另外通过观察这个return返回值,只要这个View的CLICKABLE和LONG_CLICKABLE或者CONTEXT_CLICKABLE有一个为True,那么返回值就是True,onTouchEvent方法会消耗当前事件。<br />
看下一段代码:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
这段代码的意思是如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent(event)方法,这个onTouchEvent(event)的工作机制看起来和OnTouchListener类似,这里不深入研究.<br />
下面看一下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();
}
}
}
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();
}
mIgnoreNextUpEvent = false;
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();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// 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;
}
我们还是一行行来分解:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
....
performClick();
....
//省略
}
return true;
}
return false;
这边主要关注两点
- 可点击的view返回true,否则返回false
- 在 MotionEvent.ACTION_UP:中会进行点击事件判断
performClick()源码:
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;
}
那就是当ACTION_UP事件发生时,会触发performClick()方法,如果这个View设置了OnClickListener那么最终会执行到OnClickListener的回调方法onClick(),这也就验证了刚才所说的:onClick()方法是在onTouchEvent内部被调用的。
继续:我们为demo中的imageView设置touch事件
imageView = (ImageView) findViewById(R.id.iv);
imageView.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
Log.v("TAG","onTouch execute,"+"action is "+ViewTool.actionToString(event.getAction()));
return false;
}
});
这时点击imageView打印信息为:
09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
再为imageView增加点击事件
imageView = (ImageView) findViewById(R.id.iv);
imageView.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
Log.v("TAG","onTouch execute,"+"action is "+ViewTool.actionToString(event.getAction()));
return false;
}
});
imageView.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
Log.v("TAG","onClick execute!");
}
});
这时的单元信息为
09-16 13:03:14.682 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 13:03:14.782 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP
09-16 13:03:14.782 1720-1720/touch.touchdemo V/TAG: onClick execute!
为什么只设置setOnTouchListener时只相应了 ACTION_DOWN,增加设置了setOnClickListener时ACTION_DOWN、ACTION_UP事件都得到相应呢?
这边补充一个android分发的重要知识点:<br />
关于dispatchTouchEvent的返回<br />
-
当我们给某个控件设置了Touch事件,当点击该控件时,会触发一系列的事件,如ACTION_DOWN,ACTION_MOVE,ACTION_UP。
-
dispatchTouchEvent在进行事件分发时,如果某个ACTION返回了false,那么后面的ACTION都将得不到执行。也就是说,只有前一个ACTION返回true,后一个的ACTION才会得到执行。
当imageView只设置setOnTouchListener事件时:<br />
Imageview—不可点击setOnTouchListener<br />
-- onTouchEvent返回false(上面有分析过,不可点击onTouchEvent返回false)<br />
-- dispatchTouchEvent返回false<br />
在ACTION_DOWN时dispatchTouchEvent返回了false。后续的ACTION得不到执行。<br />
为什么设置了setOnClickListener后续的ACTION可以得到执行呢?<br />
setOnClickListener的源码:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
setOnClickListener方法,它先会去判断当前控件是否是Clickable的,如果不是Clickable的,则将当前控件设置为Clickable的。当我们调用了ImageView对象的setOnClickListener方法后,ImageView对象就已经变成了Clickable的,所以其表现和Button一致也是自然的。
View总结
-
onTouch和onTouchEvent都是在dispatchTouchEvent方法中被调用的方法。onTouch会优先于onTouchEvent被执行。
-
如果onTouch通过返回true将事件消费掉,事件便不会传递到onTouchEvent中。特别要强调的一点是,只有当mOnTouchListener不为null并且控件是enabled,onTouch方法才会得到执行。
-
dispatchTouchEvent在进行事件分发时,如果某个ACTION返回了false,那么后面的ACTION都将得不到执行。
-
setOnClickListener方法会设置view为可点击。
接下来我们看 ViewGroup的事件分发:
结合下面的布局:
<touch.touchdemo.widget.CustomLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical"
android:id="@+id/customLayout"
tools:context="touch.touchdemo.MainActivity">
<Button
android:id="@+id/btn1"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1"
/>
<Button
android:id="@+id/btn2"
android:textAllCaps="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button2"
/>
</touch.touchdemo.widget.CustomLayout>
CustomLayout 继承LinearLayout:
public class CustomLayout extends LinearLayout {
public CustomLayout(Context context) {
super(context);
}
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
return false;
}
}
为button1、button2设置点击事件为 customLayout设置setOnTouchListener。
customLayout.setOnTouchListener(new View.OnTouchListener(){
@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
Log.v("TAG","customLayout onTouch:"+ ViewTool.actionToString(arg1.getAction()));
return false;
}
});
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.v("TAG","onClick execute!");
}
});
btn2.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
点击buttion1打印信息:
09-16 13:35:08.242 24349-24349/touch.touchdemo V/TAG: onClick execute!
点击buttion2打印信息:
09-16 13:35:27.438 24349-24349/touch.touchdemo V/TAG: onClick execute!
点击空白地方打印信息:
09-16 13:35:53.670 24349-24349/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN
修改CustomLayout中onInterceptTouchEvent的返回值为true:
public class CustomLayout extends LinearLayout {
public CustomLayout(Context context) {
super(context);
}
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
return true;
}
}
这时点击button1
09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN
这时点击button1
09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN
这时点击空白地方
09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN
这是为什么呢?点击事件得不到执行了,只有ACTION_DOWN得到相应。<br />
这需要分析下ViewGroup的dispatchTouchEvent。源码比较长这里就不贴出来了,有兴趣的可以自己去看看。<br />
我们可以用一段伪代码来说明ViewGroup的dispatchTouchEvent主要作用
public boolean dispatchTouchEvent(MotionEvent e) {
boolean consumed = false;
if (onInterceptTouchEvent(e)) {
consumed = onTouchEvent(e);
} else {
for (View view: childs) {
consumed = view.dispatchTouchEvent(e);
if (consumed) {
break;
}
}
if (!consumed) {
consumed = onTouchEvent(e);
}
}
return consumed;
}
- 首先判断ViewGroup的onInterceptTouchEvent是否拦截,如果拦截执行自身的onTouchEvent
- 不拦截向下分发给自view去执行。
- 如果子view中有相应的处理(dispatchTouchEvent返回true),ViewGroup的dispatchTouchEvent返回true。
- 如果子view中没有相应的处理(dispatchTouchEvent返回flase),ViewGroup会再执行自身的onTouchEvent。
我们可以用一张流程图来说明这个过程:<br />
Paste_Image.png结合这张图有兴趣的同学可以跑下demo中的流程打印验证下。
结合上面说的。我们来分析下ViewPager是怎么处理滑动冲突的:
Viewpager套Viewpager时的事件处理<br />
Paste_Image.pngdemo中我们简单的写一个示例Viewpager套Viewpager如上图<br />
可以看到滑动点在里面的viewpager时,里面的viewpager滑动,滑动点在外面的viewpager只外面的viewpager滑动。<br />
我们看下Viewpager中的onInterceptTouchEvent实现:<br />
代码很多,关键是在ACTION_MOVE时,他是如果判断拦截与不拦截(拦截返回true和不拦截false)<br />
找到关键代码:
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
canScroll(this, false, (int) dx, (int) x, (int) y)) {
// Nested view has scrollable area under this point. Let it be handled there.
mLastMotionX = x;
mLastMotionY = y;
mIsUnableToDrag = true;
return false;
}
可以看出在viewpager的onInterceptTouchEvent的MotionEvent.ACTION_MOVE:<br />
会去判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理。<br />
这种拦截法叫做:外部拦截法
所谓外部拦截法是指所有的触摸事件都会先经过经过父容器的传递,从而父容器在需要此触摸事件的时候就可以拦截此触摸事件,否者就传递给子View。这样就可以解决滑动冲突的问题,这种方法比较符合触摸事件的传递、处理机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法中根据滑动冲突处理规则做相应的拦截即可。<br />
可用下面的伪代码来表示:
@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: {
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;
}
我们继续:
现在的代码我们不兼容4.0以前的版本了,所有viewpager嵌套viewpager的实现简单了很多<br />
在 API13及前面的版本Viewpager 套Viewpager 直接写存在兼容问题。<br />
我可以通过源码来看为什么存在兼容:
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
canScroll(this, false, (int) dx, (int) x, (int) y)) {
// Nested view has scrollable area under this point. Let it be handled there.
mLastMotionX = x;
mLastMotionY = y;
mIsUnableToDrag = true;
return false;
}
canScroll的实现
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
// 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);
}
ViewCompat.canScrollHorizontally的调用
public static boolean canScrollHorizontally(View v, int direction) {
return IMPL.canScrollHorizontally(v, direction);
}
static final ViewCompatImpl IMPL;的实现
static final ViewCompatImpl IMPL;
static {
final int version = android.os.Build.VERSION.SDK_INT;
if (version >= 23) {
IMPL = new MarshmallowViewCompatImpl();
} else if (version >= 21) {
IMPL = new LollipopViewCompatImpl();
} else if (version >= 19) {
IMPL = new KitKatViewCompatImpl();
} else if (version >= 17) {
IMPL = new JbMr1ViewCompatImpl();
} else if (version >= 16) {
IMPL = new JBViewCompatImpl();
} else if (version >= 15) {
IMPL = new ICSMr1ViewCompatImpl();
} else if (version >= 14) {
IMPL = new ICSViewCompatImpl();
} else if (version >= 11) {
IMPL = new HCViewCompatImpl();
} else if (version >= 9) {
IMPL = new GBViewCompatImpl();
} else if (version >= 7) {
IMPL = new EclairMr1ViewCompatImpl();
} else {
IMPL = new BaseViewCompatImpl();
}
}
可以找到api13以以下的canScrollHorizontally的实现:
public boolean canScrollHorizontally(View v, int direction) {
return (v instanceof ScrollingView) &&
canScrollingViewScrollHorizontally((ScrollingView) v, direction);
}
这边(v instanceof ScrollingView),因为v为viewpager,(v instanceof ScrollingView)为fasle,所有api13以以前的canScrollHorizontally反false,即没有实现滑动判断,永远都是flase。<br />
可以找到api14以以上的canScrollHorizontally的实现:
public boolean canScrollHorizontally(View v, int direction) {
return ViewCompatICS.canScrollHorizontally(v, direction);
}
最终调用的是view.java中的
public boolean canScrollHorizontally(int direction) {
final int offset = computeHorizontalScrollOffset();
final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
这里帮你做了是否可滑动判断。<br />
到这里我们就从源码层面分析了Viewpager 套Viewpager 兼容问题<br />
我们来兼容下:<br />
先介绍另一种滑动冲突的解决方法<br />
内部拦截法:<br />
- 内部拦截法是指父容器不拦截任何触摸事件,所有的触摸事件都传递给子元素,如果子元素需要此触摸事件就直接消耗掉,否者就交由父容器进行处理,(通过内部子元素来进行是否进行拦截)这种方法和Android中的事件传递、处理机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。这种方法需要重写子元素的dispatchTouchEvent方法。
- 子 View 可以使用 requestDisallowInterceptTouchEvent 影响去父 View 的分发,可以决定父 View 是否要调用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用调用 onInterceptTouchEvent 来判断拦截,而就是不拦截。
用伪代码表示为:
子元素的dispatchTouchEvent方法中<br />
@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 = y - mLastY;
if (父容器需要当前触摸事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
在demo代码中的的实现为:
public class ViewPagerCompat2 extends ViewPager {
/** 触摸时按下的点 **/
PointF downP = new PointF();
/** 触摸时当前的点 **/
PointF curP = new PointF();
private int first = 1;
private float mLastMotionX;
private float mLastMotionY;
public ViewPagerCompat2(Context context) {
super(context);
}
public ViewPagerCompat2(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);//告诉父view不拦截
first = 1;
mLastMotionX = x;
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
if (first == 1) {
if (Math.abs(x - mLastMotionX) < Math.abs(y - mLastMotionY)) {
first = 0;//y轴滑动拦截
getParent().requestDisallowInterceptTouchEvent(false);
} else {
//x轴滑动不拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
}
上面这种内部拦截法。当然兼容也可以使用外部拦截法:<br />
既然ViewPager在API14以上可以正常滑动重写了canScrollHorizontally(int)方法,查看ViewPager的canScrollHorizontally(int)方法源码发现此方法不存在版本兼容问题,在API13及其以下版本上也可直接调用。于是乎解决办法就是继承ViewPager重写canScroll(View, boolean, int, int, int)方法,直接调用canScrollHorizontally(int)即可,如下:
public class ViewPagerCompat extends ViewPager {
public ViewPagerCompat(Context context) {
super(context);
}
public ViewPagerCompat(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
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;
}
}
}
if(checkV){
// Direct call ViewPager.canScrollHorizontally(int)
if(v instanceof ViewPager){
return ((ViewPager) v).canScrollHorizontally(-dx);
}else{
return ViewCompat.canScrollHorizontally(v, -dx);
}
}else{
return false;
}
}
}
<br />
<br />
<br />
<br />
网友评论