在手机开发中,我们往往是与屏幕在做交互,而实体按键就寥寥几个。但是在Android TV开发中,按键就用得多了,大多数情况下我们是用遥控器按键来实现与电视的交互。

  在Android 开发中,有时候会遇到这么一个需求:
  在按下特定的按键序列之后,启动某一个隐藏功能,或者是快捷启动某个东西。

  那么我们如何实现这个触发过程呢?

  我们通过需求分析,来定义一个接口。接口要定义哪些方法呢?

  首先,这个触发过程应该是有一些限制条件的吧,也就是在某些条件下才可以启动对组合键的监听,那么我们需要有个方法,来表示是否允许启动对组合键的监听。这个方法我们就叫做boolean allowTrigger()吧。

  其次,我们应该要监听用户输入的每一个按键,是否对应接下来组合键各个位置的键值。比如组合键是1+2+3+4+5,那么用户按下1和2的时候应该是在检测有效的,但是本应该按下3的时候用户按下了6,那么不对应组合键的位置了,应该是无效的输入了。还有,总不能一直在监听用户接下来的组合输入吧,比如用户现在按下了1和2,那么按照我们的代码流程是继续探测用户是否输入3,但是用户要是过了一两个小时之后再输入呢?所以我们还需要探测用户输入按键的时间,来决定要不要继续响应组合键的探测。我们把上述这个过程的方法叫做boolean checkKey(int keycode,long eventTime);吧,第一个参数代表了键值,第二个代表了事件的时间。

  然后,我们应该要有个方法,来判断是否已经完成了组合键的输入了吧?这里我们把这个方法叫做boolean checkMultKey();

  这个过程中,我们也应该提供一个清除掉输入的方法吧,叫做void clearKeys();

  最后,还要提供一个触发方法,就叫做onTrigger();吧

  基于以上的分析,编写出了接口IMultKeyTrigger:

/**
 * 组合按键触发功能的接口
 *
 */
public interface IMultKeyTrigger {
    /**
     * 是否允许触发,也就是触发组合键的条件
     * @return
     */
    boolean allowTrigger();
    /**
     * 检查输入的按键是否是对应组合键某个位置
     * @param keycode
     * @param eventTime
     * @return
     */
    boolean checkKey(int keycode,long eventTime);
    /**
     * 检查组合键是否已经输入完成
     * @return
     */
    boolean checkMultKey();
    /**
     * 清除所有记录的键
     */
    void clearKeys();
    /**
     * 组合键触发事件
     */
    void onTrigger();
}

  在这里我们写一个具体实现类,实现按下1-5键的时候就自动弹出提示框。
  在实现类里面我们先定义几个变量:

//组合键序列
private final static int[] MULT_KEY = new int[]{1,2,3,4,5};

//是否限定要在时间间隔里再次输入按键
private static boolean ALLAW_SETTING_DELAYED_FLAG= true;

//允许用户在多少时间间隔里输入按键
private static int CHECK_NUM_ALLAW_MAX_DELAYED = 10000;

//记录用户连续输入了多少个有效的键
private static int check_num = 0;

//最后一次用户输入按键的时间
private static long lastEventTime = 0;

  重写第一个方法, allowTrigger(),为了方便,我们直接让它返回true,表示任意条件下都允许触发。

  重写checkKey方法,代码如下:

@Override
    public boolean checkKey(int keycode, long eventTime) {
        // TODO Auto-generated method stub
        boolean check;
        int delayed;
        //转换为实际数值
        int num = keycode - KeyEvent.KEYCODE_0;
        Log.i(TAG, "checkKey lastEventTime="+lastEventTime);
        Log.i(TAG, "checkKey num= "+num+" , eventTime = "+eventTime);
        //首次按键
        if(lastEventTime==0){
            delayed = 0;
        }else{
            //非首次按键
            delayed = (int)(eventTime-lastEventTime);
        }
        check = checkKeyValid(num, delayed);

        lastEventTime = check?eventTime:0L;

        Log.i(TAG, "checkKey check key valid = "+check);
        return check;
    }

  代码首先是让键值码转换为实际数值,比如键值码是KeyEvent.KEYCODE_2,那么减去KeyEvent.KEYCODE_0得出的num便为2.
