編輯:關於Android編程
對於Android事件分發機制,我們在開發的過程中,肯定曾經遇到在最外層添加了ScrollView之後ListView無法正常滑動、我們的圖片輪播在左右滑動圖片為什麼感覺很難控制。這些都是我們用戶在屏幕上進行交互的一系列操作,因此深入了解Android事件分發機制是非常的重要。
事件分發的概念
所謂點擊事件的事件分發,就是當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View(ViewGroup也繼承於View),這個傳遞的過程就叫做分發過程。Android的觸摸事件分發傳遞過程中,最重要的是可以分為:
Activity
dispatchTouchEvent(MotionEvent) //事件分開 onTouchEvent(MotionEvent) //事件處理ViewGroup
dispatchTouchEvent(MotionEvent) onInterceptTouchEvent(MotionEvent) //事件攔截 onTouchEvent(MotionEvent)View
dispatchTouchEvent(MotionEvent) onTouchEvent(MotionEvent)然而在用戶點下屏幕之後,我們通過下面這張圖對觸摸事件要有一個整體的了解。

示例
大家在有了整體了解之後,我們今次主要分析的是View的事件分發。
我們通過下面簡單的代碼來了解一下。我們自定以一個CustomButton繼承Button,然後把跟View的事件傳播有關的方法進行復寫,然後再Log打印下。
CustomButton:我們重寫一下onTouchEvent和dispatchTouchEvent方法並打印。
public class CustomButton extends Button {
private static final String TAG = "CustomButton";
public CustomButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "onTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
}
簡單看一下我們的布局文件:
最後是我們的MainActivity代碼:
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private CustomButton mbtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mbtn = (CustomButton) findViewById(R.id.button);
mbtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick Event");
}
});
mbtn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
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:
break;
}
return false;
}
});
}
}
好了上面就是大致的代碼,我們接著編譯運行一下,看看打印的結果:
10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_DOWN 10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_DOWN 10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_DOWN 10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_MOVE 10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_MOVE 10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_MOVE 10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_UP 10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_UP 10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_UP 10-27 14:03:05.326 14668-14668/com.example.pc.myapplication I/MainActivity: onClick EVENT
從打印結果來看,無論是DOWN,MOVE,UP的動作,執行的步驟都是
1. dispatchTouchEvent
2. setOnTouchListener的onTouch
3. onTouchEvent
分析
那我們就根據打印來看一下dispatchTouchEvent的源碼。我們在CustomButton類中按CTRL+O,搜索dispatchTouchEvent方法,你會發現dispatchTouchEvent()是直接指向View類中的dispatchTouchEvent(),這是因為Button類沒有覆寫dispatchTouchEvent(),Button類繼承TextView類,而TextView類也沒有覆寫dispatchTouchEvent(),最後是TextView類繼承View類,所以我們CustomButton覆寫的dispatchTouchEvent()直接指向它的父類View中。(可能有同學產生疑問雖然是先執行dispatchTouchEvent(),但是為什麼呢?還會有Activity–>ViewGroup–>View這樣分發下來,那麼Activity又是怎麼執行到dispatchTouchEvent()的呢?這部分的問題不是我們這篇討論分析的范圍,我們先定性理解先執行dispatchTouchEvent()。)
public boolean dispatchTouchEvent(MotionEvent event) {
···
boolean result = false;
···
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
···
return result;
}
我們直接看重點部分,判斷if(onFilterTouchEventForSecurity(event)),這個主要是判斷當前事件到來的時候,窗口有沒有被遮擋,如果被遮擋則會直接返回false。接著是將mListenerInfo賦給ListenerInfo的li對象,ListenerInfo是什麼呢?
static class ListenerInfo {
···
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
protected OnContextClickListener mOnContextClickListener;
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
···
}
是我們平時用到的一些監聽,然而mListenerInfo又是從哪裡賦值的?繼承找
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
從給出的注釋就可以理解得到,當我們在視圖中設置一個setOnTouchListener(),那麼就會注冊一個回調函數調用。
接著往下有四個條件判斷:li != null ; li.mOnTouchListener != null; (mViewFlags & ENABLED_MASK) == ENABLED; li.mOnTouchListener.onTouch(this, event)
第一個li != null,如果我們設置了setOnTouchListener(),在ListenerInfo li = mListenerInfo;中就會被賦值。
第二個li.mOnTouchListener != null,在setOnTouchListener()源碼中可以看到賦值。不為空。
第三個(mViewFlags & ENABLED_MASK) == ENABLED,是判斷當前點擊的控件是否是enable的,按鈕默認都是enable的,因此這個條件恆定為true。
第四個,mOnTouchListener.onTouch(this, event),其實也就是去回調控件注冊touch事件時的onTouch方法的返回值,默認值是返回false。
上面的四個判斷條件都成立整個方法賦值返回true。有一個不成立都會賦值返回false。繼續接下下面的判斷。!result && onTouchEvent(event)。
第一個根據上面的判斷返回值result,如果result為false才會接著執行onTouchEvent(event)。
小總結:上面的源碼分析,證明了之前的打印信息順序。先執行dispatchTouchEvent(),然後執行mOnTouchListener.onTouch(this, event)的回調返回,滿足條件之後再調用onTouchEvent()。
我們緊接著看一下onTouchEvent(event)源碼:
由於源碼較長,這裡分段來講述。
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);
}
如果當前View是Disabled不可用狀態狀態且是可點擊則會消費掉事件(return true);
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
如果設置了mTouchDelegate,則會將事件交給代理者處理,直接return true;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
···
return true;
}
return false;
在一開始的判斷滿足其中一個情況就返回true,否則返回false。接下就是我們事件分發的重點。
MotionEvent.ACTION_DOWN
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
設置mHasPerformedLongPress=false;表示長按事件還未觸發;
isInScrollingContainer判斷上一層結構,判斷是否在一個滾動的容器中;
如果在滾動容器中會做一個短延遲,區分滾動還是長按,接著最終的方法都是差不多,記錄橫縱坐標和檢測長按監聽。
MotionEvent.ACTION_MOVE
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;
檢測橫縱坐標的變化傳遞給畫板或者子視圖;
判斷觸摸點有沒有移出我們的View,如果移出了:
執行removeTapCallback();
然後判斷是否包含PRESSED標識,如果包含,移除長按的檢查:removeLongPressCallback();
其實ACTION_DOWN與ACTION_MOVE都進行了一些必要的設置與置位,重點是ACTION_UP。
MotionEvent.ACTION_UP
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;
首先判斷了是否被按下 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;接下來判斷是不是可以獲得焦點,同時嘗試去獲取焦點;再處理點擊下顯示效果;清除長按回調。經過上述的種種判斷之後我們重點看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;
}
終於到看到li.mOnClickListener.onClick(),這裡檢測了當前View是否設置了onClickListener,如果設置了那麼回調它的onClick方法,所以驗證了我們一開始打印的數據,onClick()在onTouch()的後面執行,因為onClick()方法是在onTouchEvent內部被調用的。
接下來的是我們處理完performClick()後的一些狀態標識、狀態的改變和回調的操作。
總結:經過一系列的源碼,我們了解到dispatchTouchEvent()、onTouch()、onTouchEvent()。在View的dispatchTouchEvent中調用onTouch()和onTouchEvent(),onTouch優先於onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點擊的控件必須是enable的(ImageView、TextView非enable)。因此如果你有一個控件是非enable的,那麼給它注冊onTouch事件將永遠得不到執行。
但是,我們上述說的只是事件分發的事件流向分發部分,我們將在下一篇事件分發之ViewGroup篇來理解事件分發另一個重點————事件攔截。
學習理解Android菜單Menu操作
今天看了pro android 3中menu這一章,對Android的整個menu體系有了進一步的了解,故整理下筆記與大家分享。PS:強烈推薦《Pro Android 3
用原生VideoView進行全屏播放時的問題
之前參加了一個課程,裡面有一節講到了用視頻作為啟動界面。講師用的是自定義VideoView,重寫onMeasure方法,因為原生的VideoView在那情況下不能實現全屏
Android Multimedia框架總結(六)C++中MediaPlayer的C/S架構
前面幾節中,都是通過java層調用到jni中,jni向下到c++層並未介紹看下Java層一個方法在c++層 MediaPlayer後續過程frameworks/av/me
談談Material Design之CoordinatorLayout
本文主要介紹一下如何使用CoordinatorLayout先看看官方是怎麼介紹Material Design的 We challenged ourselves to cr