android 焦点作用,Android焦点分发和移动的原理

本文详细解析了Android Activity中EditText自动获取焦点的原因,并深入探讨requestFocus、clearFocus和焦点分发机制,包括ViewRootImpl的处理逻辑,焦点的传递规则,以及ViewGroup的焦点请求和处理。还揭示了clearFocus看似无效实则是因为焦点转移时机的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如果Activity里有EditText,那么打开Activity后,EditText会自动获取焦点。

为什么呢,很多时候我们不想要这个效果,参照网上的方法将father layout设置成获取焦点就解决问题。知其然知其所以然,翻了一下代码,答案隐藏在ViewRootImpl.performTraversals方法中,就是那个view绘制的核心方法,中间有一段:

private void performTraversals() {

//...

if (mFirst) {

// handle first focus request

if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: mView.hasFocus()="

+ mView.hasFocus());

if (mView != null) {

if (!mView.hasFocus()) {

mView.requestFocus(View.FOCUS_FORWARD);

if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: requested focused view="

+ mView.findFocus());

} else {

if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: existing focused view="

+ mView.findFocus());

}

}

}

//...

}

当是第一个view时,会调用requestFocus获取焦点。ViewRootImpl相关内容自行看android的窗口机制,这个不是今日的目标,本文要讲的是:

requestFocus和背后的焦点分发机制;

clearFocus真的无效吗?

如果让焦点按意志移动。

be7e84832632

demo

写了个测试用的demo,上面很多EditText啦,还有上下左右前后等焦点的控制键。

View是否能获取焦点

让View获取焦点,直接调用requestFocus,最终会调用到requestFocusNoSearch:

private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {

// need to be focusable

if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||

(mViewFlags & VISIBILITY_MASK) != VISIBLE) {

return false;

}

// need to be focusable in touch mode if in touch mode

if (isInTouchMode() &&

(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {

return false;

}

// need to not have any parents blocking us

if (hasAncestorThatBlocksDescendantFocus()) {

return false;

}

handleFocusGainInternal(direction, previouslyFocusedRect);

return true;

}

requestFocusNoSearch校验View的属性,获取焦点的前提条件是“可见的”和“可聚焦的”,并且“可聚焦的”需要同时符合:

android:focusable="true"

android:focusableInTouchMode="true"

接着调用了hasAncestorThatBlocksDescendantFocus,这个需要了解View的descendantFocusability属性。这对我来说是新概念,以前没有用过,后文还会涉及,现在先储备知识。

beforeDescendants:ViewGroup会优先其子view而获取到焦点

afterDescendants:ViewGroup只有当其子view不需要获取焦点时才获取焦点

blocksDescendants:ViewGroup会覆盖子view而直接获得焦点

private boolean hasAncestorThatBlocksDescendantFocus() {

final boolean focusableInTouchMode = isFocusableInTouchMode();

ViewParent ancestor = mParent;

while (ancestor instanceof ViewGroup) {

final ViewGroup vgAncestor = (ViewGroup) ancestor;

if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS

|| (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {

return true;

} else {

ancestor = vgAncestor.getParent();

}

}

return false;

}

hasAncestorThatBlocksDescendantFocus就很好理解,如果有祖先ViewGroup设置成blocksDescendants,那么它的子孙View都不能获取焦点。

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();

}

}

handleFocusGainInternal实现View获取焦点的具体逻辑,所以requestFocusNoSearch默认返回true。handleFocusGainInternal里面最重要的是调用了mParent.requestChildFocus,通知它的父view处理焦点。mParent的类型是ViewParent,每一个view都会保存它的父view,基本上实现类就是ViewGroup。

然后触发onFocusChanged这个listener,最后触发invalidate进行ui更新。

在继续探究requestChildFocus的代码前,先认真讲讲焦点的分发过程。

焦点分发过程

有个大家族,已经经历多代,族人角色可以这样定义:

成员:View

有子女的成员:ViewGroup

辈分最高的长老:DecorView

家族中有一件宝贝,持有在一名成员手上。别的家族想参观,首先需要找长老。

