Android View

来源:互联网 发布:淘宝衣服 编辑:程序博客网 时间:2024/06/06 12:35

当我们触碰手机屏幕,便会产生一个触碰事件。由于View体系是以一种树状结构存在的(参考
在我的博文Android View - 控件架构),那么哪个View或者ViewGroup会响应这个事件呢?Android系统提供了一套完善的事件分发,拦截,处理机制,帮助开发者完成准确的事件分发和处理。

在《Android群英传》中有这么一个例子,可以帮助我们理解事件分发,拦截,处理机制。

假设有一家公司的员工分级如下:
总经理,级别最高。
软件开发部部长,级别次于总经理。
软件开发者,级别最低。
总经理从董事会接收到一个开发任务(添加xx功能),那么总经理就会把任务分给软件开发部部长,部长又把任务安排给软件开发者。开发者完成任务后,报告部长完成任务,部长觉得任务完成得不错,便报告总经理,总经理看了,也觉得不错,就签了名,交给董事会。那么这个任务也就算是完成了。
其实上面例子就是View分发处理机制的原理。

我们先理解3个与事件分发,拦截,处理机制相关方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。

dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event)

用于事件的分发。如果事件能传递到View(A),那么View(A)的dispatchTouchEvent一定会被调用。

onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent event)

用于事件的拦截。如果返回true,表示拦截事件,事件就不再往子View传递。如果返回false,则相反。

onTouchEvent

public boolean onTouchEvent(MotionEvent event)

用于事件的处理。如果返回true,表示不用父View处理事件,也就是事件不再回到父View,那么事件到此结束。父View的onTouchEvent方法就不会被调用。如果返回false,则相反。

稍微了解了这3个方法,我们再代码理解上面“总经理-部长-开发者”的例子:

总经理 - ViewGroupA
部长 - ViewGroupB
开发者 - MyView

ViewGroupA

package com.johan.testviewevent;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.FrameLayout;public class ViewGroupA extends FrameLayout {    public ViewGroupA(Context context, AttributeSet attrs) {        super(context, attrs);    }    public ViewGroupA(Context context) {        super(context);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        Log.e(getClass().getName(), "ViewGroupA dispatchTouchEvent ---");        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");        return super.onInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.e(getClass().getName(), "ViewGroupA onTouchEvent ---");        return super.onTouchEvent(event);    }}

ViewGroupB

package com.johan.testviewevent;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.FrameLayout;public class ViewGroupB extends FrameLayout {    public ViewGroupB(Context context, AttributeSet attrs) {        super(context, attrs);    }    public ViewGroupB(Context context) {        super(context);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        Log.e(getClass().getName(), "ViewGroupB dispatchTouchEvent ---");        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");        return super.onInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.e(getClass().getName(), "ViewGroupB onTouchEvent ---");        return super.onTouchEvent(event);    }}

MyView

package com.johan.testviewevent;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;public class MyView extends View {    public MyView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public MyView(Context context) {        super(context);    }    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        Log.e(getClass().getName(), "MyView dispatchTouchEvent ---");        return super.dispatchTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.e(getClass().getName(), "MyView onTouchEvent ---");        return super.onTouchEvent(event);    }}

发现View比View少重写一个方法:onInterceptTouchEvent,因为这个方法是判断是否拦截事件,View是不包含其他View的,不用拦截事件,只处理就好。

xml布局文件

<com.johan.testviewevent.ViewGroupA 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"    tools:context="com.johan.testviewevent.MainActivity" >    <com.johan.testviewevent.ViewGroupB        android:layout_width="400dp"        android:layout_height="400dp"        android:background="@android:color/holo_blue_bright"        >        <com.johan.testviewevent.MyView            android:id="@+id/my_view"            android:layout_width="200dp"            android:layout_height="200dp"            android:background="@android:color/holo_green_light"            />    </com.johan.testviewevent.ViewGroupB></com.johan.testviewevent.ViewGroupA>

此时,View的结构是这样的:

activity

package com.johan.testviewevent;import android.app.Activity;import android.os.Bundle;public class MainActivity extends Activity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);    }}

MainActivity只是设置了布局文件。

