android事件分發(fā)
示例代碼地址https://github.com/kinglong123/androiddistribution
基礎(chǔ)知識(shí)
事件主要有 down (MotionEvent.ACTION_DOWN),move(MotionEvent.ACTION_MOVE),up(MotionEvent.ACTION_UP)。
基本上的手勢(shì)均由 down 事件為起點(diǎn),up 事件為終點(diǎn),中間可能會(huì)有一定數(shù)量的 move 事件。這三種事件是大部分手勢(shì)動(dòng)作的基礎(chǔ)。
先來(lái)分析View的分發(fā)
結(jié)合下面的布局
<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設(shè)置點(diǎn)擊事件和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;
}
});
這時(shí)點(diǎn)擊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事件的返回值設(shè)為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;
}
});
這時(shí)點(diǎn)擊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
發(fā)現(xiàn)點(diǎn)擊事件沒(méi)有執(zhí)行
onClick()方法沒(méi)有被執(zhí)行,這里我們把這種現(xiàn)象叫做點(diǎn)擊事件被onTouch()消費(fèi)掉了,事件不會(huì)在繼續(xù)向onClick()方法傳遞了
onTouch中返回了true時(shí)底層到底發(fā)生了什么?為什么在onTouch中返回了true,事件便不會(huì)繼續(xù)向下傳遞了?onTouch和onTouchEvent的區(qū)別到底在哪里?為了解決我們心中的疑惑,我們必須去深入分析相關(guān)的源代碼了。
補(bǔ)充知識(shí)點(diǎn):Android中所有的事件都必須經(jīng)過(guò)disPatchTouchEvent(MotionEvent ev)這個(gè)方法的分發(fā)。<br />
然后決定是自身消費(fèi)當(dāng)前事件還是繼續(xù)往下分發(fā)給子控件處理。<br />
那么我們看看這個(gè)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;
}
代碼有點(diǎn)多,我們一步步來(lái)看:
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);
}
最前面這一段就是判斷當(dāng)前事件是否能獲得焦點(diǎn),如果不能獲得焦點(diǎn)或者不存在一個(gè)View那我們就直接返回False跳出循環(huán),接下來(lái):
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
設(shè)置一些標(biāo)記和處理input與手勢(shì)等傳遞,不用管,到這里:
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))是用來(lái)判斷View是否被遮住等,ListenerInfo是View的靜態(tài)內(nèi)部類,專門用來(lái)定義一些XXXListener等方法的,到了重點(diǎn):
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
設(shè)置一些標(biāo)記和處理input與手勢(shì)等傳遞,不用管,到這里:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
很長(zhǎng)的一個(gè)判斷,一個(gè)個(gè)來(lái)解釋:第一個(gè)li肯定不為空,因?yàn)樵谶@個(gè)If判斷語(yǔ)句之前就new了一個(gè)li,第二個(gè)條件li.mOnTouchListener != null,怎么確定這個(gè)mOnTouchListener不為空呢?我們?cè)赩iew類里面發(fā)現(xiàn)了如下方法:
/**
* 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;
}
意味著只要給控件注冊(cè)了onTouch事件這個(gè)mOnTouchListener就一定會(huì)被賦值,接下來(lái)(mViewFlags & ENABLED_MASK) == ENABLED是通過(guò)位與運(yùn)算來(lái)判斷這個(gè)View是否是ENABLED的,我們默認(rèn)控件都是ENABLED的,所以這一條也成立;最后一條li.mOnTouchListener.onTouch(this, event)是判斷onTouch()的返回值是否為True,我們后面把默認(rèn)為False的返回值改成了True,所以這一整系列的判斷都是True,那么這個(gè)disPatchTouchEvent(MotionEvent ev)方法直接就返回了True,那么接下來(lái)的代碼都不會(huì)被執(zhí)行。<br />
這就解釋了上面為什么setOnTouchListener的毀掉onTouch返回true時(shí),onClick不執(zhí)行了。<br />
結(jié)合上面的代碼可以得到結(jié)論:<br />
<br />
1 . OnTouchListener的優(yōu)先級(jí)比onTouchEvent要高,聯(lián)想到剛才的小Demo也可以得出OnTouchListener 中的onTouch方法優(yōu)先于onClick()方法執(zhí)行(onClick()是在onTouchEvent(event)方法中被執(zhí)行的這個(gè)待會(huì)會(huì)說(shuō)到)
<br />
2 . 如果控件(View)的onTouch返回False或者mOnTouchListener為null(控件沒(méi)有設(shè)置setOnTouchListener方法)或者控件不是ENABLE的情況下會(huì)調(diào)用onTouchEvent方法,此時(shí)dispatchTouchEvent方法的返回值與onTouchEvent的返回值一樣。
繼續(xù)分析dispatchTouchEvent方法里面onTouchEvent的實(shí)現(xiàn)
/**
* 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;
}
代碼還是很多,我們依然一段一段來(lái)分析,最前面的一段代碼:
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);
}
根據(jù)前面的分析我們知道這一段代碼是對(duì)當(dāng)前View處于不可用狀態(tài)的情況下的分析,通過(guò)注釋我們知道即使是一個(gè)不可用狀態(tài)下的View依然會(huì)消耗點(diǎn)擊事件,只是不會(huì)對(duì)這個(gè)點(diǎn)擊事件作出響應(yīng)罷了,另外通過(guò)觀察這個(gè)return返回值,只要這個(gè)View的CLICKABLE和LONG_CLICKABLE或者CONTEXT_CLICKABLE有一個(gè)為True,那么返回值就是True,onTouchEvent方法會(huì)消耗當(dāng)前事件。<br />
看下一段代碼:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
這段代碼的意思是如果View設(shè)置有代理,那么還會(huì)執(zhí)行TouchDelegate的onTouchEvent(event)方法,這個(gè)onTouchEvent(event)的工作機(jī)制看起來(lái)和OnTouchListener類似,這里不深入研究.<br />
下面看一下onTouchEvent中對(duì)點(diǎn)擊事件的具體處理流程:
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;
}
我們還是一行行來(lái)分解:
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;
這邊主要關(guān)注兩點(diǎn)
- 可點(diǎn)擊的view返回true,否則返回false
- 在 MotionEvent.ACTION_UP:中會(huì)進(jìn)行點(diǎn)擊事件判斷
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;
}
那就是當(dāng)ACTION_UP事件發(fā)生時(shí),會(huì)觸發(fā)performClick()方法,如果這個(gè)View設(shè)置了OnClickListener那么最終會(huì)執(zhí)行到OnClickListener的回調(diào)方法onClick(),這也就驗(yàn)證了剛才所說(shuō)的:onClick()方法是在onTouchEvent內(nèi)部被調(diào)用的。
繼續(xù):我們?yōu)閐emo中的imageView設(shè)置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;
}
});
這時(shí)點(diǎn)擊imageView打印信息為:
09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
再為imageView增加點(diǎn)擊事件
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!");
}
});
這時(shí)的單元信息為
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!
為什么只設(shè)置setOnTouchListener時(shí)只相應(yīng)了 ACTION_DOWN,增加設(shè)置了setOnClickListener時(shí)ACTION_DOWN、ACTION_UP事件都得到相應(yīng)呢?
這邊補(bǔ)充一個(gè)android分發(fā)的重要知識(shí)點(diǎn):<br />
關(guān)于dispatchTouchEvent的返回<br />
當(dāng)我們給某個(gè)控件設(shè)置了Touch事件,當(dāng)點(diǎn)擊該控件時(shí),會(huì)觸發(fā)一系列的事件,如ACTION_DOWN,ACTION_MOVE,ACTION_UP。
dispatchTouchEvent在進(jìn)行事件分發(fā)時(shí),如果某個(gè)ACTION返回了false,那么后面的ACTION都將得不到執(zhí)行。也就是說(shuō),只有前一個(gè)ACTION返回true,后一個(gè)的ACTION才會(huì)得到執(zhí)行。
當(dāng)imageView只設(shè)置setOnTouchListener事件時(shí):<br />
Imageview—不可點(diǎn)擊setOnTouchListener<br />
-- onTouchEvent返回false(上面有分析過(guò),不可點(diǎn)擊onTouchEvent返回false)<br />
-- dispatchTouchEvent返回false<br />
在ACTION_DOWN時(shí)dispatchTouchEvent返回了false。后續(xù)的ACTION得不到執(zhí)行。<br />
為什么設(shè)置了setOnClickListener后續(xù)的ACTION可以得到執(zhí)行呢?<br />
setOnClickListener的源碼:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
setOnClickListener方法,它先會(huì)去判斷當(dāng)前控件是否是Clickable的,如果不是Clickable的,則將當(dāng)前控件設(shè)置為Clickable的。當(dāng)我們調(diào)用了ImageView對(duì)象的setOnClickListener方法后,ImageView對(duì)象就已經(jīng)變成了Clickable的,所以其表現(xiàn)和Button一致也是自然的。
View總結(jié)
onTouch和onTouchEvent都是在dispatchTouchEvent方法中被調(diào)用的方法。onTouch會(huì)優(yōu)先于onTouchEvent被執(zhí)行。
如果onTouch通過(guò)返回true將事件消費(fèi)掉,事件便不會(huì)傳遞到onTouchEvent中。特別要強(qiáng)調(diào)的一點(diǎn)是,只有當(dāng)mOnTouchListener不為null并且控件是enabled,onTouch方法才會(huì)得到執(zhí)行。
dispatchTouchEvent在進(jìn)行事件分發(fā)時(shí),如果某個(gè)ACTION返回了false,那么后面的ACTION都將得不到執(zhí)行。
setOnClickListener方法會(huì)設(shè)置view為可點(diǎn)擊。
接下來(lái)我們看 ViewGroup的事件分發(fā):
結(jié)合下面的布局:
<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設(shè)置點(diǎn)擊事件為 customLayout設(shè)置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
點(diǎn)擊buttion1打印信息:
09-16 13:35:08.242 24349-24349/touch.touchdemo V/TAG: onClick execute!
點(diǎn)擊buttion2打印信息:
09-16 13:35:27.438 24349-24349/touch.touchdemo V/TAG: onClick execute!
點(diǎn)擊空白地方打印信息:
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;
}
}
這時(shí)點(diǎn)擊button1
09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN
這時(shí)點(diǎn)擊button1
09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN
這時(shí)點(diǎn)擊空白地方
09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN
這是為什么呢?點(diǎn)擊事件得不到執(zhí)行了,只有ACTION_DOWN得到相應(yīng)。<br />
這需要分析下ViewGroup的dispatchTouchEvent。源碼比較長(zhǎng)這里就不貼出來(lái)了,有興趣的可以自己去看看。<br />
我們可以用一段偽代碼來(lái)說(shuō)明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是否攔截,如果攔截執(zhí)行自身的onTouchEvent
- 不攔截向下分發(fā)給自view去執(zhí)行。
- 如果子view中有相應(yīng)的處理(dispatchTouchEvent返回true),ViewGroup的dispatchTouchEvent返回true。
- 如果子view中沒(méi)有相應(yīng)的處理(dispatchTouchEvent返回flase),ViewGroup會(huì)再執(zhí)行自身的onTouchEvent。
我們可以用一張流程圖來(lái)說(shuō)明這個(gè)過(guò)程:<br />

結(jié)合這張圖有興趣的同學(xué)可以跑下demo中的流程打印驗(yàn)證下。
結(jié)合上面說(shuō)的。我們來(lái)分析下ViewPager是怎么處理滑動(dòng)沖突的:
Viewpager套Viewpager時(shí)的事件處理<br />

demo中我們簡(jiǎn)單的寫一個(gè)示例Viewpager套Viewpager如上圖<br />
可以看到滑動(dòng)點(diǎn)在里面的viewpager時(shí),里面的viewpager滑動(dòng),滑動(dòng)點(diǎn)在外面的viewpager只外面的viewpager滑動(dòng)。<br />
我們看下Viewpager中的onInterceptTouchEvent實(shí)現(xiàn):<br />
代碼很多,關(guān)鍵是在ACTION_MOVE時(shí),他是如果判斷攔截與不攔截(攔截返回true和不攔截false)<br />
找到關(guān)鍵代碼:
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 />
會(huì)去判斷當(dāng)前顯示的頁(yè)面是否可以滑動(dòng),如果可以滑動(dòng),則將該事件丟給當(dāng)前顯示的頁(yè)面處理。<br />
這種攔截法叫做:外部攔截法
所謂外部攔截法是指所有的觸摸事件都會(huì)先經(jīng)過(guò)經(jīng)過(guò)父容器的傳遞,從而父容器在需要此觸摸事件的時(shí)候就可以攔截此觸摸事件,否者就傳遞給子View。這樣就可以解決滑動(dòng)沖突的問(wèn)題,這種方法比較符合觸摸事件的傳遞、處理機(jī)制。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在該方法中根據(jù)滑動(dòng)沖突處理規(guī)則做相應(yīng)的攔截即可。<br />
可用下面的偽代碼來(lái)表示:
@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 (父容器需要當(dāng)前觸摸事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
我們繼續(xù):
現(xiàn)在的代碼我們不兼容4.0以前的版本了,所有viewpager嵌套viewpager的實(shí)現(xiàn)簡(jiǎn)單了很多<br />
在 API13及前面的版本Viewpager 套Viewpager 直接寫存在兼容問(wèn)題。<br />
我可以通過(guò)源碼來(lái)看為什么存在兼容:
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的實(shí)現(xiàn)
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的調(diào)用
public static boolean canScrollHorizontally(View v, int direction) {
return IMPL.canScrollHorizontally(v, direction);
}
static final ViewCompatImpl IMPL;的實(shí)現(xiàn)
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的實(shí)現(xiàn):
public boolean canScrollHorizontally(View v, int direction) {
return (v instanceof ScrollingView) &&
canScrollingViewScrollHorizontally((ScrollingView) v, direction);
}
這邊(v instanceof ScrollingView),因?yàn)関為viewpager,(v instanceof ScrollingView)為fasle,所有api13以以前的canScrollHorizontally反false,即沒(méi)有實(shí)現(xiàn)滑動(dòng)判斷,永遠(yuǎn)都是flase。<br />
可以找到api14以以上的canScrollHorizontally的實(shí)現(xiàn):
public boolean canScrollHorizontally(View v, int direction) {
return ViewCompatICS.canScrollHorizontally(v, direction);
}
最終調(diào)用的是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;
}
}
這里幫你做了是否可滑動(dòng)判斷。<br />
到這里我們就從源碼層面分析了Viewpager 套Viewpager 兼容問(wèn)題<br />
我們來(lái)兼容下:<br />
先介紹另一種滑動(dòng)沖突的解決方法<br />
內(nèi)部攔截法:<br />
- 內(nèi)部攔截法是指父容器不攔截任何觸摸事件,所有的觸摸事件都傳遞給子元素,如果子元素需要此觸摸事件就直接消耗掉,否者就交由父容器進(jìn)行處理,(通過(guò)內(nèi)部子元素來(lái)進(jìn)行是否進(jìn)行攔截)這種方法和Android中的事件傳遞、處理機(jī)制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來(lái)較外部攔截法稍顯復(fù)雜。這種方法需要重寫子元素的dispatchTouchEvent方法。
- 子 View 可以使用 requestDisallowInterceptTouchEvent 影響去父 View 的分發(fā),可以決定父 View 是否要調(diào)用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用調(diào)用 onInterceptTouchEvent 來(lái)判斷攔截,而就是不攔截。
用偽代碼表示為:
子元素的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 (父容器需要當(dāng)前觸摸事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
在demo代碼中的的實(shí)現(xiàn)為:
public class ViewPagerCompat2 extends ViewPager {
/** 觸摸時(shí)按下的點(diǎn) **/
PointF downP = new PointF();
/** 觸摸時(shí)當(dāng)前的點(diǎn) **/
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軸滑動(dòng)攔截
getParent().requestDisallowInterceptTouchEvent(false);
} else {
//x軸滑動(dòng)不攔截
getParent().requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
}
上面這種內(nèi)部攔截法。當(dāng)然兼容也可以使用外部攔截法:<br />
既然ViewPager在API14以上可以正?;瑒?dòng)重寫了canScrollHorizontally(int)方法,查看ViewPager的canScrollHorizontally(int)方法源碼發(fā)現(xiàn)此方法不存在版本兼容問(wèn)題,在API13及其以下版本上也可直接調(diào)用。于是乎解決辦法就是繼承ViewPager重寫canScroll(View, boolean, int, int, int)方法,直接調(diào)用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 />