《深入理解Android:卷三》深入理解控件系统读书笔记(下)

本篇文章承接上一篇《深入理解Android:卷三》深入理解控件系统读书笔记(中),从输入事件在控件树中的派发和处理,以及Activity层面,继续深入了解Android的控件系统。

6.5 深入理解输入事件的派发

控件树中的输入事件派发是由ViewRootImpl为起点,沿着控件树一层一层传递给目标控件,最终再回到ViewRootImpl的一个环形过程。当一个输入事件被派发给ViewRootImpl所在的窗口时,InputEventReceiverLooper会被唤醒并触发InputEventReceiver.onInputEvent()回调,控件树的输入事件派发便起始于这一回调

6.5.1 触摸模式

可以获取焦点的控件分为两类:

  • 在任何情况下都可以获取焦点的控件,如文本框
  • 仅在键盘操作时可以获取焦点的控件,如菜单项、按钮等
    而触摸模式(TouchMode)正是为了管理两者的差异而引入的概念,Android通过进入或退出触摸模式实现两者之间的无缝切换。这个概念是一个系统级概念,也就是说会对所有窗口产生影响。系统是否处于触摸模式取决于WMS的一个成员变量mInTouchMode,而确定是否进入或者退出触摸模式则取决于用户对某一个窗口执行的操作。
      窗口的ViewRootImpl会根据用户操作,通过WMS的接口setInTouchMode()设置WMS.mInTouchMode使得系统进入或退出触摸模式。而当其他窗口进行relayout操作时会在WMS.relayoutWindow()的返回值中添加或删除RELAYOUT_RES_IN_TOUCH_MODE标记使得它们得知系统目前的操作模式。
      注意,只有拥有ViewRootImpl的窗口才能影响触摸模式,或对触摸模式产生响应。通过WMS的接口直接创建的窗口必须手动地维护触摸模式。

6.5.2 控件焦点

1. 获取焦点的条件
  当系统处于触摸模式时,仅当拥有FOCUSABLE_IN_TOUCH_MODE标记的控件才能获取焦点

2. 获取焦点

void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
            if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            //把PFLAG_FOCUSED标记加入mPrivateFlags中,表示此控件已经拥有焦点        
            mPrivateFlags |= PFLAG_FOCUSED;

            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

            //将这一变化告知父控件,目的是保证控件树中只有一个控件拥有焦点。并且在viewRootImpl中触发一次“”遍历“”以便对控件树进行重绘
            if (mParent != null) {
                mParent.requestChildFocus(this, this);
            }

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

            //通知对此控件焦点变化感兴趣的监听者。InputMethodManager的focusIn()和focusOut()也会在这里被调用以更新输入法的状态
            onFocusChanged(true, direction, previouslyFocusedRect);
          //更新控件的Drawable状态,使得控件在随后的绘制中国得以高亮显示
            refreshDrawableState();
        }
    }

3. 控件树的焦点体系
  mParent.requestChildFocus()的实现者是ViewGroupViewRootImplViewGroup实现的目的之一是用于将焦点从上一个焦点控件手中夺走,即将PFLAG_FOCUSED标记从控件的mPrivateFlags中移除。而另一个目的是将这一操作继续向控件树的根部进行回溯,直到ViewRootImplViewRootImplrequestChildFocus()会将焦点控件保存起来备用,并引发一次“遍历”。
  新的焦点体系的建立过程是通过在ViewGroup.requestChildFocus()方法的回溯过程中进行mFocused=child这一赋值操作完成的。当回溯完成后,mFocused=child将会建立起一个单向链表,使得从根控件开始通过mFocused可以沿着这一单向链表找到位于链表尾端的实际拥有焦点的控件。

mFocused所建立的焦点体系

  而旧有的焦点体系的销毁过程则是通过在回溯过程中调用mFocused.unFocus()完成

   @Override
    void unFocus() {
       if (mFocused == null) {
            //表明位于链表的尾端,自身是焦点的实际拥有者
            super.unFocus();
        } else {
           //将unFocus()传递给下一个控件
            mFocused.unFocus();
            mFocused = null;
        }
    }

