View事件分发机制

前言:其实一直想写一篇关于事件分发的文章,平时上班也忙,一直没有时间,正好周末休息可以有时间了,发现这个不是很复杂的东西老是遗忘,实际网上有很多关于这样的文章,View的事件分发,ViewGroup的事件分发,其实很多都是说的很笼统,配置案例演示下就结束了,没有真正说的底层原理的东西,比如事件传替Activity—–>PhoneWindow—->ViewGroup—->View,整个流程说的不是很详细,我想如果要想说清这些事情,绝非三言两语就能说清的,我必须首先跟你说关于View的事件处理机制,接着会分析ViewGroup的事件分发机制,最后得说说window这个东西,包括我们都知道事件是由activity最先的dispatchTouchEvent来分发的,那么activity的事件到底又是怎么传替给他的?接下来我将会带领大家一起梳理下,再讲ViewGroup事件分发之前呢,先来了解下View的事件分发。
新建项目TouchEvent。代码如下:
新建MyChildButton继承自Button

/**
 * 
 * @author xyy
 *
 */
public class MyChildButton extends Button {

    public MyChildButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.v(MainActivity.TAG,"MyChildButton:dispatchTouchEvent = ACTION_DOWN");
            break;

        case MotionEvent.ACTION_MOVE:
            Log.v(MainActivity.TAG,"MyChildButton:dispatchTouchEvent = ACTION_MOVE");

            break;

        case MotionEvent.ACTION_UP:
            Log.v(MainActivity.TAG,"MyChildButton:dispatchTouchEvent = ACTION_UP");
            break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.v(MainActivity.TAG, "MyChildButton:onTouchEvent = ACTION_DOWN");
            break;

        case MotionEvent.ACTION_MOVE:
            Log.v(MainActivity.TAG, "MyChildButton:onTouchEvent = ACTION_MOVE");

            break;

        case MotionEvent.ACTION_UP:
            Log.v(MainActivity.TAG, "MyChildButton:onTouchEvent = ACTION_UP");
            break;
        }
        return super.onTouchEvent(event);
    }

}

布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/scr"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#0000FF"
    android:gravity="center" >

    <com.xyy.demo.MyChildButton
        android:id="@+id/button"
        android:layout_height="100dp"
        android:layout_width="100dp"
        android:background="#00FF00"
        />

</LinearLayout>

主Activity

/**
 * 
 * @author xyy
 *
 */
public class MainActivity extends Activity {

    private MyChildButton image;

    public static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        image = (MyChildButton)findViewById(R.id.button);
    }

}

可以看到代码异常的简单,自定义了一个Button继承自Button,进行了Touch事件信息大打印,最后在MainActivity里面加载。最终效果:

这里写图片描述

手指轻轻的点击了下屏幕上的绿色按钮,在控制台打印了如下信息:

09-03 16:17:29.497: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 16:17:29.497: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 16:17:29.517: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 16:17:29.517: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_UP

刚刚我没有滑动,如果有滑动操作那么应该会有一系列的Move事件:

09-03 16:19:14.377: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 16:19:14.377: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 16:19:14.487: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.487: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.507: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.507: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.537: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.537: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.567: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 16:19:14.567: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 16:19:14.597: V/MainActivity(2027): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 16:19:14.597: V/MainActivity(2027): MyChildButton:onTouchEvent = ACTION_UP

就结果分析,首先是当前View(MyChildButton)的dispatchTouchEvent方法被调用,捕获了down事件,看来和我们之前说的没错,接着onTouchEvent方法也被打印,就结果我们开始分析源码,不带着大家兜圈子了,我们最终在基类View里面找到dispatchTouchEvent方法。代码如下:

 /**
 * 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.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    if (!onFilterTouchEventForSecurity(event)) {
        return false;
    }

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

可以发现代码不是很多,就几行,我们也需要找到我们需要的代码就行了,也就是说当我们点击按钮的时候最先被调用的就是当前方法,接下来一起看看这个方法,
首先在13行判断mOnTouchListener是否为null 并且判断当前View是否为enable的,默认情况下控件都是enable的,再者又判断mOnTouchListener.onTouch(this, event)它的返回值,如果两者都为true那么成立,首先看看mOnTouchListener是个什么鬼?继续查看View源码

 private OnTouchListener mOnTouchListener;
 /**
 * Register a callback to be invoked when a touch event is sent to this view.
 * @param l the touch listener to attach to this view
 */
public void setOnTouchListener(OnTouchListener l) {
    mOnTouchListener = l;
}

