Android按键事件及手势事件(一)

2023-02-05  本文已影响0人  古早味蛋糕
一、按键事件

App开发对按键事件的检测与处理,主要包括如何检测控件对象的按键事件、如何检测活动页面的物理按键、以返回键为例说明“再按一次返回键退出”的功能实现

1、检测软键盘

手机上的输入按键一般不另外处理,直接由系统按照默认情况操作。有时为了改善用户体验,需要让App拦截按键事件,并进行额外处理。譬如使用编辑框有时要监控输入字符中的回车键,一旦发现用户敲了回车键,就将焦点自动移到下一个控件,而不是在编辑框中输入回车换行。拦截输入字符可通过注册文本观测器TextWatcher实现,但该监听器只适用于编辑框控件,无法用于其他控件。因此,若想让其他控件也能监听按键操作,则要另外调用控件对象的setOnKeyListener方法设置按键监听器,并实现监听器接口OnKeyListener的onKey方法。
监控按键事件之前,首先要知道每个按键的编码,这样才能根据不同的编码值进行相应的处理。按键编码的取值说明见表2-1。注意,监听器OnKeyListener只会检测控制键,不会检测文本键(字母、数字、标点等)。

按键编码的取值说明.png
实际监控结果显示,每次按下控制键时,onKey方法都会收到两次重复编码的按键事件,这是因为该方法把每次按键都分成按下与松开两个动作,所以一次按键变成了两个按键动作。解决这个问题的办法很简单,就是只监控按下动作(KeyEvent.ACTION_DOWN)的按键事件,不监控松开动作(KeyEvent.ACTION_UP)的按键事件。
部分代码示例:(完整代码KeySoftActivity)
// 在发生按键动作时触发
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        desc = String.format("%s软按键编码是%d,动作是按下", desc, keyCode);
        if (keyCode == KeyEvent.KEYCODE_ENTER) {
            desc = String.format("%s,按键为回车键", desc);
        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
            desc = String.format("%s,按键为删除键", desc);
        } else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
            desc = String.format("%s,按键为搜索键", desc);
        } else if (keyCode == KeyEvent.KEYCODE_BACK) {
            desc = String.format("%s,按键为返回键", desc);
            // 延迟3秒后启动页面关闭任务
            new Handler(Looper.myLooper()).postDelayed(() -> finish(), 3000);
        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
            desc = String.format("%s,按键为加大音量键", desc);
        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
            desc = String.format("%s,按键为减小音量键", desc);
        }
        desc = desc + "\n";
        tv_result.setText(desc);
        // 返回true表示处理完了不再输入该字符,返回false表示给你输入该字符吧
        return true;
    } else {
        // 返回true表示处理完了不再输入该字符,返回false表示给你输入该字符吧
        return false;
    }
}

虽然按键编码表存在主页键、任务键、电源键的定义,但这3个键并不开放给普通App,普通App也不应该拦截这些按键事件。


软键盘的检测结果.png
2、检测物理按键

除了给控件注册按键监听器外,还可以在活动页面上检测物理按键,即重写Activity的onKeyDown方法。onKeyDown方法与前面的onKey方法类似,同样拥有按键编码与按键事件KeyEvent两个参数。当然,这两个方法也存在不同之处,具体说明如下:
(1)onKeyDown只能在活动代码中使用,而onKey只要有可注册的控件就能使用。
(2)onKeyDown只能检测物理按键,无法检测输入法按键(如回车键、删除键等),onKey可同时检测两类按键。
(3)onKeyDown不区分按下与松开两个动作,onKey区分这两个动作。
部分代码:

// 在发生物理按键动作时触发
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    desc = String.format("%s物理按键的编码是%d",desc,keyCode);
    if (keyCode == KeyEvent.KEYCODE_BACK){
        desc = String.format("%s,按键为返回键",desc);
        // 延迟3秒后启动页面关闭任务
        new Handler(Looper.myLooper()).postDelayed(() -> finish(),3000);
    }else  if (keyCode == KeyEvent.KEYCODE_VOLUME_UP){
        desc = String.format("%s,按键为加大音量键", desc);
    }else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN){
        desc = String.format("%s,按键为减小音量键", desc);
    }
    desc = desc +"\n";
    tv_result.setText(desc);
    return true;// 返回true表示不再响应系统动作,返回false表示继续响应系统动作
}