然后判断是首次按键的话则delayed 为 0,不是的话则取与上一次按键的时间间隔。

  然后再调用checkKeyValid方法,如果返回true则更新最后一次按键的时间,否则重置为0。因为如果返回false代表按键无效,则要重置。
checkKeyValid是写在实现类里面的私有方法,源码如下:

/**
     * 传入用户输入的按键
     * @param num
     * @param delayed 两次按键之间的时间间隔
     * @return
     */
    private  boolean checkKeyValid(int num,int delayed){
        Log.i(TAG, "checkKey num= "+num+" , delayed = "+delayed);
        //如果超过最大时间间隔,则重置
        if(ALLAW_SETTING_DELAYED_FLAG&& delayed>CHECK_NUM_ALLAW_MAX_DELAYED){
            check_num = 0;
            return false;
        }
        //如果输入的数刚好等于校验位置的数,则有效输入+1
        if(check_num<MULT_KEY.length&&MULT_KEY[check_num]==num){
            check_num++;
            return true;
        }else{
            check_num = 0;//如果输入错误的话,则重置掉原先输入的
        }
        return false;
    }

  checkKeyValid传入的第二个参数是时间间隔,如果是首个按键的话则是0。

  然后先判断时间间隔是否有效(也就是如果是允许设置时间间隔,并且时间间隔小于我们设置的最大时间间隔,才有效)。

  其次判断如果这个数值对应我们探测组合键的序列的话则check_num++;表示接下来探测下一个位置,并返回true,表示此次输入按键有效。

  重写checkMultKey()方法,很显然。只需要满足已经检测完的个数是等于组合键的个数的话,说明用户已经输入完了有效的组合键。源码如下:

@Override
    public boolean checkMultKey() {
        // TODO Auto-generated method stub
        return check_num == MULT_KEY.length;
    }

  重写clearKeys()方法,其实在这里需要重置的变量就是lastEventTime和check_num。代码如下:

@Override
    public void clearKeys() {
        // TODO Auto-generated method stub
        lastEventTime = 0;
        check_num = 0;
    }

  最后重写一个onTrigger(),就是执行触发的方法。在这里我们测试弹出一个提示框,源码如下:

@Override
    public void onTrigger() {
        // TODO Auto-generated method stub
        //只有完成组合键的输入后才能触发弹出提示框
        if(checkMultKey()){
            startAlert();
        }else{
        //否则不能随便就调用触发
            throw new RuntimeException("you must be sure checkMultKey() is return true.");
        }
    }
    private void startAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle("测试");
        builder.setMessage("组合键触发了");
        builder.show();
    }

  看到上面弹出提示框需要context,我们在构造方法里传入context即可。

  这样,我们就已经把实现类做好了,让我们在Activity上面监听按键,来完成我们的组合键监听吧。
创建一个上面我们写的接口实现类:

private IMultKeyTrigger  multKeyTrigger= new MyMultKeyTrigger(this);

  在Activity的onKeyDown方法里监听数字按键1-9:

@Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        Log.d(TAG, "keyCode:" + keyCode);
             if(handlerMultKey(keyCode, event)){
                return true;
            }
        return super.onKeyDown(keyCode, event);
}
public boolean handlerMultKey(int keyCode, KeyEvent event) {
        boolean vaildKey = false;
        if (keyCode >= KeyEvent.KEYCODE_0
                && keyCode <= KeyEvent.KEYCODE_9 && multKeyTrigger.allowTrigger()) {
            // 是否是有效按键输入
            vaildKey = multKeyTrigger.checkKey(keyCode, event.getEventTime());
            // 是否触发组合键
            if (vaildKey && multKeyTrigger.checkMultKey()) {
                    //执行触发
                    multKeyTrigger.onTrigger();
                    //触发完成后清除掉原先的输入
                    multKeyTrigger.clearKeys();

            }

        }

  在这里我们就编写完成了,由于比较简单,就不过给出运行截图了。
最后,贴上实现类的源码吧:

public class MyMultKeyTrigger implements IMultKeyTrigger {
    private static String TAG =MyMultKeyTrigger.class.getSimpleName();

    //组合键序列
    private final static int[] MULT_KEY = new int[]{1,2,3,4,5};


    //是否限定要在时间间隔里再次输入按键
    private static boolean ALLAW_SETTING_DELAYED_FLAG = true;

    //允许用户在多少时间间隔里输入按键
    private static int CHECK_NUM_ALLAW_MAX_DELAYED = 10000;

    //记录用户连续输入了多少个有效的键
    private static int check_num = 0;

    private static long lastEventTime = 0;//最后一次用户输入按键的时间

    public Context context;

    public MyMultKeyTrigger(Context context){
        this.context = context;
    }
    @Override
    public boolean allowTrigger() {
        // TODO Auto-generated method stub
        return true;
    }
    @Override
    public boolean checkKey(int keycode, long eventTime) {
        // TODO Auto-generated method stub
        boolean check;
        int delayed;
        //转换为实际数值
        int num = keycode - KeyEvent.KEYCODE_0;
        Log.i(TAG, "checkKey lastEventTime="+lastEventTime);
        Log.i(TAG, "checkKey num= "+num+" , eventTime = "+eventTime);
        //首次按键
        if(lastEventTime==0){
            delayed = 0;
        }else{
            //非首次按键
            delayed = (int)(eventTime-lastEventTime);
        }
        check = checkKeyValid(num, delayed);

        lastEventTime = check?eventTime:0L;

        Log.i(TAG, "checkKey check key valid = "+check);
        return check;
    }
    /**
     * 传入用户输入的按键
     * @param num
     * @param delayed 两次按键之间的时间间隔
     * @return
     */
    private  boolean checkKeyValid(int num,int delayed){
        Log.i(TAG, "checkKey num= "+num+" , delayed = "+delayed);
        //如果超过最大时间间隔,则重置
        if(ALLAW_SETTING_DELAYED_FLAG && delayed>CHECK_NUM_ALLAW_MAX_DELAYED){
            check_num = 0;
            return false;
        }
        //如果输入的数刚好等于校验位置的数,则有效输入+1
        if(check_num<MULT_KEY.length&&MULT_KEY[check_num]==num){
            check_num++;
            return true;
        }else{
            check_num = 0;//如果输入错误的话,则重置掉原先输入的
        }
//      return check_num==MULT_KEY_RESTART.length;
        return false;
    }
    @Override
    public void clearKeys() {
        // TODO Auto-generated method stub
        lastEventTime = 0;
        check_num = 0;
    }

    @Override
    public boolean checkMultKey() {
        // TODO Auto-generated method stub
        return check_num == MULT_KEY.length;
    }

    @Override
    public void onTrigger() {
        // TODO Auto-generated method stub
        if(checkMultKey()){
            startAlert();
        }else{
            //throw new RuntimeException("you must be sure checkMultKey() is return true.");
        }
    }
    private void startAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle("测试");
        builder.setMessage("组合键触发了");
        builder.show();

    }
}

  可能有读者觉得,干嘛要编写个接口呢,直接写上面这个类也可以的。其实在项目开发里面,提倡的是面向接口编程或者说抽象编程,这样带来的好处是已扩展,易维护。比如上面的需求:在特定条件下,触发的是另外一个组合按键,就是按下5-9键后触发启动一个界面。那么我们只需要实现这个接口,在activity里面,不需要更改源码,只需要在创建对象的时候,根据条件而选择创建某个实现类,这就是接口带来的多态的好处。