[Android]自定义View实现单独注册某个按键的事件监听

  由于最近在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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值