分别检测到了加大音量键、减小音量键、返回键。


物理按键检测.png
3、接管返回按键

检测物理按键最常见的应用是淘宝首页的“再按一次返回键退出”,在App首页按返回键,系统默认的做法是直接退出该App。有时用户有可能是不小心按了返回键,并非想退出该App,因此这里加一个小提示,等待用户再次按返回键才会确认退出意图,并执行退出操作。“再按一次返回键退出”的实现代码很简单,在onKeyDown方法中拦截返回键即可,完整码:BackPressActivity具体代码如下:

// 在发生物理按键动作时触发
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) { // 按下返回键
        if (needExit) {
            finish(); // 关闭当前页面
        }
        needExit = true;
        Toast.makeText(this, "再按一次返回键退出!", Toast.LENGTH_SHORT).show();
        return true;
    } else {
        return super.onKeyDown(keyCode, event);
    }
}

重写活动代码的onBackPressed方法也能实现同样的效果,该方法专门响应按返回键事件,具体代码如下:

@Override
public void onBackPressed() {
    if (needExit){
        finish();//关闭当前页面
        return;
    }
    needExit = true;
    Toast.makeText(this,"再按一次返回键退出!",Toast.LENGTH_LONG).show();
}

效果如图所示:

再按一次返回键退出”的提示窗口.png
二、触摸事件

屏幕触摸事件的相关处理,内容有:手势事件的分发流程,包括3个手势方法、3类手势执行者、派发与拦截处理;手势事件的具体用法(包括单点触摸和多点触控);一个手势触摸的具体应用——手写签名功能的实

1、手势事件的分发流程

Android现在可自动识别特定的几种触摸手势,包括按钮的点击事件、长按事件、滚动视图的上下滚动事件、翻页视图的左右翻页事件等。不过对于App的高级开发来说,系统自带的几个固定手势显然无法满足丰富多变的业务需求。这就要求开发者深入了解触摸行为的流程与方法,并在合适的场合接管触摸行为,进行符合需求的事件处理。
与手势事件有关的方法主要有3个(按执行顺序排列),分别说明如下:
● dispatchTouchEvent:进行事件分发处理,返回结果表示该事件是否需要分发。默认返回true表示分发给子视图,由子视图处理该手势,不过最终是否分发成功还得根据onInterceptTouchEvent方法的拦截判断结果;返回false表示不分发,此时必须实现自身的onTouchEvent方法,否则该手势将不会得到处理。
● onInterceptTouchEvent:进行事件拦截处理,返回结果表示当前容器是否需要拦截该事件。返回true表示予以拦截,该手势不会分发给子视图,此时必须实现自身的onTouchEvent方法,否则该手势将不会得到处理;默认返回false表示不拦截,该手势会分发给子视图进行后续处理。
● onTouchEvent:进行事件触摸处理,返回结果表示该事件是否处理完毕。返回true表示处理完毕,无须处理上一级视图的onTouchEvent方法,一路返回结束流程;返回false表示该手势事件尚未完成,返回继续处理上一级视图的onTouchEvent方法,然后根据上一级onTouchEvent方法的返回值判断直接结束或由上上一级处理。
上述手势方法的执行者有3个(按执行顺序排列),具体说明如下:
● 页面类:包括Activity及其派生类。页面类可调用dispatchTouchEvent和onTouchEvent两个方法。
● 容器类:包括从ViewGroup类派生出的各类容器,如各种布局Layout和ListView、GridView、Spinner、ViewPager、RecyclerView、Toolbar等。容器类可调用dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法。
● 控件类:包括从View类派生的各类控件,如TextView、ImageView、Button等。控件类可调用dispatchTouchEvent和onTouchEvent两个方法。

只有容器类才能调用onInterceptTouchEvent方法,这是因为该方法用于拦截发往下层视图的事件,而控件类已经位于底层,只能被拦截,不能拦截别人。页面类没有下层视图,所以不能调用onInterceptTouchEvent方法。三类执行者的手势处理流程如图所示


