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

上一篇文章, 一点见解: 焦点那点事(一), 了解了焦点相关的一些基本知识, 提到焦点切换的关键方法ViewParent#focusSearch, 本文接着看, 焦点是从什么时候产生的, 又是如何在控件间切换的, 当控件被移除或者新增进布局时焦点又会发生什么变化.

焦点产生

页面创建出来后, 什么时候开始分发焦点?
关于页面创建流程的和绘制过程的文章有很多, 这里不再累述, 通过这些文章, 我们可以知道页面控件的绘制入口是ViewRootImpl#performTraversals方法.

在这个方法中, 如果是第一次执行这个方法, 同时, ViewRoot相关联的DecorView没有焦点控件, 那么就会调用DecorView#requestFocus, 实际上也就是调用了ViewGroup#requestFocus, 上一篇文章一点见解: 焦点那点事(一)介绍过, 在这个方法里, 会遍历子控件, 执行View#requestFocus直到某个控件持有焦点.

疑问: home键退出页面, 然后返回时, 如果当前页面没有焦点, 还会走一次requestFocus, 这种情况是哪里触发的?

焦点切换

虽然在触摸模式也能产生焦点, 但是一般不会用到, 因此这里着重分析通过键盘操作来切换焦点的情况.

起点

既然是通过键盘切换焦点, 因此从键盘事件开始入手.
关于输入事件的处理流程已经有很多文章了, 这个也不是本文关注的重点, 因此不再累述, 可以参考原来Android触控机制竟是这样的?.

概括起来就是

  1. ViewRootImpl通过一个Receiver接收硬件发送过来的事件(包括触摸事件和键盘事件)
  2. 然后ViewRootImpl会把这些事件放在队列中
  3. 然后再按顺序取出这些事件通过InputStage相关类分发出去, 最后会执行InputStage#onProcess()方法
  4. 其中在ViewPostImeInputStage类中, 如果输入的事件是键盘事件, 那么就会调用ViewPostImeInputStage#processKeyEvent()方法

processKeyEvent()

在这个方法里, 会先把事件传递给ViewGroup#dispatchKeyEvent()方法, 如果这个方法没有消费掉这个事件, 并且这个事件是方向事件按下事件, 例如KeyEvent.KEYCODE_DPAD_LEFT等, 那么就会触发焦点切换, 也就是focusSearch方法.

ViewGroup#dispatchKeyEvent()

首先看这个方法, 因为在ViewRootImpl中持有的是DecorView, 它本质上是一个FrameLayout, 因此分发键盘事件时实际调用的会是ViewGroup#dispatchKeyEvent().

在这个方法里

  1. 如果这个ViewGroup持有焦点, 那么就会直接调用View#dispatchKeyEvent
  2. 如果是它的子控件持有焦点, 那么就会调用子控件View#dispatchKeyEvent

View#dispatchKeyEvent里面

  1. 询问OnKeyListener是否消费这个事件
  2. 消费确认相关的按键事件, 例如KeyEvent.KEYCODE_DPAD_CENTER

由上可以知道, 一般情况下, ViewGroup#dispatchKeyEvent()只会消费确认事件, 方向事件是会继续执行下一步的.

触发焦点切换

方向事件按下事件表明, 在按下的时候就会触发焦点切换了, 这解释了为什么长按方向键会一直切换焦点.

焦点切换时

  1. 如果当前已经存在焦点, 那么就调用当前焦点控件的View#focusSearch(int), 这个方法又会马上调用ViewParent#focusSearch(View, int)方法, 注意区分这两个方法, 虽然同名, 但不是同一个方法.
  2. 如果不存在焦点, 那么就会调用ViewRootImpl#focusSearch, 这个方法直接调用了FocusFinder#findNextFocus来查找合适的控件
  3. 当找到具体的控件后, 就会调用该控件的requestFocus方法

这个过程说明

  1. 按下方向键时, 如果没有控件持有焦点, 那么我们不能控制候选控件的选择
  2. 按下方向键时, 如果有控件持有焦点, 那么可以通过重写这个控件的父控件ViewParent#focusSearch来控制候选控件的选择
  3. 无论是如何得到候选控件, 这个控件是通过requestFocus来获取焦点的, 后续流程参考一点见解: 焦点那点事(一)

焦点控件失去焦点资格

上一篇文章提到控件要获取焦点必须符合

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

unFocusable和unVisibility

