相信很多刚接触AndroidTV开发的开发者,都会被各种焦点问题给折磨的不行。不管是学技术还是学习其他知识,都要学习和理解其中原理,碰到问题我们才能得心应手。下面就来探一探Android的焦点分发的过程。
Android焦点事件的分发是从ViewRootImpl的processKeyEvent开始的,源码如下:
源码比较长,下面我就慢慢来讲解一下具体的每一个细节。
dispatchKeyEvent方法返回true代表焦点事件被消费了。
ViewGroup的dispatchKeyEvent()方法的源码如下:
(2)ViewGroup的dispatchKeyEvent执行流程
(3)下面再来瞧瞧view的dispatchKeyEvent方法的具体的执行过程
惊奇的发现执行了onKeyListener中的onKey方法,如果onKey方法返回true,那么dispatchKeyEvent方法也会返回true
可以得出结论:如果想要修改ViewGroup焦点事件的分发,可以这么干:
注意:实际开发中,理论上所有焦点问题都可以通过给dispatchKeyEvent方法增加监听来来拦截来控制。
(1)dispatchKeyEvent方法返回false后,先得到按键的方向direction值,这个值是一个int类型参数。这个direction值是后面来进行焦点查找的。
(2)接着会调用DecorView的findFocus()方法一层一层往下查找已经获取焦点的子View。
ViewGroup的findFocus方法如下:
View的findFocus方法
说明:判断view是否获取焦点的isFocused()方法, (mPrivateFlags & PFLAG_FOCUSED) != 0 和view 的isFocused()方法是一致的。
其中isFocused()方法的作用是判断view是否已经获取焦点,如果viewGroup已经获取到了焦点,那么返回本身即可,否则通过mFocused的findFocus()方法来找焦点。mFocused其实就是ViewGroup中获取焦点的子view,如果mView不是ViewGourp的话,findFocus其实就是判断本身是否已经获取焦点,如果已经获取焦点了,返回本身。
(3)回到processKeyEvent方法中,如果findFocus方法返回的mFocused不为空,说明找到了当前获取焦点的view(mFocused),接着focusSearch会把direction(遥控器按键按下的方向)作为参数,找到特定方向下一个将要获取焦点的view,最后如果该view不为空,那么就让该view获取焦点。
(4)focusSearch方法的具体实现。
focusSearch方法的源码如下:
可以看出focusSearch其实是一层一层地网上调用父View的focusSearch方法,直到当前view是根布局(isRootNamespace()方法),通过注释可以知道focusSearch最终会调用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦点view是通过FocusFinder来找到的。
(5)FocusFinder是什么?
它其实是一个实现 根据给定的按键方向,通过当前的获取焦点的View,查找下一个获取焦点的view这样算法的类。焦点没有被拦截的情况下,Android框架焦点的查找最终都是通过FocusFinder类来实现的。
(6)FocusFinder是如何通过findNextFocus方法寻找焦点的。
下面就来看看FocusFinder类是如何通过findNextFocus来找焦点的。一层一层往下看,后面会执行findNextUserSpecifiedFocus()方法,这个方法会执行focused(即当前获取焦点的View)的findUserSetNextFocus方法,如果该方法返回的View不为空,且isFocusable = true && isInTouchMode() = true的话,FocusFinder找到的焦点就是findNextUserSpecifiedFocus()返回的View。
(7)findNextFocus会优先根据XML里设置的下一个将获取焦点的View ID值来寻找将要获取焦点的View。
看看View的findUserSetNextFocus方法内部都干了些什么,OMG不就是通过我们xml布局里设置的nextFocusLeft,nextFocusRight的viewId来找焦点吗,如果按下Left键,那么便会通过nextFocusLeft值里的View Id值去找下一个获取焦点的View。
可以得出以下结论:
1. 如果一个View在XML布局中设置了focusable = true && isInTouchMode = true,那么这个View会优先获取焦点。
2. 通过设置nextFocusLeft,nextFocusRight,nextFocusUp,nextFocusDown值可以控制View的下一个焦点。
Android焦点的原理实现就这些。总结一下:
为了方便同志们学习,我这做了张导图,方便大家理解~
② 怎么让android 页面失去焦点
在网上找了好久,有点监听软键盘事件,有点调用 clearFouse()方法,但是测试了都没有!xml中也找不到相应的属性可以关闭这个默认行为
1 解决之道:在EditText的父级控件中找一个,设置成
Android:focusable="true"
android:focusableInTouchMode="true"
这样,就把EditText默认的行为截断了!
<LinearLayout
style="@style/FillWrapWidgetStyle"
android:orientation="vertical"
android:background="@color/black"
android:gravity="center_horizontal"
android:focusable="true"
android:focusableInTouchMode="true"
>
<ImageView
android:id="@+id/logo"
style="@style/WrapContentWidgetStyle"
android:background="@drawable/dream_dictionary_logo"
/>
<RelativeLayout
style="@style/FillWrapWidgetStyle"
android:background="@drawable/searchbar_bg"
android:gravity="center_vertical"
>
<EditText
android:id="@+id/searchEditText"
style="@style/WrapContentWidgetStyle"
android:background="@null"
android:hint="Search"
android:layout_marginLeft="40dp"
android:singleLine="true"
/>
</RelativeLayout>
</LinearLayout>
2 还有一个方法也可以非常简单的实现这个功能:
EditText对象的clearFocus();
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(editMsgView.getWindowToken(), 0);(关闭软键盘。。。)
3更多问题解决办法请参考android学习手册,例子、源码、文档全部搞定,采用androidstudo的目录结构,360手机助手中下载。下面是截图。
③ android 中如何设置焦点的位置。
默认从上倒下 从左到右第一个可以输入的控件作为焦点,如果不想默认可以指定某个view或得焦点
④ Android移动应用中的焦点分析
简单一点理解,在移动应用中,焦点就是当前正在处理事件的位置。在手机应用中,最有可能用到焦点的就是EditText,如果同一个界面中有多个EditText,通常情况下同一时间只有一个能够输入内容,此时,这个EditText就获取了焦点。
在Android中,对焦点的设置分为两种情况,TouchMode和非TouchMode。现在的手机基本都是触摸屏,我们用手指触摸屏幕来操作Android应用时,处于TouchMode。除了TouchMode之外,还有非TouchMode,利用外接设备来操作早衫应用。比如键盘。使用Genymotion模拟器的时候,一个陆兄腔界面上有多个控件时,可以用电脑tab键来进行移动,被选中的控件会高亮显示,这时候就是非TouchMode,被选中的控件获得了焦点。
在手机应用中,用到焦点的时候并不多,但是TV应用中,需要用遥控器来操作选中控件,这时候就需要对焦点进行处理了。关于焦点,常用方法如下:
在View类中, isFocusable() 和 isFocusableInTouchMode() 获取到的结果都是false,也就是说,直接继承自View的控件是不能获取焦点的。我们常用控件中对这两个方法进行了改写,比如EditText,这两个方法都是true,而Button则只有 isFocusable() 返回true。这也就是为什么我们用tab键选取Button的时候能够高亮显示,而鼠标点击(模拟触控)的时候不能高亮显示的原因了。如果想在点击的时候也能高亮显示Button,需要手动设置 setFocusableInTouchMode(true) ,就可以了。
如果想对控件的焦点状态进行监听,需要设置 setOnFocusChangeListener() ,只要控件的焦点状态发生变化(获得或者失去焦点),都会调用 onFocusChange 方法
关于焦点的移动,默认的算法会寻找指定方向上最近的可以获取焦点的元素(非TouchMode)。另外在创建控件的时候,也可以指定寻找焦点的方向,设置nextFocusDown、nextFocusLeft、nextFocusRight 和 nextFocusUp的值为指定元素就可以了。看以下例子:
这里指定了上面的button向上寻找焦点时,下一个元素是id为bottom的元素,也就是说,上面的Button在获取了焦点之后,继续按向上键,系统会将焦点移动到id为bottom的元素上,而不是继续向上。
在开发手机应用的过程中,对焦点的处理并不多,它与事件是两个不同的体系,通常情况下焦点和事件是相互独立并不冲突。但是在Button的点击事件中会有一点问题。如果我们队一个button设置了 setFocusableInTouchMode(true) ,使他可以获取焦点,那么我们点击这个button的时候,第一次点击并不会执行 onClick() 方法,而是执行 onFocusChange() 。第二次点击的时候才会执行 onClick() 方法。看起来好像 onFocusChange() 消耗了点击事件,实际上并不是的。
这个问题我们看一下源码就清楚了:
onClick() 方法是在onTouchEvent的ACTION_UP里调用的,看一下View的onTouchEvent方法:
可以看到,只有当focusTaken为false的时候才会执行onClick,focusTaken的值默认是false的,但是在 isFocusable() && isFocusableInTouchMode() && !isFocused() 为true的时候,会去 requestFocus 获取焦点,并将值赋给focusTaken。
关键在于 isFocused() ,如果当前Button没尘码有获取焦点, isFocused() 返回false, !isFocused() 值为ture,Button就会去获取焦点,从而导致 focusTaken 为true, onClick 方法就不会执行了,只有Button已经获取了焦点的时候才会执行onClick方法。