更多分享:http://www.cherylgood.cn
- 之前我们在Scroller的使用详解中,在onMeasure方法中可能你会看到 childView.setClickable(true);为什么要设置childView为true呢,假如不设置的话,你会发现ACTION_MOVE并没有执行。为什么会出现这样的问题呢?此时我是一脸懵逼的,要想彻底搞明白,对于Android事件分发机制的了解是必不可少的。
package guanaj.com.scrollerdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Scroller;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private Scroller scroller;
private LinearLayout llContent;
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.m_button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG,"#onClick 我被点击了");
}
});
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
Log.i(TAG,"#onTouch ACTION_DOWN ");
break;
}
case MotionEvent.ACTION_MOVE:{
Log.i(TAG,"#onTouch ACTION_MOVE ");
break;
}
case MotionEvent.ACTION_UP:{
Log.i(TAG,"#onTouch ACTION_UP ");
break;
}
default:{
Log.i(TAG,"#onTouch");
}
}
return false;
}
});
}
04-18 16:23:20.022 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_DOWN
04-18 16:23:20.052 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.062 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.082 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.132 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.132 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.152 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.162 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.172 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_UP
04-18 16:23:20.192 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onClick 我被点击了
可能你会发现,onClick回调方法没有返回值,OnTouch回调方法返回个false?懵逼的我尝试着把false变成true……..懵逼的事情发生了。
04-18 16:35:42.672 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_DOWN
04-18 16:35:42.702 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.722 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.732 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.762 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.772 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.772 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_UP
懵逼的OnClick事件没有触发~~懵逼的我只能为了满足小小的好奇心,到网上查了些资料以及查看了源码:
1、首先从android系统的机制入手:我们的按钮是在activity里面的,而我们的activity是怎么来的呢,这里我们要引出一个类,Window:官方描述:
- 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. The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.
- 定义顶级窗口外观和行为策略的一个抽象基类。 应该使用这个类的实例作为添加到窗口管理器的顶级视图。 它提供标准的UI策略,如背景,标题区域,默认的键处理等。这个抽象类的唯一现有实现是android.view.PhoneWindow,当你需要一个窗口应该实例化它。
2、线索转移,现在开始追踪PhoenWindow,一直追到Activity.java,此时在Activity.java 的源码中看到如下代码:
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback {
看到了Window的字样,感觉应该是他了,然后从attach函数中看到:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
结论:phoneWindow其实是创建了activity之后才创建它的,PhoneWindow作为activity的窗口顶级类进行存在,Activity展示的内容也是通过PhoneWindow来设置的,此时可以回想activity中的setContentView(R.layout.activity_main);继续追源码可以看到,调用了window的setContentView方法,而前面说过,window的唯一实现类是PhoneWindow。
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID); //调用getWindow方法,返回mWindow
initWindowDecorActionBar();
}
...
public Window getWindow() {
return mWindow;
}
此时开始猜想,当activity里面的元素被点击时,事件的转发第一个会不会是传递给PhoneWindow呢?其实PhoneWindow中存在一个内部类DecorView对象…..好像有点偏题了,好吧,直接给出PhoneWindow的追踪结论:
- 左边的frameLayout放的是我们的系统状态栏,右边的frameLayout存放的就是我们在activity中setContentView的布局内容了。
我们从activity的dispatchTouchEvent源码里也能获得准确的信息:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
重点转移到getWindow().superDispatchTouchEvent(),getWindow()返回当前Activity的顶层窗口Window(PhoneWindow)对象,我们直接看Window API的superDispatchTouchEvent()方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
继续看源码:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
我们继续看installDecor源码:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
if (mTitleView != null) {
mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(com.android.internal.R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
} else {
mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
if (mActionBar != null) {
mActionBar.setWindowCallback(getCallback());
if (mActionBar.getTitle() == null) {
mActionBar.setWindowTitle(mTitle);
}
final int localFeatures = getLocalFeatures();
if ((localFeatures & (1 << FEATURE_PROGRESS)) != 0) {
mActionBar.initProgress();
}
if ((localFeatures & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
mActionBar.initIndeterminateProgress();
}
boolean splitActionBar = false;
final boolean splitWhenNarrow =
(mUiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;
if (splitWhenNarrow) {
splitActionBar = getContext().getResources().getBoolean(
com.android.internal.R.bool.split_action_bar_is_narrow);
} else {
splitActionBar = getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowSplitActionBar, false);
}
final ActionBarContainer splitView = (ActionBarContainer) findViewById(
com.android.internal.R.id.split_action_bar);
if (splitView != null) {
mActionBar.setSplitView(splitView);
mActionBar.setSplitActionBar(splitActionBar);
mActionBar.setSplitWhenNarrow(splitWhenNarrow);
final ActionBarContextView cab = (ActionBarContextView) findViewById(
com.android.internal.R.id.action_context_bar);
cab.setSplitView(splitView);
cab.setSplitActionBar(splitActionBar);
cab.setSplitWhenNarrow(splitWhenNarrow);
} else if (splitActionBar) {
Log.e(TAG, “Requested split action bar with ” +
“incompatible window decor! Ignoring request.”);
}
// Post the panel invalidate for later; avoid application onCreateOptionsMenu
// being called in the middle of onCreate or similar.
mDecor.post(new Runnable() {
public void run() {
// Invalidate if the panel menu hasn’t been created before this.
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!isDestroyed() && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_ACTION_BAR);
}
}
});
}
}
}
}
从代码可以看到:
~~ 然而framelayout并没有DispatchTouchEvent方法,而framelayout继承自viewgroup,那么肯定在viewgroup里面了。
接下来我们就看DecorView类的superDispatchTouchEvent()方法
public boolean superDispatchTouchEvent(MotionEvent event)
{
return super.dispatchTouchEvent(event);
}
在里面调用了父类FrameLayout的dispatchTouchEvent()方法,而FrameLayout中并没有dispatchTouchEvent()方法,所以我们直接看ViewGroup的dispatchTouchEvent()方法,继续看源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 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;
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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 ArrayList preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
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;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary 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;
}
重点语句:先检测是否允许拦截touch事件,disallowIntercept 为false表示允许拦截,然后调用onInterceptTouchEvent 看是否真的要拦截。所以如果想让父view不拦截我们的touch事件,一般通过requestDisallowInterceptTouchEvent 设置disallowIntercept的值来控制。
// 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;
}
判断了拦截之后,就会便利可以接收touch事件的child view。将事件分发下去:
for (int i = childrenCount - 1; i >= 0; i–) {
…
//只转发给focus的childview
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
……
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
…….
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
我们继续看dispatchTransformedTouchEvent这个方法:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
可以看到,里面判断childview是否null,是的话调用super.dispatchTouchEvent继续讲事件分发出去,也就是假如该viewgroup没有childview 了,就分发给自己,否则调用的childview的dispatchTouchEvent方法,然后就消费结果返回;
-我们继续看childview的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
……
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;
}
从我们demo的测试结果可以知道,先执行了onTouchEvent才执行onClick,而在view的dispatchTouchEvent里并没有看到onClick相关的代码,所以onClick肯定在onTouchEvent中被调用的,所以我们继续看onTouchEvent方法的源码:
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, x, y);
}
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;
}
从代码里发现了个很好玩的东西mTouchDelegate:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
首先判断控件是否是可点击的。如果是就进入switch判断中去;重点看case 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;
}
可以看到, 我们设置了onClickListener,就会调用onClick方法,并且返回true
重点,在switch语句最后会返回个ture,所以你会发现,只要执行了switch,最后都会返回true;为什么会返回true呢?其实这里涉及到touch事件的层级传递:
当给控件注册了touch事件后,每次点击它的时候都会触ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件;但是,一旦你在执行某一个action的时候返回了false,后面的action就不会再执行了。假如你在ACTION_DOWN返回false,那么后面的都不会在执行了。也就是说,在dispatchTouchEvent进行事件分发的时候,只有前一个action返回true,才会触发下一个action;
我们再看一次View.dispatchTouchEvent代码;
public boolean dispatchTouchEvent(MotionEvent event) {
……
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;
}
从图中我们可以看到,在viewgroup跟view之间是一个递归的形式,不断的遍历视图层次结构!