从这节开始慢慢接触自定义View的相关知识,包括View的事件体系以及View的工作原理等等。这次主要来学习一下安卓的事件处理机制,作为学习自定义View的前奏,能对之后理解自定义View有所帮助。
所谓的事件包括系统事件和用户事件,用户事件就是指用户发出的可被识别的操作,例如用户按下按钮、点击屏幕等,系统事件则是由系统激发,例如定时器等等。事件被激发后需要被分发然后处理,这里我们先不用考虑事件的分发机制是怎样的,Android中支持两种事件处理机制:基于监听的事件处理机制和基于回调的事件处理机制。
参考菜鸟教程:
基于监听的事件处理机制流程模型图如下图所示:
基于监听的事件处理机制中由事件源、事件监听器、事件三类对象组成 ,处理流程如下:
事件监听机制是一种委派式的事件处理机制,事件源(组件)事件处理委托给事件监听器,当事件源发生指定事件时,就通知指定事件监听器,执行相应的操作。这里以按钮控件的事件监听为例,基于监听的事件处理机制有如下几种使用形式:
1.匿名内部类
public class MainActivity extends AppCompatActivity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(),"点击了按钮",Toast.LENGTH_SHORT).show();
}
});
}
}
2.使用内部类
public class MainActivity extends AppCompatActivity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
button.setOnClickListener(new MyOnClickListener());
}
class MyOnClickListener implements View.OnClickListener{
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(),"点击了按钮",Toast.LENGTH_SHORT).show();
}
}
}
3.使用外部类
这种形式用的比较少,因为外部类不能直接访问用户界面类中的组件,要通过构造方法将组件传入使用。
public class MyClick implements OnClickListener {
private TextView textshow;
public MyClick(TextView txt)
{
textshow = txt;
}
@Override
public void onClick(View v) {
textshow.setText("点击了按钮");
}
}
MainActivity:
public class MainActivity extends AppCompatActivity {
private Button btnshow;
private TextView txtshow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnshow = (Button) findViewById(R.id.btnshow);
txtshow = (TextView) findViewById(R.id.textshow);
btnshow.setOnClickListener(new MyClick(txtshow));
}
}
4.直接使用Activity作为事件监听器
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
button.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(),"点击了按钮",Toast.LENGTH_SHORT).show();
}
}
5.布局文件中直接绑定到标签
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void myclick(View view){
Toast.makeText(getApplicationContext(),"点击了按钮",Toast.LENGTH_SHORT).show();
}
}
不考虑外部类方式,以上方式都能实现如下效果:
基于监听的事件处理机制Demo
activity_main.xml布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mylayout"
tools:context=".MainActivity">
</FrameLayout>
自定义View:
public class MyView extends View {
private Paint paint;
private Bitmap bitmap;
Float x,y;
public MyView(Context context) {
super(context);
x = 200f;
y = 200f;
paint = new Paint();
bitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.s_jump);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmap,x,y,paint);
if(bitmap.isRecycled())//判断图片是否回收,没有回收的话强制收回图片
{
bitmap.recycle();
}
}
}
MainActivity:里面添加对View的基于监听的事件处理
public class MainActivity extends AppCompatActivity {
private FrameLayout frameLayout;
private MyView myView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
frameLayout = findViewById(R.id.mylayout);
myView = new MyView(MainActivity.this);
//基于监听的事件处理机制
myView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
myView.x = event.getX()-200;
myView.y = event.getY()-200;
myView.invalidate();
return true;
}
});
frameLayout.addView(myView);
}
}
效果如下:图片位置随着点击位置的改变而改变
方法回调
方法回调是将一种功能定义与功能分开的手段,一种解耦合的设计思想;在Java中回调是通过接口来实现的, 作为一种系统架构,必须要有自己的运行环境,且需要为用户提供实现接口;实现依赖于客户,这样就可以达到接口统一,实现不同,系统通过在不同的状态下"回调"我们的实现类,从而达到接口和实现的分离。
在Android中基于回调的事件处理机制使用场景主要有两个:
(1)自定义view
当用户在组件上激发某个事件时,组件有自己特定的方法会负责处理该事件。通常用法:继承基本的UI组件,重写该组件的事件处理方法,即自定义view。常见的有以下事件处理方法:
通过Demo来学习,新建自定义控件MyButton继承Button:
public class MyButton extends androidx.appcompat.widget.AppCompatButton {
private static String TAG = "MyButton";
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG,"onTouchEvent方法被调用");
return true;
}
}
修改activity_main.xml布局文件为自定义控件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.demo.mybutton.MyButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="按钮"/>
</LinearLayout>
修改MainActivity.java代码,在MainActivity中添加对返回键点击/松开的事件监控:
public class MainActivity extends AppCompatActivity {
private static String TAG = "MyButton";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode){
case KeyEvent.KEYCODE_BACK:
Log.i(TAG,"onKeyDown方法被调用,按下了了返回键");
break;
}
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode){
case KeyEvent.KEYCODE_BACK:
Log.i(TAG,"onKeyUp方法被调用,松开了返回键");
break;
}
return true;
}
}
我们重写了三个回调方法,当发生点击事件后就不需要我们在代码中再进行事件监听器的绑定就可以完成回调,组件会处理对应的事件,即事件由事件源(组件)自身处理。效果如下:
点击按钮后再点击返回键:
一直点击住不放:
(2)基于回调的事件传播
通过Demo来验证一下,修改MyButton.java代码:
public class MyButton extends androidx.appcompat.widget.AppCompatButton {
private static String TAG = "MyButton";
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG,"自定义按钮的onTouchEvent方法被调用");
return false;//返回false说明事件没有处理完就会传播出去
}
}
MainActivity.java代码:
public class MainActivity extends AppCompatActivity {
private static String TAG = "MyButton";
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(TAG,"监听器的onTouch法被调用");
return false;
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG,"Activity的onTouchEvent方法被调用");
return false;
}
}
运行后点击按钮:
从运行结果可知,事件传播的顺序是: 组件绑定的事件监听器->自定义view组件的回调方法->Activity的回调方法,和上图中标注的顺序一样。
前面学习事件处理机制的时候也看出来,前者是基于监听而后者是基于回调。
onTouch(View v, MotionEvent event):这里面的参数依次是触发触摸事件的组件,触碰事件event 封装了触发事件的详细信息,包括事件的类型、触发时间等信息,例如上图中的event.getX(),event.getY()。除此之外还可以对触摸的动作类型进行判断,使用event.getAction( )完成:
onTouchEvent更多的是用于自定义的view,所有的view类中都重写了该方法,而这种触摸事件是基于回调的,也就是说如果我们返回的值是false的话,那么事件会继续向外传播,由外面的容器或者Activity进行处理。除此之外还涉及到了手势(Gesture),之后再进行学习。onTouchEvent和onTouchListener在作用上是类似的。
这里就以上面那个随着图片位置点击位置改变而改变的例子进行修改,如果采用**onTouchEvent( )**基于回调处理的话:
修改MyView.java文件:
public class MyView extends View {
private Paint paint;
private Bitmap bitmap;
Float x,y;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
x = 200f;
y = 200f;
paint = new Paint();
bitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.s_jump);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
x = event.getX()-200;
y = event.getY()-200;
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmap,x,y,paint);
}
}
修改布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.demo.myontounch.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
修改MainActivity.java:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
在上面的基础上,看下多点触控的实现。这里参考单指拖动,双指缩放的Demo实现。
所谓的多点触控就是多个手指在屏幕上进行操作,可以通过event.getAction() & MotionEvent.ACTION_MASK来实现判断是哪种操作,这样就可以使用多点操作中两个特有的操作:
可以通过MotionEvent对象的getPointerCount()方法判断当前有多少个手指在触摸,然后可以通过getX/getY(index)来获得第index+1个触摸点的位置。
布局文件,设置对图片的放缩策略和显示方式采用matrix方式,即矩阵变换:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="matrix"
android:src="@drawable/nav_icon"/>
</LinearLayout>
MainActivity.java文件:
public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
private ImageView imageView;
private Matrix matrix = new Matrix();//用于变换图片的矩阵
private Matrix prematrix = new Matrix();//用于存储矩阵信息
private int flag = 0;
private PointF start = new PointF();//第一个按下的点,PointF代表坐标x/y是Float类型的 Point是int类型
private PointF mid = new PointF();//第一个与第二个按下点的中点
private float Dis = 1; //两点之间的距离
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageview);
imageView.setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
ImageView view = (ImageView) v;
switch (event.getAction()&MotionEvent.ACTION_MASK){
//单指按下
case MotionEvent.ACTION_DOWN:
matrix.set(view.getImageMatrix());//matrix保存图片信息
prematrix.set(matrix);//将matrix的值赋给prematrix
start.set(event.getX(),event.getY());//记录当前按下点坐标
flag = 1;//flag为1代表单手按下
break;
//双指按下
case MotionEvent.ACTION_POINTER_DOWN:
Dis = distance(event);//计算两点之间的距离
if (Dis > 10f){
prematrix.set(matrix);//将matrix的值赋给prematrix
mid = middle(event);//计算中心点坐标
flag = 2;//flag为2代表双手按下
}
break;
//手指松开
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
flag = 0;
break;
//单指滑动
case MotionEvent.ACTION_MOVE:
if(flag == 1){//单手按下移动->图片滑动
matrix.set(prematrix);//将prematrix的值赋给ematrix
//矩阵后乘,变换在set之后,pre在set之前
matrix.postTranslate(event.getX() - start.x, event.getY() - start.y);
}else if(flag == 2) {//双手按下移动->缩放
float newDist = distance(event);
if (newDist>10f){
matrix.set(prematrix);//将prematrix的值赋给ematrix
float scale = newDist/Dis;//缩放比例等于新距离/老距离
matrix.postScale(scale,scale,mid.x,mid.y);
}
}
break;
}
view.setImageMatrix(matrix);
return true;
}
//计算先后按下两点之间距离 平方相加开根号
private float distance(MotionEvent event) {
//getX(0)代表第一个按下的点的X,getX(1)代表第二个按下的点的X
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x*x+y*y);
}
//计算先后按下两点之间的中点
private PointF middle(MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
return new PointF(x / 2, y / 2);
}
}
接下来看一下手势,例如平常使用全面屏手机时的全面屏手势,可以提升用户体验。手势和上面学习的单点或多点触控是不同的,手势是连续触碰的行为。Android中手势交互的执行顺序如下:
其中MotionEvent类用于封装手势、触摸笔等动作事件,还可以记录横轴和纵轴的坐标。 GestureDetector:是用来识别各种手势。 OnGestureListener是一个手势交互的监听接口,其中提供了多个抽象方法, 并根据GestureDetector的手势识别结果调用相对应的方法。
GestureListener 提供了下述的回调方法:
通过Demo来验证一下这几个回调方法,按照上面所说的步骤,首先创建GestureDetector对象,创建时需实现GestureListener传入,然后将组件上的TouchEvent的事件交给GestureDetector进行处理:
MainActivity.java:
public class MainActivity extends AppCompatActivity {
private MyGestureListener myGestureListener;
private GestureDetector mDetector;
private final static String TAG = "MyGesture";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myGestureListener = new MyGestureListener();
mDetector = new GestureDetector(this,myGestureListener);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mDetector.onTouchEvent(event);
}
public class MyGestureListener implements GestureDetector.OnGestureListener{
@Override
public boolean onDown(MotionEvent e) {
Log.d(TAG, "onDown:按下");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
Log.d(TAG, "onShowPress:手指按下一段时间,不过还没到长按");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.d(TAG, "onSingleTapUp:手指离开屏幕的一瞬间");
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d(TAG, "onScroll:在触摸屏上滑动");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.d(TAG, "onLongPress:长按并且没有松开");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.d(TAG, "onFling:迅速滑动,并松开");
return false;
}
}
}
效果如下,按下然后立马松开:
在触摸屏上滑动:
迅速滑动,并松开:
手指按下一段时间,不过还没到长按:
长按并且没有松开: