Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發實例 >> Android UI 事件研究

Android UI 事件研究

編輯:Android開發實例

1. 創建一個布局文件,布局如下,只有一個TextView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView 
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/hello"
    android:layout_gravity="center"
    android:gravity="center"
    android:textAppearance="?android:attr/textAppearanceSmall"
    android:textSize="30sp"
    android:textStyle="bold"
    />
</LinearLayout>

2. 創建HomeActivity,顯示這個布局

public class HomeActivity extends Activity {
    private TextView tv;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

3. 在View類的onTouchEvent方法上打上斷點

准備工作結束,下面開始debug調試

觸摸界面上的TextView,程序運行到斷點處停下,看一下執行流程

從這張圖上可以看出:

1. HomeActivity的dispatchTouchEvent方法可以捕獲所有的在屏幕上觸發的事件,從他開始事件的分配

2. 事件的分派是從activity上的最頂級view開始進行分配,一層層往下傳遞給觸發事件的view,可以看一下界面的hiberarchy views圖

3.所有的view控件都有dispatchTouchEvent方法,但是View類是繼承自View,ViewGroup類自己重寫了View類中的dispatchTouchEvent方法。

看一看具體的方法:

Activity:

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }

onUserInteraction();這個方法在Activity中是空函數體,當按鍵,觸摸,或者是軌跡球事件被分配到Activity的時候,這個方法就會觸發。如果你想知道activity運行時,

用戶與當前activity進行了那些交互,可以實現這個方法。這個方法最主要的還是幫助activity維護狀態欄通知,這個方法僅僅在action down的時候才會觸發,move,up時都不會觸發**********************************************************
        if (getWindow().superDispatchTouchEvent(ev)) {//
            return true;
        }

ev事件最終是被ViewGroup類中的dispatchTouchEvent(MotionEvent ev)方法進行了處理,我認為他是進行了事件在view hiberarchy中的傳遞,如果有view處理了這個事件,並且返回了ture,那麼這個事件就算consume了,不會再交給activity中的方法進行處理。如果視圖中沒有一個view處理這個事件,或者是處理了,但是返回了false,那麼還會去調用activity中的處理各種事件的方法onTouchEvent

**********************************************************
        return onTouchEvent(ev);

在Activity中,onTouchEvent(ev);方法是空函數體,也就是說這個方法是對外提供的,看看這個方法的說明,Called when a touch screen event was not handled by any of the views under it.  This is most useful to process touch events that happen  outside of your window bounds, where there is no view to receive it.

如果視圖中沒有一個view控件處理這個事件,那麼就由onTouchEvent處理,這個方法在activity中重寫即可。

    }

總結:如果你的activity中沒有重寫onUserInteraction,onTouchEvent這兩個方法,那麼這個方法將會在hiberarchy view中傳遞,處理。

Activity中完成了事件的分發,交給了視圖層,從頂層開始一層層傳遞給底層的view

從視圖層中可以看到,最頂層的是PhoneWindow,activity首先將事件傳遞給PhoneWindow,PhoneWindow不是View,他是Window的實現類,他將事件傳遞給DecorView,DecorView是繼承了FrameLayout的子類,FrameLayout沒有重寫dispatchTouchEvent()方法,他繼承自ViewGroup的dispatchTouchEvent方法。他首先會調用自己的onInterceptTouchEvent方法自己處理該事件(這個方法純view是沒有的,只有ViewGroup類才有,因為Viewgroup類統一監控他的子所有view),如果自己沒有處理或者是返回了false,就會遍歷自己的所有的子view,找到合適的view進行事件的處理,首先處理down事件,找到合適的view,記錄,這樣再處理隨後的up,cancle等事件,就不用重新去查找了。

/**
       我們知道無論是ACTION_DOWN,還是ACTION_UP,或者是ACTION_CANCEL,這幾種事件肯定是順序的,
       一個view觸發了ACTION_DOWN,那麼ACTION_UP,ACTION_CANCEL事件也會隨之而來,ACTION_DOWN事件肯定是先觸發,
       所以在處理ACTION_DOWN的時候,我們就能知道是誰觸發了這個事件,記住它,等處理以後的ACTION_UP,ACTION_CANCEL事件
       就不用在去重新獲得target了
      
       */
    @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;//得到一個矩形,包含自個坐標,上下左右

        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
            if (disallowIntercept || !onInterceptTouchEvent(ev)/**自己先進行處理,返回true,就不會分發該事件了*/) {
                // 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;
                //遍歷自己的子view
                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);
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                mMotionTarget = child;//這個child View 就是觸發事件的那個view,就交交給他處理,不用再分發了,結束
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }
        上面處理了ACTION_DOWN,下面的代碼處理其他的action
      // ***************************************************************************
      //如果是up事件,或者是cancle事件,isUpOrCancel為true
        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;
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.(不是ViewGroup,而只是veiw)
            ev.setLocation(xf, yf);
            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;
            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);

        return target.dispatchTouchEvent(ev);
    }

框架不斷分發這個事件,最後找到TextView是這個事件的觸發者,TextView得到這個事件後,又是怎麼處理的呢,首先是TextView的dispatchTouchEvent方法(繼承自View)捕獲這個事件,然後調用自己的onTouchEvent方法處理這個事件