可见ViewGroup.unFocus()unFocus()调用沿着mFocused所描述的链表沿着控件树向下遍历,直到焦点的实际拥有者。焦点的实际拥有者会拥有ViewGroup.unFocus(),它会将PFLAG_FOCUSED移除,并更新DrawableState以及onFocusChanged()方法的调用

View2-1-1获取焦点后的新焦点体系

4. ViewGroup的requestFocus()
  在ViewGroup上调用requestFocus()会根据其DescendantsFocusability特性的不同而产生三种不同的结果。注意ViewGroup.onRequestFocusInDescendants()会负责遍历其所有子控件,并将requestFocus()转发给他们,该方法中的direction并不是控件在屏幕上的位置,而是他们在mChildren列表中的位置,因此只有递增(FOCUS_FORWARD)或递减两种

5. 下一个焦点控件的查找
  最终的实现逻辑会走到FocusFinder.findNextFocus()

public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
        mFocusedRect.set(focusedRect);
        return findNextFocus(root, null, mFocusedRect, direction);
    }

    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;
        ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
        //1. 首先将尝试依照开发者的设置选择下一个拥有焦点的控件
        if (focused != null) {
            next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
        }
        if (next != null) {
            return next;
        }

        //2. 使用内置算法查找
        ArrayList<View> focusables = mTempList;
        try {
            focusables.clear();
            //3. 将控件树中所有可以获取焦点的控件存储到focusable列表中
            effectiveRoot.addFocusables(focusables, direction);
            if (!focusables.isEmpty()) {
                //4. 调用findNextFocus()另一个重载完成查找
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
            }
        } finally {
            focusables.clear();
        }
        return next;
    }

查找下一个焦点的内置算法,在查找最合适的候选焦点时,有如下几个比较原则:

  • 与起始位置比较,倘若一个控件A位于指定方向上,而控件B位于指定方向的另一侧,则控件A是更佳候选。
  • 将其实位置沿着查找方向延伸到无限远,行程的形式被称为BEAM一条杠。倘若一个控件A与BEAM存在交集,而另一个控件B没有,则与BEAM存在交集的控件A为更佳候选
  • 当无法通过BEAM确定更佳候选时(如两个控件与BEAM同时存在交集,或同时不存在交集),则通过比较两控件与焦点控件相邻边的中点的距离进行确定,距离近着为更佳候选。注意在进行距离计算时FocusFinder为指定方向增加了一个权重,以LEFT方向查找为例,其距离计算公式为(13dxdx + dy*dy),也就是说这个距离对于X方向的距离更佳敏感。
    更佳焦点候选的比较原则(以LEFT方向为例)

6.5.3 输入事件派发的综述

输入系统的派发终点是InputEventReceiver,作为空间系统最高级别的管理者,ViewRootImpl便是InputEventReceiver的一个用户,它从InputEventReceiver中获取时间,然后将它们按照一定的流程派发给所有可能感兴趣的对象,包括ViewPhoneWindowActivity以及Dialog