我们打开应用,触碰一下MyView绿色色块。打印结果如下:

从打印结果我们可以知道事件的传递顺序和处理顺序。

事件传递顺序
ViewGroupA(总经理)-> ViewGroupB(部长)-> MyView(开发者)

事件处理顺序
MyView(开发者)-> ViewGroupB(部长)-> ViewGroupA(总经理)

从打印结果我们还知道,ViewGroup接收到事件(调用dispatchTouchEvent)后,然后会判断是否拦截事件(调用onInterceptTouchEvent),默认是不拦截事件的。

董事会再分配了一个任务,但是这个任务只能由总经理处理,此时总经理需要任务拦下来,不用分配给部长,更不用安排给开发者,自己处理。

我们从代码中实现,只要重写ViewGroupA的onInterceptTouchEvent返回true就可:

// ViewGroupA类@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");    return true;}

打印结果:

只有ViewGroupA(总经理)处理了事件。

董事会又有一个任务来了,如需要确认某些需求可以做。总经理接收到任务后,因为此事需要软件开发部部长来判断,所以把任务安排到了部长,身为部长,肯定有一定的资历,所以觉得这个任务自己就可以搞定,并不用开发者来做。所以部长就把任务给拦下来了,自己处理。处理完后,需要报告总经理,然后由总经理向董事会说明。

代码实现:
恢复ViewGroupA的代码,ViewGroupA(总经理)不能拦截事件,否则ViewGroupB(部长)不能接收到事件:

// ViewGroupA类@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");    return super.onInterceptTouchEvent(ev);}// ViewGroupB类@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");    return true;}

打印结果:

董事会又来了一个小开发任务,不太care的小任务。同样,总经理接收到任务后,安排给部长,部长自然把任务分到开发者手中。开发者接收到任务后,可能公司给得待遇不能满足开发者的要求,开发者想跳槽了,就随便做,报告部长完成任务。当然部长觉得开发者完成的不行,不敢报告总经理,而且这个任务不太重要,所以决定不上报,任务到此结束。

我们用代码模拟这个场景:
我们只需要改ViewGroupB的代码,不再拦截事件,但是要处理事件,不在返回给父View(ViewGroupA)。

@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");    return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event) {    Log.e(getClass().getName(), "ViewGroupB onTouchEvent ---");    return true;}

打印结果:

事件回到ViewGroupB就结束了,因为ViewGroupB的onTouchEvent方法返回true。

其实不止打印了上面的结果,还有下面的结果:

我们待会再分析。

董事会觉得上次的任务没有收到报告,虽然不是什么大事,但是还是觉得有必要做一下。总经理接收到任务,安排到了部长,部长看到了,还是这个任务,分给开发者,并督促一定要做好,否则滚蛋。此时,开发者已经找到心仪的公司了,做人要有始有终嘛,便完成了任务,但是不打算报告部长,任务到此结束。

我们在用代码模拟一下:

// MyView类@Overridepublic boolean onTouchEvent(MotionEvent event) {    Log.e(getClass().getName(), "MyView onTouchEvent ---");    return true;}

打印结果:

这里也一样,还打印了其他结果:

为什么onTouchEvent返回true时,会重复执行多次呢??

因为onTouchEvent返回true,表示要处理触碰事件。一个完整的事件,应该包括 按下手指-移动(或许没有)- 抬起手指,毕竟要处理事件嘛,当然想要获取所有用户手指触碰的事件,所以会分发每个触碰事件。

我们从代码去理解,改一下MyView的onTouchEvent方法的代码:

// MyView类@Overridepublic boolean onTouchEvent(MotionEvent event) {    Log.e(getClass().getName(), "MyView onTouchEvent --- ");    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");            break;        case MotionEvent.ACTION_MOVE :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");            break;        case MotionEvent.ACTION_UP :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");            break;        default:            break;    }    return true;}

看看打印结果你就知道了:

这你应该清楚了吧。我们再来看看如果返回false,是什么事件呢:

// MyView类@Overridepublic boolean onTouchEvent(MotionEvent event) {    Log.e(getClass().getName(), "MyView onTouchEvent --- ");    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");            break;        case MotionEvent.ACTION_MOVE :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");            break;        case MotionEvent.ACTION_UP :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");            break;        default:            break;    }    return false;}

