編輯:關於Android編程
最近總是遇到關於Android Touch事件的問題,如:滑動沖突的問題,以前也花時間學習過Android Touch事件的傳遞機制,可以每次用起來的時候總是忘記了,索性自己總結一下寫篇文章避免以後忘記了,其實網上關於Touch事件的傳遞的文章真的很多,但是很少有系統性的,都是寫了一個簡單的demo運行了一下,對於我們了解Android Touch事件基本上沒有任何幫助。
今天我打算從源碼的角度來分析一下Touch事件的傳遞機制。在了解Touch事件之前,最好了解下Android中窗口的創建過程,這樣對於Android窗口的整體結構和事件的傳遞過程會了解更深。我就把事件的始點定在PhoneWindow中的DecorView吧,至於是誰把事件傳遞給DecorView的我們先不用去關心它。(如果想深入研究,請閱讀我的另外一篇文章Android中按鍵事件傳遞機制)我們只需要知道它的上家是通過dispatchTouchEvent方法將事件分發給DecorView就行了,我進入到該方法瞧瞧究竟。
在閱讀之前最好閱讀Android窗口創建過程
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //該Callback就是該DecorView附屬的Activity,可以看我的另外一篇文章《Android中窗口的創建過程》 final Callback cb = getCallback(); //如果cb!=null && mFeatureId<0 就執行Activity中的dispatchTouchEvent方法,對於應用程序窗口 //這兩個條件一般是滿足的 return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super .dispatchTouchEvent(ev); }在DecorView中事件通過dispatchTouchEvent方法被分發到了Activity中,相信Activity對於每個Android開發者都不會陌生吧,那我們就進入Activity的dispatchTouchEvent方法中。
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //getWindow返回什麼?如果閱讀過我的《Android中窗口創建過程》的都知道就是PhoneWindow,如果PhoneWindow中的superDispatchTouchEvent方法返回了true, //那麼該Touch事件就被PhoneWindow給消費掉了,不會再繼續傳遞,如果返回false,那麼就會執行Activity的onTouchEvent方法 if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }進入PhoneWindow中的superDispatchTouchEvent方法:
@Override public boolean superDispatchTouchEvent(MotionEvent event) { //mDecor是一個DecorView類型變量 return mDecor.superDispatchTouchEvent(event); }
public boolean superDispatchTouchEvent(MotionEvent event) { //直接調用父類的dispatchTouchEvent方法 return super.dispatchTouchEvent(event); }
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; //可以通過requestDisallowInterceptTouchEvent方法來設置該變量的值,通常是false boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { // this is weird, we got a pen down, but we thought it was // already down! // XXX: We should probably send an ACTION_UP to the current // target. mMotionTarget = null; } // If we're disallowing intercept or if we're allowing and we didn't // intercept //onInterceptTouchEvent在默認情況下是返回false的,所以這裡通常是可以進去的 if (disallowIntercept || !onInterceptTouchEvent(ev)) { // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN); // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; //遍歷ViewGroup的孩子,如果觸摸點在某一個子View中,則調用在子View的dispatchTouchEvent for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { // offset the event to the view's coordinate system final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; //調用了某一個子View 的dispatchTouchEvent ,如果這個子View 的dispatchTouchEvent返回true,那麼意味著這個事件 //已經被這個子View消費了,不會繼續傳遞 if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { // Note, we've already copied the previous state to our local // variable, so this takes effect on the next event mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; //對於一個Action_down事件,如果走到了這裡,說明所有的子View 都沒有消費掉這個事件,那麼它就調用父類的 //的dispatchTouchEvnet方法,ViewGroup的父類就是View if (target == null) { // We don't have a target, this means we're handling the // event as a regular view. ev.setLocation(xf, yf); if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } return super.dispatchTouchEvent(ev); } // if have a target, see if we're allowed to and want to intercept its // events if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL); ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // clear the target mMotionTarget = null; // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true; } if (isUpOrCancel) { mMotionTarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; } return target.dispatchTouchEvent(ev); }
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
對於任何一款Android應用,展現給用戶最上面的通常就是一個View,如Button,ImageView等等,也就是說一些觸摸事件最終都是傳遞給了這個控件,如果控件消費了這些事件,那麼就停止傳遞了,如果沒有消費,那麼就交給控件所屬ViewGroup的onTouchEvnet處理,我們就看看View的dispatchTouchEvent方法吧
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; //(A) if ((viewFlags & ENABLED_MASK) == DISABLED) { // 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; } } //(B) 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 (!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(); } //(C) if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { mPrivateFlags |= PRESSED; refreshDrawableState(); postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 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 int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // 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; } //(D) return true; } return false; }
0x00閱讀本文前,建議讀者首先閱讀Android加殼原理,參考文章Android中的Apk的加固(加殼)原理解析和實現。如果沒有看過這篇文章,本文理解起來比較困難。0x
本文詳細描述了如何實現如下圖中的微信啟動界面. 該類啟動界面的特點是在整個Application的生命周期裡, 它只會出現在第一次進入應用時, 即便按回退鍵到桌面之後.
Android上讓人頭疼的莫過於從網絡上獲取圖片,然後顯示圖片,最後還要考慮到圖片的回收問題,這之間只要有任何一個環節有問題都可能直接OOM。尤其在需要展示圖片的列表頁面
前言Android應用中的APK安全性一直遭人诟病,市面上充斥著各種被破解或者漢化的應用,破解者可以非常簡單的通過破解工具就能對一個APK進行反編譯、破解、漢化等等,這樣