参考文献:

Android中的视图焦点Focus的详细介绍

1. 介绍

在非触摸屏设备中接收事件和处理响应的控件是具有焦点(Focused)的控件。一个窗口中一个时间内只能有一个具有焦点的控件。在现在的智能 TV 电视应用中视图的焦点控制就非常重要了。

焦点的特性

  • ViewGroup 中有一个 mFocued 成员来保存子视图中哪个子视图是具有焦点的视图,并且这样一直会递归下去。比如某个视图层次下的根视图 ROOT 下有 A,B,C 三个子视图,而 B 下面又有 B1,B2,B3 三个子视图,而这时候 B3 是具有焦点的子视图,那么在 B 中的 mFocued 保存的是 B3,而 ROOT 下的 mFocued 保存的是 B。但同一个窗口焦点只有一个,B3 获取焦点的时候,B 没有焦点。
  • ViewGroup 没有焦点并不代表其子视图也没有焦点,这里没有父子制约关系。(默认 ViewGroup 无法聚焦)
  • 任何时候一个窗口内都只有一个视图具有焦点,或者所有视图都无焦点。
  • 并不是所有视图都可以获取焦点。

2. 基本用法

可以聚焦

我们要设置一个视图是否可以获取焦点可以通过如下方法来完成:

// 设置视图是否可以获得焦点
public void setFocusable(boolean focusable)

对于触摸设备来说我们可以设置一个视图在被触摸时是否可以成为焦点视图。我们可以通过如下方法:

// 设置视图是否在触摸模式下可以获得焦点 
 public void setFocusableInTouchMode(boolean focusableInTouchMode)

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

判断视图是否为焦点

下面两个方法用来判断某个视图是否是焦点视图以及是否获取了焦点:

// 是否当前视图就是焦点视图
public boolean isFocused() 
// 当前视图是否是焦点视图,或者子视图里面有焦点视图。
public boolean hasFocus()

hasFocus 和 isFocused 区别主要在 ViewGroup 上,前者只要自己或者儿子视图是焦点视图都返回 true,而后者是一定要自己是焦点视图。

判断视图能否获取焦点

我们可以用如下方法来判断视图是否可见并且可以获得焦点,如果自己不可获得焦点则会递归调用子视图判断是否可以获得焦点。 从上可见 has 和 is 的区别是是否是只判断自身。

public boolean isFocusable();   // 只判断自身
public final boolean isFocusableInTouchMode()  // // 获取视图是否在触摸模式下获得焦点
public boolean hasFocusable();  // 除了判断自身外还判断子视图
android:focusable="true"
android:focusableInTouchMode="true"

清除焦点

如果我们要清除某个具有焦点视图的焦点属性就可以调用如下方法:

public void clearFocus();

清除视图的焦点时,会激发视图的 onFocusChanged 的调用,并且往上遍历调用 clearChildFocus 将 mFocued 的值置空,然后再从根视图中再次遍历将某个最佳的视图设置成为焦点视图。因为清除某个视图的焦点属性时,系统为了保证拥有一个具有焦点的视图,就会再次遍历整个视图树来重新设置具有焦点的视图。

也就是说清除焦点后,会从根节点重新寻找焦点。

查找焦点

获取当前窗口获取焦点的视图。

View view = getCurrentFocus();

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

public View findFocus();

下面的方法是 ViewGroup 中的方法,获取直接的焦点子视图,也就是返回 mFocued 数据成员,不包含自身。

public View getFocusedChild();

下面的方法中如果调用者是 View 并且自身可以获取焦点,那么就将自身加入到views数组里面去,如果自身是 ViewGroup 则将里面的可获取焦点的子视图加入到 views 里面去。

// 这里的 direction 参数貌似没有什么作用。
public void addFocusables(ArrayList<View> views, int direction)

下面的方法可以获取一个 View 或者 ViewGroup 下所有可获取焦点的子视图列表。如果调用的对象是 View 则可能返回自身,如果调用的对象是 ViewGroup 则返回自身和下面所有子视图中可获取焦点的子视图。