到这里大家应该熟悉了,mOnTouchListener 是View内部一个监听,赋值是在setOnTouchListener(OnTouchListener l)方法里面,很显然我们没有对其进行赋值,很显然这里的判断是不成立的,那么直接走
return onTouchEvent(event);
再来看看onTouchEvent(event)源码

 /**
 * 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) {
        // 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 (!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();
                            }
                        }
                    }

                    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;
        }
        return true;
    }

    return false;
}

从代码可以看出,想比较前面的dispatchTouchEvent方法要多的多,但是没关系,我们只需要寻找我们需要的代码就行了
前面是一些初始化变量信息的部分不用看,直接看在第23行判断当前View是否可以点击或者长点击,很显然我们的按钮默认是肯定可以点击的,那么进入方法体内部,下面是一系列的action_up,action_move,action_down事件的判断,最后可以看到代码最终返回个true结束了,注意代码的括号。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                ......
                ......
                ......
                break;

        }
        return true;
    }

可以发现只要进入这个判断内部,那么最终一定会返回true回去,这里跟你提一点,在Android事件分发之中,必须要前一个action返回true情况下,后面action才会响应。返回true代表当然View将此次事件进行自己响应消费掉了,这个事件也就没有了,如果返回false代表对此次事件不响应也不消费,那么事件将会回传,一般是由父控件的onTouchEvent来接受。到这里相信你已经了解的差不多了对打印结果应该也没有什么迷惑 了,在我们点击按钮的时候控制台打印了,action_down和action_up,很显然方法最终走到onTouchEvent方法中,又判断控件是可点击的,紧接着一系列的各种action判断,如果有滑动操作,那么action_move事件将会被打印。

接着我们给我们的Button按钮注册一个点击事件如下:

public class MainActivity extends Activity {

    private MyChildButton button;

    public static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        button = (MyChildButton)findViewById(R.id.button);

        button.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {

            }
        });
    }

}

再看控制台打印结果:

09-03 17:10:55.557: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 17:10:55.557: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 17:10:55.627: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 17:10:55.627: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_MOVE
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_MOVE
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 17:10:55.637: V/MainActivity(12466): MyChildButton:onTouchEvent = ACTION_UP
09-03 17:10:55.647: D/MainActivity(12466): MainActivity onclick

在点击按钮的时候我滑了下屏幕所以有很多action_move事件打印,从打印结果中可以看到onclick事件是最后被打印的,也就是说在up之后,好大家先带着这个疑问继续向下看代码

回过头来在上面我们提到在dispatchTouchEvent方法中因为mOnTouchListener我们没有赋值,为空才走的判断把,大家还记得吧?那么现在问题来了,如果不为空呢?那么赋值又是在哪里赋值的呢?相信你应该也比较熟悉,看代码:

public class MainActivity extends Activity {

    private MyChildButton button;

    public static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        button = (MyChildButton)findViewById(R.id.button);

        button.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Log.d(TAG, "MainActivity onclick");
            }
        });

        button.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "MainActivity  onTouch");
                return false;
            }
        });
    }

}

可以看到在activity中我们设置touch事件,也对它进行赋值了,很显然这里new 的OnTouchEventListener匿名内部类传给了mOnTouchListener这个变量。这个方法也是用来处理事件的,那么onTouchEvent也是用来处理事件的,会不会多余呢?两个方法又有什么不同?其实从android设计框架来说,对于dispatchTouchEvent,TouchEvent这些方法是系统默认的,但是如果我们想建立自己的事件分发逻辑了,那么android设计者又给我们预留了OnTouchListener这个接口,让我们在这个方法中我们对事件的处理,从而不用系统的onTouchEvent方法。
这个时候可以发现,我已经在主activity中将OnTouchListener进行赋值了,下面看打印结果:

09-03 17:20:39.517: V/MainActivity(14023): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 17:20:39.517: D/MainActivity(14023): MainActivity  Button onTouch
09-03 17:20:39.517: V/MainActivity(14023): MyChildButton:onTouchEvent = ACTION_DOWN
09-03 17:20:39.557: V/MainActivity(14023): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 17:20:39.557: D/MainActivity(14023): MainActivity  Button onTouch
09-03 17:20:39.557: V/MainActivity(14023): MyChildButton:onTouchEvent = ACTION_UP
09-03 17:20:39.557: D/MainActivity(14023): MainActivity Button onclick

就打印结果来分析:
这时候我们在主页面给按钮设置了点击事件,也设置了setOnTouchListsner事件方法监听,

很显然Button的dispatchTouchEvent方法会被第一打印,接着可以看到activity里面设置的touch事件onTouch方法被打印,然后再打印了Button的TouchEvent方法,Button 单击事件最后打印,为什么会是这样现象?那么我们就来分析下代码:
首先在dispatchTouchEvent方法里面我们对其mOnTouchListener已经进行赋值了所以它不为null,然后看mOnTouchListener.onTouch(this, event)返回值,可以发现这里的onTouch方法优先于Button的onTouchEvent方法先执行,也就证明了打印结果。然后这里默认是返回false,那么也就是还是不成立,又会走入Button的onTouchEvent方法中,接着又和之前分析的一样了,走到这里还有个疑问,那就是Button的点击事件是什么时候调用的?虽然我们已经从打印结果看出是在action_up以后。
我们继续分析代码,这时候我把onTouch方法默认值改为true,

button.setOnTouchListener(new OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.d(TAG, "MainActivity  Button onTouch");
        return true;
    }
});

这时候再看打印结果:

09-03 17:33:09.837: V/MainActivity(16708): MyChildButton:dispatchTouchEvent = ACTION_DOWN
09-03 17:33:09.837: D/MainActivity(16708): MainActivity  Button onTouch
09-03 17:33:09.867: V/MainActivity(16708): MyChildButton:dispatchTouchEvent = ACTION_UP
09-03 17:33:09.867: D/MainActivity(16708): MainActivity  Button onTouch

可以看到这时候点击事件没有了,这是怎么回事?
我们继续分析代码,当onTouch方法返回true的时候,这个if判断也就返回了true,这时候直接return true出去了,很显然后面的onTouchEvent方法就不在执行了,到这里位置我们可以结合打印结果不难猜出onclick方法是在onTouchEvent方法中调用的,下面我直接找到这里代码;
可以看到在Button的action_up的时候有个 performClick();方法

public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        if (mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick(this);
            return true;
        }

        return false;
    }

一切都是那么清晰了,在Buttonaction_up的时候,会进入此方法,并且判断mOnClickListener 是否为null,如果不为null则执行 mOnClickListener.onClick(this);

/**
 * Register a callback to be invoked when this view is clicked. If this view is not
 * clickable, it becomes clickable.
 *
 * @param l The callback that will run
 *
 * @see #setClickable(boolean)
 */
