android requestFocus()和cleanFocus()源码分析

在最近写的项目中遇到由focus引起的问题,例如:

  1. 在两个嵌套的RecyclerView中,外层滑动停止后,由于内层的RecyclerView获取了焦点,导致外层又自动滑动了一些距离;

  2. 需要监听EditText的焦点变化来控制界面的显示于隐藏,在调用EditText的cleanFocus()方法时,回调接口会调用两次,并且EditText的焦点依然还在。

对遇到的这两个问题的解决方法都一样,就是在外层,或根布局中添加focusInTouchModel:true,focus:true两个属性,优先获取到焦点就可以了。

布局文件中和focus有关的属性

//是否可获取焦点
focusable:true|false
//触摸模式下是否可获取焦点
focusableInTouchMode:true|false
descendantFocusability:blocksDescendants|beforeDescendants|afterDescendants

这里解释一下descendantFocusability的三个属性值,分别代表的意思:

  1. blocksDescendants:ViewGroup拦截,不让子 view获取焦点。
  2. beforeDescendants:ViewGroup优先尝试(尝试的意思是,根据View或ViewGroup当前状态来判断是否能得到焦点,如是否可见,是否可获取焦点等等,在View的requestFocus方法的注释中提到,下同)获取焦点,若ViewGroup没拿到焦点,再遍历子 view(包括所有直接子 view和间接子 view),让子 view尝试获取焦点。
    3.afterDescendants:先遍历子 view,让子 view尝试获取焦点,若所有子 view(包括所有直接子 view和间接子 view)都没拿到焦点,才让ViewGroup尝试获取焦点。

下面是ViewGroup的与descendantFocusability有关的源码

    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " ViewGroup.requestFocus direction="
                    + direction);
        }
        //获取当前viewgroup的descendantFocusability属性值
        int descendantFocusability = getDescendantFocusability();

        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS:
                //直接调用超类也就是View的方法获取焦点,忽略子 view
                return super.requestFocus(direction, previouslyFocusedRect);
            case FOCUS_BEFORE_DESCENDANTS: {
                //当前ViewGroup先尝试获取焦点,返回值表示是否拿到来焦点,若没有,则让子 view尝试获取  
                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);
        }
    }
requestFocus() 方法的执行流程

上面看的是ViewGroup的requestFocus()方法,而ViewGroup的这个方法只是对ViewGroup和子 view之间获取焦点的顺序做的一个处理,要拿到焦点,最终还是要通过调用View的requestFocus()方法来拿到焦点。所以接下来主要看的是View的方法。

public final boolean requestFocus() {
        return requestFocus(View.FOCUS_DOWN);
    }
public final boolean requestFocus(int direction) {
        return requestFocus(direction, null);
    }
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        return requestFocusNoSearch(direction, previouslyFocusedRect);
    }
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,判断是否有父view拦截
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }

        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }

可以看到,在外部调用requestFocus()这个无参方法后,经过层层传递后,最终是调用requestFocusNoSearch(int direction, Rect previouslyFocusedRect)方法,这几个方法的返回值表示是否拿到了焦点,在里面可以看到有3个if判断语句,这就是我们上面提到的“尝试”的意思了,即需要根据当前状态判断,通过判断,最后直接返回了true,表示拿到了焦点,而在返回之前,还执行了一个很重要的方法handleFocusGainInternal(direction, previouslyFocusedRect),来看看里面都干了些什么:

void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " requestFocus()");
        }
        //  若当前已拿到焦点,则方法结束,所以一个已获焦点的view,在执行requestFocus()后,它的onFocusChanged方法是不会回调的
        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            //标记已拿到焦点
            mPrivateFlags |= PFLAG_FOCUSED;
        //上个拿到焦点的view
            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
            
            if (mParent != null) {
              //递归调用,重置ViewGroup的mFocused的值,重置oldFocus的mPrivateFlags标记位
                mParent.requestChildFocus(this, this);
            }

            if (mAttachInfo != null) {
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }

            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();
        }
    }

ViewGroup的requestChildFocus(this, this)方法

 @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变量值
            mFocused = child;
        }
        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }
    }

然后通过View的unFocus()重置标记位

void unFocus(View focused) {
        if (DBG) {
            System.out.println(this + " unFocus()");
        }
      //注意看两个参数都是false
        clearFocusInternal(focused, false, false);
    }
/**
     * Clears focus from the view, optionally propagating the change up through
     * the parent hierarchy and requesting that the root view place new focus.
     *
     * @param propagate whether to propagate the change up through the parent
     *            hierarchy
     * @param refocus when propagate is true, specifies whether to request the
     *            root view place new focus
     */
    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);
            }
        }
    }
requestFocus()主要流程是判断view当前状态是否能拿到焦点,若能,则清除原来获取焦点的view的标记位,然后返回结果。这里说明一下:ViewGroup的mFocused属性,它表示当前ViewGroup的一个直接子 view获取,而真正拿到焦点的view则是mFocused或mFocused的子 view。
再来看看CleanFocus()方法
public void clearFocus() {
        if (DBG) {
            System.out.println(this + " clearFocus()");
        }
        //注意参数
        clearFocusInternal(null, true, true);
    }
/**
     * Clears focus from the view, optionally propagating the change up through
     * the parent hierarchy and requesting that the root view place new focus.
     *
     * @param propagate whether to propagate the change up through the parent
     *            hierarchy
     * @param refocus when propagate is true, specifies whether to request the
     *            root view place new focus
     */
    void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
        //若当前没有拿到焦点,则直接返回
        if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
            mPrivateFlags &= ~PFLAG_FOCUSED;

            if (propagate && mParent != null) {
            //将ViewGroup的mFocused置空
                mParent.clearChildFocus(this);
            }

            onFocusChanged(false, 0, null);
            refreshDrawableState();
            //rootViewRequestFocus()让根视图重新设置focus,就是之前说的requestFocus方法,按一定顺序重新给焦点的过程
            if (propagate && (!refocus || !rootViewRequestFocus())) {
                notifyGlobalFocusCleared(this);
            }
        }
    }

看了上面的代码可以解释上面遇到的第二个问题了,当EditText是第一个能获得焦点的view时,执行cleanFocus()方法,在重置焦点标记位时会调用一次onFocusChanged回调方法,在之后的requestFocus流程中,又得到了焦点,所以经历了两次回调,所以在父 view或根视图加上focusable和focusableInTouchMode为true时,就解决了这个问题。

关于focus的内容说到这里就完了,相信大家看完这个,对focus应该会有了解了,有不对的地方,欢迎大家指出,互相学习。虽然遇到focus的问题比较少,而且解决起来也很简单,但还是研究了一下源码,主要就是学习看源码的方法,就是debug,跟着一步一步的走,搞清代码执行的流程。

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

推荐阅读更多精彩内容