一点见解: 焦点那点事(一)

Android开发使用的手机一般处于触摸模式, 因此默认情况下并不会有焦点, 所以之前一直对焦点不是很熟悉. 但是在电视端开发上, 焦点的处理可以说直接影响了用户体验, 因此借此熟悉下焦点处理的流程.

本文着重介绍焦点相关的一些关键方法, 先从局部了解下焦点的一些基础规则和行为特点.

获取焦点的前提

  1. View#isFocusable返回true, 如果在触摸模式, 则View#isFocusableInTouchMode也要返回true
  2. 控件必须可见
  3. 控件相关的父控件, 包括祖父控件等, ViewGroup#getDescendantFocusability()不能为ViewGroup#FOCUS_BLOCK_DESCENDANTS

View

获取焦点

调用View#requestFocus系列方法

进入View#requestFocusNoSearch

在该方法中会对控件的当前状态进行判断, 如果不符合获取焦点的前提则直接返回false告知调用方, 控件不会获取焦点

只要符合前提就会继续执行, 最终必定返回true, 不论当前控件的焦点状态是否有改变

符合前提则进入 View#handleFocusGainInternal

如果控件已经持有焦点, 则不会做任何事情, 直接结束流程

如果没有焦点,

  1. 改变焦点标志位, 此时View#isFocused就会返回true
  2. 通过ViewParent#requestChildFocus通知父控件即将获取焦点
  3. 通知其他部件焦点状态发生变化(略, 本文不关心)
  4. 触发OnGlobalFocusChangeListener的回调
  5. 触发OnFocusChangeListener回调
  6. 重绘, 结束流程

清除焦点

调用View#clearFocus主动放弃焦点

如果控件本身没有焦点, 则什么都不会发生

如果控件持有焦点

  1. 改变焦点标志位
  2. 通过ViewParent#clearChildFocus通知父控件, 当前控件放弃焦点
  3. 触发OnFocusChangeListener回调
  4. 调用当前控件的根控件(rootView)的requestFocus方法
  5. 如果步骤4中没有找到新的焦点控件, 则触发OnGlobalFocusChangeListener的回调, 注: 如果找到新的焦点控件, 那么新的控件获取焦点的过程中就会回调OnGlobalFocusChangeListener, 所以这里只有没找到才进行步骤5

注: 由上流程可以知道, 如果根控件查找控件的时候找到的控件还是这个控件, 那么OnFocusChangeListener就会被调用两次, 先失去焦点, 然后又获取到焦点

ViewGroup

焦点分发策略DescendantFocusability

  1. FOCUS_BLOCK_DESCENDANTS: 拦截焦点, 直接自己尝试获取焦点
  2. FOCUS_BEFORE_DESCENDANTS: 首先自己尝试获取焦点, 如果自己不能获取焦点, 则尝试让子控件获取焦点
  3. FOCUS_AFTER_DESCENDANTS: 首先尝试把焦点给子控件, 如果所有子控件都不要, 则自己尝试获取焦点

获取焦点

根据焦点分发策略决定下面两个方法的调用顺序

通过View#requestFocus自己获取焦点

ViewGroup看作View, 直接走View获取焦点的流程来获取焦点

进入onRequestFocusInDescendants

可以传入方向来改变遍历的顺序, 默认是从0递增

遍历子控件, 调用子控件的View#requestFocus来尝试把焦点给可见的子控件, 某个子控件成功获取到焦点后, 停止遍历

注: 重写该方法可以改变ViewGroup分发焦点给子控件的行为, 例如遍历顺序

清除焦点

如果焦点控件不是它的子控件, 那么直接把当前的ViewGroup看作ViewView#clearFocus流程, 反之则调用焦点控件的View#clearFocus.

注: 区别在于重新分发焦点时的选择范围.

ViewParent

ViewParent是一个接口, 表示了一个父控件应该具备的功能, ViewGroup实现了该接口.

与焦点相关的接口有4个

clearChildFocus

当子控件主动放弃焦点的时候会通过这个方法通知父控件.

ViewGroup的默认实现中, 会置空当前焦点控件, 表示该父控件下没有子控件获取焦点, 接着把这个事件通知给上级父控件.

