事件机制

简介

提出问题:
Android是如何响应对其中的某个视图的触控操作呢?
Android是如何响应对手机的按键的操作呢?

解决方案:
MotionEvent机制(触屏)
KeyEvent机制(按键)

触屏

触屏的操作:
最基本的操作类型:
down : 手指按下
move : 手指在屏幕上移动
up : 手指从屏幕上离开
触屏操作的顺序:
down-->move-->move-->-->up
对屏幕的任何一个操作, 系统都会创建一个MotionEvent对象来对应这个操作
点击和长按也类似,就是down,然后移动的位置还是在整个视图对象的范围

触屏API

MotionEvent : 触屏事件
int ACTION_DOWN=0 : 代表down
Int ACTION_MOVE=2 ; 代表move
Int ACTION_UP=1 : 代表up
getAction() : 得到事件类型值
getX() : 得到事件发生的x轴坐标(相对于当前视图)
getRawX() :得到事件发生的x轴坐标(相对于屏幕左顶点)
getY() : 得到事件发生的y轴坐标(相对于当前视图)
getRawY() :得到事件发生的y轴坐标(相对于屏幕左顶点)
Activity
boolean dispatchTouchEvent(MotionEvent event) : 分发事件
boolean onTouchEvent(MotionEvent event) :
View
boolean dispatchTouchEvent(MotionEvent event) : 分发事件
boolean onTouchEvent(MotionEvent event) : 处理事件的回调方法
void setOnTouchListener(OnTouchListener l) : 设置事件监听器
void setOnClickListener(OnClickListener l) : 设置点击监听
void setOnLongClickListener(OnLongClickListener l) : 设置长按监听
void setOnCreateContextMenuListener(OnCreateContextMenuListener l) :
ViewGroup
boolean dispatchTouchEvent(MotionEvent ev) : 分发事件
boolean onInterceptTouchEvent(MotionEvent ev) :

具体的理解

首先产生这些事件的最终缘由还是我们手指触摸了屏幕
而且触摸屏幕只是触摸当前的Activity
如果从最底层开始说起,那就是屏幕了,首先现在我们手机使用的是电容屏幕
我们触摸屏幕就会有电压变化,Android的底层就会根据这个变化
产生事件对象,这个对象首先从底层驱动一直向上传,最终我们关注的是
这个事件传到Activity,这里就要仔细看下了
首先Android的视图的层次管理就是父视图对象只是管理子视图对象
子视图的子视图对象父视图不管理,只是将它交给子视图对象管理
由上面的API可以知道Activity有两个回调方法
dispatchTouchEvent(分发事件)
onTouchEvent(处理事件)
那么事件已经传输到Activity这里了,那么Activity就开始分发事件
如果这个事件是因为触摸Activity里面的某个子视图对象产生的
就会分发这个事件到那个子视图对象,如果还是因为触发子视图的子视图
产生的,就继续这样分发,一直分发.
分发到最后的视图对象,视图对象也会进行分发,但是发现没有子视图分发了
就看看是否要处理了,处理有两种方式:
视图对象的回调方法
dispatchTouchEvent(分发事件)
onTouchEvent(处理事件)
监听器对象的回调方法:
View.OnTouchListener()里面的onTouch(View v, MotionEvent event)
监听器的方法会先执行
监听器看看要不要处理
然后返回值boolean就是要不要消费这个事件,
这个事件只能被消费一次,如果有人消费了这个事件
那么这个事件就不会继续向上面传送回去进行处理了
如果一直没有人来处理和消费这个事件,最后Activity就会处理和消费它
还有得注意的是:
如果传入的ACTION_DOWN事件到子视图,但是子视图没有处理和消费这个事件
而是返回到父视图这里进行处理和消费
那么以后有ACTION_MOVE,ACTION_UP事件,这个父视图都不会将这个
事件传送到子视图里面,而是自己进行处理,
因为连ACTION_DOWN事件都没有处理和消费,其他的事件就不会用到或者说没有

练习图片拖动

功能描述:
int getLeft()
得到当前视图左顶点相对父视图的X轴坐标
int getTop()
得到当前视图左顶点相对父视图的Y轴坐标
int getRight()
得到当前视图右下角点相对父视图的X轴坐标
int getBottom()
得到当前视图右下角点相对父视图的Y轴坐标
layout(int left, int top, int right, int bottom) :
动态指定当前视图在父视图中的定位, 参数为相对父视图的坐标
ViewParent getParent() :
<RelativeLayout 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="${relativePackage}.${activityClass}" >

<ImageView
android:id="@+id/iv_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/btn_star"/>
</RelativeLayout>
package com.jane.motionevent;

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Toast;

