Android随笔随想-GUI-触摸事件分发
Android随笔随想-GUI-触摸事件分发
基于Android2.3的随笔分析
1. 随笔
1.1 触摸事件的产生
触摸事件的产生等背景资料以及android底层的处理,可以参照本随笔的最后部分中的资料
1.2 触摸事件与ViewRoot的关联
在上篇中,提到了ViewRoot在setView时的几个操作,现在我们稍微回顾一下:
ViewRoot.setView()
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs,
View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mWindowAttributes.copyFrom(attrs);
attrs = mWindowAttributes;
if (view instanceof RootViewSurfaceTaker) {
mSurfaceHolderCallback =
((RootViewSurfaceTaker)view).willYouTakeTheSurface();
if (mSurfaceHolderCallback != null) {
mSurfaceHolder = new TakenSurfaceHolder();
mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
}
}
Resources resources = mView.getContext().getResources();
CompatibilityInfo compatibilityInfo = resources.getCompatibilityInfo();
mTranslator = compatibilityInfo.getTranslator();
if (mTranslator != null || !compatibilityInfo.supportsScreen()) {
mSurface.setCompatibleDisplayMetrics(resources.getDisplayMetrics(),
mTranslator);
}
boolean restore = false;
if (mTranslator != null) {
restore = true;
attrs.backup();
mTranslator.translateWindowLayout(attrs);
}
......
if (!compatibilityInfo.supportsScreen()) {
attrs.flags |= WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW;
}
mSoftInputMode = attrs.softInputMode;
mWindowAttributesChanged = true;
mAttachInfo.mRootView = view;
mAttachInfo.mScalingRequired = mTranslator != null;
mAttachInfo.mApplicationScale =
mTranslator == null ? 1.0f : mTranslator.applicationScale;
if (panelParentView != null) {
mAttachInfo.mPanelParentWindowToken
= panelParentView.getApplicationWindowToken();
}
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
mInputChannel = new InputChannel();
try {
res = sWindowSession.add(mWindow, mWindowAttributes,
getHostVisibility(), mAttachInfo.mContentInsets,
mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
unscheduleTraversals();
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
if (mTranslator != null) {
mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets);
}
mPendingContentInsets.set(mAttachInfo.mContentInsets);
mPendingVisibleInsets.set(0, 0, 0, 0);
if (Config.LOGV) Log.v(TAG, "Added window " + mWindow);
if (res < WindowManagerImpl.ADD_OKAY) {//handle exception
......
}
if (view instanceof RootViewSurfaceTaker) {
mInputQueueCallback =
((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();
}
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue(mInputChannel);
mInputQueueCallback.onInputQueueCreated(mInputQueue);
} else {
InputQueue.registerInputChannel(mInputChannel, mInputHandler,
Looper.myQueue());
}
view.assignParent(this);
mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0;
mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0;
}
}
}
在上面核心的操作有两个
- 将mWindow通过Wms的client的proxy端,设置给了Wms
- 注册InputQueue的callback-InputHandler和InputQueue的InputChannel(InputChannel的功能,个人未知)
InputHandler用于接收系统输入系统的通知.在系统收到触摸和按键消息时,会通过这个callback交给App来处理
private final InputHandler mInputHandler = new InputHandler() {
public void handleKey(KeyEvent event, Runnable finishedCallback) {
startInputEvent(finishedCallback);
dispatchKey(event, true);
}
public void handleMotion(MotionEvent event, Runnable finishedCallback) {
startInputEvent(finishedCallback);
dispatchMotion(event, true);
}
};
系统在决定将事件交给我们的app来处理时
- 触摸事件,会回调handleMotion()的方法
- 按压事件,会回调handleKey()的方法
那么我们接下来顺着这条线,往下找即可
1.3 触摸事件的分发处理
我们以ACTION_DOWN为例,分析触摸事件的分发处理
先来整体的看下触摸事件处理分发的序列图
1.3.1 整体序列图
1.3.2 ViewRoot中的控制分发
ViewRoot.mInputHandler.handleMotion
public void handleMotion(MotionEvent event, Runnable finishedCallback) {
startInputEvent(finishedCallback);
dispatchMotion(event, true);
}
- 标记了事件的callback
- 交给ViewRoot进行处理,注意,这里的第二个参数是true,也就是这种事件,都需要发送确认的消息
ViewRoot.dispatchMotion()
private void dispatchMotion(MotionEvent event, boolean sendDone) {
int source = event.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
dispatchPointer(event, sendDone);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
dispatchTrackball(event, sendDone);
} else {
// TODO
Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event);
if (sendDone) {
finishInputEvent();
}
}
}
- 判断了事件的来源,是手指按压,还是轨迹球
- 如果是手指按压,dispatchPointer()
- 如果是轨迹球,dispatchTraceball
其他情况,直接不支持,给运行事件的callback即可
我们的是按压ACTIO_DOWN,因而走的是dispatchPointer()
ViewRoot.disatchPointer()
private void dispatchPointer(MotionEvent event, boolean sendDone) {
Message msg = obtainMessage(DISPATCH_POINTER);
msg.obj = event;
msg.arg1 = sendDone ? 1 : 0;
sendMessageAtTime(msg, event.getEventTime());
}
系统交给我们要处理的事件,也是通过handler的方式来处理
ViewRoot.handleMessage()
......
case DISPATCH_POINTER: {
MotionEvent event = (MotionEvent) msg.obj;
try {
deliverPointerEvent(event);
} finally {
event.recycle();
if (msg.arg1 != 0) {
finishInputEvent();
}
if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
}
} break;
......
- 直接deliverPointerEvent()
- 将事件回收
- 如果是需要sendDone,那么通知finishInputEvent
在这里的sendDone以及event的回收我们就不再看了,感兴趣的自己可以看看
ViewRoot.deliverPointerEvent()
private void deliverPointerEvent(MotionEvent event) {
//1. 屏幕尺寸的转换
if (mTranslator != null) {
mTranslator.translateEventInScreenToAppWindow(event);
}
boolean handled;
if (mView != null && mAdded) {
//2. 如果是down事件,保证是touchMode
// enter touch mode on the down
boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
if (isDown) {
ensureTouchMode(true);
}
......
if (mCurScrollY != 0) {
event.offsetLocation(0, mCurScrollY);
}
if (MEASURE_LATENCY) {
lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
}
//3. 交给DecorView做处理
handled = mView.dispatchTouchEvent(event);
if (MEASURE_LATENCY) {
lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
}
//4. 如果DecorView没有处理,并且是down的事件,那么将事件进行一些边界的处理,如果有合适的view,再交给DecorView做处理
if (!handled && isDown) {
int edgeSlop = mViewConfiguration.getScaledEdgeSlop();
final int edgeFlags = event.getEdgeFlags();
int direction = View.FOCUS_UP;
int x = (int)event.getX();
int y = (int)event.getY();
final int[] deltas = new int[2];
if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
direction = View.FOCUS_DOWN;
if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
deltas[0] = edgeSlop;
x += edgeSlop;
} else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
deltas[0] = -edgeSlop;
x -= edgeSlop;
}
} else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
direction = View.FOCUS_UP;
if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
deltas[0] = edgeSlop;
x += edgeSlop;
} else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
deltas[0] = -edgeSlop;
x -= edgeSlop;
}
} else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
direction = View.FOCUS_RIGHT;
} else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
direction = View.FOCUS_LEFT;
}
if (edgeFlags != 0 && mView instanceof ViewGroup) {
View nearest = FocusFinder.getInstance().findNearestTouchable(
((ViewGroup) mView), x, y, direction, deltas);
if (nearest != null) {
event.offsetLocation(deltas[0], deltas[1]);
event.setEdgeFlags(0);
mView.dispatchTouchEvent(event);
}
}
}
}
}
- 屏幕尺寸的转换(大小屏幕的处理,假如说当前app并不是显示全屏的,而是以某种比例来显示的,可以采取这样的方式,将我们实际屏幕的坐标转为app中的坐标,之前小米的大屏幕手机显示小屏幕模式,从这里来看,最主要的更改也就是这里的屏幕尺寸转换)
- 如果是down事件,保证是touchMode(此时会通知wms,touchMode发生了更改,并且会寻找当前窗口的焦点,然后requestFocus,处理逻辑忽略,因为对事件分发没有影响)
- 交给DecorView做处理
- 如果DecorView没有处理,并且是down的事件,那么将事件进行一些边界的处理,如果有合适的view,再交给DecorView做处理
从上述的分析来看,目前对我们的流程跟踪有效的是DecorView.dispatchTouchEvent(),即第三步
小总结一下
- ViewRoot与输入系统的结合是ViewRoot的InputChannel和InputHandler,InputHandler用于接收输入系统的回调通知
- ViewRoot以Handler的方式来接收和处理InputHandler的事件
- ViewRoot在具体要处理这个Event时,如果需要,会做屏幕尺寸的转换;down事件,会通知Wms,本地也会做更改;交给DecorView来处理;如果是down事件,并且没有处理,那么会边界的处理,如果调整的范围内,能够找到合适的view,还是会再次交给DecorView来处理
那么下面,我们继续跟踪DecorView的dispatchTouchEvent()即可
1.3.3 DecorView体系的处理
DecorView.dispatchTouchEvent()
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
.dispatchTouchEvent(ev);
}
DecorView是继承自FrameLayout的自定义view,这里直接根据是否有callback和mFeatureId的值,决定是给callback,还是直接处理
对于Activity,是符合callback != null 并且 mFeatureId<0的,因而是给callback.dispatchTouchEvent()来处理的.在前面的随笔中,也提到过,构建Activity时,PhoneWindow的callback是Activity,那么直接查看Activity的dispatchTouchEvent即可
Activity.dispatchTouchEvent()
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
- ACTION_DOWN事件,通知onUserInteraction()(可以看看onUserInteraction()的说明,用户到底有没有操作这个Activity,可以在这里做处理)
- 获取window,然后调用superDispatchTouchEvent()来消费,如果消费了,返回true,没有消费,继续
调用Activity的onTouchEvent()
从分析上看,我们查看第二步和第三步即可
PhoneWindow.superDispatchTouchEvent()
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
直接是交给了DecorView处理,继续跟踪
DecorView.superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView也没有做什么特殊处理,直接是调用了ViewGroup的dispatchTouchEvent()方法,变为了普通的viewgroup处理,暂时我们先不继续的看ViewGroup的体系,下个chapter分析这个,前面Activity的部分还没有结束.
Activity.onTouchEvent()
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
return false;
}
默认Actiivty是不做事件的处理的,因而是直接返回false的
小总结一下
从ViewRoot交给DecorView,DecorView开始了自己的事件分发的道路,在这里
- DecorView是直接交给Activity来处理
- Activity通知Activity端,onUserInteraction()
- Activity交给PhoneWindow来处理(PhoneWindow也间接的交给了真正的View体系来处理这个事件)
- 如果PhoneWindow没有处理,那么Activity自己便消费了
从上面便可以看出,事件是DecorView自己主动将事件交给Activity来处理的,因而事件的体系可以这么理解.
根布局DecorView,自己主动派发事件给Activity,Activity可以认作是拦截器,默认情况下,拦截器只是间接的将事件转发给DecorView做正常的View体系的事件分发即可
1.3.4 View体系的处理
view体系的处理分为两部分,一部分是ViewGroup的控制分发,另外一部分是View的控制分发处理
1.3.4.1 ViewGroup的控制分发
下图为ViewGroup控制分发的图示
ViewGroup.dispatchTouchEvent()
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
- 如果filterTouchEventForSecurity不是安全的,那么直接返回false(可以查看下源码,窗口在模糊时,返回的是false,具体发生情况需要查询资料)
- 获取GroupFlags,是否不允许拦截,赋值给disallowIntercept
- 如果是ACTION_DOWN,清空MotionTarget,如果disallowIntercept为true或者自身的viewGroup不做拦截即onINterceptTouchEvent为false,开始寻找viewGroup的直接孩子节点作为目标对象.倒序遍历,判断子view中是否包含点击的位置,如果包含,则将事件的坐标转换为子view相对于父view,即当前view的坐标,并且给子view设置CANCEL_NEXT_UP_EVENT,此时调用子view的派发事件方法,dispatchTouchEvent(),如果子view消费了,那么设置mMotionTarget为子view,返回true.如果没有,继续遍历.
- 判断当前事件是否是upOrCancel,保存到isUpOrCancel,如果是,GroupFlag中设置为ALLOW_INTERCEPT,影响下次的事件分发
- 如果targetView为null,那么将viewGroup作为普通的view,直接调用super.dispatchTouchEvent,然后返回调用的值,在此之前,会判断CANCEL_NEXT_UP_EVENT的值,自己并不会记录motion_target,但是viewGroup的父view会记录这个值
- 如果是允许拦截,并且viewgroup的拦截返回了true,表示确实要拦截这个事件,那么给targetView手动传递一个cancel的事件,并且主动调用targetView的dispatchTouchEvent(),清空MotionTarget,返回true
- 再次判断isUpOrCancel,清空motionTarget
- 到这里时,target != null , 此时,将坐标转为targetView的坐标,如果有CANCEL_NEXT_UP_EVENT的标记,将事件转为ACTION_CANCEL,删除target的mRipvateFlags,清空mMotionTarget的值
- 调用target的分发事件方法
逻辑简单图示
备注: 此图缺少disallowIntercept的逻辑
小总结一下
- viewGroup在分发时,在寻找目标时,是ACTION_DOWN的事件,如果子view没有消费ACTION_DOWN,并且自身也没有消费ACTION_DOWN,那么接下来的事件,就不再传递给此viewGroup
- 子view可以请求父view,不做拦截的处理,这样子view的处理优先级是高于父view的,从ACTION_DOWN的处理便知.disallowIntercept的判断在前
- dispatchTouchEvent 用于控制分发事件的流程,onInterceptTouchEvent()用于表示是否拦截事件,onTouchEvent()表示具体的事件消费
- view体系在控制分发时,均为父view,保存子view作为motionTargetView,如果自己作为targetView,是由父view保存状态的
1.3.4.2 View的控制分发
View.dispatchTouchEvent()
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (!onFilterTouchEventForSecurity(event)) {
return false;
}
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
- 校验touchEvent是否是安全的,如果不是,返回false
- 在view是enable的,并且mOnTouchListener不是null时,回调mOnTouchListener的onTouch方法,如果mOnTouchListener返回true,那么直接返回true,否则,继续
- 返回View的onTouchEvent方法
小总结一下
- 我们在设置了普通view的onTouchListener,并且返回true,那么view的onTouchEvent()是不会调用的
- 在view的onTouchEvent()消费了down事件,并且父view,将此事件都传递过来时,onTouchEvent()是一直在调用的.
View.onTouchEvent()
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
- 判断是否是enabled,如果不是,返回clickable与long_clickable的结果,并不是直接返回的false
- 如果mTouchDelegate不为null,并且onTouch消费了这个事件,直接返回true
在view是clickable或者是long_clickable的情况下,按照事件的流程来分析这部分
ACTION_DOWN:
- 构建了一个CheckForTap的类,用于检测点击事件
- view的privateTag中,添加Prepressed的标记,并且设置mHasPerformedLongPress为false
- 通过view内置的handler,post一个tabTimeout的一个检测点击事件的runnable: CheckForTab
从这里也可以看到,并不是我们在触摸到屏幕时,假如当前view会消费这个事件,便会直接显示按压的状态,而是有一个PREPRESSED的状态,接下来在到了tapTimeout的状态,才会处于按压的状态
- ACTION_MOVE:
如果在我们的移动过程中,超过了v和slop的边界,那么便会:
- 移除tabCallback
- 如果当前处于PRESSED的状态,移除LONG_PRESSED的callback,回复privateflag的pressed状态为normal
更新drawable
- ACTION_UP:
如果当前处于按压和PRESSED的状态下,才会做以下的处理,其他情况,不做处理
- 如果当前是focusable,,并且是focusableInTouchMode但是没有获取到焦点,那么会做焦点的获取,并且结果赋值给focusTaken
- 如果当前没有执行LongPress,移除LongPressCallback,即不再做longClickable的callback检测
- 如果当前的focusTaken是false,即当前我们是焦点,或者说requestFocus失败时,会执行mPerformClick或者直接执行performClick(在post失败后)
- 如果当前在prepressed的情况下,会更新privateFlags为PRESSED,更新为按压的图片,然后postDelay一个恢复的state
- 而不是4的情况,并且是post mUnsetPressedState失败时,会直接执行mUnsetPressedState
- 移除tap的callback
从上面也可以看出,对于在PREPRESSED和PRESSED的状态下,均会执行click的操作的:performClick()
ACTION_CANCEL:
- 更新privateTag的值为normal的,unpressed的
- 更新drawable的状态
- 移除tapCallback
在上面我们只是看到了设置按压状态的分析,并没有具体调用onClickListenener和onLongClickListener的具体代码
根据上面我们的线索,继续分析
CheckForTabClick
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
refreshDrawableState();
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
- 更新privateFlags的状态为PRESSED,移除PREPRESSED的状态
- 更新drawable的状态为按压
- 如果是long_clickable的状态,那么继续post一个longClick的检查,
在按压和move的状态下,我们看到并没有按压的事件处理,不是时间到了就处理,而是在up时performClick的类中进行的处理
PerformClick
private final class PerformClick implements Runnable {
public void run() {
performClick();
}
}
View.performClick()
/**
* Call this view's OnClickListener, if it is defined.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
- 在mOnClickListener不为null时,播放声音,并且调用mOnClickListener.onClick()的方法,返回true
- 在mOnClickListener为null时,直接返回false
接着上述的CheckForTap,我们查看longClick的处理
View.postCheckForLongClick()
private void postCheckForLongClick(int delayOffset) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
- 标记mHasPerformedLongPress为false
- 构建CheckForLongPress的runnable对象,用于post后检测longPress
- mPendingCheckForLongPress.rememberWindowAttachCount记录window attach的次数
- 通过handler post longPress的处理
在时间到了之后,查看CheckForLongPress的runnable的实现
View.CheckForLongPress
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}
判断当前是否是在按压,并且parent不为null,然后windowAttachCount也是相同的,如果performLongClick()为true,便设置为mHasPerformedLongPress为true
View.performLongClick()
/**
* Call this view's OnLongClickListener, if it is defined. Invokes the context menu if the
* OnLongClickListener did not consume the event.
*
* @return True if one of the above receivers consumed the event, false otherwise.
*/
public boolean performLongClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
if (mOnLongClickListener != null) {
handled = mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
handled = showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
- 如果设置了longClickListener,调用mOnLongClickListener.onLongClick()方法,并且赋值给handled
- 如果没有handled,调用showContextMenu()
- 返回handled的结果
小总结一下
- view在触摸状态下,有三个状态
1) PREPRESSED
2) PRESSED
3) LONG_PRESSED
- 在PRESSED和PRESSED的区别在于,是否显示的是按压
- view的事件在enabled为true,并且有clickable或者long_cliable,便可以消费事件,onTouchEvent便可以返回true,即不一定是要有OnClickListener或者OnLongClickListener的
- 在没有long_click或者long_click的处理返回false时,单击事件还是可以触发的
从这里我们也看到了,ListView的onItemClickListener不是只有listView自身可以触发,在内部view收到long_click时,但是view自身没有消费时,也是可以触发的(具体可以查看showContextMenu()的实现).
2. 随想
2.1 事件分发处理,是至顶而下分发,至顶而下拦截,默认情况下自下而上消费
2.2. TouchDelegate为什么要设置给ParentView,而不是自己
从ViewGroup的contains方法来看,在做点击事件的包含时,并没有delegate的相关调用,因而,这儿区域的扩大,可能是有问题的,因而做法是给view的parent设置touchDelegate,并且父View是enabled,这样才能接收到这个事件.
3. 相关资源
柯元旦-android内核剖析-Ch13 View工作原理