事件是可以被识别的操作,如按下确定按钮,选择某个单选按钮或者复选框,到达某个时间点。每一种控件有自己可以识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件等等。事件有系统事件和用户事件。系统事件由系统激发,如时间每隔24小时,银行储户的存款发生变更。用户事件由用户的操作激发,如用户点击按钮,在文本框中输入文本。
在前端程序开发过程中,事件处理是一份重要的工作,应用程序必须为用户或者系统的事件提供响应,这种响应就是事件处理。Android支持两种事件处理机制:基于监听的事件处理机制和基于回调的事件处理机制,下面来讲解这两种事件处理机制。
1、基于监听的事件处理机制
在基于监听的事件处理机制中,主要涉及三类对象:
1)事件源:事件发生的组件。
2)事件:是对整个事件信息的封装,例如对于触控的操作,事件对象中分装了触摸的坐标点;
3)事件处理器:完成具体的事件处理;
监听机制下,事件处理采用委托机制,也就是说当事件源发生事件后,触发事件监听器,事件监听器根据注册的事件处理程序,将具体的事件委托给具体的处理程序进行处理。
下面给出最常用的两种监听事件处理程序注册机制:
1、在布局标签中直接指定事件处理函数
我们可以直接在布局文件中,指定事件处理函数,如下所示:
布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/loginButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="loginButtonClick" android:text="登陆" /> </LinearLayout> |
Activity
package com.zhangw.kailyard;
import android.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View;
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
public void loginButtonClick (View view){ AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage("登陆成功"); builder.show(); } } |
我们在Activity中定义了loginButtonClick方法,并在布局文件中通过指定按钮的onclick属性为方法名称,完成事件的绑定。
2、通过具体的对象实现事件处理
更多的情况下,我们采用对象完成具体的事件处理。为了能够处理特定的事件,对象必须满足一定的契约,也就是要实现特定的接口。通常我们是通过匿名类来实现给定的接口,完成事件处理程序的注册,也可以通过内部类,外部类完成时间处理程序的注册。下面给出一个通过匿名类实现事件处理的代码。
布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/loginButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="登陆" /> </LinearLayout> |
Activity
package com.zhangw.kailyard;
import android.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button;
public class MainActivity extends AppCompatActivity { private Button loginButton;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
loginButton = findViewById(R.id.loginButton); // 根据ID查找组件 loginButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setMessage("登录成功"); builder.show(); } }); } } |
这里需要讲解一下,首先我们通过findViewById,根据按钮的ID获取按钮对象,然后调用setOnClickListener给按钮注册Click事件的处理程序。
setOnClickListener的方法说明说下图所示:
根据方法的签名,我们知道需要传递一个实现了View.OnClickListener接口(View类的内部接口)的对象,我们可以看到View.OnClickListener接口只有一个方法:
我们通过匿名类实现了该接口,并传递给了setOnClickListener方法。
这里还需要注意内部匿名类中,如何访问外部对象的this指针。代码如下图所示:
注意:
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setMessage("登录成功"); builder.show(); |
这段代码用于弹出一个提示框,后期课程将会给大家详细讲解。
2、基于回调的事件处理机制
下面介绍基于回调的事件处理机制,所谓基于回调的事件处理机制是指事件源和事件处理程序统一了,当事件发生时,直接调用事件源相关的方法完成具体事件处理。针对View对象,Android提供了很多默认的事件处理方法,例如onTouchEvent、onKeyDown等,当我们自定义View时,只需要重新这些方法,就可以按照自己的业务逻辑去完成具体的事件处理。
下面通过一个简单的自定义View演示基于回调的事件处理机制。
自定义TouchButton代码:
package com.practise.salary.hello;
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.widget.Button;
public class TouchButton extends Button { public TouchButton(Context context, AttributeSet attributeSet) { super(context, attributeSet); }
@Override public boolean onTouchEvent(MotionEvent motionEvent){ super.onTouchEvent(motionEvent); Log.v("基于回调的onTouchEvent事件处理", "按钮中" + motionEvent.toString()); return true; }
@Override public boolean onKeyDown(int keyCode, KeyEvent keyEvent){ super.onKeyDown(keyCode, keyEvent); Log.v("基于回调的onKeyDown事件处理", "按钮中" + keyEvent.toString()); return true; } } |
布局文件使用自定的Button
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#fff" >
<com.practise.salary.hello.TouchButton android:layout_width="match_parent" android:layout_height="match_parent" android:text="点击测试回调事件" />
</LinearLayout> |
运行的结果如下图所示:
在事件处理过程中,有一个重要的概念就是事件传播。也就是说当我们触发了事件后,是否激发后续的事件处理函数。
大家注意,上述的事件回调方法都返回一个boolean值,如果方法返回true,则表示方法已经处理完成了事件,不会传播出去,如果为false,则表示未处理完成事件,需要传播。我们修改一下上面的样例,演示一下事件传播的过程。
首先我们自定义一个布局容器,如下所示:
自定义布局文件
package com.practise.salary.hello;
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout;
public class CustLinearLayout extends LinearLayout { public CustLinearLayout(Context context, AttributeSet attributeSet){ super(context, attributeSet); }
@Override public boolean onTouchEvent(MotionEvent motionEvent){ super.onTouchEvent(motionEvent); Log.v("基于回调的onTouchEvent事件处理", "布局中" + motionEvent.toString()); return true; } } |
其次,我们修改MainActivity,重写onTouchEvent方法,如下所示:
MainActivity代码
package com.practise.salary.hello;
import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent;
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
@Override public boolean onTouchEvent(MotionEvent motionEvent){ super.onTouchEvent(motionEvent); Log.v("基于回调的onTouchEvent事件处理", "Activity中" + motionEvent.toString()); return true; } } |
最后,我们使用两个自定的空间完成页面的设计。
Layout代码
<?xml version="1.0" encoding="utf-8"?> <com.practise.salary.hello.CustLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#fff" >
<com.practise.salary.hello.TouchButton android:layout_width="match_parent" android:layout_height="match_parent" android:text="点击测试回调事件" />
</com.practise.salary.hello.CustLinearLayout> |
Ok,我们运行后,点击按钮,发现只是回调了按钮中的onTouchEvent方法,如下图所示:
下面我们把按钮控件中的onTouchEvent的返回值改成false,再次运行,我们会发现触发了自定义布局的onTouchEvent事件,如下图所示:
最后,我们把布局中的onTouchEvent事件的返回值改成false,运行后我们发现,触发了Activity的onTouchEvent事件,如下图所示:
事件传播只能传播到Activity。
下面我们修改MainActivity代码,在为TouchButton注册一个事件处理程序,代码如下:
修改的后的MainAcitvity代码
package com.practise.salary.hello;
import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View;
public class MainActivity extends Activity { private TouchButton touchButton;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); touchButton = (TouchButton)findViewById(R.id.touchButton);
touchButton.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.v("基于事件的onTouchEvent事件处理", "onTouch事件中" + event.toString()); return false; } }); }
@Override public boolean onTouchEvent(MotionEvent motionEvent){ super.onTouchEvent(motionEvent); Log.v("基于回调的onTouchEvent事件处理", "Activity中" + motionEvent.toString()); return true; } } |
运行后如下图所示:
我们发现委托的事件处理优先于回调的事件处理。
下面我们把onTouch的事件处理方法返回值改成true,在此运行,如下图所示,
我们会发现事件的传播被阻止了。
事件传播不一定是从下到上,也有可能是从上到下,下面我们分析一下onInterceptTouchEvent事件。
onInterceptTouchEvent是ViewGoup里面的一个事件,他用来拦截Touch操作的。下面我们修改上述代码,并新增加一个MiddleLinearLayout布局。下面给出源代码:
MainAcitvity
package com.practise.salary.hello;
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); } } |
CustLinearLayout
package com.practise.salary.hello;
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout;
public class CustLinearLayout extends LinearLayout { public CustLinearLayout(Context context, AttributeSet attributeSet){ super(context, attributeSet); }
@Override public boolean onInterceptTouchEvent(MotionEvent motionEvent){ super.onInterceptTouchEvent(motionEvent); Log.v("onInterceptTouchEvent", "布局中" + motionEvent.toString()); return true; }
@Override public boolean onTouchEvent(MotionEvent motionEvent){ super.onTouchEvent(motionEvent); Log.v("基于回调的onTouchEvent事件处理", "布局中" + motionEvent.toString()); return false; } } |
MiddleLinearLayout
package com.practise.salary.hello;
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout;
public class MiddleLinearLayout extends LinearLayout { public MiddleLinearLayout(Context context, AttributeSet attributeSet){ super(context, attributeSet); }
@Override public boolean onInterceptTouchEvent(MotionEvent motionEvent){ super.onInterceptTouchEvent(motionEvent); Log.v("onInterceptTouchEvent", "MiddleLinearLayout布局中" + motionEvent.toString()); return true; }
@Override public boolean onTouchEvent(MotionEvent motionEvent){ super.onTouchEvent(motionEvent); Log.v("基于回调的onTouchEvent事件处理", "MiddleLinearLayout布局中" + motionEvent.toString()); return false; } } |
TouchButton
package com.practise.salary.hello;
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.Button;
public class TouchButton extends Button { public TouchButton(Context context, AttributeSet attributeSet) { super(context, attributeSet); }
@Override public boolean onTouchEvent(MotionEvent motionEvent){ super.onTouchEvent(motionEvent); Log.v("基于回调的onTouchEvent事件处理", "按钮中" + motionEvent.toString()); return false; } } |
activity_main
<?xml version="1.0" encoding="utf-8"?> <com.practise.salary.hello.CustLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#fff" > <com.practise.salary.hello.MiddleLinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.practise.salary.hello.TouchButton android:id="@+id/touchButton" android:layout_width="match_parent" android:layout_height="match_parent" android:text="点击测试回调事件" /> </com.practise.salary.hello.MiddleLinearLayout>
</com.practise.salary.hello.CustLinearLayout> |
下面我们运行,结果如下图所示:
我们发现只是触发了CustLinearLayout中的相关事件,子元素的所有事件都没有触发。
下面我们把CustLinearLayout的onInterceptTouchEvent方法返回值改成false,再次运行如下图所示:
我们发现除了CustLinearLayout中的相关事件,MiddleLinearLayout中的相关事件也被触发了。
最后我们把MiddleLinearLayout的onInterceptTouchEvent方法返回值改成false,再次运行,如下图所示
我们发现所有的事件都触发了。
下图我们总结了一下Touch相关的事件触发顺序
从上图中我们可以明显看出来,onInterceptTouchEvent事件的传递是从上到下的。
Android的事件处理机制比较凌乱,所幸大多数情况下我们只是简单的通过监听机制完成事件的处理,大家如果在具体的工作中遇到了问题,一定要细心分析,多做一些模拟程序测试一下。本节内容就到此,希望通过本节大家掌握Android的事件处理原理,至于各种组件具体有哪些的事件,我们在学习Android组件时在给大家详细介绍。
(张伟:2018年9月20日)
(转载时请注明来源)