问题描述:

两个各自带有一个EditText的Fragment按顺序添加到同一个Activity上。使用自定义软键盘(该软键盘按相对位置直接显示在DecorView的Bottom上),在第二个Fragment中调用软键盘的隐藏键盘方法(通过清除RootView(DecorView)的焦点来实现清除子View(EditText)的焦点,来触发绑定在软键盘上的EditText所注册的View.OnFocusChangeListener方法实现键盘隐藏)。
在源码中RootView清除焦点后会寻找一个View来重新获取焦点,这时问题(1)出现了,隐藏在后面(被第二个Fragment覆盖)的第一个Fragment的EditText获取了焦点,其所注册的View.OnFocusChangeListener方法被触发,这时界面上显示出第一个Fragment的EditText所绑定的软键盘。扩展小问题为什么在清除焦点后需要重新找一个View来获取焦点呢?为什么是第一个Fragment的EditText获取到焦点呢?

问题(2)是该自定义键盘是显示在哪个View上的,或者是直接在Windows上显示?

问题(3)如果需要在点击按钮(如设置密码的确定按钮)后在加载视图弹出来之前隐藏键盘,那么需要找到另外一个控件来获取焦点;

问题(4)View的requestFocus()方法先判断是否为可获取焦点,再判断是否在touch模式下可获取焦点,为何要进行二次判断?isFocusableisFocusableInTouchMode的有什么区别?

问题解决或答疑:

问题(1):一个简单的解决方式是在第三个Fragment(本应用中恰好有第三个Fragment)上的某个控件(选第一个控件,减少查找时间)设置focusablefocusableInTouchModetrue,并且在onCreateView()中让该控件获取焦点。
扩展小问题:对于第一个小问题,这是View的绘制机制所决定的,在ViewRootImpl绘制View的核心方法performTraversals()中,会判断是否是在touch模式下,如果是在touch模式下,就会找到他的祖先View去调用requestFocus()方法。这么做的原因我猜想是为了减少用户操作以提高用户体验,也即如果在界面上有EditText或者其他需要获取焦点的View,用户就不需要多一次点击操作来显示软键盘或者实现其他什么目的。
对于第二个小问题,requestFocus()方法最终会调用handleFocusGainInternal方法,在里面触发mParent.requestChildFocus。如果是ViewGroup的话,它会判断是父类先获取焦点还是子类先获取焦点,如果父类没有阻挡子类获取焦点的话,会调用到onRequestFocusInDescendants方法,在这个方法中,它会按顺序遍历ViewGroup用来存储子View的mChildren数组,直到拿到可以获取焦点的View。而在FragmentTransaction.commit()操作在添加View时,最终调用的是ViewGroup的addView()方法,也即一个一个Fragment往后添加,所以在遍历mChildren数组时会首先遍历第一个Fragment,所以第一个Fragment中的EditText获取到了焦点。

问题(2):自定义软键盘为了在任意界面都可以显示,所以使用getDecorView来获取顶层View,然后按相对位置把软键盘View添加到该View底部。

问题(3):可以设置EditText的父控件的isFocusableInTouchMode属性为true,然后在点击按钮时让父控件主动获取焦点requestFocus()

问题(4):这要从android交互模式说起,第一部android手机同时支持touch模式、trackball模式、keyboard模式,发展到现在在手机上touch模式占了用户交互的绝大部分,部分手机还保留home键、菜单键和返回键。而且因为android系统需要保持在不同类型设备上强大的适应性,比如pos机(keyboard模式和touch模式)、android系统智能电视(keyboard模式)、android系统可视电话(keyboard模式、trackball模式),所以设计者对不同模式间的切换做了大量设计和完善工作。
设计者在08年12月发布的开发者博客中说到,在touch模式是没有焦点的,任何在其他模式下被选中或者说获得焦点而高亮的item在用户点击屏幕进入touch模式后都会变成未被选择的,当用户使用其他交互模式,比如trackball模式时,原本被选中的item会重新获得焦点。所以最开始的android新人开发者们很容易碰到的一个问题是在touch模式下使用 ListView.getSelectedItemPosition()会返回 INVALID_POSITION,正确的用法应该是使用click listeners或者choice模式,而使用click listeners对现在的新人开发者来说可能是很平常的操作。但他们还说在某些特别的情况下当用户使用touch模式时控件仍然可以获取焦点,这种模式就是为了使控件接收到文本输入,像EditText。
所以isFocusable表示控件在任何模式下是否可以获得焦点,而在touch模式中还需要加上isFocusableInTouchMode使得控件在此模式下能够获得焦点。

-完-

参考:

Android焦点分发和移动的原理
Android窗口机制(四)ViewRootImpl与View和WindowManager
Android Touch Mode
Fragment FragmentManager FragmentTransaction 深入理解