Android焦点分发和移动的原理

如果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的窗口机制,这个不是今日的目标,本文要讲的是:

  1. requestFocus和背后的焦点分发机制;
  2. clearFocus真的无效吗?
  3. 如果让焦点按意志移动。

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<View> 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<View> 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焦点常用的方法和原理,有建议或疑问可以交流一下。

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

推荐阅读更多精彩内容