View体系详解(1)
前言:看了大概一个月SystemUI的相关源码,里面关于自定义View的知识比较多,迫使自己要去了解以前不太懂的显示子系统的知识,以前只知道一些粗略的view知识,如它是用来显示具体画面的,它的载体是window,它可以复写事件处理函数去处理某些点击事件,自定义view要实现onMeasure, onLayout, onDraw等,但是一直比较模糊,只是知道个大概,经过一阵子的源码和博客的阅读,对view体系有了许多新认识和领悟,因此记录下来。计划分以下几部分:
View体系详解(1):View体系总览
*****写的不好请理解,由于自己知识水平和技术经验所限,不可避免有错漏的地方,恳请指正。*******
View体系总览
1.首先我们要明确View的概念是什么?源码在View的class文件中有一个介绍:
* This class represents the basic building block for user interface components. A View
* occupies a rectangular area on the screen and is responsible for drawing and
* event handling.
所以view相当于一个可以在屏幕上面绘制内容的有限区域,并且负责处理具体的交互事件。这也回答了我们为什么自定义view的时候要经过三个步骤:测量(Measure),布局(Layout),还有绘画(Draw),并且当用户希望界面可以响应我们的操作时,我们为什么会调用setOnClickListener()等函数去设置处理事件的逻辑函数,这个后面细说。
2.那么View又怎么被管理呢?我们在xml文件中定义了view,它怎么就能显示在屏幕上了?我们可以看看在AS中新建一个app工程,最初的xml文件中的写法(部分内容省略),然后调用代码:setContentView(R.layout.activity_main) , 画面就有了,一个空白界面,中间是熟悉的Hello World!字样。
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
</FrameLayout>
我们可以看到,外面是一层FrameLayout,查看源码便可以发现,它的父类是ViewGroup,再上去的父类便是View,由此我们可以知道View的管理是通过View树的形式来实现的,比如一开始的画面,它的根节点便是FrameLayout,也就是一个布局,子节点便是被包含在其中的TextView,而setContentView()是封装了xml文件解析,DecorView(activity的根view节点)实例化,view树生成等逻辑,这也是为什么如果你要找到xml文件中定义的TextView实例,要在该函数之后去调用findViewById(),因为只有调用setContentView()后,我们的xml文件的内容才被解析,TextView实例才会创建并被赋值一个唯一的id,这样我们才能找到它的实例。
既然如此,那么我们可以在布局里面,再嵌套别的布局么?答案是可以的,因为布局也是一个view,不过它可以容纳别的view在里面,因为布局是继承自ViewGroup的,这就说明它本身是一个根节点view。从这我们也可以看出这里用的是组合模式,无论单个view还是一个布局,都可以容纳在他们的根节点布局中,层层嵌套。
3.如此又引入一个新问题,既然布局中可以嵌套布局,那么怎么管理这些根节点?他们的优先级如何管理呢?如果都让View来处理这些,那么View的职责就十分庞大,不利于维护,所以要把这部分的内容分离出去管理,让view去专注于内容的显示,如此一来,Window就出现了,那么它们的层级就可以同下图来表示。之前网上总是说window是一个抽象的概念,它负责管理view,什么的,我初看十分不解,其实window就是对应一块surface,任何画面的显示都要在surface上绘画出来,所以window的概念可以理解为是上层显示系统的基础,我们需要它来显示画面,那么就需要去WMS中申请一个window,有了Window之后,系统才会执行绘制流程让我们写的view去显示出来,该流程后面会细说。
4.层级关系确定后,再看它们的关系,如下面的UML图:
这个类图可以这么理解:view通过ViewParent接口跟View树根节点沟通, 而View树根节点通过ViewManager这个接口与WindowManager沟通,而WindowManager又通过ViewRootImpl当中的WindowSession类去跟WMS沟通。
我们可以看到,定义了一个自定义view,或者原生的view,均需要继承view这个父类,其中view有个重要的成员变量:ViewParent mParent 。在图中ViewParent是一个接口,实现这个接口的有ViewGroup和ViewRootImpl,那么mParent是指向谁呢?答案在代码中可以找到。ViewParent接口定义了一系列与父节点沟通的接口函数,子节点可以通过这个接口跟父节点沟通,既然如此,根据层次关系和类图,每个子view的mParent变量指向的便是自己的父节点,也就是ViewGroup,而ViewGroup这个父节点的mParent变量指向的则是ViewRootImpl,这也进一步证明了它们的层级关系。
管理view的责任,交给ViewManager这个接口,里面就三个函数,分别对应着add,update,remove,由WindowManagerImpl去实现,于是我们获取到了WindowManager的服务引用后,通过下面一段代码就可以把我们一个view显示出来:
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); //布局参数是用来定义这个window的属性的
TextView view = new TextView(this);
view.setText("this is text view");
wm.addView(view, lp);
我们可以在类图中看到,ViewGroup也实现了ViewManager的接口,但是它跟WindowManager的表现是不一样的,它的实现是管理view树的接口,如在addView实现函数中,它是把子类加入到了自己的view树结构里,这其中并没有跟WMS的交互。
下面通过一个例子来缕一缕他们的关系吧!
先看界面,这是我自己的手机,我写的一个apk推到手机中,非常简单,就是一个activity,其中有三个TextView对象,分别为1,2,3,还自定义了一个继承于FrameLayout的MyView,里面啥也没干。
代码如下,其中MyView为FrameLayout子类,我在xml文件中用它作为根布局,里面嵌套了一个TextView,也就是view3.
public class MainActivity extends AppCompatActivity {
MyView myView;
TextView view1;
TextView view2;
TextView view3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myView = findViewById(R.id.MyView);
view3 = findViewById(R.id.text);
view1 = new TextView(this);
view1.setText("this is view1");
view1.setTextColor(Color.RED);
myView.addView(view1);
view2 = new TextView(this);
view2.setText("this is view2");
view2.setTextColor(Color.GREEN);
WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(300, 200, 2, 0, 0);
wm.addView(view2, lp);
}
}
xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<com.android.myview.MyView
android:id="@+id/MyView"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="this is view3"
</com.android.myview.MyView>
问题如下:
1. 这个activity里,有几个窗口?
2.view1,2,3,分别属于哪个窗口?
3.都有addView的调用,那么它们的区别是什么呢?
如果都回答正确,那么view体系的层级关系以及类之间的关系,也就清楚了。
答案如下:
1. 2个窗口,即两个surface,白色背景的为一个窗口,这里叫它window1,黑色背景的也为一个窗口,叫它window2。
2.view1是通过MyView的函数addView()添加的,其中MyView继承于FrameLayout,FrameLayout继承于ViewGroup,所以MyView对象是顶层View,view1自然是依附在window1里面,这里的addView操作是把view1这个view添加到MyView的这个view树中去,这里并没有新的窗口产生。而view2虽然也是一个TextView,它甚至都不是一个ViewGroup的子类,仅仅是单个view,但是它也可以作为一个根节点去通过WindowManager的addView()接口把自己添加到一个新窗口中,也就是我们看到的黑色背景window2,所以view2是依附在window2中。至于view3自然也是属于window1的,因为它定义在xml文件中,是属于MyView的子view,它是嵌套在MyView中的。
3.ViewGroup和WindowManager中都实现了addView接口,但是它们的逻辑不一样,也是因为它们所处在的模块不同导致。ViewGroup是属于View的,它是View树的根节点,调用addView()是把子view添加到自己的View树中,然后重新绘制,这样view就显示出来了,但是没有新窗口产生,还是原来ViewGroup所在的窗口,而WindowManager的addView()则是通过把传入的view添加到一个新创建的window中去显示,也就是说它不属于任何的ViewGroup,这是它们的本质区别,由此我们也可以看到接口编程的作用,一个接口,不同的实现有不同的行为。
总结:
(1). view是通过view树的方式管理的,view树里面可以有多个节点,view树的根节点为顶层view,下面为子view,共同的载体为window,window为一个surface,即一张画布,有了画布我们才能把view的内容画出来,但是view的层级关系,如何绘制出来等,window并不关心,这使得它们各司其职。
(2). ViewGroup实现了ViewParent和ViewManager,ViewRootImpl实现了ViewParent,WindowManager则实现了ViewManger,从接口的实现可以看出它们三者是view的管理者,但是管理的方式不同:ViewGroup管理的对象为自己的子view,ViewRootImpl管理的是ViewGroup对象,子view的parent是ViewGroup,而ViewGroup的parent则为ViewRootImpl。 而WindowManager管理的是ViewRootImpl,ViewRootImpl的实例会在WindowManager模块中被创建起来并管理,所以view的层级关系为:window > viewroot > viewgroup > view。
知道了view的体系架构以后,接下来把调用流程梳理清楚,比如什么时候触发绘制流程,把一个新的子view添加到view树中会发生什么,调用WindowManager的addView()后系统如何创建window,如何实现一个自定义view等等,这将会在:《View体系详解(2):自定义View流程以及系统相关行为 》 中去讲解。