View事件的传递之一_单独看Button的传递

来源:互联网 发布:淘宝设计师助手官网 编辑:程序博客网 时间:2024/06/05 16:33

android中touch事件的传递在android系统中是非常重要的,搞清楚这个:

1.可以自定义写出非常多优美的自定义控件---Android控件之搓以及国内产品经理之懒让国内android苦逼程序员都深有体会。

2.理解了手势的传递也可以在处理多层View嵌套引发的手势冲突问题时快速的找到问题的所在,进行bug的修复

3.手势传递运用了很多设计理念,包括著名的观察者模式,学习google大神的代码也可以提高自身的代码修养。

因此虽然是个菜逼,我们还是一起来研究研究view上的手势传递机制吧。

先从最简单的Button来研究,并且是点击button不考虑其它view的拦截。也就是事件从activity顺利的传递到了button这一层以及事件在button层处理的分析。

首先先上测试demo代码:

布局文件:activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context=".MainActivity" >    <Button        android:id="@+id/bt"        android:layout_alignParentLeft="true"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="button" />        </RelativeLayout>

MainActivity.java

public class MainActivity extends Activity {private static final String TAG = "MainActivity";private Button btn;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);btn = (Button) findViewById(R.id.bt);btn.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {showLog("button ontouch...event is.."+event.getAction());return false;}});btn.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {showLog("button onclick");}});}@Overridepublic void onUserInteraction() {showLog("onUserInteraction");}@Overridepublic boolean onTouchEvent(MotionEvent event) {showLog("onTouchEvent...ev is "+event.getAction());return super.onTouchEvent(event);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {showLog("dispatchTouchEvent...ev is "+ev.getAction());return super.dispatchTouchEvent(ev);}public void showLog(String msg){Log.d(TAG, msg);}}


