[ING] 谈谈Android的焦点

Posted by 阿呆 on 2018-12-28

最近在工作过程中遇到软键盘的一些问题,因为需要打造很好的用户体验,软键盘的交互是避免不了的坑,网上的内容大多比较分散,也没有说的很清楚,我这里做一下总结。

Main refer(sort by refer percent):
sydMob: https://www.jianshu.com/p/1cd49233cf52
欧阳: https://www.jianshu.com/p/5fff395b9e2f
何俊林: http://blog.csdn.net/hejjunlin/article/details/52263256

前言

有个前提是,触摸屏和一些按键式的流程是不一样的,当然都涉及到焦点控制相关,后面我们会来分析它们的不同
Android焦点相关逻辑大部分都在View、ViewGroup和FocusFinder三个类中

ViewRoot

一个Window中View根节点DecorView的mParent称为ViewRoot,在Android 4.0后ViewRoot对应ViewRootImpl,它不是View的子类,而是个ViewParent,ViewRootImpl是连接Window和DecorView的纽带,View的焦点,按键,布局,渲染等流程都是从ViewRoot中开始的

什么是焦点

baidu:焦点,计算技术语,计算机程序中所谓的焦点,就是关注的区域当前光标被激活的位置,是哪个控件被选中,可以被操作

焦点在Android中也就是Focus,称为Focus机制,以后我们看到Focus就是指的焦点,那什么是焦点机制呢?焦点机制,其实就是最受关注的那一个,但是最受关注的那一个并不一定说响应所有的操作,很多人把焦点和点击弄混了,以为我点击了某个空间,这个控件就获取焦点了,这个是不一定的,后面会有说明。 (TODO:那么焦点的有无会影响到什么呢?)

获取焦点的两种方式

如果请求有触摸获取焦点的能力,在xml中的配置是
’android:focusableInTouchMode=“true”’

请求有普通获取焦点的能力(可以理解为物理键盘),在xml中的配置是
’android:focusable=“true”’

1.focusable
主要为电视、手表、机顶盒等等非触摸设备提供获取焦点的方式,如果设置为true,则键盘上下左右选中,焦点会随之移动

2.foucsableInTouchMode
对于手机而言,基本都是TouchMode,就是当你触摸一个控件的时候,这个控件会获取到焦点;但是注意,有些控件默认是不具备有触摸获取焦点的功能的。

设置一个视图是否可以获取焦点:

1
2
3
4
5
6
7
8
// 设置视图是否可以获取焦点
public void setFocusable(boolean focusable)
// 设置视图是否可以获取焦点
public final boolean isFocusable()

----------- TouchMode --------------
public void setFocusableInTouchMode(boolean focusableInTouchMode)
public final boolean isFocusableInTouchMode()

在触摸设备下,一个视图想要获得焦点必须要 setFocusable和 setFocusableInTouchMode同时为true才可以获取焦点

那么为什么让他们默认不具备点击获取焦点呢?
原因是这些控件有时候可能会想要先响应点击事件,如果触摸获取焦点功能打开后,点击的时候默认是不会调用点击事件的,这个时候会先让这个控件获取焦点;TODO:相当于拦截了第一次点击事件么?那么如果再次点击呢?)
EditText是默认有触摸获取焦点功能的,并将第一时间抢占焦点;这就解释了为什么当一个页面有EditText的时候,我们进入的时候默认有光标,键盘弹出,这就是焦点在 EditText上面

上面那些默认不具触摸获取焦点功能的控件,当你点击它的时候,调用isFocused()返回false,这个时候默认的触发它的点击事件,但是你可以配置 focusableInTouchMode=true,让他们能够获取到焦点,这时点击他们则不会触发点击事件,而是 OnFocusChangeListener,这个时候获取到了焦点,当你再点击的时候才会触发点击事件
逻辑类似下面:

1
2
3
4
5
6
event->
if(isFocused){
onclicklistener.onclick();
}else(facousInTouchMode && !isFocused){
onFocusChangeListener.onFocusChange();
}

并且,下面一些结论(也就是焦点机制遵循的原则)我们可以先记一下,对后面的理解会有帮助

ViewGroup中有一个mFocued成员来保存它的直接子视图中是谁具有焦点
ViewGroup没有焦点并不代表其子视图也没有焦点,这里没有父子制约关系
任何时候一个窗口内最多只有一个视图具有焦点(可能所有视图都没有焦点)
并不是所有视图都可以获取焦点

为什么会出现EditText自动获取到焦点的问题 ?

这种情况一般是布局中存在 EditText,EditText会默认获取焦点,弹出键盘,解决方法就是,在它的父布局中添加 focusableInTouchMode=true.之所以会被EditText夺取焦点,是因为它的父布局默认是没有这个能力的(TODO:默认必须有个控件要获取到焦点么?)

several functions

'boolean isFocused()'
是否当前视图就是焦点视图

'boolean hasFocus()'
当前视图或者其子视图是否持有焦点

区别与联系:
hasFocus和isFocused(自己一定要是焦点视图)区别主要在ViewGroup上

'void clearFocus()'
清除视图的焦点,当一个视图具有焦点的时候,调用clearFocus,onFocusChanged()会回调,并且往上遍历调用 clearChildFocus 将 mFocued值置空。讲这么多,其实就是重置视图状态,最后,再从根视图中再次遍历将某个最佳的视图设置成为焦点视图;因为清楚某个视图的焦点属性时,系统为了保证拥有一个具有焦点的视图,就会再次遍历整个视图树来重新设置具有焦点的视图。我想,这也是EditText在start up of Activity 阶段会自动获取焦点的原因

'View findFocus()'
用来查找具有焦点的视图,如果是View则判断自己是否有焦点,如果是ViewGroup则自己就是焦点返回自己,否则返回儿子视图里面的焦点视图。如果都没有焦点视图时则返回null

'View getFocusedChild()'
ViewGroup中的方法,获取直接的焦点子视图,也就是返回 mFocued数据成员

'void addFocusables(ArrayList views,int direction)'
将可获取焦点的View加入这个views表中,当调用者是ViewGroup时,将里面的可获取焦点的子视图加入到views里面去(TODO当子视图和ViewGroup都可以获取焦点呢?)

View焦点

基本流程

requestFocus ->

if(focusable)
parent.requestChildFocus
else
end.

onFocusChange
refreshDrawableState
End

获取焦点

requestFocus’ # View.java

1
2
3
4
5
6
7
8
9
10
11
public final boolean requestFocus(){
return requestFocus(View.FOCUS_DOWN);//默认向下
}

public final boolean requestFocus(int direction){
return requestFocus(direction,null);
}

public boolean requestFocus(int direction,Rect previouslyFocusedRect){
return requestFocusNoSearch(direction,previouslyFocusedRect);`
}

我们来看第三个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private boolean requestFocusNoSerach(int direction ,Rect previouslyFocusedRect){
// need to be focusable
if ((mViewFlags & FOCUSABLE) != FOCUSABLE
|| (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}

// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}

// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}

handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}

void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}

if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;

View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

if (mParent != null) {
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}

if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}

onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
流程比较简单,如果当前没有焦点,先设置焦点标志,再通知 parent,然后刷新图片

主要的流程在 mParent 的 requestChildFocus 里面,那里会逐层向上修改焦点View并清除原来有焦点的View的焦点。onFocusChange 会触发 invalidate 刷新,然后调用 onFocusChangeListener。默认情况下,每个View只能设置一个 onFocusChangeListener,可以重写onFocusChange方法,实现回调多个 onFocusCHangeListener的需求呢

ViewGroup的焦点