注1: 这个方法名有点让人误解, 应该把这个方法看作一个回调, 表明了一个状态, 在这个方法中并没有做清除焦点的操作, 实际的清除动作是在View#clearFocus中完成的, 这个方法也是在这个流程中被调用的. 而且是在子控件已经放弃焦点后调用.
注2: 区分主动放弃和因为其他控件获取了焦点而被动丢失焦点的情况

requestChildFocus

当子控件获取了焦点后, 通过这个方法通知父控件. 同clearChildFocus类似, 应该把这个方法看作是一个回调.

ViewGroup的默认实现中, 因为同时只会有一个焦点, 因此在这里应该把旧焦点清除掉, 大致流程如下

  1. 如果焦点分发策略为FOCUS_BLOCK_DESCENDANTS则什么也不干
  2. 如果父控件自身有焦点, 通过View#unFocus清除焦点
  3. 如果父控件当前已经有焦点控件, 并且和新的控件不一致, 那么通过View#unFocus清除旧焦点控件的焦点
  4. 向上传递这个事件

内部清除焦点View#unFocus

这个方法和View#clearFocus相同点在于都会执行View#clearFocusInternal方法, 区别在于unFocus只会执行clearFocus中, 上文清除焦点中提到的1, 3步骤, 因此不会通知父控件, 不会触犯requestChildFocus回调, 因为这个方法是在子控件被动失去焦点时调用的, 所以也不会触发焦点分发.

因此新旧焦点切换的大致流程是

  1. 新焦点控件获取焦点
  2. 新焦点控件通知父控件
  3. 父控件清除旧焦点控件的焦点
  4. 旧焦点控件回调OnFocusChangeListener
  5. 触发OnGlobalFocusChangeListener的回调
  6. 新焦点控件回调OnFocusChangeListener

focusableViewAvailable

通知父控件, 子控件的状态发生改变, 从不能获取焦点, 变成可能可以获取焦点.

有两种情况会被调用

  1. 子控件从unFocusable变为focusable
  2. 子控件从不可见变为可见, 即使它不是focusable也会调用, 因此它的子控件可能可以获取焦点.

ViewGroup中的默认实现只是在符合条件的情况下把这个事件向上传递给自己的父控件.

focusSearch(View, int)

查找指定方向中最近的, 想要获取焦点的控件.

这个方法直接决定了焦点的移动规则, 非常重要.

ViewGroup的默认实现中, 会一直向上传递, 直到根控件, 接着调用FocusFinder#findNextFocus方法查找合适的控件. 稍后再分析这个方法.

View中有一个同名的方法focusSearch(int), 该方法直接调用了父控件的focusSearch(View, int)来查找下一个焦点控件

findNextFocus

查找步骤大致如下

手动指定

如果有通过android:nextFocusDown等手动指定控件, 则返回对应方向的控件

动态计算
  1. 获取所有可以获取焦点的控件的集合
  2. 计算相对当前焦点控件的坐标
  3. 根据方向选择合适的控件

总结

  1. 分析的过程要注意区分ViewViewGroup的差异和新焦点和旧焦点控件的方法调用.
  2. ViewParent是一个接口, 其中一些方法应该看作是回调, 子控件通过这些回调通知父控件焦点状态发生了变化, 提醒父控件进行相关处理, 确保只有一个焦点存在
  3. 某个控件获取焦点的同时, 旧焦点控件也会失去焦点, 这个动作是在requestChildFocus中发生的.
  4. 焦点移动的关键方法是focusSearch(View, int), 下一篇文章一点见解: 焦点那点事(二)接着分析焦点移动的发起点和过程.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 上一篇文章, 一点见解: 焦点那点事(一), 了解了焦点相关的一些基本知识, 提到焦点切换的关键方法ViewPar...
    AssIstne阅读 2,701评论 7 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,201评论 0 17
  • 相信每个人都有时间过的好快的感觉。平时事务繁忙的人这种感觉会更快一些,每个时间点都安排着事情,他们的感觉是时间太快...
    不莱梅阅读 233评论 0 2
  • >Linker中主要的两个源点是dlopen和dlsym。 * dlopen传入两个参数,返回一个文件句柄。传入的...
    sakuradream阅读 727评论 0 0