改变控件的这两个状态, 最终会调用View#setFlags方法, 在该方法中, 如果焦点控件是变为了不可见或者不可获取焦点, 那么就会调用View#clearFocus来清除焦点, 跟手动清除焦点流程一样.

FOCUS_BLOCK_DESCENDANTS

如果父控件突然变为了FOCUS_BLOCK_DESCENDANTS, 不会影响当前焦点控件的状态, 只会影响下一次焦点分发/查找的流程.

焦点控件被移除

控件被移除, 最终都会调用ViewGroup#removeViewInternal方法, 在这个方法中, 首先会调用View#unFocus来清除焦点, 具体参考上一篇文章的介绍, 因为View#unFocus方法不会调用ViewParent#clearChildFocus, 因此ViewGroup会主动调用自己的clearChildFocus方法, 紧接着会调用View#rootViewRequestFocus方法, 在这个方法中会调用getRootView()#requestFocus, 然后就会遍历一次控件树来重新分发焦点.

控件获得焦点资格

和失去焦点资格类似, 最终会调用View#setFlags方法, 然后调用ViewParent#focusableViewAvailable方法, 默认实现中会一直向上级父控件传递, 最终就会调用ViewRootImpl#focusableViewAvailable方法, 在这个方法中, 两种情况下这个新控件可以获得焦点

  1. 如果当前没有焦点控件, 那么就会调用这个新获得焦点资格的控件的requestFocus方法
  2. 如果当前有焦点控件, 同时新的这个控件是当前焦点控件的子控件, 而这个焦点控件的焦点分发策略为FOCUS_AFTER_DESCENDANTS, 那么还是会调用requestFocus来把焦点给这个新的控件

新增控件(有焦点资格)

通过addView方式添加控件, 都会调用ViewGroup#addViewInner方法, 在这个方法中, 如果新增的控件的hasFocus方法为true, 那么就会调用父控件的ViewParent#requestChildFocus, 参考上一篇文章可以知道, 在这个方法里会把现有的焦点控件的焦点清除掉. 也就是说, 新增的控件如果持有焦点, 那么就会替换现有的控件成为焦点控件.

如果新增的控件没有持有焦点, 即使它有焦点资格, 也不会有任何焦点相关的回调

注意: 新增(addView)控件时, 无论这个控件会不会获得焦点, ViewParent#focusableViewAvailable都不会被调用.

总结

  1. 页面第一次刷新布局时会通过根控件的requestFocus来寻找第一个焦点控件
  2. 当键盘输入方向事件时, 页面会通过ViewParent#focusSearch来寻找下一个焦点控件, 并调用它的requestFocus方法
  3. 焦点控件的可见性或者focusable属性发生变化, 导致该控件不能继续持有焦点, 那么就会清除焦点, 并重新通过根控件的requestFocus来分发焦点
  4. 当控件从不能持有焦点变为可以持有焦点, 会触发ViewParent#focusableViewAvailable, 并在两种情况下会替换旧焦点控件.
  5. 当焦点控件从布局中移除, 会重新通过根控件的requestFocus来分发焦点
  6. 当可以获取焦点的控件新增进布局时, 不会调用ViewParent#focusableViewAvailable, 如果该控件被加入布局前已经持有焦点, 那么就会替换旧焦点控件, 否则就不会触发焦点相关方法.

RecyclerView是一个非常常用的控件, 其中列表中的子控件会复用/移除/新增等, 因此焦点的处理也比较特殊, 下一篇会详细分析RecyclerView的焦点处理逻辑, 以此得到移除焦点控件后重新分发焦点的解决方案.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • Android开发使用的手机一般处于触摸模式, 因此默认情况下并不会有焦点, 所以之前一直对焦点不是很熟悉. 但是...
    AssIstne阅读 8,103评论 1 18
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,805评论 25 707
  • 前言 相信很多刚接触AndroidTV开发的开发者,都会被各种焦点问题给折磨的不行。不管是学技术还是学习其他知识,...
    砺雪凝霜阅读 5,566评论 15 25
  • 还行吧,毕竟没怎么学好c。
    实在想不出昵称丶阅读 177评论 0 0
  • 三天就走过我的二十年 事出有因,某月某一天在一起逛沃尔玛,三人竟然深深被自助烧烤工具吸引久久不愿离去,更萌生了众筹...
    无处留欢喜阅读 285评论 0 1