打印结果:

接收的是一个down事件,由于我们不处理这个事件,返回给上级处理,所以也就不会执行多次了。

requestDisallowInterceptTouchEvent

我们在开发中可能会遇到这种情况:在ViewPager中嵌套了一个横滑列表,在拖动横滑列表时同样可能导致ViewPager的tab切换。
now,我们可以在横滑列表中使用这个方法,来阻止ViewPager拦截滑动事件。

我们先看一下ViewGroup的dispatchTouchEvent方法:

注意,代码中有一个标志:FLAG_DISALLOW_INTERCEPT,这个标志是决定ViewGroup是否要执行onInterceptTouchEvent方法。子View可以通诺getParent方法获取到父View,调用父View的requestDisallowInterceptTouchEvent设置这个标志,从而阻止父View拦截事件,子View才有机会响应事件。但是ViewGroup在分发ACTION_DOWN事件就已经重置了FLAG_DISALLOW_INTERCEPT,也就是说,如果父View重写onInterceptTouchEvent事件时,ACTION_DOWN事件返回true,也就是拦截了ACTION_DOWN事件,即使子类调用了父View的requestDisallowInterceptTouchEvent(true)也不能阻止父View拦截事件。

我们代码测试一下:

// ViewGroupB类@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");    return true;}// MyView类@Overridepublic boolean onTouchEvent(MotionEvent event) {    Log.e(getClass().getName(), "MyView onTouchEvent --- ");    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");            break;        case MotionEvent.ACTION_MOVE :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");            // 阻止ViewGroupB拦截事件            getParent().requestDisallowInterceptTouchEvent(true);            break;        case MotionEvent.ACTION_UP :            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");            // 允许ViewGroupB拦截事件            getParent().requestDisallowInterceptTouchEvent(false);            break;        default:            break;    }    return true;}

打印结果为:

结果显示,MyView并没有能阻止ViewGroupB拦截事件。我们改一下代码,让ViewGroupB拦截ACTION_DOWN事件:

// ViewGroupB类@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");    if (ev.getAction() == MotionEvent.ACTION_DOWN) {        return false;    }    return true;}

打印结果:

MyView响应了事件,成功阻止ViewGroupB拦截除ACTION_DOWN以为的事件。

我们处理在ViewPager中嵌套了一个横滑列表,在拖动横滑列表时同样可能导致ViewPager的tab切换这种情况也是这么处理(因为没找到ViewPager源码,估计ViewPager也是没有拦截ACTION_DOWN事件吧,暂时还不知道)。

点击事件

如果MyView设置了点击事件(setOnClickListener)和触碰事件(setOnTouchListener),流程会怎么样呢?

代码实现:(ViewGroupA,ViewGroupB,MyView代码回到原始,请看上面原始代码)

package com.johan.testviewevent;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.View.OnClickListener;import android.view.View.OnTouchListener;public class MainActivity extends Activity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        findViewById(R.id.my_view).setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                Log.e(getClass().getName(), "MyView setOnClickListener --");            }        });        findViewById(R.id.my_view).setOnTouchListener(new OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                Log.e(getClass().getName(), "MyView setOnTouchListener --");                return false;            }        });    }}

我们看看结果:

省略重复一段log:

我们可以看到,MyView先响应我们设置的触碰事件(setOnTouchListener),然后再响应MyView重写的onTouchEvent,最后才响应我们设置的点击事件(setOnClickListener),这样方便开发者在外界设置响应事件。

从流程可以知道,如果我们在触碰事件(setOnTouchListener)中返回true,那么在MyView重写的onTouchEvent方法不会响应,设置的点击事件(setOnClickListener)也不会响应。如果在MyView重写的onTouchEvent方法中返回true,设置的点击事件(setOnClickListener)不会响应。setOnLongClickListener和setOnClickListener同理。

如果MyView设置点击事件或者触碰事件,都代表MyView已经处理了事件,事件到此结束,ViewGroupB的onTouchEvent就不再执行。

有兴趣的可以阅读《Android开发艺术探索》的View事件体系一章。

原创粉丝点击