1.ViewRootImpl`的输入事件队列

ViewRootImpl.setView()中,新的窗口被创建之后,ViewRootImpl使用WMS分配的InputChannel以及当前线程的Looper一起创建了InputEventReceiver的子类WindowInputEventReceiver的一个实例,并将其保存在ViewRootImpl.mInputEventReceiver成员中,这标记着从设备驱动到本窗口的输入事件通道的正式建立。至此,每当有输入事件到来时,ViewRootImpl都可以通过WindowInputEventReceiver.onInputEvent()回调得到这个事件并进行处理

      @Override
        public void onInputEvent(InputEvent event) {
            enqueueInputEvent(event, this, 0, true);
        }

void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
        //1.将InputEvent对应的InputEventReceiver封装成QueuedInputEvent
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

        // Always enqueue the input event in order, regardless of its time stamp.
        // We do this because the application or the IME may inject key events
        // in response to touch events and we want to ensure that the injected keys
        // are processed in the order they were received and we cannot trust that
        // the time stamp of injected events are monotonic.

        //2. 追加到单向链表,ViewRootImpl将会沿着链表从头至尾地逐个处理输入事件
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            last.mNext = q;
            mPendingInputEventTail = q;
        }
        mPendingInputEventCount += 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);

        if (processImmediately) {
          //3. 倘若第三个参数为true,则直接在当前线程中开始对输入事件的处理工作
            doProcessInputEvents();
        } else {
            //4. 否则将处理事件的请求发送给主线程的Handler,随后进行处理。这是为了避免旧事件尚未处理完毕时开始了新的事件处理流程
            scheduleProcessInputEvents();
        }
    }

doProcessInputEvents()在所有输入事件处理完成之前它都不会释放主线程的占用权。这种处理方式使得performTraversals()无法在单个输入事件处理后立刻得到执行,因输入事件所导致的requestLayout()invalidate()操作会在输入事件全部处理完毕之后由一次performTraversals()统一完成。当队列中存在较多事件时这种方式带来的效率提升不言而喻。

2. 分道扬镳的事件处理
  在deliverInputEvent()方法中,不同类型的输入事件的处理开始分道扬镳:

  • deliverKeyEvent(),用于派发按键类型的事件。它选择的是基于焦点的派发策略。
  • deliverPointerEvent(),用于派发标准的触摸事件。它选择的是基于位置的派发策略。
  • deliverTrackballEvent(),用于派发轨迹球事件。
  • deliverGenericMotionEvent(),用于派发其他的Motion事件。悬浮世佳、游戏手柄等会在这里被处理。

3.共同的中点——finishInputEvent()

输入事件在ViewRootImpl中的处理流程

6.5.4 按键事件的派发

按键事件的派发流程就是沿着mFocused成员所构成的单向链表进行遍历的过程。
  围绕着输入法,ViewRootImpl.deliverKeyEvent()揭示了按键事件派发的三个阶段。受限控件树中的控件可以在输入法处理按键事件之前,通过View.dispatchKeyEventPreIme()方法获得处理机会。倘若控件未在此时消费事件,那么按键事件将会被派发给输入法。倘若输入法也没有消费这一事件,则ViewRootImpl.deliverKeyEventPostIme()将使得控件第二次有机会处理此事件

1. 按键事件的初次派发
  开发者可以通过重写View.onKeyPreIme()获得优先于输入法进行按键事件的处理。同样,也可以通过重写View.dispatchKeyPreIme()做同样的事。区别在于,前者仅当控件拥有焦点时才会被调用,而后者是mFocused链表上的所有控件都会被调用。(这个区别同样适用于其他输入事件相关的dispatchXXX()onXXX()

2. 输入法对按键事件的处理
  输入法所在的窗口时无法获得焦点的,因此需要将按键事件派发给处于焦点状态的窗口。ViewRootImpl将会在收到事件后首先转发给输入法,当输入法对此事件不感兴趣时再将其发送给控件树。
  派发给输入法的条件是mLastWasImTarget成员为true,即本窗口可能是输入法的输入目标。这一取值来源于窗口的LayoutParams.flagsFLAG_NOT_FOCUSABLEFLAG_ALT_FOCUSABLE_IM两个标记的存在情况。
  InputMethodManager.dispatchKeyEvent()方法将会通过Binder将按键事件发送给当前输入法所在的InputMethodService,并在那里通过onKeyXXX()系列事件处理方法中得到处理。

3. 按键事件的最终派发
  deliverKeyEventPostIme()负责按键事件的最终派发,onKeyDown()系列回调以及onKeyListener()监听者都会得到触发,同时一些系统内置的按键功能也将在这里进行处理。
  按照优先级,可以列出如下几个可能消费事件的对象或行为:

  • TouchMode。如果导致了触摸模式的终止,此事件会被消费
  • 控件树中的控件。View.dispatchKeyEvent()方法会将事件派发给控件树
  • mFallbackEventHandler。与PhoneWindowManager类似,它提供了一个进行系统级按键处理的场所,只不过它的处理优先级低得多,当需要为某个按键定义一个系统级的功能,并允许应用程序修改此按键的功能时,可以在PhoneFallbackEventHandler类中进行实现,例如使用音量减调整系统音量,应用程序也可以将音量键挪作他用。注意每个ViewRootImpl都有各自的PhoneFallbackEventHandler该实例。
  • 焦点游走。它主要感兴趣的是方向键和TAB键的按下事件,它将根据按键选择一个焦点的查找方向,然后通过View.focusSearch()方法选择一个控件并使其获得焦点。

6.6 Activity与控件系统

6.6.1 PhoneWindow

Window类有三个最核心的组件:WindowManager.LayoutParams、一棵控件树以及Window.Callback。目前Android中使用的Window类的实现就是PhoneWindowWindow类中提供了用于修改LayoutParams的接口等通用功能实现,而PhoneWindow类则负责具体的外观模板的实现。简而言之,它就是一个用于快速构建窗口外观的工具类。值得注意的是,它和PhoneWindowManager之间没有任何关系。后者只是WMS的一个组成部分,用于提供与窗口管理相关的策略。

1.选择窗口外观与设置显示内容
  Activity.requestWindowFeature()决定了窗口的外观模板,Activity.setContentView()则设置一棵控件树用于显示在Activity中。前者要在后者之前调用,否则无效。
  其中Activity.setContentView()的核心调用链是:Activity.setContentView()->PhoneWindow.setContentView()->PhoneWindow.installDecor()->PhoneWindow.generateLayout()

2.DecorView的特点
  DecorViewPhoneWindow的关系十分密切,它利用自己根控件爱的身份为PhoneWindow偷取了很多控件系统内部的信息,其中就包括控件的生命周期信息以及输入事件。
  DecorView作为控件树的根,并不像其他ViewGroup那样将事件派发给子控件,而是将事件发送给Window.Callback。作为Window.Callback的实现者的ActivityDialog自然就有能力接收输入事件。当DecorView接收到事件之后,会首先将其交给Callback(通常是ActivityDialog)的dispatchTouchEvent()过目,后者会将事件交还给DecorView进行常规的事件派发,倘若事件在派发过程中没有被消费掉,Callback再自行消费这一事件,对于其他事件也是一样。因此,Callback的实现者如Activity或Dialog中的dispatchXXX()会咸鱼控件树中的任何一个控件进行事件处理,而它们的onXXX()则仅当事件没有被任何一个控件树消费时才有机会进行事件处理。

6.2 Activity窗口的创建与显示

Activity创建完之后的第一件事就是进行初始化,完成窗口令牌等重要信息的移交,而初始化就发生在Activity.attch()中。在该方法中,Activity获取了创建窗口所需的所有条件:PhoneWindowWindowManager,以及一个来自AMS的窗口令牌。
Activity窗口的显示发生在Activity.onResume()之后:

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
       .............

        // 1. Activity.onResume()会被调用
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {
            final Activity a = r.activity;

           ..............

            //2.创建窗口。当ActivityClientRecord的window成员为null时,表示此Activity尚未创建窗口。
            //此时需要将PhoneWindow中的控件树交给WindowManager完成窗口的创建。这种情况对应于
            //Activity初次创建的情况(即onCreate()被调用的情况)。如果Activity因为某种原因被暂停,如新的                
            //Activity覆盖其上或者用户按了Home键,虽说Activity不再处于Resume状态,
            //但是其窗口并没有从WMS中移除,只是它不可见而已
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                //设置窗口类型为BASE_APPLICATION。这表示窗口属于一个Activity
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }

            // Get rid of anything left hanging around.
            cleanUpPendingRemoveWindows(r, false /* force */);

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            //3. 使Activity可见
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
               .......

                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
    }

可见,当Activity.onResume()被调用时,Activity的窗口其实尚未显示甚至尚未创建。也就是说Activity的显示发生在onResume()之后。其实除非Activity被销毁(onDestroy()),其所属窗口都会存在于WMS之中,这期间的onStart()/onStop()所导致的可见性的变换都是通过修改DecorView()的可见性实现窗口的显示与隐藏的。

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

推荐阅读更多精彩内容