第一次我们在方法中返回false MainActivity.java
btn.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {showLog("button ontouch...event is.."+event.getAction());return false;//返回false}});
然后程序运行时点击button,查看log

01-01 18:33:45.210: D/MainActivity(16155): dispatchTouchEvent...ev is 0
01-01 18:33:45.210: D/MainActivity(16155): onUserInteraction
01-01 18:33:45.210: D/MainActivity(16155): button ontouch...event is..0
01-01 18:33:45.310: D/MainActivity(16155): dispatchTouchEvent...ev is 1
01-01 18:33:45.310: D/MainActivity(16155): button ontouch...event is..1
01-01 18:33:45.320: D/MainActivity(16155): button onclick

再将该方法中返回true MainActivity.java

btn.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {showLog("button ontouch...event is.."+event.getAction());return true;//返回true}});
运行并查看log

01-01 18:32:42.560: D/MainActivity(15713): dispatchTouchEvent...ev is 0
01-01 18:32:42.560: D/MainActivity(15713): onUserInteraction
01-01 18:32:42.560: D/MainActivity(15713): button ontouch...event is..0
01-01 18:32:42.660: D/MainActivity(15713): dispatchTouchEvent...ev is 1
01-01 18:32:42.660: D/MainActivity(15713): button ontouch...event is..1


通过Log我们可以得出如下几个结论:

1.不论如何,点击事件一定是先执行了dispatchTouchEvent的,

并且按下执行一次,抬起执行一次

2.如果在onTouchListener()方法中返回false,则会执行button的onClick()方法;

如果返回为true,则不会执行

3.0表示按下,1表示抬起

4.执行的顺序是

按下:dispatchTouchEvent-->onUserInteraction-->button.setOnTouch()

抬起:dispatchTouchEvent-->button.setOnTouch()

----可能会有onClick,并且抬起不执行onUserInteraction

5.都没有执行复写的onTouchEvent()方法。

了解了结论后我们就需要去源码中寻找为什么会这么执行了。

基于上述是无论怎么点击一定会先执行dispatchTouchEvent方法,

因此就要去找寻父类中的dispatchTouchEvent方法了。

当前类父类是activity.java,因此跳转到这个界面

Activity.java

 /**     * 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) {//所有的touch事件第一步都是按下屏幕,即第一下ev.getAction() == MotionEvent.ACTION_DOWN            onUserInteraction();//所以会走onUserInteraction()方法。        }//在根activity中寻找这个方法 public void onUserInteraction() {//什么都没有,因此默认这一步是不做任何处理的,当然我们也可以在子activity中复写这个方法进行一些处理}         
<span style="white-space:pre"></span><pre name="code" class="java"><pre name="code" class="java">//然后开始执行这一步,这一步就会调用到当前window最基类View的dispatchTouchEvent()方法
 
if (getWindow().superDispatchTouchEvent(ev)) {
//getWindow().superDispatchTouchEvent(ev)这个方法就会进行一大串复杂的操作,如果最后返回值为true,则activity的dispatchTouchEvent就返回ture//事件就被消费了,就不会再执行activity的onTouchEvent了,这也就是为什么并没有在点击button时执行onTouchEvent,因为上面那个方法一定返回true了,//也就是事件被拦截了 return true; } return onTouchEvent(ev); }
1.这个activity.java里的方法何时被调用?
Called to process touch screen events -->当触摸屏幕事件时,要处理屏幕事件便会调用这个方法。
2.这个方法的用处
You can override this to intercept all touch screen events before they are dispatched to the window.
可以在子类activity中复写这个方法在触摸事件被分发到窗口之前来打断掉所有的触摸屏幕的事件。怎么做到?其实就是复写后不调用super.即可
那么反推它的用处就是所有触摸事件的入口。
因为触摸屏幕一定是在activity中进行的,因此所有的触摸事件都会最先调用到这个方法
3.这个方法返回值得作用
Return true if this event was consumed.-->如果返回true,就证明这一次touch事件被消费了。所谓被消费就是类似钱被花了,就没有了。

看上面代码我们可以知道为什么都没有执行activity复写的onTouchEvent方法了

因为在上一句代码已经被消费了,就不会再走下面的了

下面我们主要来分析getWindow().superDispatchTouchEvent(ev)这个方法
这个方法是走viewRoot中的DispatchTouchEvent(ev),一般activity中的布局的根view都是ViewGroup,所以会走ViewGroup中的DispatchTouchEvent(ev)方法
如果没有被拦截会逐层传递,直到传递到View的DispatchTouchEvent(ev)方法-->如果点击在了view上


但是这整个流程太麻烦,我们现在分析的是单个View的dispatchTouchEvent(ev)方法,因此我们假设在点击按钮时它的根view都没有进行拦截,从而就传到了button中,
那么我们接下来就要分析button中的dispatchTouchEvent(ev)方法了。

Button.java:

public class Button extends TextView {    public Button(Context context) {        this(context, null);    }    public Button(Context context, AttributeSet attrs) {        this(context, attrs, com.android.internal.R.attr.buttonStyle);    }    public Button(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }    @Override    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {        super.onInitializeAccessibilityEvent(event);        event.setClassName(Button.class.getName());    }    @Override    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {        super.onInitializeAccessibilityNodeInfo(info);        info.setClassName(Button.class.getName());    }}
发现button中并没有这个方法,那就找它的父类,TextView.java
发现TextView中也没有这个方法,那么只能找到最父类View中了


View.java---DispatchTouchEvent()

先看对这个方法的注释:

/**     * Pass the touch screen motion event down to the target view, or this     * view if it is the target.     *     * @param event The motion event to be dispatched.     * @return True if the event was handled by the view, false otherwise.     */
这个方法在view中的作用?
Pass the touch screen motion event down to the target view, or this
view if it is the target.
将屏幕touch事件传递给目标的view,如果目标view就是这个view,那么就在此处理
得出的结论是view是不会拦截touch事件的,它只会传递(如果它不是目标view)或者处理touch(如果它是目标view)

这里面的主要代码:

 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                return true;            }  if (onTouchEvent(event)) {return true;}

根据源码我们得知如果第一层if()都满足那么这个函数就不会在执行了,而是直接消费掉了,return true;如果没有都满足则会执下面的OnTouchEvent()方法在进行判断。

注意:当前类是View.java,不是我们此前说的Activity.java。虽然有些方法名字一样,但是不要混淆

那下面我们就判断一下在Button的情况下第一个If()中的条件是否满足:

1.li是否为空?根据代码我们得到

 ListenerInfo li = mListenerInfo;
因此判断li是否为空,也就是判断mListenerInfo是否为空

在查看源码:View.java

ListenerInfo getListenerInfo() {if (mListenerInfo != null) {return mListenerInfo; }mListenerInfo = new ListenerInfo();return mListenerInfo;  }
 也就是说只要调用这个函数,mListenerInfo一定不为空。什么时候会调用这个方法?

查看源码: View.java

 public void setOnClickListener(OnClickListener l) {if (!isClickable()) {setClickable(true);}getListenerInfo().mOnClickListener = l;}
 public void setOnTouchListener(OnTouchListener l) {getListenerInfo().mOnTouchListener = l;}
只要调用了这上述两个方法,那么li就不会为空。

回到MainActivity.java的代码中

btn.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {showLog("button ontouch...event is.."+event.getAction());return false;}});
调用了其中一个方法,因此li是不会为空的。


