现在的位置: 首页 > 综合 > 正文

android ui事件分析

2014年02月20日 ⁄ 综合 ⁄ 共 13828字 ⁄ 字号 评论关闭

 

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,显示这个布局,同时给TextView配置touch事件

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,程序运行到断点处停下,看一下执行流程

image

从这张图上可以看出:

1. HomeActivity的dispatchTouchEvent方法可以捕获所有的在屏幕上触发的事件,从他开始事件的分配

2. 事件的分派是从activity上的最顶级view开始进行分配,一层层往下传递给触发事件的view,可以看一下界面的hiberarchy views图

3. dispatchTouchEvent方法的作用是对事件进行分发,将事件分发给他的子view

image

3.所有的view控件都有dispatchTouchEvent方法,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.

         事件首先分发给activity,然后由activity分发给window,activity是当前window的一个逻辑管理类,他不是类似于XXXLayout的布局容器。
     * @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时都不会触发**********************************************************

//将事件传递给该activity管理的视图层,该视图层的顶层容器是Window,如果该window中没有一个view处理这个事件,就会返回false,就会执行onTouchEvent方法了,这就实现了事件由Activity进行分发,分发给视图层进行事件处理,如果事件没有被吃掉,再由Activity进行处理。       

if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
 }

每个Activity的显示视图都是放在Window中的,Window是个容器,负责渲染视图。

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中完成了事件的分发,交给了视图层,从顶层开始一层层传递给底层的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,分别打上两个断点

image

运行程序,首先程序运行到onTouch方法的断点处,此时是ACTION_DOWN产生的事件,F8,又停在了这个断点,这时action为ACTION_MOVE,F8,又停在了这个断点处,此时actrio为ACTION_UP,到现在,touch事件正式结束,F8,停在了onClick方法的断点处,看图

image

从上图可以看出,红线标注的三行与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);

}

抱歉!评论已关闭.