TouchMode
android焦点的概念其实一直都在,我们平时做android开发的时候往往只有EditText有焦点,其他都不需要关心焦点。但是做ott开发,焦点就是经常要考虑的问题。
早期的android不支持触屏,是通过trackball来控制的,后来多了触屏,android 增加了一种概念,叫TouchMode。当用户通过触屏操作手机的时候自动进入叫TouchMode,当用户通过trackball或者遥控器操作手机或者android tv的时候,就会自动退出叫TouchMode。
在TouchMode的时候,大部分控件是不需要焦点的(EditText除外)。在非TouchMode的时候,控件首先要获得焦点,然后才能点击。
focusable与EditText
如果想要一个控件可以获取到焦点,往往用如下代码
android:focusableInTouchMode="true"
android:focusable="true"
focusable代表的是能不能获取到焦点,focusableInTouchMode代表的是TouchMode下能否获取到焦点。
默认情况下大部分的view都是无法获取焦点的,但是EditText可以,为什么?看下面的代码就明白了,构造的时候设置了这个参数。
public EditText(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.editTextStyle);
}
//themes.xml
<item name="editTextStyle">@style/Widget.EditText</item>
//styles.xml
<style name="Widget.EditText">
<item name="focusable">true</item>
<item name="focusableInTouchMode">true</item>
<item name="clickable">true</item>
...
</style>
获取焦点
对应工程Focus
焦点从上往下分发
activity第一次performTraversals的时候会请求焦点,如下所示,下边的mView是DecorView,调用DecorView的requestFocus,这个方法会逐渐往下去寻找可以获取焦点的view,找到一个可以获取焦点的view,就返回true。
//performTraversals:2023, ViewRootImpl (android.view)
if (mView != null) {
if (!mView.hasFocus()) {
mView.requestFocus(View.FOCUS_FORWARD);
}
}
这里的requestFocus会调用ViewGroup的requestFocus,主要根据descendantFocusability来分发焦点,DecorView是FOCUS_AFTER_DESCENDANTS
的(一般的ViewGropup是FOCUS_BEFORE_DESCENDANTS
),所以DecorView焦点分发的顺序如下所示,先子后父,如果子view里处理了,就不用给父view了。注意下边的requestFocus和super.requestFocus是完全不一样的,requestFocus是ViewGroup的方法,super.requestFocus是View的方法(是不是跟事件分发非常像?)。
requestFocus->1、onRequestFocusInDescendants
2、super.requestFocus
onRequestFocusInDescendants的流程如下,可以看到主要内容就是让visible的子view去requestFocus.这个子view的requestFocus,又会进行焦点分发,直到某个view处理了焦点。
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
...
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
焦点请求
焦点分发最后,可能有个view拿到了焦点,他的View::requestFocus就会被调用。requestFocus()->requestFocusNoSearch->handleFocusGainInternal
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
handleFocusGainInternal代码如下,重点是L12,可以看到会调用mParent.requestChildFocus。requestChildFocus会往上一直调用此方法。
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);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
requestChildFocus的调用大概是这样,第二个参数就是获得焦点的那个view,第一个参数是this的子view,是第二个参数的祖先。
p.requestChildFocus(c,c)
p1.requestChildFocus(p,c)
....
ViewRootImpl.requestChildFocus(pN,c)
requestChildFocus的代码如下所示,可以看到这里主要做的就是mFocused的赋值,还有旧的mFocused的unFocus,以及调用上层的requestChildFocus
。
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);
}
mFocused = child;
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
最终会调用到ViewRootImpl的requestChildFocus,如下所示,主要就是scheduleTraversals,这个会触发一次doTraversal->performTraversals。
public void requestChildFocus(View child, View focused) {
if (DEBUG_INPUT_RESIZE) {
Log.v(TAG, "Request child focus: focus now " + focused);
}
checkThread();
scheduleTraversals();
}
onFocusChanged
整个过程中只有得到焦点的那个view会被回调onFocusChanged(true,,)。当然这里是首次获取焦点,如果焦点转移,那又不一样了,肯定会有2个onFocusChanged被调用。
流程总结
可以看到之前的,requestFocus流程可以分为以下二部。
//performTraversals:2023, ViewRootImpl (android.view)
if (mView != null) {
if (!mView.hasFocus()) {
mView.requestFocus(View.FOCUS_FORWARD);
}
}
第一步,将focus分发到某个具体的view,触发这个view的View::requestFocus
DecorView.requestFocus(ViewGroup)->...->AL.requestFocus(ViewGroup)->BButton.requestFocus(View)
第二步,最终拿到focus的view触发 handleFocusGainInternal。mParent.requestChildFocus
是递归朝上的,会导致各个ViewGroup的mFocused被设置。
handleFocusGainInternal->1、mParent.requestChildFocus
2、onFocusChange
ViewGroup内焦点分发
刚才提到descendantFocusability这个参数,这个参数决定了,子view和父view如何处理焦点,代码如下,简单易懂。
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
}
onRequestFocusInDescendants是子view来处理焦点,super.requestFocus是让父view自己处理焦点,这里注意区分View的requestFocus和ViewGroup的requestFocus。
FOCUS_BLOCK_DESCENDANTS | 只有父view可以处理焦点,子view资格被剥夺 |
---|---|
FOCUS_BEFORE_DESCENDANTS | 父view先处理,如果没处理好,那交由子view |
FOCUS_AFTER_DESCENDANTS | 子view先处理,如果处理不好交友父view |
点击获取焦点
//View
public boolean onTouchEvent(MotionEvent event) {
。。。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}