三类执行者的手势处理流程.png

以上流程图涉及3个手势方法和3种手势执行者,尤其是手势流程的排列组合千变万化,并不容易解释清楚。对于实际开发来说,真正需要处理的组合并不多,所以只要把常见的几种组合搞清楚就能应付大部分开发工作,这几种组合说明如下。
(1)页面类的手势处理。它的dispatchTouchEvent方法必须返回super.dispatchTouchEvent,如果不分发,页面上的视图就无法处理手势。至于页面类的onTouchEvent方法,基本没有什么作用,因为手势动作要由具体视图处理,页面直接处理手势没有什么意义。所以,页面类的手势处理可以不用关心,直接略过。
(2)控件类的手势处理。它的dispatchTouchEvent方法没有任何作用,因为控件下面没有子视图,无所谓分不分发。至于控件类的onTouchEvent方法,如果要进行手势处理,就需要自定义一个控件,重写自定义类中的onTouchEvent方法;如果不想自定义控件,就直接调用控件对象的setOnTouchListener方法,注册一个触摸监听器OnTouchListener,并实现该监听器的onTouch方法。所以,控件类的手势处理只需关心onTouchEvent方法。
(3)容器类的手势处理。这才是真正要深入了解的地方。容器类的dispatchTouchEvent与onInterceptTouchEvent方法都能决定是否将手势交给子视图处理。为了避免手势响应冲突,一般要重写dispatchTouchEvent或者onInterceptTouchEvent方法。两个方法的区别可以这么理解:前者是大领导,只管派发任务,不会自己做事情;后者是小领导,尽管有拦截的权利,不过也得自己做点事情,比如处理纠纷等。容器类的onTouchEvent方法近乎摆设,因为需要拦截的在前面已经拦截了,需要处理的在子视图已经处理了。
经过上面的详细分析,常见的手势处理方法有下面3种:
● 页面类的dispatchTouchEvent方法:控制事件的分发,决定把手势交给谁处理。
● 容器类的onInterceptTouchEvent方法:控制事件的拦截,决定是否要把手势交给子视图处理。
● 控件类的onTouchEvent方法:进行手势事件的具体处理。

为方便理解dispatchTouchEvent方法,先看下面不派发事件的自定义布局代码:

public class NotDispatchLayout extends LinearLayout {
    public NotDispatchLayout(Context context) {
        super(context);
    }
    public NotDispatchLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    // 在分发触摸事件时触发
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mListener != null) {
            mListener.onNotDispatch();
        }
        // 一般容器默认返回true,即允许分发给下级
        return false;
    }

    private NotDispatchListener mListener; // 声明一个分发监听器对象
    // 设置分发监听器
    public void setNotDispatchListener(NotDispatchListener listener) {
        mListener = listener;
    }

    // 定义一个分发监听器接口
    public interface NotDispatchListener {
        void onNotDispatch();
    }
    
}

活动页面实现的onNotDispatch方法代码如下完整代码:EventDispatchActivity

// 在分发触摸事件时触发
@Override
public void onNotDispatch() {
    desc_no = String.format("%s%s 触摸动作未分发,按钮点击不了了\n"
            , desc_no, DateUtil.getNowTime());
    tv_dispatch_no.setText(desc_no);
}

不派发事件的处理效果如下图所示。图的上面部分为正常布局,此时按钮可正常响应点击事件;图的下面部分为不派发布局,此时按钮不会响应点击事件,取而代之的是执行不派发布局的onNotDispatch方法


分发事件.png

为方便理解onInterceptTouchEvent方法,再看拦截事件的自定义布局代码:

public class InterceptLayout extends LinearLayout {

    public InterceptLayout(Context context) {
        super(context);
    }

    public InterceptLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 在拦截触摸事件时触发
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mListener != null) {
            mListener.onIntercept();
        }
        // 一般容器默认返回false,即不拦截。但滚动视图会拦截
        return true;
    }

    private InterceptListener mListener; // 声明一个拦截监听器对象
    // 设置拦截监听器
    public void setInterceptListener(InterceptListener listener) {
        mListener = listener;
    }

    // 定义一个拦截监听器接口
    public interface InterceptListener {
        void onIntercept();
    }

}

