編輯:關於Android編程
PS一句:最終還是選擇CSDN來整理發表這幾年的知識點,該文章平行遷移到CSDN。因為CSDN也支持MarkDown語法了,牛逼啊!
最近在簡書和微博還有Q群看見很多人說Android自定義控件(View/ViewGroup)如何學習?為啥那麼難?其實答案很簡單:“基礎不牢,地動山搖。”
不扯蛋了,進入正題。就算你不自定義控件,你也必須要了解Android控件的觸摸屏事件傳遞機制(之所以說觸摸屏是因為該系列以觸摸屏的事件機制分析為主,對於類似TV設備等的物理事件機制的分析雷同但有區別。哈哈,誰讓我之前是做Android TV BOX的,悲催!),只有這樣才能將你的控件事件運用的如魚得水。接下來的控件觸摸屏事件傳遞機制分析依據Android 5.1.1源碼(API 22)。
從一個例子分析說起吧。如下是一個很簡單不過的Android實例:
public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {
private LinearLayout mLayout;
private Button mButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLayout = (LinearLayout) this.findViewById(R.id.mylayout);
mButton = (Button) this.findViewById(R.id.my_btn);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(null, OnTouchListener--onTouch-- action=+event.getAction()+ --+v);
return false;
}
@Override
public void onClick(View v) {
Log.i(null, OnClickListener--onClick--+v);
}
}
如上代碼很簡單,但凡學過幾天Android的人都能看懂吧。Activity中有一個LinearLayout(ViewGroup的子類,ViewGroup是View的子類)布局,布局中包含一個按鈕(View的子類);然後分別對這兩個控件設置了Touch與Click的監聽事件,具體運行結果如下:
當穩穩的點擊Button時打印如下: @Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(null, OnTouchListener--onTouch-- action=+event.getAction()+ --+v);
return true;
}
再次點擊Button結果如下:
看見了吧,如果onTouch返回true則onClick不會被調運了。
好了,經過這個簡單的實例驗證你可以總結發現:
Android控件的Listener事件觸發順序是先觸發onTouch,其次onClick。 如果控件的onTouch返回true將會阻止事件繼續傳遞,返回false事件會繼續傳遞。對於伸手黨碼農來說其實到這足矣應付常規的App事件監聽處理使用開發了,但是對於復雜的事件監聽處理或者想自定義控件的碼農來說這才是剛剛開始,只是個熱身。既然這樣那就繼續喽。。。
其實Android源碼無論哪個版本對於觸摸屏事件的傳遞機制都類似,這裡只是選用了目前最新版本的源碼來分析而已。分析Android View事件傳遞機制之前有必要先看下源碼的一些關系,如下是幾個繼承關系圖:
怎麼樣?看了官方這個繼承圖是不是明白了上面例子中說的LinearLayout是ViewGroup的子類,ViewGroup是View的子類,Button是View的子類關系呢?其實,在Android中所有的控件無非都是ViewGroup或者View的子類,說高尚點就是所有控件都是View的子類。
這裡通過繼承關系是說明一切控件都是View,同時View與ViewGroup又存在一些區別,所以該模塊才只單單先分析View觸摸屏事件傳遞機制。
在Android中你只要觸摸控件首先都會觸發控件的dispatchTouchEvent方法(其實這個方法一般都沒在具體的控件類中,而在他的父類View中),所以我們先來看下View的dispatchTouchEvent方法,如下:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
dispatchTouchEvent的代碼有點長,咱們看重點就可以。前面都是設置一些標記和處理input與手勢等傳遞,到24行的if (onFilterTouchEventForSecurity(event))
語句判斷當前View是否沒被遮住等,接著26行定義ListenerInfo局部變量,ListenerInfo是View的靜態內部類,用來定義一堆關於View的XXXListener等方法;接著if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
語句就是重點,首先li對象自然不會為null,li.mOnTouchListener呢?你會發現ListenerInfo的mOnTouchListener成員是在哪兒賦值的呢?怎麼確認他是不是null呢?通過在View類裡搜索可以看到:
/**
* 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;
}
li.mOnTouchListener是不是null取決於控件(View)是否設置setOnTouchListener監聽,在上面的實例中我們是設置過Button的setOnTouchListener方法的,所以也不為null;接著通過位與運算確定控件(View)是不是ENABLED 的,默認控件都是ENABLED 的;接著判斷onTouch的返回值是不是true。通過如上判斷之後如果都為true則設置默認為false的result為true,那麼接下來的if (!result && onTouchEvent(event))
就不會執行,最終dispatchTouchEvent也會返回true。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
語句有一個為false則if (!result && onTouchEvent(event))
就會執行,如果onTouchEvent(event)返回false則dispatchTouchEvent返回false,否則返回true。
這下再看前面的實例部分明白了吧?控件觸摸就會調運dispatchTouchEvent方法,而在dispatchTouchEvent中先執行的是onTouch方法,所以驗證了實例結論總結中的onTouch優先於onClick執行道理。如果控件是ENABLE且在onTouch方法裡返回了true則dispatchTouchEvent方法也返回true,不會再繼續往下執行;反之,onTouch返回false則會繼續向下執行onTouchEvent方法,且dispatchTouchEvent的返回值與onTouchEvent返回值相同。
所以依據這個結論和上面實例打印結果你指定已經大膽猜測認為onClick一定與onTouchEvent有關系?是不是呢?先告訴你,是的。下面我們會分析。
在View的觸摸屏傳遞機制中通過分析dispatchTouchEvent方法源碼我們會得出如下基本結論:
觸摸控件(View)首先執行dispatchTouchEvent方法。 在dispatchTouchEvent方法中先執行onTouch方法,後執行onClick方法(onClick方法在onTouchEvent中執行,下面會分析)。 如果控件(View)的onTouch返回false或者mOnTouchListener為null(控件沒有設置setOnTouchListener方法)或者控件不是enable的情況下會調運onTouchEvent,dispatchTouchEvent返回值與onTouchEvent返回一樣。 如果控件不是enable的設置了onTouch方法也不會執行,只能通過重寫控件的onTouchEvent方法處理(上面已經處理分析了),dispatchTouchEvent返回值與onTouchEvent返回一樣。 如果控件(View)是enable且onTouch返回true情況下,dispatchTouchEvent直接返回true,不會調用onTouchEvent方法。上面說了onClick一定與onTouchEvent有關系,那麼接下來就分析分析dispatchTouchEvent方法中的onTouchEvent方法。
上面說了dispatchTouchEvent方法中如果onTouch返回false或者mOnTouchListener為null(控件沒有設置setOnTouchListener方法)或者控件不是enable的情況下會調運onTouchEvent,所以接著看就知道了,如下:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
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 & 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) {
// 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();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
我勒個去!一個方法比一個方法代碼多。好吧,那咱們繼續只挑重點來說明呗。
首先地6到14行可以看出,如果控件(View)是disenable狀態,同時是可以clickable的則onTouchEvent直接消費事件返回true,反之如果控件(View)是disenable狀態,同時是disclickable的則onTouchEvent直接false。多說一句,關於控件的enable或者clickable屬性可以通過java或者xml直接設置,每個view都有這些屬性。
接著22行可以看見,如果一個控件是enable且disclickable則onTouchEvent直接返回false了;反之,如果一個控件是enable且clickable則繼續進入過於一個event的switch判斷中,然後最終onTouchEvent都返回了true。switch的ACTION_DOWN與ACTION_MOVE都進行了一些必要的設置與置位,接著到手抬起來ACTION_UP時你會發現,首先判斷了是否按下過,同時是不是可以得到焦點,然後嘗試獲取焦點,然後判斷如果不是longPressed則通過post在UI Thread中執行一個PerformClick的Runnable,也就是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;
}
這個方法也是先定義一個ListenerInfo的變量然後賦值,接著判斷li.mOnClickListener是不是為null,決定執行不執行onClick。你指定現在已經很機智了,和onTouch一樣,搜一下mOnClickListener在哪賦值的呗,結果發現:
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
看見了吧!控件只要監聽了onClick方法則mOnClickListener就不為null,而且有意思的是如果調運setOnClickListener方法設置監聽且控件是disclickable的情況下默認會幫設置為clickable。
我勒個去!!!驚訝吧!!!猜的沒錯onClick就在onTouchEvent中執行的,而且是在onTouchEvent的ACTION_UP事件中執行的。
到此上面例子中關於Button點擊的各種打印的真實原因都找到了可靠的證據,也就是說View的觸摸屏事件傳遞機制其實也就這麼回事。
其實上面分析完View的觸摸傳遞機制之後已經足夠用了。如下的實例驗證可以說是加深閱讀源碼的理解,還有一個主要作用就是為將來自定義控件打下堅實基礎。因為自定義控件中時常會與這幾個方法打交道。
我們自定義一個Button(Button實質繼承自View),如下:
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, dispatchTouchEvent-- action= + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, onTouchEvent-- action=+event.getAction());
return super.onTouchEvent(event);
}
}
其他代碼如下:
public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {
private LinearLayout mLayout;
private TestButton mButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLayout = (LinearLayout) this.findViewById(R.id.mylayout);
mButton = (TestButton) this.findViewById(R.id.my_btn);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(null, OnTouchListener--onTouch-- action=+event.getAction()+ --+v);
return false;
}
@Override
public void onClick(View v) {
Log.i(null, OnClickListener--onClick--+v);
}
}
其實這段代碼只是對上面例子中的Button換為了自定義Button而已。
可以發現,如上打印完全符合源碼分析結果,dispatchTouchEvent方法先派發down事件,完事調運onTouch,完事調運onTouchEvent返回true,同時dispatchTouchEvent返回true,然後dispatchTouchEvent繼續派發move或者up事件,循環,直到onTouchEvent處理up事件時調運onClick事件,完事返回true,同時dispatchTouchEvent返回true;一次完整的View事件派發流程結束。
將TestButton類的onTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, onTouchEvent-- action=+event.getAction());
return true;
}
點擊Button打印如下:
可以發現,當自定義了控件(View)的onTouchEvent直接返回true而不調運super方法時,事件派發機制如同4.2.1類似,只是最後up事件沒有觸發onClick而已(因為沒有調用super)。
所以可想而知,如果TestButton類的onTouchEvent修改為如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, onTouchEvent-- action=+event.getAction());
super.onTouchEvent(event);
return true;
}
點擊Button如下:
整個派發機制和4.2.1完全類似。
將TestButton類的onTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, onTouchEvent-- action=+event.getAction());
return false;
}
點擊Button如下:
你會發現如果onTouchEvent返回false(也即dispatchTouchEvent一旦返回false將不再繼續派發其他action,立即停止派發),這裡只派發了down事件。至於後面觸發了LinearLayout的touch與click事件我們這裡不做關注,下一篇博客會詳細解釋為啥(其實你可以想下的,LinearLayout是ViewGroup的子類,你懂的),這裡你只用知道View的onTouchEvent返回false會阻止繼續派發事件。
同理修改如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, onTouchEvent-- action=+event.getAction());
super.onTouchEvent(event);
return false;
}
點擊Button如下:
將TestButton類的dispatchTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, dispatchTouchEvent-- action= + event.getAction());
return true;
}
點擊Button如下:
你會發現如果dispatchTouchEvent直接返回true且不調運super任何事件都得不到觸發。
繼續修改如下呢?
將TestButton類的dispatchTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, dispatchTouchEvent-- action= + event.getAction());
super.dispatchTouchEvent(event);
return true;
}
點擊Button如下:
可以發現所有事件都可以得到正常派發,和4.2.1類似。
將TestButton類的dispatchTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, dispatchTouchEvent-- action= + event.getAction());
return false;
}
點擊Button如下:
你會發現事件不進行任何繼續觸發,關於點擊Button觸發了LinearLayout的事件暫時不用關注,下篇詳解。
繼續修改如下呢?
將TestButton類的dispatchTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, dispatchTouchEvent-- action= + event.getAction());
super.dispatchTouchEvent(event);
return false;
}
點擊Button如下:
你會發現結果和4.2.3的第二部分結果一樣,也就是說如果dispatchTouchEvent返回false事件將不再繼續派發下一次。
修改dispatchTouchEvent返回值為true,onTouchEvent為false:
將TestButton類的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, dispatchTouchEvent-- action= + event.getAction());
super.dispatchTouchEvent(event);
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, onTouchEvent-- action= + event.getAction());
super.onTouchEvent(event);
return false;
}
點擊Button如下:
修改dispatchTouchEvent返回值為false,onTouchEvent為true:
將TestButton類的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基礎代碼保持不變:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(null, dispatchTouchEvent-- action= + event.getAction());
super.dispatchTouchEvent(event);
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(null, onTouchEvent-- action= + event.getAction());
super.onTouchEvent(event);
return true;
}
點擊Button如下:
由此對比得出結論,dispatchTouchEvent事件派發是傳遞的,如果返回值為false將停止下次事件派發,如果返回true將繼續下次派發。譬如,當前派發down事件,如果返回true則繼續派發up,如果返回false派發完down就停止了。
這個例子組合了很多種情況的值去驗證上面源碼的分析,同時也為自定義控件打下了基礎。仔細理解這個例子對於View的事件傳遞就差不多了。
上面例子也測試了,源碼也分析了,總得有個最終結論方便平時寫代碼作為參考依據呀,不能每次都再去分析一遍源碼,那得多蛋疼呢!
綜合得出Android View的觸摸屏事件傳遞機制有如下特征:
觸摸控件(View)首先執行dispatchTouchEvent方法。 在dispatchTouchEvent方法中先執行onTouch方法,後執行onClick方法(onClick方法在onTouchEvent中執行,下面會分析)。 如果控件(View)的onTouch返回false或者mOnTouchListener為null(控件沒有設置setOnTouchListener方法)或者控件不是enable的情況下會調運onTouchEvent,dispatchTouchEvent返回值與onTouchEvent返回一樣。 如果控件不是enable的設置了onTouch方法也不會執行,只能通過重寫控件的onTouchEvent方法處理(上面已經處理分析了),dispatchTouchEvent返回值與onTouchEvent返回一樣。 如果控件(View)是enable且onTouch返回true情況下,dispatchTouchEvent直接返回true,不會調用onTouchEvent方法。 當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發下一個action(也就是說dispatchTouchEvent返回true才會進行下一次action派發)。
帶觸控的圖表類,後期會把這個功能類,添加到這個框架裡:效果如下public class BaseFundChartView extends View implements
TimePickerDialog(時間選擇對話框) 創建TimePickerDialog時間選擇對話框: 1.創建一個類繼承DialogFra
仿360安全衛士懸浮窗雖然360安全衛士很流氓,但是我相信安裝的人不在少數,它有一個讓人很憂傷的功能,就是即時你關閉了360安全衛士,你手機的左邊距或者右邊距會有一個蟲蟲
今天隨便逛逛CSDN,看到主頁上推薦了一篇文章Android 快速開發系列 打造萬能的ListView GridView 適配器,剛好這兩天寫項目自己也封裝了類似的Com