由于最近在Android TV的项目开发中遇到一个问题,需要对某个view进行多次的按键监听,而我们都知道,通过调用setOnKeyListener实现了OnKeyListener接口之后我们就可以实现对按键进行监听了。
比如有如下代码:
view.setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
Log.i(TAG, "keyCode:" + keyCode);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
//TO DO
return true;
case KeyEvent.KEYCODE_DPAD_RIGHT:
//TO DO
return false;
default:
break;
}
}
return false;
}
});
我们同过实现这几个view的按键监听事件,通过keyCode来判断是哪个按键,在按下的时候做出某些响应。看上去也没什么毛病。
但是问题来了,在我前文中监听了这个view的左右按键之后,我又想监听关于他的上下键那该怎么办?总不能我再一次调用setOnKeyListener实现了OnKeyListener接口,然后实现这个按键的监听事件吧?那这样子前文中实现的左右按键的监听就被覆盖了。
通过源码我们也可以看得出来:
/**
* Register a callback to be invoked when a hardware key is pressed in this view.
* Key presses in software input methods will generally not trigger the methods of
* this listener.
* @param l the key listener to attach to this view
*/
public void setOnKeyListener(OnKeyListener l) {
getListenerInfo().mOnKeyListener = l;
}
实际上我们实现的OnKeyListener接口就是赋值给了getListenerInfo().mOnKeyListener,而再调用一次setOnKeyListener那么上一个实现的监听就被覆盖了,那怎么办呢?
问题的根本原因在于:
对于按键监听来说,View只是提供对按键的监听,并没有提供对单个按键的监听(当然有些键除外,比如确定按钮).
那有没有可能自定义一个View,实现只监听某个按键的事件呢,答案肯定是有的。
我们先来说一下思路,然后再具体写出代码。思路如下:
1、我们要实现对某个按键进行监听,就应该要自定义View提供一个addKeyListener()的方法可以来加入一个按键监听;
2、addKeyListener方法应该是接收一个接口实现对象,接口的定义必须至少包含有得到按键码和按键事件这两个方法。我们才能通过实现这个接口从而编写这个按键码的事件;
3、由于要注册的按键有若干个,我们应该要选择一个集合来存储他们,这里可以通过一个Map集合,把按键码作为key,接口实现作为value,从而得到某个key的监听实现;
4、自定义View应该是默认实现了setOnKeyListener,然后我们在OnkeyListener的接口实现中,来响应我们在Map集合中注册的某个按键监听,为了防止外界调用setOnKeyListener而被覆盖,我们要把setOnKeyListener的原本实现给屏蔽掉。
基于以上的分析过程,让我们来试试吧
首先先自定义一个View,自定义View需要继承View的构造方法:
public class MyView extends ImageView {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context,AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
然后我们在定义一个接口,这里为了区分OnkeyListener接口,我就把它命名为OnKeyLogicListener吧,定义如下:
/**
* 某个按键的监听类
*/
public interface OnKeyLogicListener {
/**
* 按键的处理逻辑
*/
boolean onKeyLogic(android.view.View v);
/**
* 指定按键,比如 KeyEvent.KEYCODE_DPAD_DOWN
* @return 按键码
*/
int getKeyCode();
}
接口很清晰,就是通过实现这个接口,在getKeyCode方法中返回按键码,在onKeyLogic中编写事件逻辑,
然后,我们选则一个Map集合来存储这个按键码到接口的映射关系,这里采用HashMap的话,编译器会警告我们说,使用SparseArray来代替HashMap,什么是SparseArray呢,它比HashMap好在哪里呢,在这里不是重点就不多说了。因为我们这里的key是整数型,而SparseArray里面是提供了整数型的key到value的映射关系(只能是int,比如key是String的话那就不能用SparseArray)
好了,我们在自定义View里面来定义一个成员变量:
SparseArray<OnKeyLogicListener> keyMapListener;
接下来我们在自定义View里面来编写一个内部的OnkeyListener实现类,如下:
class OnMyKeyListener implements android.view.View.OnKeyListener {
@Override
public boolean onKey(android.view.View v, int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
Log.i(TAG, "OnMyKeyListener keyCode="+keyCode+",event="+event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
OnKeyLogicListener listener =keyMapListener==null?null: keyMapListener.get(keyCode);
return listener==null?false:listener.onKeyLogic(v);
}
return false;
}
}
看到这里的实现,读者应该就比较明白了吧。正是因为我们自己来实现onKeyListener,然而onKeyListener只是对集合里面注册的按键事件进行反馈,比如我集合里面有个上键,那么通过查找集合里面的记录,如果有对上键的注册则会执行listener.onKeyLogic(v),没有的话则直接返回false。
接下来在自定义View里面编写添加按键监听的方法:addKeyLogicListener,方法实现如下:
public void addKeyLogicListener(OnKeyLogicListener listener) {
if (onMyKeyListener == null) {
onMyKeyListener = new OnMyKeyListener();
super.setOnKeyListener(onMyKeyListener);
}
if(keyMapListener==null){
keyMapListener = new SparseArray<MyView.OnKeyLogicListener>();
}
keyMapListener.put(listener.getKeyCode(), listener);
}
该方法首先判断onMyKeyListener 是否为空,onMyKeyListener 就是上面我们写的OnMyKeyListener接口实现类。用的时候才加载监听,所以会先判断一下为空则先设置本身的按键监听,。同时也要判断一下集合是否为空则new出来一个,然后把按键码作为key,接口作为value保存在集合中。
最后为了不被外界调用setOnkeyListenr后覆盖了我们本来做的接口实现,那么可以重写setOnkeyListenr方法,把方法的实现屏蔽掉即可。
到这里我们就基本完成了对任意单个按键的注册了。比如我在Activity中,要注册上键的监听,那么很简单。只需要调用addKeyLogicListener方法,做法如下:
view.addKeyLogicListener(new OnKeyLogicListener() {
@Override
public boolean onKeyLogic(View v) {
// TODO Auto-generated method stub
return false;
}
//返回上键的按键码
@Override
public int getKeyCode() {
// TODO Auto-generated method stub
return KeyEvent.KEYCODE_DPAD_UP;
}
});
这样就可以实现单个按键的监听了,如需再注册下键、左右键则再一次调用即可。
说到这里,我们确实是已经实现了标题说的对某个按键的监听,但是我为什么要说是“基本完成呢”。
不知道读者有没有注意到,在上面的代码中,我们为了实现某个监听,而屏蔽掉了原本的按键监听,这会带来什么影响呢?
假设,我的项目现在来了新需求,需要对这个view单独处理上下左右的按键,然后对其他按键实现统一监听处理。
这如何是好啊?坑爹的需求这不是让我尴尬么?
那,能不能做到外界既可以调用setOnKeyListener来处理按键,又可以调用addKeyLogicListener来单独处理某个按键呢? 答案是肯定的。
那我们前面的分析思路就应该做出一些改变了。
关键在于我们内部实现的OnMyKeyListener的处理逻辑,我们可以在定义一个OnKeyListener变量,来存放外界通过调用setOnKeyListener而传进来的OnKeyListener接口实现类,然后在OnMyKeyListener类中的实现逻辑可以是:
如果,集合中有这个按键,那么处理OnKeyLogicListener实现的按键事件,如果没有,那么把这个按键的响应转发到外界实现的OnKeyListener去(如果外界调用了setOnkeyListener方法的话)。
考虑一种情况,如果集合中有这个按键,而外界通过调用setOnkeyListener后也实现了这个按键事件,那么怎么处理呢,当然这里事件分发规则都是我们自己来定义的,这里我们可以定义成优先处理OnMyKeyListener,再通过onKeyLogic方法的返回值来决定要不要把事件也传给OnKeyListener。
因此,基于以上的分析,我们把原本的OnMyKeyListener类的方法修改一下,变成如下:
/***
* OnKeyListener实现类
* 会优先响应{@link #OnKeyLogicListener}接口的按键处理
* 再根据返回值决定要不要响应{@link #setOnKeyListener()}接口
*
*/
class OnMyKeyListener implements android.view.View.OnKeyListener {
@Override
public boolean onKey(android.view.View v, int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
Log.i(TAG, "OnMyKeyListener keyCode="+keyCode+",event="+event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
OnKeyLogicListener listener =keyMapListener==null?null: keyMapListener.get(keyCode);
Log.i(TAG, "OnMyKeyListener listener is null? "+(listener==null));
//如果OnKeyLogicListener为空则响应原始的按键监听
if (listener == null ) {
return OnOriginalKey(v, keyCode, event);
}
//OnKeyLogicListener不为空则优先被响应,其后再决定是否传递事件到原始的按键监听
if(!listener.onKeyLogic(v)){
return OnOriginalKey(v, keyCode, event);
}else{
return true;
}
}else{
//ACTION_UP动作还是被原来的监听
return OnOriginalKey(v, keyCode, event);
}
}
}
/**
* 原本的按键响应
*/
private boolean OnOriginalKey(android.view.View v, int keyCode, KeyEvent event) {
return onKeyListener != null ? onKeyListener.onKey(v,
keyCode, event) : false;
}
在上面我也写了注释,相信不难理解,这个处理逻辑就是:
如果集合里面没有这个按键,那么就传递到“原本的按键监听“(指的就是我们原本通过setOnkeyListener设置的监听).
如果集合有这个按键,那么优先处理这个按键的事件,其次如果事件返回了false,那么才会再传递到“原本的按键监听“,
下面的这个OnOriginalKey这是对onKeyListener 的处理逻辑做一个封装,也就是如果onKeyListener 不为空则把请求传递到onKeyListener上。
当然这个onKeyListener我们也要定义在我们的自定义View里面的成员变量,最后我们还得修改一下setOnkeyListener的逻辑:
/**
* 还是可以用原始的按键监听接口
*/
@Override
public void setOnKeyListener(android.view.View.OnKeyListener l) {
// TODO Auto-generated method stub
if (l != null) {
onKeyListener = l;
}
//实际上注册的是OnMyKeyListener实现类
if(onMyKeyListener==null){
onMyKeyListener = new OnMyKeyListener();
super.setOnKeyListener(onMyKeyListener);
}
// super.setOnKeyListener(l);
}
可以看到,其实外界传入进来的OnKeyListener接口实现类只是做一个赋值,并没有真正转到super.setOnKeyListener(l)去,而是传了onMyKeyListener。
所以对于View的机制来说,本质上监听的还是原来的那个 getListenerInfo().mOnKeyListener。只不过我们做了一层事件分发。
最后给出这个自定义view的代码:
public class HomeImgView extends ImageView {
private static final String TAG = HomeImgView.class.getSimpleName();
OnMyKeyListener onMyKeyListener;
OnKeyListener onKeyListener;
SparseArray<OnKeyLogicListener> keyMapListener;
public HomeImgView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public HomeImgView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HomeImgView(Context context) {
super(context);
}
/**
* 还是可以用原始的按键监听接口
*/
@Override
public void setOnKeyListener(android.view.View.OnKeyListener l) {
// TODO Auto-generated method stub
if (l != null) {
onKeyListener = l;
}
//实际上注册的是OnMyKeyListener实现类
if(onMyKeyListener==null){
onMyKeyListener = new OnMyKeyListener();
super.setOnKeyListener(onMyKeyListener);
}
// super.setOnKeyListener(l);
}
*/
/**
* 使用该方法添加的按键监听,会比{@link #setOnKeyListener()}被优先响应
* @param listener
*/
public void addKeyLogicListener(OnKeyLogicListener listener) {
if (onMyKeyListener == null) {
onMyKeyListener = new OnMyKeyListener();
super.setOnKeyListener(onMyKeyListener);
}
if(keyMapListener==null){
keyMapListener = new SparseArray<HomeImgView.OnKeyLogicListener>();
}
keyMapListener.put(listener.getKeyCode(), listener);
}
/**
* 某个按键的监听类
*/
public interface OnKeyLogicListener {
/**
* 按键的处理逻辑
* @return 返回true则不再响应该按键在{@link #setOnKeyListener()}的处理
*/
boolean onKeyLogic(android.view.View v);
/**
* 指定按键,比如 KeyEvent.KEYCODE_DPAD_DOWN
* @return 按键码
*/
int getKeyCode();
}
/***
* OnKeyListener实现类
* 会优先响应{@link #OnKeyLogicListener}接口的按键处理
* 再根据返回值决定要不要响应{@link #setOnKeyListener()}接口
*
*/
class OnMyKeyListener implements android.view.View.OnKeyListener {
@Override
public boolean onKey(android.view.View v, int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
Log.i(TAG, "OnMyKeyListener keyCode="+keyCode+",event="+event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
OnKeyLogicListener listener =keyMapListener==null?null: keyMapListener.get(keyCode);
Log.i(TAG, "OnMyKeyListener listener is null? "+(listener==null));
//如果OnKeyLogicListener为空则响应原始的按键监听
if (listener == null ) {
return OnOriginalKey(v, keyCode, event);
}
//OnKeyLogicListener不为空则优先被响应,其后再决定是否传递事件到原始的按键监听
if(!listener.onKeyLogic(v)){
return OnOriginalKey(v, keyCode, event);
}else{
return true;
}
}else{
//ACTION_UP动作还是被原来的监听
return OnOriginalKey(v, keyCode, event);
}
}
}
/**
* 原本的按键响应
*/
private boolean OnOriginalKey(android.view.View v, int keyCode, KeyEvent event) {
return onKeyListener != null ? onKeyListener.onKey(v,
keyCode, event) : false;
}
}