活动页面实现的onIntercept方法代码如下:

// 在拦截触摸事件时触发
@Override
public void onIntercept() {
    desc_yes = String.format("%s%s 触摸动作被拦截,按钮点击不了了\n", desc_yes,
            DateUtil.getNowTime());
    tv_intercept_yes.setText(desc_yes);
}

拦截事件的处理效果如下图所示。下图的上面部分为正常布局,此时按钮可正常响应点击事件;下图的下面部分为拦截布局,此时按钮不会响应点击事件,取而代之的是执行拦截布局的onIntercept方法。


拦截事件.png
2、接管手势事件处理

dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法的输入参数都是手势事件MotionEvent,其中包含触摸动作的所有信息,各种手势操作都从MotionEvent中获取触摸信息并判断处理。
下面是MotionEvent的常用方法:
● getAction:获取当前的动作类型。动作类型的取值说明见下表:

动作类型的取值说明.png
● getEventTime:获取事件时间(从开机到现在的毫秒数)。
● getX:获取在控件内部的相对横坐标。
● getY:获取在控件内部的相对纵坐标。
● getRawX:获取在屏幕上的绝对横坐标。
● getRawY:获取在屏幕上的绝对纵坐标。
● getPressure:获取触摸的压力大小。
● getPointerCount:获取触控点的数量,如果为2就表示有两个手指同时按压屏幕。如果触控点数目大于1,坐标相关方法就可以输入整数编号,表示获取第几个触控点的坐标信息。
为方便理解MotionEvent的各类触摸行为,下面是单点触摸的示例代码完整代码TouchSingleActivity
@SuppressLint("DefaultLocale")
public class TouchSingleActivity extends AppCompatActivity {
    private TextView tv_touch; // 声明一个文本视图对象
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_touch_single);
        tv_touch = findViewById(R.id.tv_touch);
    }

    // 在发生触摸事件时触发
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 从开机到现在的毫秒数
        int seconds = (int) (event.getEventTime() / 1000);
        String desc = String.format("动作发生时间:开机距离现在%02d:%02d:%02d",
                seconds / 3600, seconds % 3600 / 60, seconds % 60);
        desc = String.format("%s\n动作名称是:", desc);
        int action = event.getAction(); // 获得触摸事件的动作类型
        if (action == MotionEvent.ACTION_DOWN) { // 按下手指
            desc = String.format("%s按下", desc);
        } else if (action == MotionEvent.ACTION_MOVE) { // 移动手指
            desc = String.format("%s移动", desc);
        } else if (action == MotionEvent.ACTION_UP) { // 松开手指
            desc = String.format("%s提起", desc);
        } else if (action == MotionEvent.ACTION_CANCEL) { // 取消手势
            desc = String.format("%s取消", desc);
        }
        desc = String.format("%s\n动作发生位置是:横坐标%f,纵坐标%f,压力为%f",
                desc, event.getX(), event.getY(), event.getPressure());
        tv_touch.setText(desc);
        return super.onTouchEvent(event);

    }
}

单点触摸的效果如图所示。下图1为手势按下时的检测结果,图2为手势移动时的检测结果,图3为手势提起时的检测结果

手势按下时的检测结果.png
手势移动时的检测结果.png
手势提起时的检测结果.png
除了单点触摸,智能手机还普遍支持多点触控,即响应两个及以上手指同时按压屏幕。多点触控可用于操纵图像的缩放与旋转操作以及需要多点处理的游戏界面,下面是处理多点触控的示例代码完整代码TouchMultipleActivity
// 在发生触摸事件时触发
@Override
public boolean onTouchEvent(MotionEvent event) {
    // 从开机到现在的毫秒数
    int seconds = (int) (event.getEventTime() / 1000);
    String desc_major = String.format("主要动作发生时间:开机距离现在%02d:%02d:%02d\n%s",
            seconds / 3600, seconds % 3600 / 60, seconds % 60, "主要动作名称是:");
    String desc_minor = "";
    isMinorDown = (event.getPointerCount() >= 2);
    // 获得包括次要点在内的触摸行为
    int action = event.getAction() & MotionEvent.ACTION_MASK;
    if (action == MotionEvent.ACTION_DOWN) { // 按下手指
        desc_major = String.format("%s按下", desc_major);
    } else if (action == MotionEvent.ACTION_MOVE) { // 移动手指
        desc_major = String.format("%s移动", desc_major);
        if (isMinorDown) {
            desc_minor = String.format("%s次要动作名称是:移动", desc_minor);
        }
    } else if (action == MotionEvent.ACTION_UP) { // 松开手指
        desc_major = String.format("%s提起", desc_major);
    } else if (action == MotionEvent.ACTION_CANCEL) { // 取消手势
        desc_major = String.format("%s取消", desc_major);
    } else if (action == MotionEvent.ACTION_POINTER_DOWN) { // 次要点按下
        desc_minor = String.format("%s次要动作名称是:按下", desc_minor);
    } else if (action == MotionEvent.ACTION_POINTER_UP) { // 次要点松开
        desc_minor = String.format("%s次要动作名称是:提起", desc_minor);
    }
    desc_major = String.format("%s\n主要动作发生位置是:横坐标%f,纵坐标%f",
            desc_major, event.getX(), event.getY());
    tv_touch_major.setText(desc_major);
    if (isMinorDown || !TextUtils.isEmpty(desc_minor)) { // 存在次要点触摸
        desc_minor = String.format("%s\n次要动作发生位置是:横坐标%f,纵坐标%f",
                desc_minor, event.getX(1), event.getY(1));
        tv_touch_minor.setText(desc_minor);
    }
    return super.onTouchEvent(event);
}

多点触控的效果如下图所示,图1为两个手指一起移动时的检测结果,图2为两个手指一齐提起时的检测结果


 两个手指一齐移动时的检测结果.png
 两个手指一齐提起时的检测结果.png
3、跟踪滑动轨迹实现手写签名

手写签名的原理是把手机屏幕当作画板,把用户手指当作画笔,手指在屏幕上划来划去,屏幕就会显示手指的移动轨迹,就像画笔在画板上写字一样。实现手写签名需要结合绘图的路径工具Path,具体的实现步骤说明如下:
(1)按下手指时,调用Path对象的moveTo方法,将路径起点移到触摸点。
(2)移动手指时,调用Path对象的quadTo方法,记录本次触摸点与上次触摸点之间的路径。
(3)移动手指或者手指提起时,调用Canvas对象的drawPath方法,将本次触摸轨迹绘制在画布上。
于是重写自定义触摸视图的onTouchEvent方法,分别处理按下、移动、松开三种手势事件;同时重写该视图的onDraw方法,描绘起点与终点的位置,以及从起点到终点的路径线条。
按照上述思路,编写单指触摸视图的部分代码SignatureView完整代码

// 在发生触摸事件时触发
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: // 按下手指
            mPath.moveTo(event.getX(), event.getY()); // 移动到指定坐标点
            mPathPos.prePos = new PointF(event.getX(), event.getY());
            break;
        case MotionEvent.ACTION_MOVE: // 移动手指
            // 连接上一个坐标点和当前坐标点
            mPath.quadTo(mLastPos.x, mLastPos.y, event.getX(), event.getY());
            mPathPos.nextPos = new PointF(event.getX(), event.getY());
            mPathList.add(mPathPos); // 往路径位置列表添加路径位置
            mPathPos = new PathPosition(); // 创建新的路径位置
            mPathPos.prePos = new PointF(event.getX(), event.getY());
            break;
        case MotionEvent.ACTION_UP: // 松开手指
            // 连接上一个坐标点和当前坐标点
            mPath.quadTo(mLastPos.x, mLastPos.y, event.getX(), event.getY());
            break;
    }
    mLastPos = new PointF(event.getX(), event.getY());
    postInvalidate(); // 立即刷新视图(线程安全方式)
    return true;
}

手写签名的效果如下图所示,示例完整代码请点击

签名完成的画面.png
上一篇下一篇

猜你喜欢

热点阅读