2.li.mOnTouchListener是否为空?

View.java的源码中:

 public void setOnTouchListener(OnTouchListener l) {getListenerInfo().mOnTouchListener = l;}
因此只要调用了这个函数,那么li.mOnTouchListener就不为空,如上所示,mainActivity中button也设置了,因此不为空

3.(mViewFlags & ENABLED_MASK) == ENABLED

这句话的意思就是view子类控件是不是可以被enable的,button是默认可以的,imageView就默认不可以
。因此这个也成立

4.那么只剩最后一项了,li.mOnTouchListener.onTouch(this, event)

如上所知这个li.mOnTouchListener就是你在调用setOnTouchListener时的接口,

因此它的onTouch方法就是你在mainActivity自己实现的方法。

所以如果你return true,它就只执行OnTouch而不执行OnClick,

因为到这一步就被截断了。

下面我们来考虑onTouch中返回false时的情况,如果返回false,那么就会走下面的函数了

View.java

  if (onTouchEvent(event)) {  return true; }
也就是会调用OnTouchEvent,注意这个onTouchEvent是View中的,不是activity中被复写的。

因此activity中onTOuchEvent没有被调用,和view中的是没有关系的。

我们来看view中的onTouchEvent方法:

非常的长,主要看这一行View.java

if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {//如果该view是可被点击的,则执行,否则就进入了//因为button天生是可被点击的,所以就进入这个判断 switch (event.getAction()) {//第一次走actionDown,抬起时就走了actionUp
主要来看当是手指抬起时的情况,会执行一个这个函数

View.java

  if (!post(mPerformClick)) {performClick();//onClick事件就是在这个方法里执行的}
我们转到这个方法:

View.java

/**     * Call this view's OnClickListener, if it is defined.  Performs all normal     * actions associated with clicking: reporting accessibility event, playing     * a sound, etc.     *     * @return True there was an assigned OnClickListener that was called, false     *         otherwise is returned.     */    public boolean performClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        ListenerInfo li = mListenerInfo;        if (li != null && li.mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            li.mOnClickListener.onClick(this);            return true;        }        return false;    }
其中li.mOnClickListener在setOnClickListener()时就已经传递了,因此会执行这行代码

 li.mOnClickListener.onClick(this);
这个也就是在mainActivity中所定义的那个方法:

btn.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {showLog("button onclick");}});

综上,这一阶段的流程就分析完了。

总结下,当在一个Button上进行点击时,android内部的执行流程是:

1.执行当前activity的dispatchTouchEvent方法,

如果你在activity的子类中复写这个方法并且不调用super.dispatchTouchEvent()

那么整个事件流程就不会执行。

2.Activity.java中如果你是按下事件,那么会先执行onUserInteraction()方法。

然后会执行

getWindow().superDispatchTouchEvent(ev)
方法,如果返回为true,就不会再执行activity中的OnTouchEvent方法了。

这个方法会执行到当前activity的rootView中的dispatchTouchEvent方法,

假设在这个传递过程中事件没有被拦截,

那么就会传递到button的dispatchTouchEvent方法。

button并没有复写这个方法,因此就会追溯到其父父类view中的这个方法

3.View.java

执行dispatchTouchEvent()方法,首先会进行一个判断,

对于绑定了touch监听器的button来说是否满足这个判断

就取决于touch监听器中的回调方法是不是返回true了,

如果返回true,那么这个方法就被截断,

不在执行,也就无法执行onClick中方法,

如果返回false.则会向下执行


4.View.java

onTouchEvent()方法,在这个方法中有对手势的判断,

但要是走到判断这一层必须要是这个view可以被点击,

就Button而言,他可以被点击,因此可以走到这一层判断。

在手势为up的时候,会执行一个方法叫做performClick()。


5.View.java中

performClick()中就执行了view的setOnClickListener()中的onClick方法。


这样也解释了为什么down会执行onUserInterAction,其他时候不执行

为什么点击button时不执行activity的OnTouchEvent

为什么Button中OnTouch返回true就不执行onClick了,返回false就执行

以及touch事件中各个方法的执行顺序。

抽空在分析touch事件在整个viewTree中的传递。从而晚上touch事件的效果




0 0
原创粉丝点击