public class MainActivity extends Activity implements OnTouchListener
{
private ImageView iv_main;
private RelativeLayout parentView;

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

iv_main = (ImageView) findViewById(R.id.iv_main);

parentView = (RelativeLayout) iv_main.getParent();
/*
int right = parentView.getRight(); //0
int bottom = parentView.getBottom(); //0
Toast.makeText(this, right+"---"+bottom, 1).show();
*/
//设置touch监听
iv_main.setOnTouchListener(this);
}

private int lastX;
private int lastY;
private int maxRight;
private int maxBottom;

@Override
public boolean onTouch(View v, MotionEvent event)
{
//得到事件的坐标
int eventX = (int) event.getRawX();
int eventY = (int) event.getRawY();
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
//得到父视图的right/bottom
if(maxRight==0)
{
//保证只赋一次值
maxRight = parentView.getRight();
maxBottom = parentView.getBottom();
}

//第一次记录lastX/lastY
lastX =eventX;
lastY = eventY;
break;
case MotionEvent.ACTION_MOVE:
//计算事件的偏移
int dx = eventX-lastX;
int dy = eventY-lastY;
//根据事件的偏移来移动imageView
int left = iv_main.getLeft()+dx;
int top = iv_main.getTop()+dy;
int right = iv_main.getRight()+dx;
int bottom = iv_main.getBottom()+dy;

//限制left >=0
if (left < 0)
{
right += -left;
left = 0;
}
// 限制top
if (top < 0)
{
bottom += -top;
top = 0;
}
// 限制right <=maxRight
if (right > maxRight)
{
left -= right - maxRight;
right = maxRight;
}
// 限制bottom <=maxBottom
if (bottom > maxBottom)
{
top -= bottom - maxBottom;
bottom = maxBottom;
}

iv_main.layout(left, top, right, bottom);
//再次记录lastX/lastY
lastX = eventX;
lastY = eventY;
break;

default:
break;
}
return true;//所有的motionEvent都交给imageView处理
}
}

按键

操作的基本类型
down : 手指按下
up : 手指从按键上离开
按键操作的顺序: down-->down-->down-->-->up
对按键的任何一个操作, 系统都会创建一个KeyEvent对象来对应这个操作
按键的长按监听:

按键API

KeyEvent  
int ACTION_DOWN = 0 : 标识down的常量
int ACTION_UP = 1 : 标识up的常量
int getAction() : 得到事件类型
int getKeyCode() : 得到按键的keycode(唯一标识)
startTracking() : 追踪事件,
Activity
boolean dispatchKeyEvent(KeyEvent event) : 分发事件
boolean onKeyDown(int keyCode, KeyEvent event) : 按下按键的回调
boolean onKeyUp(int keyCode, KeyEvent event) : 松开按键的回调
boolean onKeyLongPress(int keyCode, KeyEvent event) :

例子

package com.jane.event;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;

public class KeyEventTestActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_key);
}

@Override
public boolean dispatchKeyEvent(KeyEvent event)
{
Log.e("TAG", "dispatchKeyEvent() action="+event.getAction()+" code="+event.getKeyCode());
return super.dispatchKeyEvent(event);
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
Log.e("TAG", "onKeyDown() action="+event.getAction()+" code="+event.getKeyCode());
/*
* 追踪事件, 用于长按监听
* 如果是black按键,默认是可以接收到长按的事件的
* 但是如果其他的按键想有长按事件,就必须event.startTracking();
*/
event.startTracking();

return true; //返回必须是true
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
Log.e("TAG", "onKeyUp() action="+event.getAction()+" code="+event.getKeyCode());

return super.onKeyUp(keyCode, event); //不执行super就可以不退出界面
//return true;
}

@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event)
{
Log.i("TAG", "onKeyLongPress() action="+event.getAction()+" code="+event.getKeyCode());
return super.onKeyLongPress(keyCode, event);
}
}

点击两次返回按键才退出

第一次按back键, 只Toast提示, 不退出应用
2S内连按两次back键才退出应用
技术点:

先做个简单点的,先显示dialog提示的

@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
//监听back键
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK)
{
//显示确定的dialog
new AlertDialog.Builder(this)
.setMessage("你确定退出吗?")
.setPositiveButton("退出", new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
//退出
finish();
}
})
.setNegativeButton("不退出", null)
.show();
return true;//不会退出了
}
return super.onKeyUp(keyCode, event);
}

再实现之前的功能

package com.jane.keyevent;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.KeyEvent;
import android.widget.Toast;

public class MainActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

private boolean exit = false;// 标识是否可以退出
private Handler handler = new Handler()
{
public void handleMessage(android.os.Message msg)
{
if (msg.what == 1)
{
exit = false;
}
}
};

@Override
public boolean onKeyUp(int keyCode, KeyEvent event)
{
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK)
{
if (!exit)
{
exit = true;
Toast.makeText(this, "再按一次就退出应用", 0).show();
// 发消息延迟2s将exit=false
handler.sendEmptyMessageDelayed(1, 2000);
return true;// 不退出
}
}
return super.onKeyUp(keyCode, event);
}
}