// 这里的 direction 参数貌似没有什么作用。
 public ArrayList<View> getFocusables(int direction)

可以看出 addFocusables 和 getFocusables 其实具有类似的功能,都是将自身或者容器视图里面的子视图中具有获取焦点能力的子视图返回到数组里面去。

下面函数和一些 getXX 函数用于设置或者获取某个视图的下一个焦点的 ID,主要用于键盘模式来移动焦点的位置。

public void setNextFocusDownId(int nextFocusDownId);

设置获取焦点策略

public void setDescendantFocusability(int focusability);

focusability可设置的值如下:

  • FOCUS_BLOCK_DESCENDANTS: 阻止子视图成为焦点视图,这样即使子视图调用了 requestFocus 也不能成为焦点视图。
  • FOCUS_BEFORE_DESCENDANTS: 当 ViewGroup 调用 requestFocus 时总是优先让自己成为焦点视图。
  • FOCUS_AFTER_DESCENDANTS: 当 ViewGroup 调用 requestFocus 时优先让里面的子视图成为焦点,只有子视图无法成为焦点时才让自己成为焦点视图。这个特性也是默认特性。

请求焦点

下面的方法用来请求成为当前焦点视图。这个方法是视图获得焦点的关键:

public final boolean requestFocus();
  • 如果调用者是 View 且自己不可见 (invisible or gone) 或者不可获得焦点 (isFocusable为false) 或者父视图不允许自己获取焦点就会返回 false 表示成为焦点视图失败 (setOnFocusChangeListener 不会被触发)。如果能够成为焦点视图,那么就会调用 onFocusChanged 方法清除其他焦点视图。
  • 如果是 ViewGroup 则根据 setDescendantFocusability 中的规则进行:如果是阻止子视图则自己进行焦点的获取,否则就按规则先子节点或者后子节点。

3. ExitText 自动获取焦点问题

而在触摸设备上通常默认情况下只有 EditText 控件才具有焦点,而我们通常会遇到的一个问题就是当进入一个具有 EditText 的界面时键盘就会自动弹出,而且有时候可能无法消失,但需求可能是进入时不弹出键盘。(测试没有遇到)

通过 setDescendantFocusability 和 requestFocus 方法的配合就可以解决那种只有一个 EditText 且一进入就自动键盘弹出的问题。因为默认的 EditText 是一个可成为焦点的视图,这样根据规则当界面展示时就会成为一个焦点视图从而弹出键盘,这样即使对 EditText 调用 clearFocus 也因为规则导致他还是焦点视图。

解决的方案是把 EditText 的一个祖先视图也设置为可获取焦点的视图 (setFocusable(true)),并且将这个祖先视图的 setDescendantFocusability 设置为 FOCUS_BEFORE_DESCENDANTS。这样当对 EditText 调用 clearFocus 或者对祖先视图调用 reqeustFoucs 时都会优先让祖先视图获得焦点。

4. 首个焦点

进入窗口时,经测试,窗口获取焦点,但没有 View 获取焦点,包括根视图。

onWindowFocusChanged() 的使用情景与作用
根据介绍可以了解,onWindowFocusChanged() 使用于以下等情景:

首次进入一个 Activity 后会在 onResume() 方法后面调用;

从 Activity 跳到另一个 Activity,新的窗口会获取焦点, 就的 Activity 的窗口会失去焦点;

打开软键盘进行输入时,窗口失去焦点;

软键盘输入完毕消失时,窗口重新获取焦点;

应用进入后台,窗口失去焦点;

应用从后台返回当前, 窗口重新获取焦点;

因此其可以有如下作用:

监控一个 Activity 是否载完毕;

在 Activity 加载后进行一些操作,如获取手机屏幕的高度和宽度;

当 Activity 挂起或恢复时,可以在方法内进行一些数据的保存或恢复的操作;

5. 注意事项

布局请求焦点不触发 onfocuschange