編輯:關於Android編程
已經有一段事件沒有寫博客了,由於最近一段時間比較忙,在趕項目,而且也有一些迷茫,不知道該寫些什麼好,正好最近遇到了一個自定義控件的事件沖突問題,所以就再網上找了很多關於事件傳遞和事件沖突的相關解決方案再結合Android系統源代碼自己了解了一下,覺得有必要寫一些東西來記錄一下,以免過一段時間忘記了而又要到網上找很多資料自己重新總結。
我遇到的項目問題是這樣的。一個自定義的下拉刷新控件,裡面包含一個ScrollView,在Scrollview的頂部有一個viewpager,下面還有其他內容,在對viewpager進行左右滑動的時候,如果是水平的滑動不會有任何問題,但是如果傾斜(就是水平有滑動距離,垂直也有滑動距離的話),下拉刷新控件的下拉頭會出現 而導致給人一種viewpager滑動不靈敏的感覺。開始沒有想象到在下拉刷新裡面做水平滑動距離和垂直滑動距離的比較的判斷,來確定是應該由子控件(viewpager)響應滑動事件還是由下拉刷新控件響應滑動事件,在網上找了很多都嘗試了,可是嘗試過還是不好使,因為我們的下拉刷新控件的滑動事件的處理邏輯全部寫在方法dispatchTouchEvent中,他是觸摸事件最開始走的地方,因此他始終會執行的,才會根據具體情況決定是否傳遞給子控件。好了不扯這麼長了,下面來講講今天的要將的主要內容吧。
在網上傳遞著的關於Android的事件傳遞的文章真的可以說是泛濫成災了,千篇一律的都是什麼返回true自己消費,返回false傳遞給子view去消費,如果子view消費不了,又再回傳給父view處理,最後父view到底是如何處理能不能夠處理的了,這就跟我子view沒得半毛錢的關系了。呵呵,其中有一些文章可能是作者真正嘗試做過實驗而得出的結論,而有一些就是看了別人的文章覺得很吊,自己有改改,沒試驗,就成自己的東西了。說道這裡,你們的記憶中關於事件傳遞的東西是不是也都是這樣的。這些東西有時候可能解決我們的問題,但是有時候你按照他們的思路去寫你的代碼,並不能解決問題,這說明他們也可能存在一些問題,至少我是這麼認為的,哈哈,我的東西我也不能保證全對。所以有些問題還是得自己去發現問題,解決問題,回歸根本(Android源代碼)。這樣才會做到有理有據。在開發中才能游刃有余。同時,事件傳遞在我們的開發中也非常重要,當你看到別人一個很nb,非常炫酷的自定義控件,他們絕逼處理過相關的事件傳遞或者事件沖突的問題。
先看一段很簡單的xml文件,不做多余的解釋:
在MainActivity中我們重寫了Activity的dispatchTouchEvent和onTouchEvent
public class MainActivity extends Activity { private TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.tv); // tv.setOnTouchListener(new OnTouchListener() { // // @Override // public boolean onTouch(View v, MotionEvent event) { // boolean flag = true; // switch (event.getAction()) { // case MotionEvent.ACTION_DOWN: // flag = false; // System.out.println("downdowndowndowndowndowndowndowndowndowndowndowndown"); // break; // case MotionEvent.ACTION_MOVE: // flag = false; // System.out.println("movemovemovemovemovemovemovemovemovemovemovemovemove"); // break; // case MotionEvent.ACTION_UP: // flag = false; // System.out.println("upupupupupupupupupupupupupupupupupupupupupupupupupupup"); // break; // default: // flag = false; // // break; // } // return false; // } // }); // tv.setOnClickListener(new OnClickListener() { // // @Override // public void onClick(View v) { // // TODO Auto-generated method stub // Toast.makeText(MainActivity.this, "onClick點擊事件觸發", 1000).show(); // } // }); // tv.setOnLongClickListener(new OnLongClickListener() { // // @Override // public boolean onLongClick(View v) { // System.out.println("onLongClick點擊事件觸發"); // return false; // } // }); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean flag = false; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: flag = false; System.out.println("dispatchTouchEvent===down"); break; case MotionEvent.ACTION_MOVE: flag = false; System.out.println("dispatchTouchEvent==move"); break; case MotionEvent.ACTION_UP: flag = false; System.out.println("dispatchTouchEvent==up"); break; default: flag = false; break; } // super.dispatchTouchEvent(ev); // return super.dispatchTouchEvent(ev); return flag; } @Override public boolean onTouchEvent(MotionEvent event) { boolean flag = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: flag = false; System.out.println("onTouchEventt===down"); break; case MotionEvent.ACTION_MOVE: flag = true; System.out.println("onTouchEvent==move"); break; case MotionEvent.ACTION_UP: flag = false; System.out.println("onTouchEvent==up"); break; default: flag = false; break; } return super.onTouchEvent(event); } }分別看看下面三張截圖的運行結果再說話:
其實 並不如大家所說的return true或者return false;會怎麼樣,當我們重寫了dispatchTouchEvent的時候,我們會發現無論他是return true或者return false本身的onTouchEvent都不會執行,只有我們執行了super.dispatchTouchEvent(在return之前調用將事件傳遞給父控件)或者 return dispatchTouchEvent(ev);才會執行到onTouchEvent方法中,這是為什麼呢,當我們執行返回true或者false的時候就不會調用父類或者父控件的dispatchTouchEvent方法了,而在Android系統中關於Activity中dispatchTouchEvent的方法是這樣的:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }看源碼的執行順序 ,如果我們重寫了dispatchTouchEvent但是沒有執行父類的dispatchTouchEvent,所有我們重寫的onTouchEvent方法也不會執行,所有會有我們的圖二的執行結果,如果我們仿照Android源碼的寫法直接寫return onTouchEvent(ev);也會達到我們想要的執行我們自己重寫的onTouchEvent方法,或者在我們return之前直接調用父類的也即super.dispatchTouchEvent方法也會達到我們即在此消費了事件但是我也傳遞給子控件了額。我們的下拉刷新就是這麼干的。至此再也不要有那種慣性思維return true消費不傳遞,return false不消費 往下傳遞。我們可以追根溯源找到我麼自己想要的答案,還可以得到多種可執行onTouchEvent方法。
說了這麼久,感覺只是糾正了大家的一種思維的慣性錯誤。我現在就通過ViewGroup和View源碼中關於dispatchTouchEvent , onIntecptTouchEvent ,onTouchEvent方法看看事件的傳遞順序以及onLongClick ,onClick ,onTouch() ,onTouchEvent方法的執行順序 。
先看看ViewGroup中關於dispatchTouchEvent的源碼是怎麼寫的:
public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // If the event targets the accessibility focused view and this is it, start // normal event dispatch. Maybe a descendant is what will handle the click. //ev 是否設置 FLAG_TARGET_ACCESSIBILITY_FOCUS這個 if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. /* * ACTION_DOWN重新設置狀態,給mFirstTouchTarget中的target發生cancel事件, * 清空mFirstTouchTarget列表 * 清空mGroupFlags的FLAG_DISALLOW_INTERCEPT標志 * mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; */ cancelAndClearTouchTargets(ev); resetTouchState(); } // Check for interception. final boolean intercepted; /*在 ACTION_DOWN 和 mFirstTouchTarget不為空的時候就會去intercept * 在ACTION_MOVE事件的時候,雖然有子view處理事件序列了, * 但是viewgroup還是有機會插手事件序列 * */ if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //是否不允許intercept 覺得參數和requestDisallowInterceptTouchEvent(true);是否非常之相似啊 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { //允許intercept intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { //這種情況比較少 // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } // If intercepted, start normal event dispatch. Also if there is already // a view that is handling the gesture, do normal event dispatch. if (intercepted || mFirstTouchTarget != null) { //取消FLAG_TARGET_ACCESSIBILITY_FOCUS ev.setTargetAccessibilityFocus(false); } // Check for cancelation. //取消PFLAG_CANCEL_NEXT_UP_EVENT標記,返回之前是否已經 PFLAG_CANCEL_NEXT_UP_EVENT final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { //遍歷子view for (int i = childrenCount - 1; i >= 0; i--) { final View child = children[i]; if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } //如果child的dispatchTouchEvent返回true,dispatchTransformedTouchEvent也會返回true,這時候條件就會成立, //newTouchTarget和alreadyDispatchedToNewTouchTarget就會被修改。這裡傳進去child,方法裡就會調用child.dispatchTouchEvent, //如果傳null就會調用super.dispatchTouchEvent if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = i; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //將處理了down事件的view的target添加到target list的頭部,此時newTouchTarget和mFirstTouchTarget是相等的 newTouchTarget = addTouchTarget(child, idBitsToAssign); //標記事件已經分發給子view alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } //如果都沒有沒找到,那麼用mFirstTouchTarget隊列中尾巴上的那個target來處理 if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // Dispatch to touch targets. //如果是ACTION_MOVE事件,上面的這個if 是沒有不會執行的,直接到這個if了 mFirstTouchTarget是一個全局變量,指向target list的第一個元素。 // Dispatch to touch targets. if (mFirstTouchTarget == null) { //進入這裡,說明前面down事件處理的子view返回false,導致沒有target被添加到list中,也就是這個事件沒有view認領。 // No touch targets so treat this as an ordinary view. //這裡有個參數傳了null,方法裡面會判斷這個參數,如果為null就調用super.dispatchTouchEvent,也就是自己來處理event。 //由於down後面的事件都沒法修改mFirstTouchTarget,所以之後的事件都在這裡執行了,該子view就沒法接收到後面的事件了 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { //能進入這裡,說明子view在處理down事件後返回了true。後面的move和up事件會直接進入這裡 // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { //這裡遍歷這個target list,挨個分發事件,為了方便理解,可以暫時認為list裡面此時就只包含一個target的,也就是當前處理事件的view的target final TouchTarget next = target.next; //如果是down事件的話,就會進入這個if裡面,由於down事件在前面已經處理了,所以直接handled = true。 //因為如果是move和up的話alreadyDispatchedToNewTouchTarget是false,newTouchTarget是null。 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { //handled會被返回到當前Activity的dispatchTouchEvent中,具體在Activity中怎麼使用可以查看其源碼 handled = true; } else { //move和up事件會進入這裡 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //看這裡,由此可知在move的時候返回true或者false只會影響到layout返回給Activity的值,由於不是down事件所以不會影響up事件的獲取。 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // Update list of touch targets for pointer up or cancel, if needed. if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { //取消或者所有手指都起開 resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { //值起開一個手指 final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }在代碼71-72行,就是看這兩個變量newTouchTarget和alreadyDispatchedToNewTouchTarget在過了這段代碼後的值有沒有改變,在down的時候可以進入這裡面,所以如果子view的dispatchTouchEvent如果返回false的話該子view的target就不會被添加到target list中了,兩個變量的值沒有被改變。但在move和up的時候就進不去這裡,所以在move和up事件時newTouchTarget是null的,alreadyDispatchedToNewTouchTarget是false。
代碼134行即以後解釋了具體的事件處理邏輯,事件就此也分發到子View當中去了。下面再來看View中的dispatchTouchEvent是怎麼處理的
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement //首先檢查一些相關條件 mOnTouchListener是否為空 也就是我們是否給view設置onTouchListener //在檢查listener是否給我們返回true如果三者條件都符合 函數直接返回 //不會再執行下面if條件了 更不會執行view的onTouchEvent方法了 //也即你寫在onTouchEvent中的代碼邏輯毫無意義了 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }從上面的代碼和解釋我們可以看出,當我們給View設置了onTouchListener的時候,重寫的onTouch要比onTouchEvent先執行,當然onTouchEvent還有可能不執行,它是否執行還得取決於onTouch是否返回false。如果返回false,onTouchEvent能夠執行,返回true,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) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) { mPrivateFlags &= ~PRESSED; refreshDrawableState(); } // 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 (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. mPrivateFlags |= PRESSED; refreshDrawableState(); } 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(); //執行click方法 } } } 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; //首先接收到ACTION_DOWN事件他是move,up事件的前提 //並且在Action_DOWN時候檢查onLongClick是否可執行是否可執行 case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; //默認onLongClick標記為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) { //判斷控件view是否可以滾動 mPrivateFlags |= PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away mPrivateFlags |= PRESSED; refreshDrawableState(); checkForLongClick(0); //檢查控件的onLongClick是否可執行 } 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 if (!pointInView(x, y, mTouchSlop)) { // 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; }我們首先看ACTION_DOWN事件,解釋上也說明,最後會檢查onLongClick事件是否可執行 我們看看checkForLongClick(0)方法是怎麼寫的:
private void checkForLongClick(int delayOffset) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { //檢查是否可長按 mHasPerformedLongPress = false; //是否執行長按設置為false if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); //通過handler延時來確定長按是否發生 //事件阈值ViewConfiguration.getLongPressTimeout() - delayOffset postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }我們可以看出最後是通過handler來處理的,我們看看CheckFoeLongPress這個runnable是怎麼實現的以及他的run方法裡面的內容
class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { //既做出了判斷 也執行了LongClick的事件 mHasPerformedLongPress = true; } } } public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; } } //performLongClick()源代碼 public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; if (mOnLongClickListener != null) { //這裡就是為什麼onLongClick方法會有返回值的原因 而這個值就決定這onClick方法是否執行了 //返回true的時候,事件就此消費完了,onClick方法就不再執行 了 //返回false的時候,onClick方法還可以繼續執行 handled = mOnLongClickListener.onLongClick(View.this); } if (!handled) { handled = showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }onLongClick的返回值直接決定著onClick方法是否能夠執行 繼續回到CheckForLongPress的run方法中 當if(pefromLongClick()) 為true的時候 也就是事件消費之後,mHasPerformedLongPress = true;這個mHasPerformedLongPress 我們在view的onTouchEvent的ACTION_UP事件之後會用到,再回到ACTION_UP分支當中,也即上面onTouchEvent代碼中的49行判斷mHasPerformedLongPress 只有當mHasPerformedLongPress值為false的時候 我們才會去移除longClick事件去執行performclick事件。這就是為什麼會在判斷完performLongClick()之後,如果返回true,就將mHasPerformedLongPress = true ,返回false的話mHasPerformedLongPress 就為默認值false了。這裡可以看出onLongClick在ACTION_DOWN事件之後判斷執行,而onClick方法在ACTION_UP方法之後 並且還需要判斷onLongClick的返回值才能決定是否能夠執行onClick方法了。
而此時我們如果細心的話還會發現在onTouchEvent方法中if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) 代碼塊執行完成後 一直是return true的,這也可以說明此時的onTouchEvent方法返回true這時候可以用慣性思維來考慮了,事件在此時此刻以及完全被消費的渣渣都不剩下了,所以,你在View(控件)響應了點擊事件之後,它不會將Touch這一類的事件再回傳遞給父類或者父控件了。也就是所有的事件在你響應手指點擊事件,手指ACTION_UP的時候全部宣告結束了額。
尼瑪,自己終於感覺把自己想要記錄的東西差不多寫完了額。好累額。不過感覺還不錯。還是來總結一下吧(父控件->子控件->父控件)
1.摒棄父控件(布局容器)的dispatchTouchEvent中return true 消費事件 return false 事件傳遞給父控件的onInterceptTouchEvent中進行攔截,由於此方法默認一直返回false,所以return false就傳遞給子控件的觀念。實際問題實際分析。
2.在觸摸事件當中ACTION_DOWN事件是重中之重,他是MOVE事件和UP事件的前提
3.同一個控件的onTouchListeren中的onTouch()方法要先於onTouchEvent()方法執行,只有當onTouchListeren中的onTouch()最後返回false的時候次控件的onTouchEvent()方法才會執行。
4.onTouchEvent先於onLongClick事件執行 ,而onLongClick事件先於onClick事件執行,onClick取決於onLongClick事件執行的返回值,當返回false的時候onClik方法才會執行,返回true的時候onClick事件不會執行。
5.所有的觸摸類事件在onLongClick或者onClick方法就執行結束了,不會在傳遞 了。
1. UIAutomatorViewer自動化測試是Android測試的趨勢, 穩定\復用, 最常用的工具就是Espresso.使用UIAutomatorViewer獲取
本文實例講述了Android編程實現支持拖動改變位置的圖片中疊加文字功能。分享給大家供大家參考,具體如下:之所以做了這麼一個Demo,是因為最近項目中有一個奇葩的需求:用
安卓系統的刷機包分線刷包和卡刷包兩種,一般後綴為ROM的是線刷包,卡刷包的後綴名是ZIP壓縮文件,本身ROM的格式上就有區別,那麼用刷機精靈的rom可以卡刷