长老不会一个个成员问,而是先找大儿子问,再找二儿子问,如此类推。儿子们也是这样问自己的儿子,过程也是如此类推。一层层地问,直到最后找到宝贝的持有人,再一层层向上通知。

宝贝就是焦点,寻找宝贝的过程就是焦点分发的过程。

ViewGroup对焦点的处理

看回handleFocusGainInternal里的requestChildFocus,view如果需要获取焦点,需要通知它的父view处理,所以我们来看ViewGroup的requestChildFocus:

@Override

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);

}

}

首先会调用unFocus清除自己的焦点,mFocused表示ViewGroup内部是否持有焦点,如果mFocused不是目标获取焦点的child,那么再清除当前mFocused的焦点,并将child赋给mFocused。

最后继续通过mParent递归调用requestChildFocus,直到顶层view,保证焦点唯一。

ViewGroup也可以获取焦点,和上面View的requestFocus方法不同:

@Override

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);

}

}

有了前面descendantFocusability属性的铺垫,ViewGroup的requestFocus很容易理解。block状态时,焦点查找交还给父View;before状态时,优先自己获取焦点;after状态时,优先子view获取焦点。

protected boolean onRequestFocusInDescendants(int direction,

Rect previouslyFocusedRect) {

int index;

int increment;

int end;

int count = mChildrenCount;

if ((direction & FOCUS_FORWARD) != 0) {

index = 0;

increment = 1;

end = count;

} else {

index = count - 1;

increment = -1;

end = -1;

}

final View[] children = mChildren;

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;

}

onRequestFocusInDescendants方法就是向子view询问焦点的逻辑,区分正反两种查找方向。只要有一个view成功获取到焦点,就返回true。

清除焦点

上面没有讲view失去焦点的处理,现在来看下ViewGroup的unFocus,还要探究一下clearFocus“无效”的背后原理。

@Override

void unFocus(View focused) {

if (DBG) {

System.out.println(this + " unFocus()");

}

if (mFocused == null) {

super.unFocus(focused);

} else {

mFocused.unFocus(focused);

mFocused = null;

}

}

ViewGroup的unFocus,最终调用了View的unFocus。

void unFocus(View focused) {

if (DBG) {

System.out.println(this + " unFocus()");

}

clearFocusInternal(focused, false, false);

}

void clearFocusInternal(View focused, boolean propagate, boolean refocus) {

if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {

mPrivateFlags &= ~PFLAG_FOCUSED;

if (propagate && mParent != null) {

mParent.clearChildFocus(this);

}

onFocusChanged(false, 0, null);

refreshDrawableState();

if (propagate && (!refocus || !rootViewRequestFocus())) {

notifyGlobalFocusCleared(this);

}

}

}

clearFocusInternal是真正操作焦点失去的地方,通过mParent调用ViewGroup的clearChildFocus。

@Override

public void clearChildFocus(View child) {

if (DBG) {

System.out.println(this + " clearChildFocus()");

}

mFocused = null;

if (mParent != null) {

mParent.clearChildFocus(this);

}

}

clearChildFocus将当前mFocused置空,通过递归向上处理直到顶层view,保证整颗view树失去焦点。

注意,unFocus我们并不能调用,View提供clearFocus,内部同样调用clearFocusInternal,它们不同的地方是refocus传入不同。

boolean rootViewRequestFocus() {

final View root = getRootView();

return root != null && root.requestFocus();

}

refocus的不同,决定是否会触发rootViewRequestFocus,因此clearFocus“无效”的问题很好理解。如果一个页面只有一个EditText,使用clearFocus清除焦点,马上地,焦点又被设置上啦,所以会有清除无效的错觉。因此,让父view自动获取焦点是很好的解决方法。

焦点查找

@Override

public View focusSearch(View focused, int direction) {

if (isRootNamespace()) {

// root namespace means we should consider ourselves the top of the

// tree for focus searching; otherwise we could be focus searching

// into other tabs. see LocalActivityManager and TabHost for more info

return FocusFinder.getInstance().findNextFocus(this, focused, direction);

} else if (mParent != null) {

return mParent.focusSearch(focused, direction);

}

return null;

}

