自定义View基础
view树绘制起点
android中大多数情况下我们的界面都是使用view进行显示,(目前我知道的除过使用surface进行显示的视图,其余的都是使用View进行显示,如果有其他方式大家可以一起讨论~)
android中界面使用view进行显示的时候界面可以看作是一颗树,父节点会包含对应的孩子节点,而这棵树的根节点为DecorView,界面上的视图都在这个父节点中显示。而Activity只负责生命周期和事件处理,不进行ui和视图控制相关的处理,视图的绘制和控制是由ViewRoot完成的。
当activity创建完毕后,会将ViewRoot和DecorView进行关联,
root = new ViewRootImpl(view.getContext(), display);
...
root.setView(view, wparams, panelParentView);
源码见WindowManagerGlobal.java,Activity创建的时候如何调用到这里可以参见博客 链接
ViewRoot和DecorView进行关联的时候调用了ViewRoot的setVIew方法,(ViewRoot实现对应于ViewRootImpl类,具体源码可以在该类中进行查看),setView方法中调用了requestLayout()方法,
而requestLayout会调用scheduleTraversal,在scheduleTraversal方法中会发送mTraversalRunnable事件,
mTraversalRunnable中会执行doTraversal方法,该方法中会调用performTraversal方法,从该方法开始,view树开始调用performMeasure(),performLayout(),performDraw()进行绘制。
performTraversal绘制流程
performTraversal()方法调用后会首先调用父容器的measure,layout,draw方法,然后递归调用到子View的measure,layout,draw方法。
MeasureSpec介绍
查看MeasureSpec的注解:
MeasureSpec携带了父view对子view宽高的要求,而该要求是通过view自身的layoutParams和父容器施加的转换规则转换得到的。
MeasureSpec实现时为了减少创建对象的开销,使用了数字进行实现。使用高两位表示SpecMode,使用低30位表示SpecSize,MeasureSpec在实现的时候也提供了相应的方法进行Mode和Size的提取,以及Mode和Size的组合。
该方法进行mode和size的组合
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
下面的两个方法则分别获得相应的size和mode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec在实现的时候规定了三种mode,如下:
UNSPECIFIED :这种情况下父容器不对view的大小做任何限制。
EXACTLY :父容器已经给出view的大小,这个时候SpecSize大小就是View最后的大小。这种情况对应于layoutparam为match_parent和具体数值的这两种情况,给出view一个明确的大小。
AT_MOST :父容器指定了一个SpecSize,View的大小不能大于这个值,具体是什么值需要看不用view的具体实现。这种情况对应于layoutparams为wrap_content,限定了view的范围,但是没有明确要求view的大小。
View在进行绘制的时候是以自身的MeasureSpec为标准的,而View 的MeasureSpec的是根据父容器的MeasureSpec和View自身的Layoutparams得到的,具体的方法参见getChildMeasureSpec()方法,将该方法中的规则总结出来表格如下:
而对于顶级View(DecorView),其MeasureSpec由自身的LayoutParams决定,具体可以参见方法ViewRootImpl中的getRootMeasureSpec()方法。
LayoutParames | SpecMode |
---|---|
MATCH_PARENT | EXACTLY,大小就是窗口的大小 |
WRAP_CONTENT | AT_MOST,大小不能超过屏幕大小 |
固定大小如(100px) | 精确模式,大小为指定的大小 |
将顶层view的测量可以总结成上表
View绘制流程介绍
measure
View的measure过程由measure方法开始,该方法为final方法,无法被继承,measure的过程主要由onMeasure方法完成,子类在使用的时候重写该方法即可,通过该方法我们可以使用viewgroup中传递过来的measureSpec来确定view测量出的大小。
查看上述代码,onMeasure()方法中会调用setMeasuredDimension方法保存view的宽和高,view的宽和高通过getDefaultSize和getSuggestedMinmumWidth方法获取。
ViewGroup的measure过程:
相比于view的measure过程,ViewGroup在measure的过程会需要测量自身的大小并且还需要调用子view的measure方法。
但是ViewGroup自身是一个抽象类,没有定义具体的测量过程,即没有实现onMeasure方法,因为不同布局的测量方式不同,比如说RelativeLayout和LinearLayout的测量方式必然不同,所以onMeasure的具体过程需要子类去自己实现,ViewGroup没有实现具体测测量过程,具体的测量过程还得依靠子类去实现,但是ViewGroup为子类提供了measureChild方法,该方法会进行所有孩子的测量,子类在实现时候可以默认调用该方法完成自身孩子的布局,但是自身的布局还得靠自己根据自己的使用场景去实现对应的onMeasure方法完成布局:
layout
Layout方法用来确定view自身的位置,而onLayout方法用来确认所有的子元素的位置,在layout方法中会调用setFrame方法首先确定自身的位置,
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
然后调用onLayout方法布局孩子的位置,
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
而onLayout方法可以看到view自身并没有实现,因为每一个布局自己对自己的孩子的布局方式不同,具体的实现需要相应的子类去重写。
draw
View在绘制的时候分为如下几步:
...
drawBackground(canvas); // 1.绘制背景
...
if (!dirtyOpaque) onDraw(canvas);// 2.绘制自身内容
dispatchDraw(canvas);//3.绘制孩子(该方法需要子类重写,从而根据子类自身的规则去绘制相应的view)
drawAutofilledHighlight(canvas);// 4.绘制相应的装饰
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas); // 4.绘制相应的装饰
}
onDrawForeground(canvas);// 4.绘制相应的装饰
drawDefaultFocusHighlight(canvas);// 4.绘制相应的装饰
ViewGroup在绘制的时候setWillNotDraw()会在中设置为true,用来优化绘制,如果我们需要在viewGroup中绘制内容时,需setWillNotDraw()为false来关闭优化。
自定义View
实现效果: 支持padding,支持自定义属性实现颜色的自定义,同时支持wrap_content。
通过重写onDraw()我们去实现一个圆形,代码实现比较简单,但是我们需要注意
1:对直接继承自View和ViewGroup的控件,padding属性是默认无法生效的,需要自己处理。
2:继承View和ViewGroup的控件,自己需要处理wrap_content属性
处理padding属性无效的问题:在我们确定宽和高的大小进行绘制的时候我们需要将padding计算在内
int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();
处理wrap_content无效的问题:
通过上表:我们可以看到当设置为wrap_content的时候specSize为parentSize,查看getDefaultSize的源码,mode为AT_MOST的时候view的大小就是specSize,所以设置wrap_content等价于match_parent:
因此当我们自定义view使用wrap_content属性的时候,我们需要自己处理,处理的代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 下面的过程处理view的书信给wrap_content
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
上面的代码中,当我们判断view的specMode为AT_MOST的时候,我们设置大小为200(这个值可以自己根据自己的需要设置),通过上面的处理方式我们解决wrap_content不生效的问题。
看到这里自己脑子里面突然有个疑问
红色的圈出来的那一部分也是会让view的specMode为AT_MOST,那我们重写了view对AT_MOST的处理,当我们设置view为match_parent的时候那岂不match_parent没办法生效了?
自己想了想,什么时候父容器的specMode为AT_MOST呢,查看上表《DecorView的测量规则》,只有当我们设置为wrap_content属性的时候才是AT_MOST,那这就意味大小只要满足孩子的大小即可,即我们自己实现的wrap_content。
增加自定义属性
首先我们在values目录 下面新建一个自定义属性文件,文件名字可以随意起,在这里我新建一个attrs.xml文件,文件中我们自定义自己需要的属性,这里定义了color属性:
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
上面的format中我们还可以填写如下值,
format | 含义 |
---|---|
reference | 代表资源id |
dimension | 尺寸 |
基本数据类型(string/integer/boolean) | 需使用者自己定义 |
代码中我们通过下面的方式使用
在xml跟布局上面增加一个app命名空间,然后我们在该命名空间下使用我们自定义的属性
<LinearLayout ...
xmlns:app="http://schemas.android.com/apk/res-auto"
...>
<com.example.customizeview.CircleView
...
app:circle_color="@color/colorAccent" />
</LinearLayout>
最后我们需要在代码中获取我们自定义的属性
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.i(TAG, "CircleView: enter constructor with attrs");
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.BLUE);
typedArray.recycle();
init();
}
mColor就是我们自己定义的颜色。
完整代码见 CircleView
View布局性能优化
布局优化首先我们应该做的就是尽可能的减少布局文件的层级,当布局的层级减少了,那绘制的性能自然就提高了,同时还可以使用下面的方式来优化我们的布局。
1.优先选择高性能的ViewGroup
android中界面布局使用android中提供的控件就可以,但是同时我们在使用的时候可以根据场景择优选择所需的控件,在选择ViewGroup的时候,可以优先选择性能较高的FrameLayout和LinearLayout,因为RelativeLayout较为费时,但是如果布局需要进行嵌套,可以考虑使用RelativeLayout来实现布局,以达到减少布局层级的目的。
2.使用include和merge标签重复利用布局,同时减少嵌套
- 使用include标签我们可以将公共的布局抽取出来,这样公共的布局我们不用总是重新再写一遍,同时include标签只支持 android:layout_ 开头的属性和android:id 属性,其他的属性不支持,同时在使用该属性的时候,如果使用了android:layout_*开头的其他属性,那么layout_width和layout_height属性必须同时存在,否则设置的android:layout_*开头的属性不起作用。
- 使用merge标签可以用来减少布局的层级,当使用include标签引用布局的时候,如果被引用布局的根布局和include标签的父布局为同一布局,就可以使用merge标签替换被引用布局的根布局标签。
-
自己在尝试的过程中发现,使用include标签时,如果被引用布局的根元素使用merge标签,include属性设置android:layout_属性将不起作用,而需要在merge标签内设置include属性,这样include标签的android:layout_属性才会起作用。(此处为自己通过demo简单实践后得到的,如果有错误还请指正)
-
不起作用的写法:
layout布局
<LinearLayout 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"
android:orientation="vertical"
tools:context=".LayoutPerformanceActivity">
<!--注意layout前面没android开头的命名空间-->
<include
android:id="@+id/reused_image"
layout="@layout/commonimageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"/>
</LinearLayout>
被引用布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher_round" />
</merge>
运行结果:layout_margin属性不起作用
- 起作用的写法
修改被引用布局结构,在merge标签中使用include标签引用布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include
android:id="@+id/reused_image"
layout="@layout/stub_import"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</merge>
stub_layout布局文件仅为一个图片:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
代码见文末链接
运行后发现自己在merge里面的include使用layout_margin属性起作用了,但是引用merge标签的include布局还是没办法使用android:layout_margin属性。
对于这个例子的简单布局来说,可以直接将stub_layout,即不带merge标签的根布局作为引用布局,这样android:layout_属性就可以起作用了,如果是一个复杂的布局,可以使用上面的方式,将需要自定义的布局包含在merge标签之内,这样就可以使用layout属性进行自定义布局了。
3.使用ViewStub标签进行按需加载
ViewStub继承了View,自身不参与任何布局和绘制过程,当我们在开发中,一些布局在初始化时没有必要一起进行初始化,这时就可以使用ViewStub来按需加载该布局,因为当view设置为Gone或者Invisiable的时候,还是会占资源,具体参见(Android 使用View Gone 与 ViewStub的区别)
但是需要注意的是ViewStub的引用布局不支持merge标签
ViewStub使用方式
<ViewStub
android:id="@+id/stub_import"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inflatedId="@+id/stub_import_view"
android:layout="@layout/stub_import" />
android:id指定的即为ViewStub的id,当ViewStub被替换后,加载后得到的布局的Id即为android:inflatedId指定的id。
加载方式:
// mViewStub为 mViewStub = findViewById(R.id.stub_import);
mViewStub.setVisibility(View.VISIBLE);