public boolean dispatchTouchEvent(MotionEvent event) {

/**檢查是否有touch事件的監聽器,如果有,就調用監聽器處理,如果沒有,就調用缺省的處理方法
       if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
               mOnTouchListener.onTouch(this, event)) {
           return true;
       }
       return onTouchEvent(event);
   }

TextView重寫了View類的onTouchEvent方法,這個方法沒有看懂

@Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            // Reset this state; it will be re-set if super.onTouchEvent
            // causes focus to move to the view.
            mTouchFocusSelected = false;
        }
       
        final boolean superResult = super.onTouchEvent(event);

        /*
         * Don't handle the release after a long press, because it will
         * move the selection away from whatever the menu action was
         * trying to affect.
         */
        if (mEatTouchRelease && action == MotionEvent.ACTION_UP) {
            mEatTouchRelease = false;
            return superResult;
        }

        if ((mMovement != null || onCheckIsTextEditor()) && mText instanceof Spannable && mLayout != null) {
           
            if (action == MotionEvent.ACTION_DOWN) {
                mScrolled = false;
            }
           
            boolean handled = false;
           
            int oldSelStart = Selection.getSelectionStart(mText);
            int oldSelEnd = Selection.getSelectionEnd(mText);
           
            if (mMovement != null) {
                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
            }

            if (mText instanceof Editable && onCheckIsTextEditor()) {
                if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) {
                    InputMethodManager imm = (InputMethodManager)
                            getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                   
                    // This is going to be gross...  if tapping on the text view
                    // causes the IME to be displayed, we don't want the selection
                    // to change.  But the selection has already changed, and
                    // we won't know right away whether the IME is getting
                    // displayed, so...
                   
                    int newSelStart = Selection.getSelectionStart(mText);
                    int newSelEnd = Selection.getSelectionEnd(mText);
                    CommitSelectionReceiver csr = null;
                    if (newSelStart != oldSelStart || newSelEnd != oldSelEnd) {
                        csr = new CommitSelectionReceiver();
                        csr.mNewStart = newSelStart;
                        csr.mNewEnd = newSelEnd;
                    }
                   
                    if (imm.showSoftInput(this, 0, csr) && csr != null) {
                        // The IME might get shown -- revert to the old
                        // selection, and change to the new when we finally
                        // find out of it is okay.
                        Selection.setSelection((Spannable)mText, oldSelStart, oldSelEnd);
                        handled = true;
                    }
                }
            }

            if (handled) {
                return true;
            }
        }

        return superResult;
    }

 

到此結束

有個問題不明白:android框架是如何區分touch和click事件的呢

實現,給界面上的TextView出側兩個監聽器,一個是View.OnClickListener,另一個是View.OnTouchListener,分別打上兩個斷點

 

運行程序,首先程序運行到onTouch方法的斷點處,此時是ACTION_DOWN產生的事件,F8,又停在了這個斷點,這時action為ACTION_MOVE,F8,又停在了這個斷點處,此時actrio為ACTION_UP,到現在,touch事件正式結束,F8,停在了onClick方法的斷點處,看圖

從上圖可以看出,紅線標注的三行與touch事件的分發不同,也就是說在view類的onTouchEvent方法裡,認為該事件是click事件,調用了click事件的響應處理。

程序一共四次遇到斷點,斷了四次,分別記錄View的onTouchEvent方法的event的參數的id值

"event"     (id=830060441712)    down
   
"event"     (id=830060509040)    move
   
"event"     (id=830060441712)    up

"event"     (id=830060441712)    up

也就是說除了move事件外,其他的三個事件都是使用的同一個MotionEvent。框架在處理完touch之後,立刻處理的click,具體原理還有待研究。

 

一些相關的代碼:

Activity中mWindow是如何初始化的?
PolicyManager:
public static Window makeNewWindow(Context context) {
        return sPolicy.makeNewWindow(context);
}
IPolicy:
public interface IPolicy {
    public Window makeNewWindow(Context context);

    public LayoutInflater makeNewLayoutInflater(Context context);

    public WindowManagerPolicy makeNewWindowManager();
}
sPolicy是怎麼來的?
PolicyManager:
Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
sPolicy = (IPolicy)policyClass.newInstance();
private static final String POLICY_IMPL_CLASS_NAME =
        "com.android.internal.policy.impl.Policy"
  // Simple implementation of the policy interface that spawns the right
// set of objects

    
public class Policy implements IPolicy {
    private static final String TAG = "PhonePolicy";

    private static final String[] preload_classes = {
        "com.android.internal.policy.impl.PhoneLayoutInflater",
        "com.android.internal.policy.impl.PhoneWindow",
        "com.android.internal.policy.impl.PhoneWindow$1",
        "com.android.internal.policy.impl.PhoneWindow$ContextMenuCallback",
        "com.android.internal.policy.impl.PhoneWindow$DecorView",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState",
    };

    static {
        // For performance reasons, preload some policy specific classes when
        // the policy gets loaded.
        for (String s : preload_classes) {
            try {
                Class.forName(s);
            } catch (ClassNotFoundException ex) {
                Log.e(TAG, "Could not preload class for phone policy: " + s);
            }
        }
    }

    public PhoneWindow makeNewWindow(Context context) {
        return new PhoneWindow(context);
    }

    public PhoneLayoutInflater makeNewLayoutInflater(Context context) {
        return new PhoneLayoutInflater(context);
    }

    public PhoneWindowManager makeNewWindowManager() {
        return new PhoneWindowManager();
    }
}

PhoneWindow:
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
   
DecorView extends FrameLayout:
     public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved