扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
DecorView →PhoneWindow →Activity→ViewGroup→view
创新互联专注于望江企业网站建设,响应式网站建设,商城网站制作。望江网站建设公司,为望江等地区提供建站服务。全流程定制网站,专业设计,全程项目跟踪,创新互联专业和态度为您提供的服务
下面我们根据按键事件的分发流程,抽丝剥茧,逐一分析。
private int processKeyEvent(QueuedInputEvent q)
1、DecorView.java
2、Activity.java
3、ViewGroup.java
4、View.java
通过该方法,接收器receiver的onKeyDown、onKeyUp、onKeyLongPress、onKeyMultiple等方法将被回调。
在上述按键事件的入口中提到的ViewRootImpl中
如果mView.dispatchKeyEvent(event)返回true,则结束事件分发;
如果返回false,则调用如下方法
继续执行后续的焦点导航流程。
焦点导航的总体流程就是:
1、View focused = mView.findFocus();//从视图树的顶层,即DecorView一层一层的递归查找当前获得焦点的view
2、View v = focused.focusSearch(direction);根据导航的方向查找下一个可获取焦点的view
3、v.requestFocus(direction, mTempRect)请求获取焦点
4、v.requestFocus(direction,mTempRect)内部,调用mParent.requestChildFocus(this, focused)逐层递归向上级通知
ViewRootImpl.java
mView即DecorView,从DecorView开始,一层一层的向下递归查找当前获得焦点的view
找到了当前获得焦点的focused,调用该焦点view的focusSearch(direction)方法查找direction方向上下一个将要获取焦点的view。
focused.focusSearch(direction)实际上会调用mParent.focusSearch(this, direction)方法,层层递归,直到调用到DecorView的focusSearch(this, direction)方法。
而DecorView继承ViewGroup,实际上最终会调用到FocusFinder.getInstance().findNextFocus(this, focused, direction),this 就是DecorView对象。
最终会调用到DecorView父类ViewGroup中的FocusFinder.getInstance().findNextFocus(this, focused, direction);
ViewGroup.java
FocusFinder.java
搜索到下一个获取焦点的view后,调用该view.requestFocus(direction, mTempRect)方法
注意:调用requestFocus(direction, mTempRect)需要区分调用者。
如果是ViewGroup,则会更加焦点获取策略,实现父View和子View之间获取焦点的优先级。
如下是ViewGroup.java 和View.java 中requestFocus方法是实现:
ViewGroup.java
View.java
View获取到焦点后,会调用mParent.requestChildFocus(this, focused)逐层递归向上级通知
ViewGroup.java
Android自定义键盘的使用
1、新建一个xml文件夹放在res目录下面,然后新建xml文件:money_keyboard.xml
2、然后在XML文件中添加按钮布局,这个布局就是键盘的样子了
3 属性介绍:
Keyboard:
存储键盘以及按键相关信息。
android:horizontalGap
按键之间默认的水平间距。
android:verticalGap
按键之间默认的垂直间距。
android:keyHeight
按键的默认高度,以像素或显示高度的百分比表示。
android:keyWidth:
按键的默认宽度,以像素或显示宽度的百分比表示。
Row:
为包含按键的容器。
Key:
用于描述键盘中单个键的位置和特性。
android:codes
该键输出的unicode值。
android:codes 官网介绍是说这个是该键的unicode 值或者逗号分隔值,当然我们也可以设置成我们想要的值,在源码中提供了几个特定的值
对照表:
android:isRepeatable
这个属性如果设置为true,那么当长按该键时就会重复接受到该键上的动作,在 删除键键 和 空格键 上通常设为true。
android:keyLabel
显示在按键上的文字。
android:keyIcon 与 keyLabel
是二选一关系,它会代替文字以图标的形式显示在键上。
android:keyWidth="33.33333%p"
每一个按钮的宽度,可以设置百分比
android:keyHeight="10%p"
每一个按钮高度,可以设置百分比
KeyboardView是一个渲染虚拟键盘的View。 它处理键的渲染和检测按键和触摸动作。
显然我们需要KeyboardView来对Keyboard里的数据进行渲染并呈现给我们以及相关的点击事件做处理。 1)//设置keyboard与KeyboardView相关联的方法。
public void setKeyboard(Keyboard keyboard)
2)//设置虚拟键盘事件的监听,此方法必须设置,不然会报错。
public void setOnKeyboardActionListener(OnKeyboardActionListener listener) 步骤上呢,做完第一步的关联,并设置第二步的事件,调用KeyboardView.setVisible(true);键盘就可以显示出来了, 是不是很简单。不过到这里还没有结束哦,接下来我们为了使用上的便利要进行相应的封装。 封装 这里我们通过继承EditText来对Keyboard与KeyboardView进行封装。
attr.xml文件,这里我们需要通过一个xml类型的自定义属性引入我们的键盘描述文件。
1、新建一个类,我取名叫KeyUtils然后在里面新建三个属性。KeyBoard用处可大了,他才是本体,可以通过设置他来切换键盘。
2、构造函数,初始下三个参数。
3、先说下预览图吧,就是效果图上的预览图,需要预览图的话的将setPreviewEnabled设置为true,不过还得在布局文件中的android.inputmethodservice.KeyboardView标签对立面设置预览布局。否则,不会有字。至于设置的布局,一个TextView就好了~
onPress: 按下触发。
onRelease:松开触发。
onKey : 松开触发,在OnRelease之前触发。
swipeLeft : 左滑动,其他同理。哈哈~就这么懒。
onText :需要在 键盘xml,也就是我此时的number.xml里面中key标签对里添加一个
对应android6.1,framework添加按键
首先看 KeyEvent 里的一段注释
\frameworks\base\core\java\android\view\KeyEvent.java
可以看到修改涉及到的文件:
frameworks/native/include/android/keycodes.h
frameworks/native/include/input/InputEventLabels.h
frameworks/base/core/res/res/values/attrs.xml
以及KeyEvent.java
另外还有一个文件是
\frameworks\base\data\keyboards\Generic.kl
手机里的位置为
/system/usr/keylayout/Generic.kl
PS : 从android4.0开始使用Generic.kl 替换了 qwerty.kl,后续版本不再使用qwerty.kl。
接下来以添加新按键为例:
假设驱动已添加对应按键0x2f8
1. \frameworks\base\data\keyboards\Generic.kl
添加新键值和对应字符串
其中0x2f8 是驱动上报的扫描键值, CHARG_STATUS 是我们自己定义的唯一字符串
2. frameworks/native/include/input/InputEventLabels.h
在 KEYCODES[] 中添加
其中DEFINE_KEYCODE是一个宏定义
将上面的宏展开就是 { CHARG_STATUS, AKEYCODE_CHARG_STATUS }
其中CHARG_STATUS对应上面定制的字符串
3. frameworks/native/include/android/keycodes.h
在该文件定义新的键值
注意键值的名字跟上一步步添加的宏展开后的名字一致,值280就是应用层接收到的keycode
4. 若有需要可重写 KeyEvent.java 中的方法,以及 attrs.xml
从上述文件可以猜到键值转化流程:
0x2f8----CHARG_STATUS---AKEYCODE_CHARG_STATUS (280)
PS :
1.调试可打开以下库文件的开关
\frameworks\native\libs\input
\frameworks\native\services\inputflinger
2. adb shell dumpsys input 查看现有输入系统
3. adb shell getevent 可查看现有的输入事件
4.在/system/usr/keylayout中还有很多Vendor_xxxx_Product_xxxx.kl 之类的配置文件,但是我们没有配置对应的vend id等,所以一直使用默认的Generic.kl。
android手机长按home键能起作用的作用有以下几类:
Home键可以说是我们每天使用最多的功能之一,一般我们使用这个按键多数是用于返回主页或调出语音助手等等。
各个版本的安卓,常按home键,屏幕会出现最近是用过的app程序图标。
对于安卓4.1来说,则更加明细,即不仅会出现最近用过的aop还会出现,正在后台运行的程序,一个简单的小窗口即可迅速切换到后台程序。
简单的理解就是,手机home键就是菜单键和主键。
* Android常用的物理按键及其触发事件
* KEYCODE_POWER 电源键
* KEYCODE_MENU 菜单键
* KEYCODE_BACK 后退键
* KEYCODE_HOME Home键
* KEYCODE_CAMERA 相机键
* KEYCODE_SEARCH 查找键
* KEYCODE_VOLUME_UP 音量键+
* KEYCODE_VOLUME_DOWN 音量键-
* KEYCODE_VOLUME_MUTE 静音
* 方向键
* KEYCODE_DPAD_CENTER
* KEYCODE_DPAD_UP
* KEYCODE_DPAD_DOWN
* KEYCODE_DPAD_LEFT
* KEYCODE_DPAD_RIGHT
* 键盘键
* 数字0~9 字母A~Z
* KEYCODE_0 ~ KEYCODE_9
* KEYCODE_A ~ KEYCODE_Z
* 提供的回调方法有
* onKeyUp()、OnKeyDown()、onKeyLongPress()
*
* @author Administrator
*
*/
public class MainActivity extends Activity {
private Button btnClose = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnClose = (Button) findViewById(R.id.btnClose);
btnClose.setOnClickListener(new closelistener());
}
class closelistener implements OnClickListener {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
finish();
}
}
/**
* 重写onKeyDown方法可以拦截系统默认的处理
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
if (keyCode == KeyEvent.KEYCODE_BACK) {
Toast.makeText(this, "后退键", Toast.LENGTH_SHORT).show();
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
Toast.makeText(this, "声音+", Toast.LENGTH_SHORT).show();
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
Toast.makeText(this, "声音-", Toast.LENGTH_SHORT).show();
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) {
Toast.makeText(this, "静音", Toast.LENGTH_SHORT).show();
return false;
} else if (keyCode == KeyEvent.KEYCODE_HOME) {
Toast.makeText(this, "Home", Toast.LENGTH_SHORT).show();
return true;
}
return super.onKeyDown(keyCode, event);
}
/**
* 重写onTouchEvent方法可以处理Touch事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
if (event.getAction() == MotionEvent.ACTION_MOVE) {
Toast.makeText(this, "ACTION_MOVE", Toast.LENGTH_SHORT).show();
} else if (event.getAction() == MotionEvent.ACTION_UP) {
Toast.makeText(this, "ACTION_MOVE", Toast.LENGTH_SHORT).show();
} else if (event.getAction() == MotionEvent.ACTION_DOWN) {
Toast.makeText(this, "ACTION_MOVE", Toast.LENGTH_SHORT).show();
}
return super.onTouchEvent(event);
}
}
更多的事件可以参考SDK文档的MotionEvent、KeyEvent两个类,在KeyEvent中如果处理了KeyEvent.KEYCODE_BACK事件,那就不会执行默认的操作,比如收到KeyEvent.KEYCODE_BACK事件后默认是退出,如果直接return那就不会处理退出了。
原文链接: Android TV按键焦点原理浅谈
本篇主要阅读 Android 源码讲解 TV 的按键事件分发原理和焦点查找原理,源码基于 Android9.0 ,首先思考几个问题:
带着这些问题,我们一起来撸 Android 源码吧!了解了系统是如何处理的有便于我们解决 TV 上一些按键和焦点的问题。
首先我们看下按键事件的入口 ViewRootImpl 类中的 ViewPostImeInputStage 内部类:
可以看到注释1,2,3,4分别判断不同事件执行不同方法,本篇主要讨论的TV焦点事件,主要看下 processKeyEvent 方法:
可以看到在该方法中执行了 mView.dispatchKeyEvent 方法,这里的 View 其实是 DecorView ,接着看下该方法:
上面首先判断了如果是第一次按下则处理panel的快捷键,如果处理了则不往下走,否则继续判断当窗口未销毁且回调非空则回调处理,如果处理了则不往下走,否则让 PhoneWindow 对应的 onKeyDown , onKeyUp 方法来处理。
接下来我们按照这个派发顺序依次来看看相关方法的实现,这里先看看 Activity 的 dispatchKeyEvent 实现:
我们看第1点 superDispatchKeyEvent 方法,可以看到该方法为一个抽象方法,而它的实现是实现它的子类 PhoneWindow :
该方法又回调用 DecorView 中的 superDispatchKeyEvent 方法:
此时,再来看下 ViewGroup 的 dispatchKeyEvent 方法:
接着看下 View 的 dispatchKeyEvent 方法:
该方法主要是判断如果有给 View 设置 OnKeyListener 事件且 View 为可用状态,则优先处理监听事件,其次调用 KeyEvent 的 dispatch 方法,接下来我们看下该方法:
该方法主要处理了按下、弹起事件,其中按下如果 mRepeatCount 重复次数大于0判断为长按,则执行长按事件。
我们继续看下 View 的 onKeyDown 方法:
按下事件判断了如果为确认相关的按键才到下一步处理,判断点击或长按条件满足,执行按下 View 正中心坐标,然后执行 checkForLongClick 检查长按方法,看下该方法如下:
我们经常会遇到电视按遥控器时长按会执行一次 onKeyDown 、 onKeyUp ,之后才是一直 onKeyDown ,松开后才执行 onKeyUp ,原因就在于这个检查长按方法是延迟的。 delayOffset 传进来的是0,所以延迟时间为 ViewConfiguration.getLongPressTimeout() ,即该类中定义的 DEFAULT_LONG_PRESS_TIMEOUT 常量。
同样的如果是触摸屏,可以看下 View 类中的 onTouchEvent 方法在按下操作的时候会开启 CheckForTap 线程检查是否是长按,该线程同样是延迟的,时间为 ViewConfiguration.getTapTimeout() ,即该类中的 TAP_TIMEOUT 常量,知道了这个你就知道如果写脚本或插件模拟长按应该间隔多长时间了,是不是一下你的模拟长按插件速度又可以更加准确快速的实现了。
不同版本系统定义的延迟时间有可能不一样,比如Google API 28 的 DEFAULT_LONG_PRESS_TIMEOUT 是500, TAP_TIMEOUT 是100,而 API 30 的 DEFAULT_LONG_PRESS_TIMEOUT 是400, TAP_TIMEOUT 也是100。
接下来再看下 Activity 的 onKeyDown :
回到 Decorview 中的 dispatchKeyEvent 方法看看 PhoneWindow 的 onKeyDown 方法:
onKeyUp 方法也可以自己再看下,以上就是浅谈按键事件的分发流程了。
总结:
上面讲解了按键事件分发流程,当上面分发完所有都没消费的时候,就会继续走 ViewRootImpl 的焦点导航流程,接下来看下 performFocusNavigation 方法:
首先我们看 mView.findFocus() ,该方法实际是调用了 ViewGroup 的 findFocus 方法:
该方法很简单,就是向下递归查找在当前页面已经获取焦点的 View ,继续看 focused.focusSearch(direction) 调用了 View 的 focusSearch 方法:
该方法向上递归查找,调用 ViewGroup 的 focusSearch 方法:
如果是根命名空间,则调用 FocusFinder 的 findNextFocus 方法查找焦点,否则继续往上查找。继续看 FocusFinder 的 findNextFocus 方法:
可以看到该方法首先查找用户指定的下一个获取焦点的 view ,如果找到了直接返回该 view ,如果没找到继续下面先添加 effectiveRoot 下的所有 view 到 focusables 集合中去,然后调用 findNextFocus 方法查找系统可获取下一个焦点的最近 view 。
我们先看下 findNextUserSpecifiedFocus 方法的实现:
通过用户指定焦点方式不是本篇的重点,这里就不贴出内部细节源码了。该方法实际就是调用 View 的 findUserSetNextFocus 方法来查找用户设置的下一个可获取焦点的 view ,然后在 while 循环中判断如果找到的是可以获取焦点并且可见的并且不是 InTouchNode 模式,则返回该焦点,否则继续循环查找直到找了一个循环没有找到可以获取焦点的或者 userSetNextFocus 为 null 跳出循环返回 null 。
再来看下系统就近原则查找的 findNextFocus 方法:
该方法主要通过 findNextFocusInRelativeDirection 在相对方向上找下一个焦点,该方法内部逻辑比较简单,这里就不贴出来了,进去看下就知道其实就是先给 focusables 排序,然后从中找到 focused 在其中的后一个或前一个 view ,如果没找到并且 focusables 不为空则返回 focusables 的第一个。
接下来我们重点看下 findNextFocusInAbsoluteDirection 方法:
再看下 isBetterCandidate 方法,该方法很关键,内部包含一系列逻辑如何成为最佳候选者:
该方法英文注释很直观,就不中文翻译了,首先看下成为候选人的 isCandidate 方法:
该方法判断了目标Rect如果在源Rect的方向一侧且不在内部的话,则为候选者,如第一个 destRect 左侧应在 srcRect 左侧左边, destRect 右侧应在 srcRect 右侧左边,其他方向同理。
接下来看下 beamBeats 方法:
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流