View和ViewGroup提供了focusSearch方法进行焦点查找,入参是当前获取焦点的view和目标查找方向,返回下一个应该获取焦点的view。focusSearch调用的是FocusFinder类,直接来看FocusFinder最常用的findNextFocus:

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {

//1

View next = null;

if (focused != null) {

next = findNextUserSpecifiedFocus(root, focused, direction);

}

if (next != null) {

return next;

}

//2

ArrayList focusables = mTempList;

try {

focusables.clear();

root.addFocusables(focusables, direction);

if (!focusables.isEmpty()) {

next = findNextFocus(root, focused, focusedRect, direction, focusables);

}

} finally {

focusables.clear();

}

return next;

}

1、预设焦点

看标记1,调用了findNextUserSpecifiedFocus,查找用户预设不同方向获取焦点的View。

private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {

// check for user specified next focus

View userSetNextFocus = focused.findUserSetNextFocus(root, direction);

if (userSetNextFocus != null && userSetNextFocus.isFocusable()

&& (!userSetNextFocus.isInTouchMode()

|| userSetNextFocus.isFocusableInTouchMode())) {

return userSetNextFocus;

}

return null;

}

里面调用了View.findUserSetNextFocus,在xml文件中,我们可以使用android:nextFocusLeft、android:nextFocusRight、android:nextFocusUp、android:nextFocusDown、android:nextFocusForward指定对应的View。

2、自动查找焦点

如果没有预设,就由程序自动查找。标记2收集root下所有能获取焦点的view,调用重载版本的findNextFocus方法。

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,

int direction, ArrayList focusables) {

//1

//...

//2

switch (direction) {

case View.FOCUS_FORWARD:

case View.FOCUS_BACKWARD:

return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,

direction);

case View.FOCUS_UP:

case View.FOCUS_DOWN:

case View.FOCUS_LEFT:

case View.FOCUS_RIGHT:

return findNextFocusInAbsoluteDirection(focusables, root, focused,

focusedRect, direction);

default:

throw new IllegalArgumentException("Unknown direction: " + direction);

}

}

这里我省略了标记1一大段代码,大约逻辑是计算焦点的矩形范围,如果当前已经有view得到焦点,直接通过view计算即可;如果没有,那么通过root和方向计算,比较简单,就不贴出来占地方。

标记2根据查找方向使用不同算法,前项和后项使用findNextFocusInRelativeDirection,上下左右使用findNextFocusInAbsoluteDirection。

对于前项和后项这种按序的查找,很容易想到需要对view进行排序,这里使用了内部类SequentialFocusComparator,根据view矩形的高低左右比较。

对于上下左右方向,需要在能获取焦点view中比较出最适合的一个。首先会设置一个差的结果,然后对每一个可以获取焦点的view调用isBetterCandidate,找到方向上离自己最近最合适的一个。算法比较复杂,有兴趣自行研究。

private fun doFocusUp() {

currentFocus?.let {

currentFocus.focusSearch(View.FOCUS_UP)?.requestFocus()

}

}

private fun doFocusDown() {

currentFocus?.let {

currentFocus.focusSearch(View.FOCUS_DOWN)?.requestFocus()

}

}

private fun doFocusLeft() {

currentFocus?.let {

currentFocus.focusSearch(View.FOCUS_LEFT)?.requestFocus()

}

}

private fun doFocusRight() {

currentFocus?.let {

currentFocus.focusSearch(View.FOCUS_RIGHT)?.requestFocus()

}

}

private fun doFocusForward() {

val focusView = currentFocus ?: return

FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_BACKWARD)?.requestFocus()

}

private fun doFocusNext() {

val focusView = currentFocus ?: return

FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_FORWARD)?.requestFocus()

}

demo里上下左右前后六个方向就是使用FocusFinder实现。focusSearch限制了只能使用上下左右四个方向,前后两个方向直接调用FocusFinder。

小结

本文总结了android焦点常用的方法和原理,有建议或疑问可以交流一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值