TextView的实现原理介绍

记得之前在腾讯面试的时候,被面试官问到这个问题。之前认为没什么特别的,一位是面试官有益问些刁钻的问题来压工资,如今看来当是确实是懂得不多啊。

今天看到就转载过来也方便自己和他人一起来了解。探寻这个简单却深奥的空间内部的password吧。 文章转载自:http://blog.csdn.net/luoshengyang/article/details/8636153 实际上,每个视图都是一个控件,这些控制能够将自己的UI绘制在窗体的画图表面上,同一时候还能够与用户进行交互。即获得用户的键盘或者触摸屏输入。在本文中。我们就具体分析窗体控件的上述实现原理。 我想面试官当是问这个问题可能仅仅是挑有代表性的控件吧。因为TextView是其他的一些基础控件,比如Button、EditText和CheckBox等直接或者间接地的父类。每个控件的实现都是相当复杂的。只是基本上都是一些细节问题,而且不同的控件有不同的实现细节。以下看看大神们是怎么去分析TextView的实现框架。 一下是上面博客的原话: 控件为了实现自己的功能而须要的东西是什么呢?有两个材料是不可缺少的。第一个材料是画布,第二个材料是用户输入。有画布才干绘制UI,而实用户输入才干与用户进行交互。

因此,接下来我们主要分析TextView的绘制流程。以及它获得用户输入的过程。用户输入主要包括键盘输入以及触摸屏输入,本文主要关注的是键盘输入。

触摸屏输入与键盘输入的获取过程是相似的,读者假设有兴趣的话,能够參照本文的内容来自己研究一下。 应用程序窗体,即Activity窗体,是由一个PhoneWindow对象,一个DecorView对象。以及一个ViewRoot对象来描写叙述的。当中,PhoneWindow对象用来描写叙述窗体对象。DecorView对象用来描写叙述窗体的顶层视图,ViewRoot对象除了用来与WindowManagerService服务通信之外,还用来接收用户输入。

窗体控件本身也是一个视图。即一个View对象,它们是以树形结构组织在一起形成整个窗体的UI的。为了简单起见。本文假设要分析的TextView控件是直接以窗体的顶层视图为父视图的。即以DecorView为父视图,如图1所看到的:

图1 窗体结构示意图以及DecorView、TextView的类关系图

    图1显示的是一个包括了TextView控件的Activity窗体的结构示意图以及DecorView、TextView的简单类关系图,从中能够看出:
    1. 用户输入首先是由ViewRoot接收,然后再分发给TextView处理;
    2. DecorView是一个视图容器。因此,它是从ViewGroup继承下来,而ViewGroup本身又是从View继承下来的;
    3. TextView是一个简单视图,因此,它是直接继承了View。
    接下来,我们就以图1所看到的的Activity窗体为例。来分析TextView控件的UI绘制框架及其获得键盘输入的过程。

一. TextView控件的UI绘制框架 Activity窗体的UI绘制操作分为三步来走,各自是測量、布局和绘制。 1. 測量 为了能告诉父视图自己的所占领的空间的大小,全部控件都必须要重写父类View的成员函数onMeasure。 TextView类的成员函数onMeasure的实现例如以下所看到的:

[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
……

@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);  

    int width;  
    int height;  

    //计算TextView控件的宽度和高度  
    ......  

    setMeasuredDimension(width, height);  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/widget/TextView.java中。
參数widthMeasureSpec和heightMeasureSpec分别用来描写叙述宽度測量规范和高度測量规范。

測量规范使用一个int值来表法,这个int值包括了两个分量。
第一个是mode分量。使用最高2位来表示。測量模式有三种,各自是MeasureSpec.UNSPECIFIED(0)、MeasureSpec.EXACTLY(1)、和MeasureSpec.AT_MOST(2)。
第二个是size分量。使用低30位来表示。

当mode分量等于MeasureSpec.EXACTLY时,size分量的值就是父视图要求当前控件要设置的宽度或者高度;当mode分量等于MeasureSpec.AT_MOST时,size分量的值就是父视图限定当前控件能够设置的最大宽度或者高度;当mode分量等于MeasureSpec.UNSPECIFIED时。父视图不限定当前控件所设置的宽度或者高度,这时候当前控件一般就依照实际需求来设置自己的宽度和高度。


