Android焦点分发策略

[TOC]

焦点分发基础知识

获取焦点的前提

  • View#isFocusable返回true, 如果在触摸模式, 则View#isFocusableInTouchMode也要返回true
  • 控件必须可见
  • 控件相关的父控件, 包括祖父控件等, ViewGroup#getDescendantFocusability()不能为ViewGroup#FOCUS_BLOCK_DESCENDANTS

焦点相关api

  • requestFocus

主动请求焦点

  • clearFocus

主动清除焦点

  • focusSearch

递归搜索需要焦点的View

  • ViewGroup.setDescendantFocusability

三个参数含义:
FOCUS_BEFORE_DESCENDANTS
ViewGroup本身相对焦点进行处理,如果沒有处理则分发给child View进行处理

FOCUS_AFTER_DESCENDANTS
先分发给Child View进行处理,如果所有的Child View都沒有处理,則自己再处理

FOCUS_BLOCK_DESCENDANTS
ViewGroup本身进行处理,不管是否处理成功,都不会分发给ChildView进行处理

  • onRequestFocusInDescendants

可以传入方向来改变遍历的顺序, 默认是从0递增,遍历子控件,调用子控件的View#requestFocus来尝试把焦点给可见的子控件, 某个子控件成功获取到焦点后, 停止遍历

注: 重写该方法可以改变ViewGroup分发焦点给子控件的行为, 例如遍历顺

  • requestChildFocus

当子控件主动放弃焦点的时候,回调这个方法通知父控件.

  • clearChildFocus

当子控件获取了焦点后, 回调这个方法通知父控件

  • focusableViewAvailable

通知父控件, 子控件的状态发生改变, 从不能获取焦点, 变成可能可以获取焦点.

  • hasFocus

当前视图是否是焦点视图或子视图里面有焦点视图

  • isFocused()

当前视图是否是焦点视图

  • findFocus

查找焦点控件

  • getFocusedChild

获取子控件的焦点view

参考博客

焦点分发源码分析

在Android中任何事件都会经由ViewRootImpl$ViewPostImeInputStage的onProcess处理.在onProcess方法中会判断不同事件类型做不同的处理.这里我们只分析触发焦点转移的KeyEvent.事件分发机制参考

当我们触发按键,最终会由底层把事件传入onProcess,在onProcess中如果是KeyEvent类型就进入processKeyEvent方法中进行处理.

processKeyEvent方法源码如下:

private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent)q.mEvent;

            if (event.getAction() != KeyEvent.ACTION_UP) {
                // If delivering a new key event, make sure the window is
                // now allowed to start updating.
                handleDispatchDoneAnimating();
            }
            //1.向View树分发KeyEvent事件
            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {
                return FINISH_HANDLED;
            }

            //........................此处省略部分代码
            
            //2.处理焦点的转移
            // Handle automatic focus changes.
           //........................此处省略部分代码
                if (direction != 0) {
                //step1 找到当前View树中的焦点View
                    View focused = mView.findFocus();
                    if (focused != null) {
                    //step2 当前焦点View根据方向搜索下一个需要焦点的View并主动请求焦点
                        View v = focused.focusSearch(direction);
                        if (v != null && v != focused) {
                            // do the math the get the interesting rect
                            // of previous focused into the coord system of
                            // newly focused view
                            focused.getFocusedRect(mTempRect);
                            if (mView instanceof ViewGroup) {
                                ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                        focused, mTempRect);
                                ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                        v, mTempRect);
                            }
                            if (v.requestFocus(direction, mTempRect)) {
                                playSoundEffect(SoundEffectConstants
                                        .getContantForFocusDirection(direction));
                                return FINISH_HANDLED;
                            }
                        }

                        // Give the focused view a last chance to handle the dpad key.
                        if (mView.dispatchUnhandledMove(focused, direction)) {
                            return FINISH_HANDLED;
                        }
                    } else {
                        // find the best view to give focus to in this non-touch-mode with no-focus
                        //step3 ViewRootImpl根据方向搜索下一个需要焦点的View并主动请求焦点
                        View v = focusSearch(null, direction);
                        if (v != null && v.requestFocus(direction)) {
                            return FINISH_HANDLED;
                        }
                    }
                }
            }

这个方法中主要做了两件事,1.向View树分发KeyEvent事件 2.处理焦点的转移,下面主要分析处理焦点的转移这条逻辑.

1.向View树分发KeyEvent事件

// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
    return FINISH_HANDLED;
}

这就是我们熟悉View层的事件分发,同理还有

  • dispatchHoverEvent

  • dispatchTouchEvent

2.处理焦点的转移

step1 : 当我们的按键有一个确定的方向,首先找到当前View树中的焦点view.

 if (direction != 0) {
    View focused = mView.findFocus();
}

step2 : 如果找到了当前焦点View,则调用当前焦点View的焦点搜索方法,并返回下一个需要焦点的View,最后需要焦点的View主动请求焦点

  View v = focused.focusSearch(direction);
  
  if (v.requestFocus(direction, mTempRect)) {
    playSoundEffect(SoundEffectConstants
            .getContantForFocusDirection(direction));
    return FINISH_HANDLED;
    }

最终View#focusSearch直接调用的是ViewPaent#focusSearch
ViewParent是一个接口,最后在ViewGroup中实现,后文分析ViewGroup#focusSearch,View的focusSearch方法如下:

public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);
        } else {
            return null;
        }
    }

step3 : 如果未找到当前焦点View,则调用ViewRootImpl的焦点搜索方法,并返回下一个需要焦点的View,最后需要焦点的View主动请求焦点

View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
    return FINISH_HANDLED;
}

本质上ViewRootImpl#focusSearch方法重写的是ViewPaent#focusSearch,此方法内部直接调用焦点帮助类,返回一个需要焦点的View.方法如下:

    @Override
    public View focusSearch(View focused, int direction) {
        checkThread();
        if (!(mView instanceof ViewGroup)) {
            return null;
        }
        return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
    }

至此,焦点分发的逻辑就进入了View层,下面分析View层的逻辑.

ViewGroup#focusSearch

因为View的focusSearch直接调用的是ViewParent的focusSearch,所以我们只分析ViewGroup的focusSearch

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

ViewGroup的focusSearch方法非常简单,如果是root View就调用焦点帮助类返回一个需要焦点的View,反之递归调用ViewGroup的focusSearch,直到找到一个需要焦点的View.

requestFocus

了解完focusSearch我们继续分析requestFocus,requestFocus在View和ViewGrope有各自不同的实现.

ViewGroup#requestFocus

@Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        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);
            }
        }
    }

ViewGroup的requestFocus会根据三种分发策略决定是自己先请求还是child先请求,最终都会调用View的requestFocus

View#requestFocus

View的requestFocus会调用到requestFocusNoSearch
在requestFocusNoSearch中做一些位运算后最终调用handleFocusGainInternal

void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {

        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中通知父容器自己请求了焦点,回调onFocusChanged,并刷新View

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

推荐阅读更多精彩内容