Android事件分發(fā)之View事件處理關(guān)鍵及示例分析
目的
網(wǎng)上已經(jīng)有很多關(guān)于事件分發(fā)的優(yōu)秀文章,為何我還要自己寫?因?yàn)閯e人總結(jié)的畢竟都是別人的,自己親自閱讀源碼不僅會(huì)讓自己更懂得原理,也會(huì)讓自己記得更清楚,而且也會(huì)發(fā)現(xiàn)另一番天地。
View處理事件的關(guān)鍵
由于所以的控件都直接或者間接繼承自View,因此View的事件分發(fā)機(jī)制就是最基礎(chǔ)的一環(huán),需要首先掌握其原理。
那么View的事件從哪里來的呢?當(dāng)然是父View(一個(gè)ViewGroup)。父View在尋找能處理事件的子View的時(shí)候,會(huì)調(diào)用子View的dispatchTouchEvent()把事件傳遞給子View,如果子View的dispatchTouchEvent()返回true,代表子View處理了該事件,如果返回flase就代表該子View不處理事件。如果所有子View都不處理該事件,那么就由父View自己處理。
今天我們這篇文章就是來分析View如何處理事件。我們重點(diǎn)關(guān)心View.dispatchTouchEvent()啥時(shí)候返回true(代表處理了事件),啥時(shí)候返回false(代表不處理事件)。
View事件處理分析
/**
* 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) {
boolean result = false;
final int actionMasked = event.getActionMasked();
// 當(dāng)窗口被遮擋,是否過濾掉這個(gè)觸摸事件
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
// 1. 外部監(jiān)聽器處理
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 2. 自己處理
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
可以看到View.dispatchTouchEvent()處理事件非常簡(jiǎn)單,要么交給外部處理,要么自己來處理。只要任何一方處理了,也就相應(yīng)的處理函數(shù)返回true,View.dispatchTouchEvent()就返回true,代表View處理了事件。否則,View.dispatchTouchEvent()返回false,也就是View不處理該事件。
首先它把事件交給外部進(jìn)行處理。外部處理指的什么呢?它指的就是交給setOnTouchListener()設(shè)置的監(jiān)聽器來處理。如果這個(gè)監(jiān)聽器處理時(shí)返回true,也就是OnTouchListener.onTouch()方法返回true,View.dispatchTouchEvent()就返回true,也就說明View處理了該事件。否則交給自己來處理,也就是交由onTouchEvent()處理。
當(dāng)然,如果要讓事件監(jiān)聽器來處理,還必須要讓View處于enabled狀態(tài)??梢酝ㄟ^setEnabled()方法來改變View的enabled狀態(tài)。并且可以通過isEnabled()查詢View是否處于enabled狀態(tài)。
當(dāng)外部無法處理時(shí),也就是上面的三個(gè)條件有一個(gè)不滿足時(shí),就交給View.onTouchEvent()來處理。此時(shí)View.onTouchEvent()的返回值就決定了View.dispatchTouchEvent()的返回值。也就是決定了View是否處理該事件。那么,我們來看下View.onTouchEvent()什么時(shí)候返回true,什么時(shí)候返回false。
public boolean onTouchEvent(MotionEvent event) {
// 判斷View是否可點(diǎn)擊(點(diǎn)擊/長(zhǎng)按)
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 處理View是disabled狀態(tài)的情況
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// ...
return clickable;
}
// 如果有處理代表,就先交給它處理。如果它不處理,就繼續(xù)交給自己處理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果可以點(diǎn)擊,最后會(huì)返回true,代表處理了View處理了事件
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
// ...
return true;
}
return false;
}
這太讓人意外了,只要View可以點(diǎn)擊(點(diǎn)擊/長(zhǎng)按),就返回true,否則返回false。
忽略觸摸代理(Touch Delegate)和CONTEXT_CLICKABLE的特性,因?yàn)椴怀S?,如果遇到了,可以再來查看?/p>
那么,View默認(rèn)可以點(diǎn)擊,長(zhǎng)按嗎?當(dāng)然是不能。這需要子View自己去設(shè)置,例如Button在構(gòu)造函數(shù)中就設(shè)置了自己可以點(diǎn)擊。
我們從代碼角度解釋下View默認(rèn)是否可以點(diǎn)擊和長(zhǎng)按
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// View構(gòu)造函數(shù)中解析android:clickable屬性
case com.android.internal.R.styleable.View_clickable:
// 第二個(gè)參數(shù)false,表示View默認(rèn)不可點(diǎn)擊
if (a.getBoolean(attr, false)) {
viewFlagValues |= CLICKABLE;
viewFlagMasks |= CLICKABLE;
}
break;
// View構(gòu)造函數(shù)中解析android:longClickable屬性
case com.android.internal.R.styleable.View_longClickable:
// 第二個(gè)參數(shù)false,表示View默認(rèn)不可長(zhǎng)按
if (a.getBoolean(attr, false)) {
viewFlagValues |= LONG_CLICKABLE;
viewFlagMasks |= LONG_CLICKABLE;
}
break;
}
在View的構(gòu)造函數(shù)中分別接下了android:clickable和android:longClickable屬性,從默認(rèn)值可以看出,View默認(rèn)是不可點(diǎn)擊和長(zhǎng)按的。也就是說View默認(rèn)不處理任何事件。
那么,我們用一張圖來解釋View如何處理觸摸事件的

通過這張圖,我們就可以清楚的了解到View.dispatchTouchEvent()在什么情況下返回 true,在什么情況下返回false。也就了解了View在什么情況下處理了事件,在什么情況下不處理事件。
View.onTouchEvent()分析
View事件處理就這么簡(jiǎn)單嗎?如果你只關(guān)心事件分發(fā)到哪里,以及誰處理了事件,那么掌握上面的流程就夠了。
但是你是否還有個(gè)疑問,View.onTouchEvent()在干啥呢?OK,如果你保持這份好奇心,那么接著往下看。
View.onTouchEvent()其實(shí)處理了三種情況
- 處理點(diǎn)擊事件
- 處理長(zhǎng)按事件
- 處理View狀態(tài)改變
- 處理tap事件
處理長(zhǎng)按事件
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
// 1. 判斷是否在一個(gè)滾動(dòng)的容器中
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
// 1.1 在滾動(dòng)容器中
// ...
} else {
// 1.2 不是在滾動(dòng)容器中
// 設(shè)置按下狀態(tài)
setPressed(true, x, y);
// 檢測(cè)長(zhǎng)按動(dòng)作
checkForLongClick(0, x, y);
}
break;
setPressed()方法首先會(huì)設(shè)置View為按下狀態(tài), 代碼如下
mPrivateFlags |= PFLAG_PRESSED;
然后,通過checkForLongClick()來檢測(cè)長(zhǎng)按動(dòng)作,這是如何實(shí)現(xiàn)呢
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
// 在長(zhǎng)按超時(shí)的時(shí)間點(diǎn),執(zhí)行一個(gè)Runable,也就是CheckForLongPres
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
private final class CheckForLongPress implements Runnable {
@Override
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
// 執(zhí)行長(zhǎng)按動(dòng)作
if (performLongClick(mX, mY)) {
// 如果處理了長(zhǎng)按動(dòng)作,mHasPerformedLongPress為true
mHasPerformedLongPress = true;
}
}
}
}
其實(shí)它是把CheckForLongPress這個(gè)Runnable加入到Message Queue中,然后在ViewConfiguration.getLongPressTimeout()這個(gè)長(zhǎng)按超時(shí)的時(shí)間點(diǎn)執(zhí)行。
這是什么意思呢?首先在ACTION_DOWN的時(shí)候我檢測(cè)到按下的動(dòng)作,那么在還沒有執(zhí)行ACTION_UP之前,如果按下動(dòng)作超時(shí)了,也就是超過了長(zhǎng)按的時(shí)間點(diǎn),那么我會(huì)執(zhí)行長(zhǎng)按動(dòng)作performLongClick()。我們現(xiàn)在看下執(zhí)行長(zhǎng)按做了哪些事情
public boolean performLongClick(float x, float y) {
// 記錄長(zhǎng)按的位置
mLongClickX = x;
mLongClickY = y;
// 執(zhí)行長(zhǎng)按的動(dòng)作
final boolean handled = performLongClick();
// 重置數(shù)據(jù)
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}
private boolean performLongClickInternal(float x, float y) {
boolean handled = false;
final ListenerInfo li = mListenerInfo;
// 1. 執(zhí)行長(zhǎng)按監(jiān)聽器處理動(dòng)作
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
// 2. 如果長(zhǎng)按監(jiān)聽器不處理,就顯示上下文菜單
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
// 3. 如果處理了長(zhǎng)按事件,就執(zhí)行觸摸反饋
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
有三個(gè)動(dòng)作在長(zhǎng)按的時(shí)候執(zhí)行
- 如果調(diào)用過
setOnLongClickListener()給View設(shè)置長(zhǎng)按事件監(jiān)聽器,那么首先把長(zhǎng)按事件交給這個(gè)監(jiān)聽器處理。如果這個(gè)監(jiān)聽器返回true,代表監(jiān)聽器已經(jīng)處理了長(zhǎng)按事件,那么直接執(zhí)行第三步的觸摸反饋,并返回。如果這個(gè)監(jiān)聽器返回了false,代表監(jiān)聽沒有處理長(zhǎng)按事件,那么就執(zhí)行第二步,交給系統(tǒng)處理。 - 當(dāng)?shù)谝徊教幚聿涣藭r(shí),系統(tǒng)自己來處理,它會(huì)顯示一個(gè)上下文菜單。
- 執(zhí)行觸摸反饋。
如果你不了解什么是上下文菜單(Context Menu)和觸摸反饋(Haptic Feednack),可以自行搜索下。
我們已經(jīng)了解了如果觸發(fā)長(zhǎng)按做了哪些動(dòng)作,但是我們也要記得觸發(fā)長(zhǎng)按的時(shí)機(jī),那就是從手指按下到抬起的時(shí)間要超過長(zhǎng)按的超時(shí)時(shí)間。如果沒有超過這個(gè)長(zhǎng)按超時(shí)時(shí)間,在ACTION_UP的時(shí)候,系統(tǒng)會(huì)怎么做呢?
case MotionEvent.ACTION_UP:
// 處于按下狀態(tài)
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 沒有執(zhí)行長(zhǎng)按動(dòng)作
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除長(zhǎng)按動(dòng)作
removeLongPressCallback();
}
}
break;
當(dāng)檢測(cè)到ACTION_UP時(shí),如果見到了View處于按下狀態(tài),但是還沒有執(zhí)行長(zhǎng)按動(dòng)作。也就是說,還沒有達(dá)到長(zhǎng)按的時(shí)間點(diǎn),手指就抬起了,那么系統(tǒng)就會(huì)移除在ACTION_DOWN添加的長(zhǎng)按動(dòng)作,之后長(zhǎng)按動(dòng)作就不會(huì)觸發(fā)了。
處理點(diǎn)擊事件
我們先分析了長(zhǎng)按事件而沒有分析點(diǎn)擊事件,其實(shí)是為了更好的講清楚點(diǎn)擊事件,看代碼
case MotionEvent.ACTION_DOWN:
if (isInScrollingContainer) {
} else {
// 設(shè)置按下狀態(tài)
setPressed(true, x, y);
}
break;
當(dāng)檢測(cè)到ACTION_DOWN事件,首先的給它設(shè)置一個(gè)按下標(biāo)記,這個(gè)前面說過。然后在沒有達(dá)到長(zhǎng)按超時(shí)這個(gè)時(shí)間點(diǎn)前,如果檢測(cè)到ACTION_UP事件,那么我們就可以認(rèn)為這是一次點(diǎn)擊事件
case MotionEvent.ACTION_UP:
// 處于按下狀態(tài)
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 沒有執(zhí)行長(zhǎng)按
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除長(zhǎng)按動(dòng)作
removeLongPressCallback();
// focusTaken是在touch mode下有效,現(xiàn)在討論的是簡(jiǎn)單的手指觸摸
if (!focusTaken) {
// 創(chuàng)建點(diǎn)擊事件
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 通過Message Queue執(zhí)行點(diǎn)擊事件
if (!post(mPerformClick)) {
performClick();
}
}
}
}
break;
Touch Mode模式與D-pad有關(guān),讀者可以查閱官方文檔說明。
有了前面關(guān)于長(zhǎng)按事件的知識(shí),這里就非常好理解了。
如果沒有執(zhí)行長(zhǎng)按動(dòng)作,就先移除長(zhǎng)按回調(diào),那么以后就不會(huì)再執(zhí)行長(zhǎng)按動(dòng)作了。相反,如果已經(jīng)執(zhí)行長(zhǎng)按動(dòng)作,那么就不會(huì)執(zhí)行點(diǎn)擊事件。
performClick()用來執(zhí)行點(diǎn)擊事件,那么來看下它做了什么
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
// 1. 首先交給外部的點(diǎn)擊監(jiān)聽器處理
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
// 2. 如果沒有外部監(jiān)聽器,就不處理了
result = false;
}
return result;
}
點(diǎn)擊事件的處理非常簡(jiǎn)單粗暴,默認(rèn)就不處理,也就是返回false。當(dāng)然,如果你想處理,調(diào)用setOnClickListener()即可。
###處理View狀態(tài)改變
響應(yīng)View狀態(tài)改變的操作都集中在setPressed()方法中,其實(shí)我們?cè)龠M(jìn)一步思考下,View只對(duì)按下和抬起的狀態(tài)進(jìn)行響應(yīng)
public void setPressed(boolean pressed) {
// 1. 判斷是否需要執(zhí)行刷新動(dòng)作
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
// 2. 設(shè)置狀態(tài)
if (pressed) {
// 設(shè)置按下狀態(tài)
mPrivateFlags |= PFLAG_PRESSED;
} else {
// 取消按下狀態(tài)
mPrivateFlags &= ~PFLAG_PRESSED;
}
// 3. 如果需要刷新就刷新View管理的Drawable狀態(tài)
if (needsRefresh) {
refreshDrawableState();
}
// 4. 如果是ViewGroup,就需要把這個(gè)狀態(tài)分發(fā)給子View
dispatchSetPressed(pressed);
}
如果手指按下了,會(huì)調(diào)用setPressed(true),如果手指抬起了,會(huì)調(diào)用setPressed(false)。
假設(shè)我們手指剛按下,那么就需要執(zhí)行第三步的刷新Drawable狀態(tài)的動(dòng)作
public void refreshDrawableState() {
// 標(biāo)記Drawable狀態(tài)需要刷新
mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
// 執(zhí)行Drawable狀態(tài)改變的動(dòng)作
drawableStateChanged();
// 通知父View,子View的Drawable狀態(tài)改變了
ViewParent parent = mParent;
if (parent != null) {
parent.childDrawableStateChanged(this);
}
}
首先設(shè)置PFLAG_DRAWABLE_STATE_DIRTY標(biāo)記,表示Drawable狀態(tài)需要更新,然后調(diào)用drawableStateChange()來執(zhí)行Drawable狀態(tài)改變動(dòng)作
@CallSuper
protected void drawableStateChanged() {
// 1. 獲取Drawable新狀態(tài)
final int[] state = getDrawableState();
boolean changed = false;
// 2. 為View管理的各種Drawable設(shè)置新狀態(tài)
final Drawable bg = mBackground;
if (bg != null && bg.isStateful()) {
changed |= bg.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;
}
}
// 3. 為StateListAnimator設(shè)置新狀態(tài),從而改變Drawable
if (mStateListAnimator != null) {
mStateListAnimator.setState(state);
}
// 4. 如果有Drawable狀態(tài)更新了,就重繪
if (changed) {
invalidate();
}
}
既然我們要給Drawable更新狀態(tài),那么就的獲取新的狀態(tài)值,這就是第一步所做的事情,我們來看下getDrawableState()如何獲取新狀態(tài)的
public final int[] getDrawableState() {
// 如果Drawable狀態(tài)沒有改變,就直接返回之前的狀態(tài)值
if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
return mDrawableState;
}
// 如果狀態(tài)值不存在,或者Drawable狀態(tài)需要更新
else {
// 創(chuàng)建狀態(tài)值
mDrawableState = onCreateDrawableState(0);
// 重置PFLAG_DRAWABLE_STATE_DIRTY狀態(tài)
mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
return mDrawableState;
}
}
剛剛,我們?cè)O(shè)置了PFLAG_DRAWABLE_STATE_DIRTY,標(biāo)志著Drawable狀態(tài)需要更新,因此這里會(huì)調(diào)用onCreateDrawableState()來獲取
protected int[] onCreateDrawableState(int extraSpace) {
// 默認(rèn)是沒有設(shè)置DUPLICATE_PARENT_STATE狀態(tài)
if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE &&
mParent instanceof View) {
return ((View) mParent).onCreateDrawableState(extraSpace);
}
int[] drawableState;
// 2. 根據(jù)各種flag, 獲取狀態(tài)
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;
}
// 2. 把狀態(tài)值轉(zhuǎn)化為一個(gè)數(shù)組
drawableState = StateSet.get(viewStateIndex);
if (extraSpace == 0) {
return drawableState;
}
}
首先根據(jù)各種標(biāo)志位,例如mPrivateFlags和mPrivateFlags2,來獲取狀態(tài)的值,然后根據(jù)狀態(tài)的值獲取一個(gè)狀態(tài)的數(shù)組。
我想你一定想直到這個(gè)狀態(tài)數(shù)組是咋樣的,我舉個(gè)例子,View默認(rèn)是enabled狀態(tài),那么mViewFlags默認(rèn)設(shè)置了ENABLED標(biāo)記,當(dāng)我們手指按下的時(shí)候,mPrivateFlags設(shè)置了PFLAG_PRESSED按下狀態(tài)標(biāo)記。如果值選擇這兩個(gè)情況來獲取狀態(tài)值,那么viewStateIndex = VIEW_STATE_PRESSED | VIEW_STATE_ENABLED,用二進(jìn)制表示就是11000。然后通過StateSet.get(viewStateIndex)轉(zhuǎn)化為數(shù)組就是[StateSet.VIEW_STATE_ENABLED, StateSet.VIEW_STATE_PRESSED]。
現(xiàn)在,我們獲取到Drawable新的狀態(tài)值,那么就可以進(jìn)行drawableStateChanged()函數(shù)的第二步,為各種Drawable設(shè)置新的狀態(tài)值,例如背景Drawable,前景Drawable。這些Drawable根據(jù)這些新的狀態(tài)值,自己判斷是否需要更新Drawable,例如更新顯示的大小,顏色等等。如果更新了Drawable,那么就會(huì)返回true,否則返回false。
drawableStateChanged()函數(shù)的第三步,還針對(duì)了StateListAnimator的處理。StateListAnimator會(huì)根據(jù)View狀態(tài)值,改變Drawable的顯示。
如果大家不了解StateListAnimator,可以網(wǎng)上查閱下它的使用,這樣就可以對(duì)View狀態(tài)改變有更深層次的理解。
drawableStateChanged()函數(shù)的第四步,如果有任意Drawable改變了狀態(tài),那么就需要View進(jìn)行重繪。
處理tap事件
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
// 1. 判斷View是否在滾動(dòng)容器中
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
// 標(biāo)記要觸發(fā)tab事件
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
// 2. 如果View在滾動(dòng)容器中,那么檢測(cè)一個(gè)tab動(dòng)作
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
}
break;
第一步,判斷View是否在一個(gè)滾動(dòng)的容器中
public boolean isInScrollingContainer() {
ViewParent p = getParent();
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}
通過循環(huán)遍歷父View,并調(diào)用父View的shouldDelayChildPressedState()方法來判斷父View是否是一個(gè)滾動(dòng)容器。
那么什么樣的ViewGroup是滾動(dòng)容器呢?例如ScrollView就是一個(gè)滾動(dòng)容器,因?yàn)樗凶屪覸iew滾動(dòng)的特性,所以shouldDelayChildPressedState()返回true。而LinearLayout就不是一個(gè)滾動(dòng)容器,它本身沒有設(shè)計(jì)滾動(dòng)特性,因此shouldDelayChildPressedState()返回false。
當(dāng)View處于一個(gè)滾動(dòng)容器中,并且容器處于滾動(dòng)中,這個(gè)View需要檢測(cè)一個(gè)tap事件,也就是表示快速點(diǎn)擊。它有個(gè)觸發(fā)的超時(shí)時(shí)間,大小為100ms(長(zhǎng)按的觸發(fā)超時(shí)時(shí)間是500ms),因此只要按下的事件超過100ms, 都算作一次tap事件。那么,我們先來看下觸發(fā)tap事件都做了啥事
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
// 先取消tab的標(biāo)記
mPrivateFlags &= ~PFLAG_PREPRESSED;
// 設(shè)置按下狀態(tài)
setPressed(true, x, y);
// 檢測(cè)長(zhǎng)按事件
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
當(dāng)觸發(fā)了tap事件,首先取消標(biāo)記,表示tap事件已經(jīng)執(zhí)行。然后,既然已經(jīng)發(fā)生了點(diǎn)擊事件,那么自然要設(shè)置按下狀態(tài)。最后由于tap事件是在長(zhǎng)按事件之前觸發(fā),那么當(dāng)tap事件觸發(fā)后,自然要去檢測(cè)長(zhǎng)按事件是否觸發(fā)。
我們剛剛說到,tap事件觸發(fā)的條件是,在滾動(dòng)容器中,從手指按下到抬起的時(shí)間要過100ms。那么如果在100ms之前抬起了手指,那么會(huì)怎么處理呢,我們來看下ACTION_UP的處理邏輯
case MotionEvent.ACTION_UP:
// 判斷tap動(dòng)作是否已經(jīng)完成
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
// 如果是按下狀態(tài)或者還沒有觸發(fā)tap動(dòng)作
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 1. 如果還沒有觸發(fā)tap動(dòng)作,就設(shè)置按下狀態(tài)
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除長(zhǎng)按回調(diào)
removeLongPressCallback();
// 2. 執(zhí)行點(diǎn)擊事件
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
// 3. 移除tap回調(diào)
removeTapCallback();
}
break;
prepressed為true表示沒有執(zhí)行tap事件,那么當(dāng)檢測(cè)到手指抬起時(shí),先設(shè)置按下狀態(tài)。如果連tap都沒執(zhí)行,肯定也不會(huì)執(zhí)行長(zhǎng)按事件,因此只會(huì)執(zhí)行點(diǎn)擊事件。最后,移除長(zhǎng)按回調(diào),這樣tap事件就不會(huì)再觸發(fā)。
如果tap事件執(zhí)行了呢?只有一點(diǎn)差別,將會(huì)在第二步,根據(jù)是否執(zhí)行了長(zhǎng)按來決定是否執(zhí)行點(diǎn)擊事件。
總結(jié)
通過本文的分析,我們可以清楚的知道View如何處理父View傳遞過來的事件,也可以清楚知道View在什么時(shí)候處理事件,什么時(shí)候不處理事件。
另外,本文也對(duì)View.onTouchEvent()作出分析,我們可以清楚知道View如何處理點(diǎn)擊事件,如何處理長(zhǎng)按事件,如何處理狀態(tài)改變,以及如何處理tap事件。
以上就是Android事件分發(fā)之View事件處理關(guān)鍵及示例分析的詳細(xì)內(nèi)容,更多關(guān)于Android事件分發(fā)View事件處理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義view實(shí)現(xiàn)側(cè)滑欄詳解
之前一直沒有寫側(cè)滑菜單的實(shí)現(xiàn)方法,今天計(jì)劃補(bǔ)上。手機(jī)開發(fā)中,往往存在很多功能沒處放的問題。我們可能會(huì)把功能放入一個(gè)菜單列表,但現(xiàn)在一種流行的做法是側(cè)滑菜單2022-11-11
基于Android實(shí)現(xiàn)個(gè)性彩色好看的二維碼
二維碼在我們?nèi)粘I钪袩o處不在,今天小編通過本教程給大家介紹基于Android實(shí)現(xiàn)個(gè)性彩色好看的二維碼,需要的朋友參考下吧2016-02-02
Android中CountDownTimer倒計(jì)時(shí)器用法實(shí)例
這篇文章主要介紹了Android中CountDownTimer倒計(jì)時(shí)器用法,以實(shí)例形式分析了Android中CountDownTimer類的相關(guān)使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
Java4Android開發(fā)教程(四)java的變量
Java 編程語言定義了如下類型的變量 :實(shí)例變量 (非靜態(tài)字段) 、類變量 (靜態(tài)字段)、局部變量以及參數(shù)。今天我們就來探討下Java變量2014-10-10
android textview設(shè)置字體的行距和字間距
這篇文章主要介紹了android textview設(shè)置字體的行距和字間距的方法,非常簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下2016-05-05
Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果(九)
這篇文章主要為大家詳細(xì)介紹了Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果的第九篇,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Java Base64位編碼與String字符串的相互轉(zhuǎn)換,Base64與Bitmap的相互轉(zhuǎn)換實(shí)例代碼
這篇文章主要介紹了Java Base64位編碼與String字符串的相互轉(zhuǎn)換,Base64與Bitmap的相互轉(zhuǎn)換實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03