TextView类的成员函数onMeasure依据上述规则计算好自己的宽度wdith和高度height之后,必须要调用从父类View继承下来的成员函数setMeasuredDimension来通知父视图它所要设置的宽度和高度,否则的话,该函数调用结束之后,就会抛出一个类型为IllegalStateException的异常。
2. 布局
前面的測量工作实际上是确定了控件的大小,可是控件的位置还未确定。控件的位置是通过布局这个操作来完毕的。
我们知道,控件是依照树形结构组织在一起的,当中,子控件的位置由父控件来设置。也就是说。仅仅有容器类控件才须要运行布局操作,这是通过重写父类View的成员函数onLayout来实现的。从Activity窗体的结构能够知道。它的顶层视图是一个DecorView,这是一个容器类控件。Activity窗体的布局操作就是从其顶层视图開始运行的。每碰到一个容器类的子控件,就调用它的成员函数onLayout来让它有机会对自己的子控件的位置进行设置,依次类推。


我们常见的FrameLayout、LinearLayout、RelativeLayout、TableLayout和AbsoluteLayout,都是属于容器类控件,因此,它们都须要重写父类View的成员函数onLayout。

因为TextView控件不是容器类控件,因此,它能够不重写父类View的成员函数onLayout。
3. 绘制
有了前面两个操作之后。控件的位置的大小就确定下来了,接下来就能够对它们的UI进行绘制了。控件为了能够绘制自己的UI,必须要重写父类View的成员函数onDraw。
TextView类的成员函数onDraw的实现例如以下所看到的:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
……

