一、相关类
在介绍自定义View前,需要先了解相关的类
ViewRoot
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,这个过程可参看如下源码:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来。
- measure 用来测量View的宽和高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽/高,但是特殊情况除外。
- layout 用来确定View在父容器中的放置位置,Layout过程决定了View的四个顶点的坐标和实际的View的宽/高,完成以后,可以通过getTop、getBottom、getLeft和getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法来拿到View的最终宽/高。
- draw 则负责将View绘制在屏幕上,Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。
DecorView
DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android版本及主题有关),上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,因此可以理解为Activity指定布局的方法不叫setview而叫setContentView,因为我们的布局的确加到了id为content的FrameLayout中。
如何得到content呢?可以这样:ViewGroup content= findViewById (R.android.id.content)。
如何得到我们设置的View呢?可以这样:content.getChildAt(0)。DecorView其实是一个FrameLayout。
MeasureSpec
MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽/高。上面提到过,这里的宽/高是测量宽/高,不一定等于View的最终宽/高。
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。
SpecMode有三类
- UNSPECIFIED 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
- EXACTLY 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
- AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
MeasureSpec和LayoutParams的对应关系
在View测量的时候,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,然后再根据这个MeasureSpec来确定View测量后的宽/高。MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
DecorView的MeasureSpec的产生过程遵守如下规则
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
固定大小(比如100dp):精确模式。
二、View的工作流程
View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制。
三、自定义View的方式
-
直接继承View
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
-
直接继承ViewGroup
这种方法主要用于实现自定义的布局,采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
代码实践 HorizontalScrollViewEx
-
继承已有的控件
用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding等
-
组合控件
组合控件,顾名思义,就是将系统原有的控件进行组合,构成一个新的控件。这种方式下,不需要开发者自己去绘制图上显示的内容,也不需要开发者重写onMeasure,onLayout,onDraw方法来实现测量、布局以及draw流程。这一种相对比较简单。
四、自定义属性
在使用Android原生控件的时候,像android:layout_width和android:padding这种以android开头的属性是系统自带的属性,对自定义的控件也可以根据需要自定义属性。
1. 在values目录下面创建自定义属性的XML
比如attrs.xml,也可以选择类似于attrs_circle_view.xml等这种以attrs_开头的文件名
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
这里只定义了一个格式为“color”的属性“circle_color”,这里的格式color指的是颜色。除了颜色格式,自定义属性还有其他格式,比如reference是指资源id,dimension是指尺寸,而像string、integer和boolean这种是指基本数据类型。
2. 在View的构造方法中解析自定义属性的值并做相应处理
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
mColor = a.getColor(styleable.CircleView_circle_color,Color.RED);
a.recycle();
3. 在布局文件中使用自定义属性
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
app:circle_color="@color/light_green"/>
使用自定义属性,必须在布局文件中添加schemas声明:xmlns:app=http://schemas.android.com/apk/res-auto。在这个声明中,app是自定义属性的前缀,当然可以换其他名字,但是CircleView中的自定义属性的前缀必须和这里的一致
参考书籍《Android开发艺术探索》