public void setOnClickListener(OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    mOnClickListener = l;
}

很显然,这里的monClickListener正是我们在主页面调用setOnClickListener方法设置的,紧接着就回回调我们事先设置的onClick方法。到此结束,可以发现此方法里面会判断当前View是否可点击,因为在Android系统里面很多控件默认情况下是不可以点击的,比如ImageView,TextView,但是没事,如果不能点击系统会自动给你设置点击,也就是说只要设置了此方法,那么控件都是可以点击的。

到此为止,你应该有这样一个疑问,我们之前在onTouch方法中返回了false和返回了true,最后发现都能够响应按下,抬起,移动事件,按理说返回false应该是不响应事件的,又是为什么呢?接着上面,我们说如果onTouch方法返回了false,那么就会进行onTouchEvent方法判断之中,然后在23行判断控件是否可点击,如果可点击那么最后必定返回的是true,虽然我们在onTouch里面返回了false,但是系统最后还是给我们返回了true。
比如说这时候我将按钮换成ImageView,并且将onTouch方法返回false,这时候打印结果是:

09-03 17:58:25.197: D/MainActivity(22595): MyChildImageView:dispatchTouchEvent = ACTION_DOWN
09-03 17:58:25.197: D/MainActivity(22595): MainActivity  imageView onTouch
09-03 17:58:25.197: D/MainActivity(22595): MyChildImageView:onTouchEvent = ACTION_DOWN

可以发现后面一系列的action都不会触发了,因为在在onTouchEvent方法中第23行判断当前View是否可点击,很显然图片默认是不可以点击的,这时候直接跳出去,直接返回了false。对于这样的现象你要么直接在onTouch方法中返回true,或者我们可以在布局文件中将图片的clickable属性设置为true也可以了,或者给图片设置上点击事件,因为在上面我们分析到,给控件设置click事件的时候如果该控件不可点击,那么系统会默认给你设置上可点击;

public class MainActivity extends Activity {

    private MyChildImageView image;

    public static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        image = (MyChildImageView)findViewById(R.id.image);

        image.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Log.d(TAG, "MainActivity imageView onclick");
            }
        });

        image.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "MainActivity  imageView onTouch");
                return false;
            }
        });
    }

}

或者

public class MainActivity extends Activity {

    private MyChildImageView image;

    public static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        image = (MyChildImageView)findViewById(R.id.image);

        image.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "MainActivity  imageView onTouch");
                return true;
            }
        });
    }

}

打印结果:

09-03 18:05:05.267: D/MainActivity(24601): MyChildImageView:dispatchTouchEvent = ACTION_DOWN
09-03 18:05:05.267: D/MainActivity(24601): MainActivity  imageView onTouch
09-03 18:05:05.307: D/MainActivity(24601): MyChildImageView:dispatchTouchEvent = ACTION_UP
09-03 18:05:05.307: D/MainActivity(24601): MainActivity  imageView onTouch

至此关于View的事件分发可以得到以下几个结论:
关于onTouch和onTouchEvent区别,onTouch优先于onTouchEvent执行,如果在onTouch里面返回了true,那么onTouchEvent将不再执行,道理很简单事件讲究层级传替,在onTouch方法里被onTouch方法里被消费了也就没有了,自然不会再传替了,onTouchEvent得到执行的依据在于我们没有设置mOnTouchListener。
只要是可点击的事件那么他的dispatchTouchEvent必定返回true,那么一定就可以消费事件,对于不可点击的控件我们可以给它设置上clickable属性即可。
至此。View的事件分发也就结束了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值