@Override  
protected void onDraw(Canvas canvas) {  
    //在画布canvas上绘制UI  
    ......  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/widget/TextView.java中。
參数canvas描写叙述的是一块画布,控件的UI就是绘制在这块画布上面的。画布提供了丰富的接口来绘制UI,比如画线(drawLine)、画圆(drawCircle)和贴图(drawBitmap)等等。有了这些UI画图接口之后。就能够随心所欲地绘制控件的UI了。
Java层的Canvas实际上是封装了C++层的SkCanvas。C++层的SkCanvas内部有一块图形缓冲区,这块图形缓冲区就是窗体的画图表面(Surface)里面的那块图形缓冲区。
窗体的画图表面里面的那块图形缓冲区实际上是一块匿名共享内存,它是SurfaceFlinger服务负责创建的。SurfaceFlinger服务创建完毕这块匿名共享内存之后,就会将其返回给窗体所运行在的进程。窗体所运行在的进程获得了这块匿名共享内存之后,就会映射到自己的进程空间来,因此。窗体的控件就能够在本进程内訪问这块匿名共享内存了,实际上就是往这块匿名共享内存填入UI数据。注意。这个过程运行完毕之后,控件的UI还没有反映到屏幕上来,因为这时候将控件的UI数据填入到图形缓冲区而已。
窗体的UI的显示是WindowManagerService服务来控制的。因此。当窗体的全部控件都绘制完毕自己的UI之后,窗体就会向WindowManagerService服务发送一个Binder进程间程通信请求。WindowManagerService服务接收到这个Binder进程间程通信请求之后,就会请求SurfaceFlinger服务刷新对应的窗体的UI
从上面的描写叙述就能够看出,控件的UI尽管是在一块简单的画布进行绘制,可是当中蕴含了丰富的知识点。而且须要应用程序进程、WindowManagerService服务和SurfaceFlinger服务三方紧密而有序的配合。一个窗体的全部控件的UI都是绘制在窗体的画图表面上的,也就是说,一个窗体的全部控件的UI数据都是填写在同一块图形缓冲区中;一个窗体的全部控件的UI的绘制操作是在主线程中运行的,其实,全部与UI相关的操作都是必须是要在主线程中运行,否则的话,就会抛出一个类型为CalledFromWrongThreadException的异常来。


为什么要规定全部与UI相关的操作都必须在主线程中运行呢?我们知道。这些与UI相关的操作都涉及到大量的控件内部状态以及须要訪问窗体的画图表面,也就是说,要大量地訪问控件类的成员变量以及窗体画图表面里面的图形缓冲区,因此,假设不将这些与UI相关的操作限定在同一个线程中运行的话,那么就会涉及到线程同步问题。

线程同步的开销是非常大的,因此,就要保证那些与UI相关的操作都在同一个线程中运行。

这个负责运行UI相关操作的线程便是应用程序进程的主线程。因此我们也将应用程序进程的主线程称为UI线程。


我们知道,应用程序进程的主线程除了负责运行与UI相关的操作之外。还负责响应用户的输入。因此。我们就要尽量地避免运行非常耗时的UI操作,否则的话。系统就会因为应用程序进程的主线程无法及时响应用户输入而弹出ANR对话框。
那么,有没有办法让某一个控件的UI享有独立的图形缓冲区呢?也就是这个控件不将自己的UI数据填入到它的宿主窗体的画图表面的图形缓冲区里面去。假设能够的话,那么我们就能够在另外一个独立的线程中绘制该控件的UI。这样做的优点是显而易见——能够在这个独立的线程运行相对照较耗时的UI绘制操作而不会导致主线程无法及时响应用户输入。答案是肯定的,在接下来的一篇文章中,我们就分析一个能够具有独立图形缓冲区的控件——SurfaceView。
二. TextView控件获取键盘输入的过程分析
每个窗体的创建的时候,都会与系统的输入管理器建立一个用户输入接收通道。输入管理器在启动两个线程,当中一个用来监控用户输入,即监控用户是否按下或者放开了键盘按键,或者是否触摸了屏幕,另外一个用来将监控到的用户输入事件分发给当前激活的窗体来处理。而这个分发过程就是通过前面建立的通道来进行的。


当前激活的窗体接收到输入管理器分发过来的用户输入事件之后,就会该事件封装成一个消息发送到当前激活的窗体所运行在的应用程序进程的主线程的消息队列中去。等到这个消息被处理的时候,就会调用与当前激活的窗体所关联的一个ViewRoot对象的成员函数deliverKeyEvent或者deliverPointerEvent来将前面接收到的用户输入分发给合适的控件。当中,ViewRoot类的成员函数deliverKeyEvent负责分发键盘输入事件。而ViewRoot类的成员函数deliverPointerEvent负责分发触摸屏输入事件。


接下来,我们就从ViewRoot类的成员函数deliverKeyEvent開始。分析一个TextView控件获得键盘输入的过程(获得触摸屏输入的过程是相似的),如图2所看到的:

图2 TextView控件获得键盘输入的过程
这个过程能够分为14个步骤,接下来我们就具体分析每个步骤。
Step 1. ViewRoot.deliverKeyEvent
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public final class ViewRoot extends Handler implements ViewParent,
View.AttachInfo.Callbacks {
……

private void deliverKeyEvent(KeyEvent event, boolean sendDone) {  
    // If mView is null, we just consume the key event because it doesn't  
    // make sense to do anything else with it.  
    boolean handled = mView != null  
            ? mView.dispatchKeyEventPreIme(event) : true;  
    if (handled) {  
        if (sendDone) {  
            finishInputEvent();  
        }  
        return;  
    }  
    // If it is possible for this window to interact with the input  
    // method window, then we want to first dispatch our key events  
    // to the input method.  
    if (mLastWasImTarget) {  
        InputMethodManager imm = InputMethodManager.peekInstance();  
        if (imm != null && mView != null) {  
            int seq = enqueuePendingEvent(event, sendDone);  
            ......  

            imm.dispatchKeyEvent(mView.getContext(), seq, event,  
                    mInputMethodCallback);  
            return;  
        }  
    }  
    deliverKeyEventToViewHierarchy(event, sendDone);  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/view/ViewRoot.java中。
參数event描写叙述的是窗体接收到的键盘事件,另外一个參数sendDone表示该键盘事件处理完毕后,是否须要向系统的输入管理器发送一个通知。
ViewRoot类的成员变量mView描写叙述的是窗体的顶层视图,即它指向的是一个DecorView对象,ViewRoot类的成员函数deliverKeyEvent首先是调用它的成员函数dispatchKeyEventPreIme来让它优先于输入法处理參数event所描写叙述的键盘事件。假设这个DecorView对象的成员函数dispatchKeyEventPreIme的返回值handled等于true,那么就说明參数event所描写叙述的键盘事件已经处理完毕,即ViewRoot类的成员函数deliverKeyEvent不用往下运行了。在这样的情况下,假设參数sendDone的值等于true,那么ViewRoot类的成员函数deliverKeyEvent在返回之前,还会调用成员函数finishInputEvent来通知系统的输入管理器,当前激活的窗体已经处理完毕刚刚发生的键盘事件了。在接下来的Step 2到Step 4中,我们再具体分析键盘事件优先于输入法分发给窗体处理的过程。
假设窗体不在输入法前面拦截參数event所描写叙述的键盘事件。接下来ViewRoot类的成员函数deliverKeyEvent就会将该键盘事件分发给输入法处理,这个分发步骤例如以下所看到的:
1. 调用InputMethodManager类的静态成员函数peekInstance获得一个类型为InputMethodManager输入法管理器imm;
2. 调用ViewRoot类的成员函数enqueuePendingEvent将參数event所描写叙述的键盘事件缓存起来,等到输入法处理完毕该键盘事件之后,再继续对它进行处理;
3. 调用第1步获得的输入法管理器imm的成员函数dispatchKeyEvent来将參数event所描写叙述的键盘事件分发给输入法处理。


这里有两个地方是须要注意的。

第一个地方是仅仅有当前窗体正在显示输入法的情况下,ViewRoot类的成员函数deliverKeyEvent才会将參数event所描写叙述的键盘事件分发给输入法处理,这是通过检查ViewRoot类的成员变量mLastWasImTarget的值是否等于true来确定的。

第二个地方是在将參数event所描写叙述的键盘事件分发给输入法处理时,ViewRoot类的成员函数deliverKeyEvent会同一时候传递一个类型为InputMethodCallback的回调接口给输入法,以便输入法处理完毕參数event所描写叙述的键盘事件之后,能够调用这个回调接口的成员函数finishedEvent来向窗体发送一个键盘事件处理完毕通知。这个类型为InputMethodCallback的回调接口就保存在ViewRoot类的成员变量mInputMethodCallback中。当它的成员函数finishedEvent被调用的时候。它就会调用ViewRoot类的成员函数deliverKeyEventToViewHierarchy来继续将參数event所描写叙述的键盘事件分发给窗体处理。


假设窗体当前不须要与输入法交互,即ViewRoot类的成员变量mLastWasImTarget的值等于false,那么ViewRoot类的成员函数deliverKeyEvent就会直接调用成员函数deliverKeyEventToViewHierarchy来将參数event所描写叙述的键盘事件分发给窗体处理。


接下来。我们就先析窗体在输入法之前处理键盘输入的过程,接着再分析窗体在输入法之后处理键盘输入的过程。
从前面的分析能够知道,ViewRoot类的成员函数deliverKeyEvent是通过调用DecorView类的成员函数dispatchKeyEventPreIme来将获得的键盘输入优先于输入法分发给窗体处理的。DecorView类的成员函数dispatchKeyEventPreIme是从父类ViewGroup继承下来的,因此,接下来我们就继续分析ViewGroup类的成员函数dispatchKeyEventPreIme的实现。
Step 2. ViewGroup.dispatchKeyEventPreIme
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
……

// The view contained within this ViewGroup that has or contains focus.  
private View mFocused;  
......  

@Override  
public boolean dispatchKeyEventPreIme(KeyEvent event) {  
    if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {  
        return super.dispatchKeyEventPreIme(event);  
    } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {  
        return mFocused.dispatchKeyEventPreIme(event);  
    }  
    return false;  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/view/ViewGroup.java中。
ViewGroup类的成员函数dispatchKeyEventPreIme首先是检查当前正在处理的视图容器能否够获得焦点。假设能够获得焦点的话,那么ViewGroup类的成员变量mPrivateFlags的FOCUSED位就会等于1。在当前正在处理的视图容器能够获得焦点的情况下,还要检查正在处理的视图容器是否已经计算过大小了。即检查ViewGroup类的成员变量mPrivateFlags的HAS_BOUNDS位是否等于1。仅仅有在已经计算过大小而且能够获得焦点的情况下,那么正在处理的视图容器才有资格处理參数event所描写叙述的键盘事件。注意。正在处理的视图容器是通过调用其父类View的成员函数dispatchKeyEventPreIme来处理參数event所描写叙述的键盘事件的。
假设当前正在处理的视图容器没有资格处理參数event所描写叙述的键盘事件,可是它有一个能够获得焦点的子视图,而且这个子视图的大小也是已经计算好了的,那么ViewGroup类的成员函数dispatchKeyEventPreIme就会将參数event所描写叙述的键盘事件分发给该子视图处理。当前正在处理的视图容器能够获得焦点的子视图是通过ViewGroup类的成员变量mFocused来描写叙述的,通过调用这个成员变量所描写叙述的一个View对象的成员函数dispatchKeyEventPreIme就可以将參数event所描写叙述的键盘事件分发给够获得焦点的子视图处理。
一个视图容器是怎样知道它的焦点子视图的呢?我们知道。当我们在屏幕上触摸一个窗体时,就会发生一个Pointer事件。这个Pointer事件关联有一个触摸点,通过检查这个触摸点当前是包括在窗体顶层视图的哪一个子视图里面,就能够知道哪一个子视图是焦点子视图了。
从上面的分析能够知道,不管是当前正在处理的视图容器获得焦点。还是它的子视图获得焦点,终于都是通过调用View类的成员函数dispatchKeyEventPreIme来在输入法之前处理參数event所描写叙述的键盘事件,因此,接下来我们就继续分析View类的成员函数dispatchKeyEventPreIme的实现。
Step 3. View.dispatchKeyEventPreIme
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
……

public boolean dispatchKeyEventPreIme(KeyEvent event) {  
    return onKeyPreIme(event.getKeyCode(), event);  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/view/View.java中。
View类的成员函数dispatchKeyEventPreIme的实现非常easy,它仅仅是通过调用另外一个成员函数onKeyPreIme来在输入法之前处理參数event所描写叙述的键盘事件。


Step 4. View.onKeyPreIme
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
……

public boolean onKeyPreIme(int keyCode, KeyEvent event) {  
    return false;  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/view/View.java中。
View类的成员函数onKeyPreIme默认是不会在输入法之前处理參数event所描写叙述的键盘事件的,因此,我们在实现自己的控件的时候,假设须要在输入法之前处理键盘输入。那么就必须重写父类View的成员函数onKeyPreIme。

在重写父类View的成员函数onKeyPreIme来处理一个键盘事件的时候。假设不希望这个键盘事件分发给输入法处理,那么就返回一个true值,否则的话。就返回一个false值。
我们假设当前获得焦点的是图1所看到的的TextView控件,可是因为TextView类没有重写其父类View的成员函数onKeyPreIme。因此,參数event所描写叙述的键盘事件接下来就会继续分发给输入法或者当前激活的窗体处理。


这一步运行完毕之后,回到前面的Step 1中。即ViewRoot类的成员函数deliverKeyEvent中,不管接下来是否须要先将一个键盘事件分发给输入法处理,终于都会调用到ViewRoot类的成员函数deliverKeyEventToViewHierarchy来继续将该键盘事件分发给当前激活的窗体处理。


Step 5. ViewRoot.deliverKeyEventToViewHierarchy
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public final class ViewRoot extends Handler implements ViewParent,
View.AttachInfo.Callbacks {
……

private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) {  
    try {  
        if (mView != null && mAdded) {  
            final int action = event.getAction();  
            boolean isDown = (action == KeyEvent.ACTION_DOWN);  
            ......  

            boolean keyHandled = mView.dispatchKeyEvent(event);  

            if (!keyHandled && isDown) {  
                int direction = 0;  
                switch (event.getKeyCode()) {  
                case KeyEvent.KEYCODE_DPAD_LEFT:  
                    direction = View.FOCUS_LEFT;  
                    break;  
                case KeyEvent.KEYCODE_DPAD_RIGHT:  
                    direction = View.FOCUS_RIGHT;  
                    break;  
                case KeyEvent.KEYCODE_DPAD_UP:  
                    direction = View.FOCUS_UP;  
                    break;  
                case KeyEvent.KEYCODE_DPAD_DOWN:  
                    direction = View.FOCUS_DOWN;  
                    break;  
                }  

                if (direction != 0) {  

                    View focused = mView != null ? mView.findFocus() : null;  
                    if (focused != null) {  
                        View v = focused.focusSearch(direction);  
                        ......  

                        if (v != null && v != focused) {  
                            ......  

                            focusPassed = v.requestFocus(direction, mTempRect);  
                        }  

                        ......  
                    }  
                }  
            }  
        }  

    } finally {  
        if (sendDone) {  
            finishInputEvent();  
        }  
        ......  
    }  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/view/ViewRoot.java中。
ViewRoot类的成员函数deliverKeyEventToViewHierarchy首先将參数event所描写叙述的键盘事件交给当前激活的窗体的顶层视图来处理,这是通过调用ViewRoot类的成员变量mView所描写叙述的一个DecorView对象的成员函数dispatchKeyEvent来实现的。
假设当前激活的窗体的顶层视图在处理完毕參数event所描写叙述的键盘事件之后,希望该键盘事件还能继续被ViewRoot类的成员函数deliverKeyEventToViewHierarchy处理,那么前面调用DecorView类的成员函数dispatchKeyEvent得到的返回值keyHandled的值就会等于false。

在这样的情况下,假设參数event描写叙述的是一个按下的键盘事件,即变量isDown的值等于true,那么ViewRoot类的成员函数deliverKeyEventToViewHierarchy就会继续检查參数event描写叙述的是否是一个DPAD事件。假设是的话,那么就可能须要改变窗体当前的焦点子视图。


假设參数event描写叙述的是一个DPAD事件。那么终于得到的变量direction的值就不会等于0。而且它描写叙述的是当前按下的是哪一个方向的DPAD键。假设这时候窗体已经有一个焦点子视图,即调用ViewRoot类的成员变量mView所描写叙述的一个DecorView对象的成员函数findFocus的返回值focused不等于null,那么接下来就要依据变量direction的值来决定下一个焦点子视图是谁。

比如。假设变量direction的值等于View.FOCUS_LEFT,那么就表示在当前的焦点子视图focused的左边查找一个最靠近的子视图作为下一个焦点子视图。这是通过调用当前焦点子视图focused的成员函数focusSearch来实现的。
一旦找到了下一个焦点子视图v,而且该子视图不是当前的焦点子视图focused,那么ViewRoot类的成员函数deliverKeyEventToViewHierarchy就须要将子视图v设置为焦点子视图。这是通过调用变量v所描写叙述的一个View对象的成员函数requestFocus来实现的。
通过前面的操作,參数event所描写叙述的键盘事件就处理完毕了。

假设这时候參数sendDone的值等于true,那么就表示须要通知系统的输入管理器,參数event所描写叙述的键盘事件已经处理完毕了。这是通过调用ViewRoot类的成员函数finishInputEvent来实现的。


接下来,我们就继续分析DecorView类的成员函数dispatchKeyEvent的实现,以便能够了解窗体的顶层视图分发键盘事件的过程。


Step 6. DecorView.dispatchKeyEvent
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class PhoneWindow extends Window implements MenuBuilder.Callback {
……

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {  
    ......  

    @Override  
    public boolean dispatchKeyEvent(KeyEvent event) {  
        final int keyCode = event.getKeyCode();  
        final boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;  
        ......  

        final Callback cb = getCallback();  
        final boolean handled = cb != null && mFeatureId < 0 ?

cb.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); if (handled) { return true; } return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event); } ...... } ......

}
这个函数定义在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
PhoneWindow类的成员函数getCallback是从父类Window继承下来的。它返回的是一个Window.Callback接口。

每个Activity组件都会实现一个Window.Callback接口。而且将这个Window.Callback接口设置到与它所关联的一个PhoneWindow对象的内部去。这样当该PhoneWindow对象接收到键盘事件的时候,就能够该键盘事件分发给与它所关联的Activity组件处理。


DecorView类的成员变量mFeatureId用来描写叙述当前正在处理的DecorView对象的特征,当它的值小于0的时候。就表示当前正在处理的一个DecorView对象是用来描写叙述一个Activity组件窗体的顶层视图的。


因此,当当前正在处理的DecorView对象描写叙述的是一个Activity组件窗体的顶层视图。而且这个Activity组件实现有一个Window.Callback接口时,DecorView类的成员函数dispatchKeyEvent就会调用该Window.Callback接口的成员函数dispatchKeyEvent来通知对应的Activity组件,它接收到一个键盘事件了。否则的话,參数event所描写叙述的键盘事件就会被分发给当前正在处理的DecorView对象的父对象来处理,这是通过调用DecorView类的父类View的成员函数dispatchKeyEvent来实现的。


我们假设当前正在处理的DecorView对象描写叙述的是一个Activity组件窗体的顶层视图,而且这个Activity组件实现有一个Window.Callback接口。那么參数event所描写叙述的键盘事件接下来就会分给该Activity组件处理。假设该Activity组件在处理完毕这个键盘事件之后,希望该键盘事件还能继续分发下去给其他对象处理。那么它所实现的Window.Callback接口的成员函数dispatchKeyEvent的返回值handled就会等于false。这时候DecorView类的成员函数dispatchKeyEvent就会将该键盘事件分发给与当前正在处理的DecorView对象所关联的一个PhoneWindow对象的成员函数onKeyDown或者onKeyUp来处理,取决于变量isDown的值是true还是false,即当前发生的键盘事件是与按键按下有关,还是与按键松开有关。


PhoneWindow类的成员函数onKeyDown和onKeyUp主要是有来监控一些特殊按键事件,比如电话键和音量键,以便能够运行一些对应的逻辑。

比如,当按下电话键时,就打开拨号程序;又如。当按下音量键时,就调节音量的大小。
接下来,我们就继续分析Activity类所实现的Window.Callback接口的成员函数dispatchKeyEvent的实现,以便能够了解键盘事件在Activity组件窗体的分发过程。
Step 7. Activity.dispatchKeyEvent
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks {
……

public boolean dispatchKeyEvent(KeyEvent event) {  
    ......  

    Window win = getWindow();  
    if (win.superDispatchKeyEvent(event)) {  
        return true;  
    }  
    View decor = mDecor;  
    if (decor == null) decor = win.getDecorView();  
    return event.dispatch(this, decor != null  
            ? decor.getKeyDispatcherState() : null, this);  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/app/Activity.java中。
Activity类的成员函数getWindow返回的是与当前正处理的Activity组件所关联的一个PhoneWindow对象,Activity类的成员函数dispatchKeyEvent获得了这个PhoneWindow对象之后。就会调用它的成员函数superDispatchKeyEvent,以便能够将參数event所描写叙述的键盘事件分发给它处理。


这个PhoneWindow对象在处理完毕參数event所描写叙述的键盘事件之后,假设希望该键盘事件能继续往下分发,那么Activity类的成员函数dispatchKeyEvent就会将该键盘事件分发给当前正在处理的Activity组件处理,这是通过调用參数event所描写叙述的一个KeyEvent对象的成员函数dispatch来实现的。
注意。在调用event所描写叙述的一个KeyEvent对象的成员函数dispatch的时候,第一个參数指定为当前正在处理的Activity组件所实现的一个KeyEvent.Callback接口。參数event所指向的一个KeyEvent对象的成员函数dispatch的运行的过程中,就会对应地调用这个KeyEvent.Callback接口的成员函数onKeyDown、onKeyUp或者onKeyMultiple来处理它所描写叙述的键盘事件,实际上就是调用Activity类的成员函数onKeyDown、onKeyUp或者onKeyMultiple来处理參数event所描写叙述的键盘事件。因此,我们在自己定义一个Activity组件时。假设须要处理分发给该Activity组件的键盘事件,那么就须要重写父类Activity的成员函数onKeyDown、onKeyUp或者onKeyMultiple。
接下来。我们就继续分析PhoneWindow类的成员函数superDispatchKeyEvent的实现,以便能够了解键盘事件在Activity组件窗体的分发过程。
Step 8. PhoneWindow.superDispatchKeyEvent
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class PhoneWindow extends Window implements MenuBuilder.Callback {
……

// This is the top-level view of the window, containing the window decor.  
private DecorView mDecor;  
......  

@Override  
public boolean superDispatchKeyEvent(KeyEvent event) {  
    return mDecor.superDispatchKeyEvent(event);  
}  

......  

}
这个函数定义在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。


PhoneWindow类的成员变量mDecor描写叙述的是当前正在处理的Activity组件窗体的顶层视图,PhoneWindow类的成员函数superDispatchKeyEvent通过调用它所指向的一个DecorView对象的成员函数superDispatchKeyEvent来处理參数event所描写叙述的键盘事件。


Step 9. DecorView.superDispatchKeyEvent
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class PhoneWindow extends Window implements MenuBuilder.Callback {
……

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {  
    ......  

    public boolean superDispatchKeyEvent(KeyEvent event) {  
        return super.dispatchKeyEvent(event);  
    }  

    ......  
}  

......  

}
这个函数定义在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
DecorView类的成员函数superDispatchKeyEvent的实现非常easy,它仅仅是调用父类ViewGroup的成员函数dispatchKeyEvent来处理參数event所描写叙述的键盘事件。


Step 10. ViewGroup.dispatchKeyEvent
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
……

@Override  
public boolean dispatchKeyEvent(KeyEvent event) {  
    if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {  
        return super.dispatchKeyEvent(event);  
    } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {  
        return mFocused.dispatchKeyEvent(event);  
    }  
    return false;  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/view/ViewGroup.java中。
ViewGroup类的成员函数dispatchKeyEvent的实现与在前面的Step 3中所介绍的ViewGroup类的成员函数dispatchKeyEventPreIme的实现是相似的,即假设当前正在处理的视图容器能够获得焦点而且该视图容器的大小已经计算好了。那么就会将參数event所描写叙述的键盘事件分发给它的父类View的成员函数dispatchKeyEvent来处理,否则的话。假设当前正在处理的视图容器有一个焦点子视图,而且这个焦点子视图的大小已经计算好了,那么就将參数event所描写叙述的键盘事件分发给该焦点子视图的父类View的成员函数dispatchKeyEvent来处理。
从前面的调用过程能够知道,当前正在处理的视图容器即为Activity组件窗体的顶层视图。我们假设在该顶层视图中。获得焦点的是一个TextView控件。而且这个TextView控件的大小已经计算好了,那么接下来就会调用这个TextView控件的父类View的成员函数dispatchKeyEvent来处理參数event所描写叙述的键盘事件。
Step 11. View.dispatchKeyEvent
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
……

private OnKeyListener mOnKeyListener;  
......  

public boolean dispatchKeyEvent(KeyEvent event) {  
    // If any attached key listener a first crack at the event.  
    //noinspection SimplifiableIfStatement  

    ......  

    if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED  
            && mOnKeyListener.onKey(this, event.getKeyCode(), event)) {  
        return true;  
    }  

    return event.dispatch(this, mAttachInfo != null  
            ?

mAttachInfo.mKeyDispatchState : null, this); } ......

}
这个函数定义在文件frameworks/base/core/java/android/view/View.java中。


当View类的成员变量mOnKeyListener的值不等于null时,它所指向的一个OnKeyListener对象描写叙述的是注冊到当前正在处理的视图的一个键盘事件监听器。在这样的情况下。假设当前正在处理的视图是处于启用状态的,即它的成员变量mViewFlags的ENABLED位等于1。那么參数event所描写叙述的键盘事件就先分给该键盘事件监听器处理,这是通过调用View类的成员变量mOnKeyListener所指向的一个OnKeyListener对象的成员函数onKey来实现的。


注冊到当前正在处理的视图的键盘事件监听器在处理完毕參数event所描写叙述的键盘事件之后。假设希望该键盘事件还能继续往下处理,那么View类的成员函数dispatchKeyEvent就会继续调用參数event所指向的一个KeyEvent对象的成员函数dispatch来处理该键盘事件。
接下来。我们就继续分析KeyEvent类的成员函数dispatch的实现,以便能够了解键盘事件在Activity组件窗体的分发过程。
Step 12. KeyEvent.dispatch
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class KeyEvent extends InputEvent implements Parcelable {
……

public final boolean dispatch(Callback receiver, DispatcherState state,    
        Object target) {    
    switch (mAction) {    
    case ACTION_DOWN: {    
        ......    
        boolean res = receiver.onKeyDown(mKeyCode, this);    
        ......    
        return res;    
    }    
    case ACTION_UP:    
        ......    
        return receiver.onKeyUp(mKeyCode, this);    
    case ACTION_MULTIPLE:    
        final int count = mRepeatCount;    
        final int code = mKeyCode;    
        if (receiver.onKeyMultiple(code, count, this)) {    
            return true;    
        }    
        ......    
        return false;    
    }    
    return false;    
}    

......    

}
这个函数定义在文件frameworks/base/core/java/android/view/KeyEvent.java中。
从前面的调用过程能够知道,參数receiver指向的是一个View对象所实现的一个KeyEvent.Callback接口,这个KeyEvent.Callback接口是用来接收当前正在处理的键盘事件。
KeyEvent类的成员变量mAction描写叙述的的是当前正在处理的键盘事件的类型,当它的值等于ACTION_DOWN、ACTION_UP和ACTION_MULTIPLE的时候,KeyEvent类的成员函数dispatch就会分别调用參数receiver所指向的一个View对象的成员函数onKeyDown、onKeyUp和onKeyMultiple来接收当前正在处理的键盘事件。
假设当前正在处理的键盘事件是与按键按下相关的。即KeyEvent类的成员变量mAction的值等于ACTION_DOWN。那么接下来就会调用參数receiver所指向的一个View对象的成员函数onKeyDown来接收当前正在处理的键盘事件。
因为前面我们已经假设了当前获得焦点的是一个TextView控件,因此,參数receiver指向的实际上是一个TextView对象。TextView类重写了父类View的成员函数onKeyDown。因此。接下来KeyEvent类的成员函数dispatch就会调用TextView类的成员函数onKeyDown来接收当前正在处理的键盘事件。


Step 13. TextView.onKeyDown
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
……

@Override  
public boolean onKeyDown(int keyCode, KeyEvent event) {  
    int which = doKeyDown(keyCode, event, null);  
    if (which == 0) {  
        // Go through default dispatching.  
        return super.onKeyDown(keyCode, event);  
    }  

    return true;  
}  

......  

}
这个函数定义在文件frameworks/base/core/java/android/widget/TextView.java中。
TextView类的成员函数onKeyDown调用另外一个成员函数doKeyDown来处理參数event所描写叙述的键盘事件,以便能够对应地改变当前获得焦点的TextView控件的UI。

当TextView类的成员函数doKeyDown的返回值which等于0的时候,就表示当前获得焦点的TextView控件希望參数event所描写叙述的键盘事件能够继续分发给它的父类View处理。这是通过调用父类View的成员函数onKeyDown来实现的。


至此,我们就分析完毕TextView控件获得键盘事件的过程了,整个TextView控件的实现框架也分析完毕了。
在Android系统中。其他的Android控件与TextView控件的实现框架都是相似的。差别就在于实现细节和所表现的UI不一样。而且它们一般都有一个共同的特点。那就是都在宿主窗体的画图表面上进行UI绘制。

怎么样。是不是认为自己干了这么久的android了,实现过非常复杂的功能,可是让你自己说说这个基本控件的原理,你能说出来吗?如今看了之后是不是认为眼前一片明亮。原来如此!

原文地址:https://www.cnblogs.com/claireyuancy/p/7363155.html