之前分析View类的dispatchTouchEvent()(见文章Android触摸事件派发(一) ViewGroup的dispatchTouchEvent())的时候,讲到会调用View类的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();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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)) {
performClickInternal();
}
}
}
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:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
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(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
if (!pointInView(x, y, touchSlop)) {
// The default action here is to cancel long press. But instead, we
// just extend the timeout here, in case the classification
// stays ambiguous.
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* mAmbiguousGestureMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= mAmbiguousGestureMultiplier;
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
开始先将坐标位置存储在变量x,y中,view的flags也存放在变量viewFlags中,事件的action存放在变量action中。该action的值可能会是ACTION_POINTER_DOWN吗?因为该事件类型action里面的第二个字节存储的是pointer index值,所以要调用getActionMasked()取得事件类型。在这个地方是可能是ACTION_POINTER_DOWN的,在多点触控中第二个手指也按到了第一个手指按到的控件上就会出现这种情况。
代码24行到26行,表明了什么是clickable状态,在mViewFlags设置了CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE中任一一个flags。其中这三个flags分别可以通过xml布局控价属性clickable、longClickable、contextClickable来设置,也分别可以使用代码setClickable()、setLongClickable()、setContextClickable()来设置。其中CONTEXT_CLICKABLE看代码注释是为了处理触控笔按键和鼠标右键的,需要设置setOnContextClickListener监听接口。
代码28行到36行,指出了控件是disabled并且是clickable,这个时候是消耗事件的。
接下来如果控件设置了触摸代理对象,是需要通过代理来判断是否消耗事件的。事件的代理对象mTouchDelegate是通过方法setTouchDelegate()设置。如果mTouchDelegate.onTouchEvent(event)返回true,则代表事件被消费了,直接返回。mTouchDelegate是TouchDelegate实例,看该类注释可以知道,它可以增大当前View的触摸面积(比自己实际的触摸面积要大),这个类应该被该代理的祖先使用。
43行代码开始的if (clickable || (viewFlags & TOOLTIP) == TOOLTIP),到分支结束返回true可知,只要控件是clickable的,就会消耗事件。TOOLTIP标识,如果设置了它,在长按或者鼠标悬浮在该控件上,会显示小的提示文字。如果设置了TOOLTIP标识,onTouchEvent()方法走到这个分支,也是会消耗事件。
后面就是处理具体的点击事件类型了,事件流的开始是ACTION_DOWN类型,先看ACTION_DOWN类型的处理过程
一、ACTION_DOWN类型处理
相关的代码贴在下面:
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
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(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
开始如果触摸源是触摸屏,将会在变量mPrivateFlags3上设置PFLAG3_FINGER_DOWN标志,并且将变量mHasPerformedLongPress的值设为false。该变量是为了在一次长按过程中执行过长按之后,避免执行点击事件。因为点击事件是在ACTION_UP中发生的,发生ACTION_UP事件会去判断该变量。
接着在clickable为false的情况下,调用checkForLongClick()方法,并且会跳出该case。通过前面的分析,如果clickabel为false,则会设置TOOLTIP标识,这个checkForLongClick在这个时候是去处理设置的TOOLTIP标识的。
长按处理
看一下该方法:
private void checkForLongClick(long delay, float x, float y, int classification) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
mPendingCheckForLongPress.setClassification(classification);
postDelayed(mPendingCheckForLongPress, delay);
}
}
可见该方法首先判断是否设置了LONG_CLICKABLE 或TOOLTIP标识,设置了其中一个都是可以执行下面的代码的。从这里也可以看到,长按是通过延时来实现的,这个方法的参数delay的值是ViewConfiguration.getLongPressTimeout(),这个是系统配置的一个值,默认400ms。该方法设置
mHasPerformedLongPress 变量为false,然后初始化mPendingCheckForLongPress变量。mPendingCheckForLongPress是一个CheckForLongPress类型实例,该类型继承Runnable,可以放到消息队列中。调用setAnchor()方法,得到锚点的位置,这个值是类型为ACTION_DOWN的坐标值,调用rememberWindowAttachCount()记录控件绑定Window的次数,调用rememberPressedState()记录控件的按下状态。这两个状态在真正执行长按方法的时候,还会再检查,如果发生变化,长按事件是不会发生的。
该方法还有个参数classification,该值是为了系统记录统计数据使用的。
接着就放到消息队列中,等待时间到了执行。接着看CheckForLongPress的run()方法,看看具体执行。
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
recordGestureClassification(mClassification);
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
先进行了检查状态,然后调用recordGestureClassification(mClassification)记录事件流的分类,再调用performLongClick(mX, mY),如果该方法返回true,则将变量mHasPerformedLongPress 设置为true,代表长按事件已经执行了。
/** Records a classification for the current event stream. */
private void recordGestureClassification(int classification) {
if (classification == TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION) {
return;
}
// To avoid negatively impacting View performance, the latency and displacement metrics
// are omitted.
FrameworkStatsLog.write(FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED, getClass().getName(),
classification);
}
/frameworks/base/core/java/statslog-framework-java-gen/gen/com/android/internal/util/FrameworkStatsLog.java
public static void write(int code, java.lang.String arg1, int arg2) {
final StatsEvent.Builder builder = StatsEvent.newBuilder();
builder.setAtomId(code);
builder.writeString(arg1);
builder.writeInt(arg2);
builder.usePooledBuffer();
StatsLog.write(builder.build());
}
recordGestureClassification(mClassification)调用了FrameworkStatsLog.write()方法,将相关信息封装成StatsEvent对象,发送给statsd,这是Android的原生服务,用来收集指标。详细见Statsd
performLongClick(mX, mY)执行了真正的长按操作,
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
………………
public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}
………………
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if ((mViewFlags & TOOLTIP) == TOOLTIP) {
if (!handled) {
handled = showLongClickTooltip((int) x, (int) y);
}
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
performLongClick又经过传递,最后的执行是在performLongClickInternal()方法中。
首先设置局部变量handled的值为false,如果控件已经设置了长按监听mOnLongClickListener,则会调用该监听的onLongClick()方法,并且通过将该方法的返回值赋予变量handled作为是否处理了长按事件。长按事件监听是通过控件的setOnLongClickListener()方法设置的。
如果长按事件返回false,或者没设置长按事件,则会接着处理显示上下文菜单,并且将处理结果返回给变量handled。
如果这两个处理结果都是false,那么会检查TOOLTIP标识,并且去执行显示Tooltip,处理完毕,再将结果返回给handled。
最后检查handled如果是true,代表处理了长按事件,会执行performHapticFeedback()方法,这个方法是为了触觉反馈,使用户感觉到了长按事件。最后将变量handled的结果返回。最终结果会返回到performLongClick()方法。
显示上下文菜单
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
根据x,y的是否是数值来区分调用showContextMenu(x, y)还是showContextMenu()。先看下showContextMenu(x, y)
public boolean showContextMenu(float x, float y) {
return getParent().showContextMenuForChild(this, x, y);
}
getParent()得到包含该控件的父控件,这个父控件是个ViewGroup类型的,看下ViewGroup类型的showContextMenuForChild(this, x, y)
@Override
public boolean showContextMenuForChild(View originalView) {
if (isShowingContextMenuWithCoords()) {
// We're being called for compatibility. Return false and let the version
// with coordinates recurse up.
return false;
}
return mParent != null && mParent.showContextMenuForChild(originalView);
}
/**
* @hide used internally for compatibility with existing app code only
*/
public final boolean isShowingContextMenuWithCoords() {
return (mGroupFlags & FLAG_SHOW_CONTEXT_MENU_WITH_COORDS) != 0;
}
@Override
public boolean showContextMenuForChild(View originalView, float x, float y) {
try {
mGroupFlags |= FLAG_SHOW_CONTEXT_MENU_WITH_COORDS;
if (showContextMenuForChild(originalView)) {
return true;
}
} finally {
mGroupFlags &= ~FLAG_SHOW_CONTEXT_MENU_WITH_COORDS;
}
return mParent != null && mParent.showContextMenuForChild(originalView, x, y);
}
可见showContextMenuForChild(View originalView, float x, float y)每次先设置FLAG_SHOW_CONTEXT_MENU_WITH_COORDS标识,然后调用showContextMenuForChild(originalView),该方法会去检测FLAG_SHOW_CONTEXT_MENU_WITH_COORDS,所以方法会返回false,然后到showContextMenuForChild(View originalView, float x, float y)里面会将FLAG_SHOW_CONTEXT_MENU_WITH_COORDS标识去除,接着调用父控件的showContextMenuForChild(originalView, x, y),这样传递到最后会传递到DecorView类中,看一下该类的对应方法,
@Override
public boolean showContextMenuForChild(View originalView) {
return showContextMenuForChildInternal(originalView, Float.NaN, Float.NaN);
}
@Override
public boolean showContextMenuForChild(View originalView, float x, float y) {
return showContextMenuForChildInternal(originalView, x, y);
}
private boolean showContextMenuForChildInternal(View originalView,
float x, float y) {
// Only allow one context menu at a time.
if (mWindow.mContextMenuHelper != null) {
mWindow.mContextMenuHelper.dismiss();
mWindow.mContextMenuHelper = null;
}
// Reuse the context menu builder.
final PhoneWindowMenuCallback callback = mWindow.mContextMenuCallback;
if (mWindow.mContextMenu == null) {
mWindow.mContextMenu = new ContextMenuBuilder(getContext());
mWindow.mContextMenu.setCallback(callback);
} else {
mWindow.mContextMenu.clearAll();
}
final MenuHelper helper;
final boolean isPopup = !Float.isNaN(x) && !Float.isNaN(y);
if (isPopup) {
helper = mWindow.mContextMenu.showPopup(getContext(), originalView, x, y);
} else {
helper = mWindow.mContextMenu.showDialog(originalView, originalView.getWindowToken());
}
if (helper != null) {
// If it's a dialog, the callback needs to handle showing
// sub-menus. Either way, the callback is required for propagating
// selection to Context.onContextMenuItemSelected().
callback.setShowDialogForSubmenu(!isPopup);
helper.setPresenterCallback(callback);
}
mWindow.mContextMenuHelper = helper;
return helper != null;
}
最终进入到showContextMenuForChildInternal(View originalView,
float x, float y),该方法是通过mWindow.mContextMenu实现上下文菜单。mWindow是PhoneWindow类型,上下文菜单是只允许存在一个的,如果之前存在,需要执行一些清理工作。然后判断x,y都是数值,之后调用 mWindow.mContextMenu.showPopup()方法。进入该方法:
public MenuPopupHelper showPopup(Context context, View originalView, float x, float y) {
if (originalView != null) {
// Let relevant views and their populate context listeners populate
// the context menu
originalView.createContextMenu(this);
}
if (getVisibleItems().size() > 0) {
EventLog.writeEvent(50001, 1);
int location[] = new int[2];
originalView.getLocationOnScreen(location);
final MenuPopupHelper helper = new MenuPopupHelper(
context,
this,
originalView,
false /* overflowOnly */,
com.android.internal.R.attr.contextPopupMenuStyle);
helper.show(Math.round(x), Math.round(y));
return helper;
}
return null;
}
该方法是先调用originalView.createContextMenu(this),并且将当前类对象作为参数传递进去。如果需要显示上下文菜单,用户需要重写控件的createContextMenu(ContextMenu menu)方法,具体实现添加菜单项,然后本方法再检测getVisibleItems().size() 的值就会大于 0,这个时候就会显示上下文菜单了,并且返回的helper也不为null。如果用户没重写createContextMenu(ContextMenu menu),自然回到本方法返回的值就为null。
来看控件的createContextMenu(ContextMenu menu)方法
public void createContextMenu(ContextMenu menu) {
ContextMenuInfo menuInfo = getContextMenuInfo();
// Sets the current menu info so all items added to menu will have
// my extra info set.
((MenuBuilder)menu).setCurrentMenuInfo(menuInfo);
onCreateContextMenu(menu);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnCreateContextMenuListener != null) {
li.mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);
}
// Clear the extra information so subsequent items that aren't mine don't
// have my extra info.
((MenuBuilder)menu).setCurrentMenuInfo(null);
if (mParent != null) {
mParent.createContextMenu(menu);
}
}
首先调用getContextMenuInfo(),该方法是为了生成一些额外信息。接着调用onCreateContextMenu(menu),如果设置了mOnCreateContextMenuListener接口,也执行接口的onCreateContextMenu()方法,最后会调用父控件的createContextMenu(menu)方法。所以用户可以重写onCreateContextMenu(menu)方法,也可以设置接口mOnCreateContextMenuListener接口进行实现具体的菜单项。
显示Tooltip
相关代码如下
if ((mViewFlags & TOOLTIP) == TOOLTIP) {
if (!handled) {
handled = showLongClickTooltip((int) x, (int) y);
}
}
………………
private boolean showLongClickTooltip(int x, int y) {
removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
return showTooltip(x, y, true);
}
………………
private boolean showTooltip(int x, int y, boolean fromLongClick) {
if (mAttachInfo == null || mTooltipInfo == null) {
return false;
}
if (fromLongClick && (mViewFlags & ENABLED_MASK) != ENABLED) {
return false;
}
if (TextUtils.isEmpty(mTooltipInfo.mTooltipText)) {
return false;
}
hideTooltip();
mTooltipInfo.mTooltipFromLongClick = fromLongClick;
mTooltipInfo.mTooltipPopup = new TooltipPopup(getContext());
final boolean fromTouch = (mPrivateFlags3 & PFLAG3_FINGER_DOWN) == PFLAG3_FINGER_DOWN;
mTooltipInfo.mTooltipPopup.show(this, x, y, fromTouch, mTooltipInfo.mTooltipText);
mAttachInfo.mTooltipHost = this;
// The available accessibility actions have changed
notifyViewAccessibilityStateChangedIfNeeded(CONTENT_CHANGE_TYPE_UNDEFINED);
return true;
}
参数fromLongClick的值为true,即是显示该Tooltip是来自长按事件。如果参数fromLongClick的值为true,那么控件必须设置ENABLED,才能显示。显示Tooltip,必须设置mTooltipInfo.mTooltipText,可以通过调用setTooltipText()方法或者在布局文件设置tooltipText属性,设置该值。显示Tooltip之前,调用hideTooltip()先隐藏之前显示的。然后新建一个TooltipPopup对象,然后调用show()显示出来。这儿还会将mAttachInfo.mTooltipHost的值设置为当前控件。
Tooltip在显示之后,手指抬起来之后一会儿,会自动消失。这块的处理是在ACTION_UP中,这儿将其代码摘录下来:
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
………………
ACTION_UP事件的时候,先将PFLAG3_FINGER_DOWN标志去除,然后检查TOOLTIP标识,如果存在该标识,需要调用handleTooltipUp()
private void handleTooltipUp() {
if (mTooltipInfo == null || mTooltipInfo.mTooltipPopup == null) {
return;
}
removeCallbacks(mTooltipInfo.mHideTooltipRunnable);
postDelayed(mTooltipInfo.mHideTooltipRunnable,
ViewConfiguration.getLongPressTooltipHideTimeout());
}
可见,在延迟ViewConfiguration.getLongPressTooltipHideTimeout()时间之后,调用mTooltipInfo.mHideTooltipRunnable,通过名字也能知道这个是为了隐藏Tooltip的。
mTooltipInfo.mHideTooltipRunnable = this::hideTooltip;
………………
void hideTooltip() {
if (mTooltipInfo == null) {
return;
}
removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
if (mTooltipInfo.mTooltipPopup == null) {
return;
}
mTooltipInfo.mTooltipPopup.hide();
mTooltipInfo.mTooltipPopup = null;
mTooltipInfo.mTooltipFromLongClick = false;
mTooltipInfo.clearAnchorPos();
if (mAttachInfo != null) {
mAttachInfo.mTooltipHost = null;
}
// The available accessibility actions have changed
notifyViewAccessibilityStateChangedIfNeeded(CONTENT_CHANGE_TYPE_UNDEFINED);
}
该方法就调用 mTooltipInfo.mTooltipPopup.hide()将其隐藏。测试Tooltip的时候,注意一下,Android的系统版本,需要在8.0及往上。
这样长按事件里面的相关处理就解释完了,接着向下看onTouchEvent()里面ACTION_DOWN的处理。来到代码的16行,执行performButtonActionOnTouchDown(event)代码,如果该方法返回true,也会跳出case。
protected boolean performButtonActionOnTouchDown(MotionEvent event) {
if (event.isFromSource(InputDevice.SOURCE_MOUSE) &&
(event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
showContextMenu(event.getX(), event.getY());
mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT;
return true;
}
return false;
}
这段代码是判断鼠标右键点击了,会调用showContextMenu()方法,显示上下文菜单(当然得实现相关方法,见显示上下文菜单),这块还会对控件设置PFLAG_CANCEL_NEXT_UP_EVENT标志,派发事件的时候,如果控件设置了该标志,事件将变成ACTION_CANCEL类型向下传递。
代码继续向下,通过isInScrollingContainer()方法,将结果设置给isInScrollingContainer,这个方法是为了得到当前控件是否处于一个可以滚动的容器(例如ScrollView)里。
public boolean isInScrollingContainer() {
ViewParent p = getParent();
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}
该方法通过父控件的shouldDelayChildPressedState()来判断,所以可以滚动的容器重写该方法,例如ScrollView类在该方法中返回true。
代码接下来判断变量isInScrollingContainer,如果为true,会将按压状态延迟一段时间,是为了区分该事件是不是一个滚动。如果是一个滚动不是按压,后面触发的ACTION_MOVE可以将该消息取消掉,不会改变控件的按压效果。看下其代码:
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
}
先设置PFLAG_PREPRESSED,然后将坐标x,y的值赋给mPendingCheckForTap对象的成员变量x,y,接着向主线程放入一个延迟消息mPendingCheckForTap,ViewConfiguration.getTapTimeout()的值为100,即延迟100ms之后执行。mPendingCheckForTap是一个CheckForTap类,继承Runnable,会被封装成一个消息放到主线程中等待执行,看下run()函数:
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
final long delay =
ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout();
checkForLongClick(delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
待到该CheckForTap类型消息执行时,去除PFLAG_PREPRESSED标识,调用setPressed(true, x, y),将控制设置为按压状态。并且计算出来长按的延迟事件,再放到主线程消息队列中等待执行。长按事件前面说过,不再说。
按压状态
看下setPressed(true, x, y):
private void setPressed(boolean pressed, float x, float y) {
if (pressed) {
drawableHotspotChanged(x, y);
}
setPressed(pressed);
}
在pressed为true的情况下,需要执行drawableHotspotChanged(x, y)。pressed为true,代表按压状态,这个时候需要改变drawbale的Hotspot,这个是为了设置RippleDrawable的控件使用的。接着就调用了setPressed()方法:
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
先通过检查之前是否已经是按压状态了,如果和pressed不相等,则需要刷新。然后根据pressed设置PFLAG_PRESSED标识还是去除该标识。如果需要刷新,则会调用refreshDrawableState()方法,最后调用dispatchSetPressed(pressed)方法将按压情况派发给子View。接下来看看refreshDrawableState():
public void refreshDrawableState() {
mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
drawableStateChanged();
ViewParent parent = mParent;
if (parent != null) {
parent.childDrawableStateChanged(this);
}
}
……………………
@CallSuper
protected void drawableStateChanged() {
final int[] state = getDrawableState();
boolean changed = false;
final Drawable bg = mBackground;
if (bg != null && bg.isStateful()) {
changed |= bg.setState(state);
}
final Drawable hl = mDefaultFocusHighlight;
if (hl != null && hl.isStateful()) {
changed |= hl.setState(state);
}
final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (fg != null && fg.isStateful()) {
changed |= fg.setState(state);
}
if (mScrollCache != null) {
final Drawable scrollBar = mScrollCache.scrollBar;
if (scrollBar != null && scrollBar.isStateful()) {
changed |= scrollBar.setState(state)
&& mScrollCache.state != ScrollabilityCache.OFF;
}
}
if (mStateListAnimator != null) {
mStateListAnimator.setState(state);
}
if (changed) {
invalidate();
}
}
refreshDrawableState()先设置PFLAG_DRAWABLE_STATE_DIRTY标识,然后调用drawableStateChanged(),去处理具体的改变。然后调用父控件的childDrawableStateChanged(this)(ViewGrou也是调用refreshDrawableState())。drawableStateChanged()首先调用getDrawableState()得到当前控件的drawable的状态,然后去设置相关的drawable,Drawable类的isStateful()方法表示它的外观会随着它的状态改变,所以在isStateful()为true的时候,调用Drawable类的setState(state)设置对应的状态。通过drawableStateChanged()可以看到涉及到的Drawable,包括背景mBackground,默认获取焦点mDefaultFocusHighlight,前景mForegroundInfo.mDrawable,滚动条 mScrollCache.scrollBar。如果mStateListAnimator 不为null,会调用mStateListAnimator.setState(state),这个是控件状态变化发生的动画。如果变量changed是true,则调用invalidate()更新视图。
首先看getDrawableState():
public final int[] getDrawableState() {
if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
return mDrawableState;
} else {
mDrawableState = onCreateDrawableState(0);
mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
return mDrawableState;
}
}
前面refreshDrawableState()已经设置了PFLAG_DRAWABLE_STATE_DIRTY标志,所以这里会通过onCreateDrawableState(0)得到当前控件对应的状态。得到状态之后就将PFLAG_DRAWABLE_STATE_DIRTY标识去除。
看一下onCreateDrawableState()方法:
protected int[] onCreateDrawableState(int extraSpace) {
if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE &&
mParent instanceof View) {
return ((View) mParent).onCreateDrawableState(extraSpace);
}
int[] drawableState;
int privateFlags = mPrivateFlags;
int viewStateIndex = 0;
if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED;
if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED;
if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED;
if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED;
if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED;
if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED;
if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested &&
ThreadedRenderer.isAvailable()) {
// This is set if HW acceleration is requested, even if the current
// process doesn't allow it. This is just to allow app preview
// windows to better match their app.
viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED;
}
if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED;
final int privateFlags2 = mPrivateFlags2;
if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) {
viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT;
}
if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) {
viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED;
}
drawableState = StateSet.get(viewStateIndex);
//noinspection ConstantIfStatement
if (false) {
Log.i("View", "drawableStateIndex=" + viewStateIndex);
Log.i("View", toString()
+ " pressed=" + ((privateFlags & PFLAG_PRESSED) != 0)
+ " en=" + ((mViewFlags & ENABLED_MASK) == ENABLED)
+ " fo=" + hasFocus()
+ " sl=" + ((privateFlags & PFLAG_SELECTED) != 0)
+ " wf=" + hasWindowFocus()
+ ": " + Arrays.toString(drawableState));
}
if (extraSpace == 0) {
return drawableState;
}
final int[] fullState;
if (drawableState != null) {
fullState = new int[drawableState.length + extraSpace];
System.arraycopy(drawableState, 0, fullState, 0, drawableState.length);
} else {
fullState = new int[extraSpace];
}
return fullState;
}
代码开头检查标识DUPLICATE_PARENT_STATE,这个标识的意思就是复制父控件的状态,如果存在该标识,则调用父控件的onCreateDrawableState()方法。后面会根据状态,将对应的值设置到变量viewStateIndex的对应bit位。可见主要对应了10中状态:
public static final int VIEW_STATE_WINDOW_FOCUSED = 1;
/** @hide */
public static final int VIEW_STATE_SELECTED = 1 << 1;
/** @hide */
public static final int VIEW_STATE_FOCUSED = 1 << 2;
/** @hide */
public static final int VIEW_STATE_ENABLED = 1 << 3;
/** @hide */
public static final int VIEW_STATE_PRESSED = 1 << 4;
/** @hide */
public static final int VIEW_STATE_ACTIVATED = 1 << 5;
/** @hide */
public static final int VIEW_STATE_ACCELERATED = 1 << 6;
/** @hide */
public static final int VIEW_STATE_HOVERED = 1 << 7;
/** @hide */
public static final int VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8;
/** @hide */
public static final int VIEW_STATE_DRAG_HOVERED = 1 << 9;
接着会通过drawableState = StateSet.get(viewStateIndex)得到drawable的状态,数组drawableState 里面是什么呢,存储的是属性资源值,对应的关系如下
static final int[] VIEW_STATE_IDS = new int[] {
R.attr.state_window_focused, VIEW_STATE_WINDOW_FOCUSED,
R.attr.state_selected, VIEW_STATE_SELECTED,
R.attr.state_focused, VIEW_STATE_FOCUSED,
R.attr.state_enabled, VIEW_STATE_ENABLED,
R.attr.state_pressed, VIEW_STATE_PRESSED,
R.attr.state_activated, VIEW_STATE_ACTIVATED,
R.attr.state_accelerated, VIEW_STATE_ACCELERATED,
R.attr.state_hovered, VIEW_STATE_HOVERED,
R.attr.state_drag_can_accept, VIEW_STATE_DRAG_CAN_ACCEPT,
R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED
};
可以举个例子,如果viewStateIndex里的bit位对应VIEW_STATE_SELECTED、VIEW_STATE_FOCUSED,则drawableState的值为{R.attr.state_selected,R.attr.state_focused}。
getDrawableState()得到对应的值之后,就会返回到drawableStateChanged()方法中,将对应状态设置到对应的Drawable中,在经过刷新界面,控件的界面就发生变化。
按压事件讲完了,先返回到ACTION_DOWN时间的处理中,上面讲的是在可以滚动的容器内部处理,接着看下其他情况的处理,也就是变量isInScrollingContainer的值为false的处理:
else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
在不能滚动的容器中,就不用判断这个触摸事件是一个滚动还是按压了,所以立即执行setPressed(true, x, y),处理按压状态。然后将一个长按事件消息放到主线程中等待执行长按事件。这些前面都说过了。
二、ACTION_MOVE类型处理
相关代码摘抄如下:
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
if (!pointInView(x, y, touchSlop)) {
// The default action here is to cancel long press. But instead, we
// just extend the timeout here, in case the classification
// stays ambiguous.
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* mAmbiguousGestureMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= mAmbiguousGestureMultiplier;
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
开始控件如果是clickable,则调用drawableHotspotChanged(x, y),通知drawable的热点发生了变化。接着通过event对象的getClassification()方法,得到手势事件的类别。事件的类别属性值有三个,CLASSIFICATION_NONE、CLASSIFICATION_AMBIGUOUS_GESTURE、CLASSIFICATION_DEEP_PRESS。CLASSIFICATION_NONE是没有额外的信息的事件;CLASSIFICATION_AMBIGUOUS_GESTURE是判断不清楚用户用途的事件,即意图模糊不清的手势事件,像滚动行为,在这个事件发生的时候,应该是被抑制的,直到这个事件分类变成别的值或事件结束;CLASSIFICATION_DEEP_PRESS是深压事件,是用户有意使劲压在屏幕上,这种类型的事件可以加速长按事件的发生。具体向下看,看看这些分类的事件都做了什么操作。
hasPendingLongPressCallback()就是检查消息队列中是否有待执行的长按事件消息。在ambiguousGesture为true并且hasPendingLongPressCallback()的情况下,检查触摸点是否已经超出了控件的范围,注意这个范围是按照控件本身的长宽加上一个默认的数值mTouchSlop围城的一个面积。在超出了控件的范围的情况下,会将长按事件消息取消掉。并且加长一下超时时间,减掉已经消耗的时间。得到这个时间,重新放到消息对列中长按事件。延长时间是将原来的长按事件乘上一个倍数mAmbiguousGestureMultiplier,该值默认为2f。并且增加范围也扩大为mAmbiguousGestureMultiplier倍放到变量touchSlop中。
接着判断pointInView(x, y, touchSlop),这个触摸点在控件的范围之外,则调用removeTapCallback()和removeLongPressCallback()这个就是取消mPendingCheckForTap消息和长按事件消息,注意,removeTapCallback()在取消mPendingCheckForTap消息的时候也会去除PFLAG_PREPRESSED标识。前面在ACTION_DOWN的时候,在滚动容器中,会将一个mPendingCheckForTap封装成消息延迟一段事件等待执行,在这块,判断出来用户手势不是一个按压而是一个滑动,会将之前的CheckForTapl事件给取消掉。接着判断PFLAG_PRESSED标识是否存在,如果存在会将该状态取消掉。并且也会将PFLAG3_FINGER_DOWN标识取消掉。
最后会判断事件是否一个深压分类事件,如果是,并且有长按事件消息待执行,调用removeLongPressCallback()去除长按事件,并且将长按事件的延迟时间设置为0,加入到主消息对列中立即执行。这也就是加速长按事件的执行。
二、ACTION_UP类型处理
ACTION_UP是正常事件流的最后一个类型,对应手指抬起的情况,相关代码如下:
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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)) {
performClickInternal();
}
}
}
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;
handleTooltipUp()在前面已经讲了,接着在clickable为false的情况下,调用removeTapCallback()和removeLongPressCallback()去除CheckForTap类型事件消息与长按事件消息,然后将变量mInContextButtonPress、mHasPerformedLongPress和mIgnoreNextUpEventJun设置为false,然后跳出当前case。从前面ACTION_DOWN类型事件分析知道,在clickable为false,有TOOLTIP标识的情况下,会将长按事件消息放入主线程消息队列中。但是在clickable为false的情况下,是不会将CheckForTap类型事件消息放入主线程消息队列中的。这块为何会在clickable为false调用removeTapCallback()?这可能是在经过中间事件的过程中修改了控件的属性,ACTION_DOWN类型事件发生时clickable为true,所以可能将CheckForTap类型事件消息放入主线程消息队列中,等待ACTION_UP类型事件发生时clickable为false了。即使没发生这些变化,调用removeTapCallback()也没关系,不会有别的影响。变量mInContextButtonPress,是在触控笔按键被按下或者鼠标右键按下的时候为true,在按键释放或触控笔提起来的时候为false。变量mIgnoreNextUpEvent,的意思是下一个ACTION_UP类型事件应该被忽略,按键释放或触控笔提起来的时候被设置为true。触摸事件暂时先忽略这两个属性。
接着检查PFLAG_PREPRESSED标识,这个标识,是在ACTION_DOWN事件发生时,在可滚动容器内设置延迟。如果在ACTION_UP类型事件监测到该标志存在时,说明CheckForTap类型事件消息还没有执行,控件处于待按压状态。如果PFLAG_PRESSED标识存在,说明控件处于按压状态。在这两种情况下,需要执行点击事件。
接着就是18到21行代码是请求焦点相关。变量focusTaken默认为false,这个变量影响着点击事件是否执行,focusTaken为true(代表获取到焦点),则点击事件不会执行,从下面的代码可以看出。在三个条件下,去执行requestFocus()获取控件焦点。三个条件分别是isFocusable()、isFocusableInTouchMode()、!isFocused()。isFocusable()方法判断该控件是否是可以获取焦点的,isFocusableInTouchMode()是判断该控件在触摸模式下是否可以获取焦点,!isFocused()是判断当前控件没有获取到焦点。isFocusableInTouchMode()是被那些isFocusable(),但是在触摸模式下不想获取焦点的控件来使用的。举个例子,如果Button开始没有获取到焦点,这样它满足isFocusable()和!isFocused()条件,如果isFocusableInTouchMode()也为true,则会导致变量focusTaken为true,前面也说了,这个变量为true,是不会执行点击事件的,显然不符合事实。这种情况就可以通过isFocusableInTouchMode()设置为fasle,来避免这种情况。requestFocus()方法见下一篇文章,他会将请求焦点的结果返回给变量focusTaken。
下面代码23行判断prepressed为true的情况下,会调用setPressed(true, x, y)方法。prepressed为true,代表CheckForTap类型事件消息还没有执行,就触发了ACTION_UP类型事件。这个时候,执行setPressed(true, x, y),将控件设置为按压状态。
接着就判断!mHasPerformedLongPress && !mIgnoreNextUpEvent条件,mHasPerformedLongPress 变量代表已经执行了长按事件,如果执行了长按事件,就不会执行点击事件了。mIgnoreNextUpEvent变量在前面也提到了,触摸事件的分析可以忽略。下面就执行removeLongPressCallback()如果存在长按消息事件,也取消掉。代码第36行,就开始判断focusTaken变量的值,从这就能看出来,focusTaken为false的条件下,才会执行点击事件。代码40行到45行,就执行点击事件。这块是将点击事件放到主线程消息队列中执行,是为了将其他可见的控件的状态先显示出来,然后再执行点击事件。如果点击事件加入主线程消息对列失败,就直接执行点击事件。点击事件方法performClickInternal()见下面。
代码49行到59行是为了执行取消按压状态,UnsetPressedState事件就是为了完成这个事,UnsetPressedState类继承Runnable,看其run()方法里,就执行了setPressed(false)。这个事件消息如果在prepressed为true的情况下,延迟ViewConfiguration.getPressedStateDuration()加入到主线程消息对列中。这个ViewConfiguration.getPressedStateDuration()的值是64。因为prepressed为true,按压状态才刚更新,所以这里需要延迟一会再抬起。其他情况下,也是先加入主线程消息对列中,如果加入失败,直接执行抬起。
最后61行代码将mPendingCheckForTap消息取消掉不能再执行了。mIgnoreNextUpEvent的值也设置为false。
点击事件
点击事件方法performClickInternal()方法代码如下:
/**
* Entry point for {@link #performClick()} - other methods on View should call it instead of
* {@code performClick()} directly to make sure the autofill manager is notified when
* necessary (as subclasses could extend {@code performClick()} without calling the parent's
* method).
*/
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
notifyAutofillManagerOnClick()是为了通知自动填充服务该控件执行了点击事件,然后接着执行performClick()方法。
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
这块主要是为了执行注册的点击事件,所以检测到li.mOnClickListener不为空,会先播放音效,然后就执行li.mOnClickListener的onClick()方法。用户的点击事件可以通过setOnClickListener()注册,也可以通过控件的xml布局文件中的属性onClick来设置。这样用户注册的点击事件就会执行。
接着看下playSoundEffect(SoundEffectConstants.CLICK):
public void playSoundEffect(int soundConstant) {
if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
return;
}
mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
}
在mAttachInfo.mRootCallbacks接口存在并且isSoundEffectsEnabled()的情况,会播放mAttachInfo.mRootCallbacks接口里的播放音效的效果。这个接口的实现是在ViewRootImpl类里实现的。isSoundEffectsEnabled()是检查控件的SOUND_EFFECTS_ENABLED标识,这个标识可以通过xml布局属性soundEffectsEnabled和setSoundEffectsEnabled()方法来设置。还有在View的不带参数构造函数中,这个标识是默认带着的。
如果要实现自己的特殊音效,也可以自定义控件重写playSoundEffect(int soundConstant)方法来实现。
ACTION_CANCEL类型处理
从前面Android触摸事件派发(一) ViewGroup的dispatchTouchEvent()可知,在ViewGroup类的dispatchTouchEvent()方法处理ACTION_CANCEL时,将该事件派发之后,会将子控件对应的触摸对象从触摸对象链中去除,后续事件流中的事件也就不会再接收到,从这个角度来看,对于该子控件来说,这个事件类型也是该控件接收到的最后一个事件类型,应该执行一些收尾工作。现在看下在onTouchEvent()中该类型的处理:
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
这里面的属性和方法基本前面都讲过,如果该控件是clickable,调用setPressed(false)取消控件的按压状态。然后调用removeTapCallback()和removeLongPressCallback()方法去除mPendingCheckForTap消息和长按事件消息,再将mInContextButtonPress、mHasPerformedLongPress、mIgnoreNextUpEvent属性都设置为false。最后将PFLAG3_FINGER_DOWN标识去除。
这样就分析完了onTouchEvent()事件的代码:
1、在一个可滚动控件里,怎么区分用户手势是一个点击,还是一个划动?
在ACTION_DOWN事件里,先放入一个延迟消息,如果在ACTION_MOVE事件里判断手指划出了控件的范围,这个时候认为是一个划动,需要取消延迟消息。如果没有,到时就会执行消息,认为是一个点击。
2、长按事件是通过延迟消息实现的,在其中还可以实现上下文菜单、TOOLTIP功能。优先顺序是长按监听>上下文菜单>TOOLTIP功能.
3、点击事件是在ACTION_UP中发生,如果不是clickable的,不会发生点击事件。如果ACTION_UP发生时,控件不是PFLAG_PRESSED状态或待按压状态,也不会执行点击事件。
4、如果控件是clickable,就会消耗事件。
如果控件是DISABLED,则控件只能是clickable才能消耗事件。
如果控件是ENABLED,则控件只要是clickable或有TOOLTIP标识,也会消耗事件。
这一条需要结合容器(ViewGroup类型)的dispatchTouchEvent()来分析问题,是否能成为容器的触摸对象。