与界面编程紧密相关的就是事件处理了,当用户在程序界面执行各种操作时,应用程序需要为用户动作提供响应动作,这种响应动作需要通过事件处理来完成。
Android提供了两种方式的事件处理:
基于回调的事件处理:主要做法是重写Android组件特定的回调方法,或重写Activity的回调方法
基于监听器的事件处理:主要做法是为Android界面组件绑定特定的事件监听器。(android为绝大部分界面组件提供了事件响应的回调方法,开发者只需要重写它们)
在事件监听的处理模型中,主要涉及如下三类对象:
Event Source(事件源):事件发生的场所,通常是各个组件,例如按钮、窗口、菜单等
Event(事件):事件封装了界面组件上发生的特定事情(通常就是一次用户操作)。如果程序需要获取界面组件上所发生事件的相关信息,一般通过Event对象来获取。
Event Listener(事件监听器):负责监听事件源所发生的事件,并对各种事件做出相应的响应。
当用户按下一个按钮或者单击某个菜单项时,这些动作就会激发一个相应的事件,该事件会触发事件源上注册的事件监听器,事件监听器调用对应的事件处理器来做出相应的响应。
Android的事件处理机制是一种委派式事件处理方式:事件源将整个事件处理委托给特定的对象(事件监听器),当该事件源发生指定的事件时,通知所委托的事件监听器,由事件监听器来处理这个事件。
每个组件均可以针对特定的事件指定一个事件监听器,每个事件监听器也可以监听一个或多个事件源。因为同一个事件源上可能发生多种事件,委派式事件处理方式可以把事件源上所有可能发生的事件分别授权给不同的事件监听器来处理,同时也可以让一类事件都使用同一个事件监听器来处理。
实现一个简单的基于监听的时间处理模型的程序。
界面上有两个组件:一个文本框、一个按钮。定义的按钮为事件源,并为按钮绑定一个事件监听器。
activity_main.xml:
MainActivity.java
public class MainActivity extends Activity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.bt);
//为按钮绑定事件监听器
button.setOnClickListener(this);
}
@Override
public void onClick(View view) {
EditText text = (EditText) findViewById(R.id.text);
text.setText("bn被单击了!");
}
}
或者
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.bt);
//为按钮绑定事件监听器
button.setOnClickListener(new MyClickListener());
}
class MyClickListener implements View.OnClickListener{
@Override
public void onClick(View view) {
EditText text = (EditText) findViewById(R.id.text);
text.setText("bn被单击了!");
}
}
}
结果:
上面的代码先为bn按钮注册事件监听器,当程序中的按钮被单击时,处理器被触发,文本框内变成被单击了。
但是对于键盘事件、触摸屏事件等,此时程序需要获取事件发生的详细信息:例如键盘事件需要获取是哪个键触发的事件;触摸屏事件需要获取事件发生的位置等,对这种包含更多信息的事件,Android会将事件信息封装成XXXEvent对象,并将该对象作为参数传入事件处理器。
package com.example.phone;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.View;
public class PlainView extends View {
public float currentX;
public float currentY;
Bitmap plane;
public PlainView(Context context){
super(context);
//定义飞机图片
plane = BitmapFactory.decodeResource(context.getResources(),
R.drawable.plane);
setFocusable(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//创建画笔
Paint p = new Paint(); canvas.drawBitmap(plane,currentX,currentY,p); //绘制飞机
}
}
package com.example.phone;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.os.Bundle;
import android.view.Display;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
public class MainActivity extends Activity {
//定义飞机速度
private int speed = 12;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
final PlainView plainView = new PlainView(this);
setContentView(plainView);
plainView.setBackgroundResource(R.drawable.ic_launcher_background);
//获取窗口管理器
WindowManager windowManager = getWindowManager();
Display display = windowManager.getDefaultDisplay();
//获得屏幕宽和高
int screenWidth = display.getWidth();
int screenHeight = display.getHeight();
plainView.currentX = screenWidth/2;
plainView.currentY = screenHeight - 40;
//为draw组件键盘事件绑定监听器
plainView.setOnKeyListener(new View.OnKeyListener() {
//获取由哪个键触发
@Override
public boolean onKey(View view, int i, KeyEvent keyEvent) {
switch (keyEvent.getKeyCode()){
case KeyEvent.KEYCODE_DPAD_DOWN:
plainView.currentY += speed;
break;
case KeyEvent.KEYCODE_DPAD_UP:
plainView.currentY -= speed;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
plainView.currentX -= speed;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
plainView.currentX += speed;
break;
}
plainView.invalidate();
return true;
}
});
}
}
在上面的实例中,直接使用了PlainView作为Activity的显示内容,并且为该PlainView增加键盘事件监听器。在程序中,先调用了KeyEvent事件对象的getKeycode()方法来获取触发事件的键,然后针对不同的键来改变飞机的坐标。
在基于事件监听的处理模型中,事件监听器必须实现事件监听器接口,Android为不同的界面组件提供了不同的监听器接口,这些接口通常以内部类的形式存在。
上面两个实例中使用的事件监听器类是内部类形式,内部类可以在当前类中复用该监听器类。
如果某个事件监听器需要被多个GUI界面所共享,而且主要是完成某种业务逻辑的实现,则可以考虑使用外部类的形式来定义事件监听器类。
定义一个外部类作为OnLongClickListener类,该事件监听器实现了发送短信的功能。
package com.example.sendsmslistener;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.telephony.SmsManager;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
public class SendSMSListener implements View.OnLongClickListener {
private Activity act;
private EditText address;
private EditText content;
public SendSMSListener(Activity act, EditText address,EditText content){
this.act = act;
this.address = address;
this.content = content;
}
@Override
public boolean onLongClick(View view) {
String addressStr = address.getText().toString();
String contentStr = content.getText().toString();
SmsManager smsManager = SmsManager.getDefault();
PendingIntent sentIntent = PendingIntent.getBroadcast(act,0,new Intent(),0);
smsManager.sendTextMessage(addressStr,null,contentStr,sentIntent,null);
Toast.makeText(act,"短信发送完成",Toast.LENGTH_LONG).show();
return false;
}
}
package com.example.sendsmslistener;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends Activity {
EditText address;
EditText content;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
address = (EditText) findViewById(R.id.address);
content = (EditText) findViewById(R.id.content);
Button bt = (Button) findViewById(R.id.send);
bt.setOnLongClickListener(new SendSMSListener(this,address,content));
}
}
结果:
上面的事件监听器类没有和任何GUI耦合,创建该监听器对象需要传入两个EditText对象和一个Activity对象。当用户长单击界面中的SEND按钮时,程序会触发SendSMSListener监听器,该监听器里的事件处理方法会向指定手机发送短信。
缺点:
这种形式可能造成程序结构的混乱。
如果Activity界面类需要实现监听器接口,会比较怪异。
主要展示使用Activity本身作为事件监听器的用法。
package com.example.layout_test;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
public class MainActivity extends Activity implements View.OnClickListener {
private TextView text;
private Button changeText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text = (TextView) findViewById(R.id.text);
changeText = (Button) findViewById(R.id.change_text);
//直接使用Activity作为事件监听器
changeText.setOnClickListener(this);
}
//实现事件处理方法
@Override
public void onClick (View view){
switch (view.getId()) {
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
text.setText("Nice to meet you");
}
}).start();
break;
default:
break;
}
}
}
主要是在该Activity类中直接定义事件处理器方法onClick (View view)
。
大部分时候,事件处理器没有什么复用价值(可复用代码通常会被抽象成业务逻辑方法),因此大部分事件监听器只是临时使用一次,匿名内部类形式的事件监听器更适合。【目前最广泛的】
主要展示使用匿名内部类作为事件监听器的用法。
package com.example.myapplication;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;
public class MixView extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
LinearLayout root = (LinearLayout) findViewById(R.id.root);
final DrawView draw = new DrawView(this);
draw.setMinimumWidth(300);
draw.setMinimumHeight(500);
draw.setOnTouchListener(new View.OnTouchListener() {
//实现事件处理方法
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
draw.currentX = motionEvent.getX();
draw.currentY = motionEvent.getY();
draw.invalidate();
return true;
}
});
root.addView(draw);
}
}
还有一种更简单的绑定事件监听器的方式,直接在界面布局文件中为指定标签绑定事件处理方法。
package com.example.applicationtest;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.bt);
}
public void MyClickListener(View view) {
EditText text = (EditText) findViewById(R.id.text);
text.setText("bn被单击了!");
}
}
结果:
基于回调的事件处理模型,事件源与事件监听器是统一的,当用户在GUI组件上激发某个事件时,组件自己特定的方法将负责处理该事件。
为了使用回调机制类处理GUI组件上所发生的事件,需要为组件提供对应的事件处理方法,只能继承GUI组件类,并重写该类的事件处理方法来实现。
方法 | 使用 |
---|---|
boolean onKeyDown(int keyCode, KeyEvent event) | 当用户在该组件上按下某个按键时触发该方法 |
boolean onKeyLongPress(int keyCode, KeyEvent event) | 当用户在该组件上长按某个按键时触发该方法 |
boolean onKeyShortcut(int keyCode, KeyEvent event) | 当一个键盘快捷键事件发生时触发该方法 |
boolean onKeyUp(int keyCode, KeyEvent event) | 当用户在该组件上松开某个按键时触发该方法 |
boolean onTouchEvent( MotionEvent event) | 当用户在该组件上触发触摸屏事件时触发该方法 |
boolean onTrackballEvent( MotionEvent event) | 当用户在该组件上触发轨迹球屏事件时触发该方法 |
自定义按钮的实现类:
package com.example.mybutton;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Button;
public class MyButton extends androidx.appcompat.widget.AppCompatButton {
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
super.onKeyDown(keyCode, event);
Log.v("kd","the onKeyDown is MyButton");
return true;
}
}
在上面的自定义MyButton类中,重写了Button类的onKeyDown(int keyCode, KeyEvent event)
方法,该方法将负责处理按钮上的键盘事件,并且在XML界面布局文件中使用MyButton组件,按钮会自己处理对应的事件。【我的理解】基于回调的事件处理会自定义事件类,并且继承组件类,例如自定义的MyButton类,在自定义的事件类中会重写事件处理方法(例如onKeyDown(int keyCode, KeyEvent event)
方法),这样事件源和事件监听器就统一在自定义的事件类中了,后面只需要在XML界面布局文件中使用就可以自己处理相应事件了。
几乎所有基于回调的事件处理方法都有一个Boolean类型的返回值,用于标识该处理方法是否能够完全处理该事件:
如果返回true,表示该处理方法可以完全处理该事件,该事件不会传播出去。
如果返回false,表示该处理方法并未完全处理该事件,该事件会传播出去。
根据该示例可以清楚的了解:组件没有完全处理事件时,监听器、回调和Activity的触发顺序。
在开发Android应用时,有时候需要让应用程序随着系统设置而进行调整,比如判断系统的屏幕方向等。
Configuration类专门用于描述手机设备上的配置信息,这些配置信息包括用户特定的配置项,也包括系统的动态设备配置。
程序可调用Activity的方法来获取系统的Configuration对象:
Configuration cfg = getResources().getConfiguration();
一旦获得了系统的Configuration对象,该对象提供了如下常用属性来获取系统的配置信息。