自定义控件的类型
- 组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成新的控件。
- 继承原有控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角图片
- 完全自定义控件。这种view需要我们自己绘制出来。比如阅读器的翻页控件
View的绘制流程
onMeasure() -> onLayout() -> onDraw()
第一步:measure测量视图大小,从顶层父视图向子视图递归调用measure方法,完成视图测量。测量视图有个关键内容部MeasureSpec,用于解析width/height MeasureSpec的int值,它是一个32位的int值,通过高2位获取测量模式,低30位获取规格大小。
第二步:layout确定View的位置,进行页面布局。从顶层父视图向子视图递归调用layout方法,即父视图根据上一步measure子视图所得到的布局大小和参数,将子视图放到合适的位置。
第三步:draw绘制视图。绘制的过程:背景(drawBackground) - 主体(onDraw) - 子视图(dispatchDraw) - 装饰(onDrawForeground 如滚动条)- 前景(onDrawForeground 前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。)
MeasureSpec
widthMeasureSpec/heightMeasureSpec是32位int类型,高2位为测量模式,低30位代表大小Size。
有三种测量模式:
- EXACTLY 精确模式:match_parent或者精确数值
- AT_MOST 最大值模式:wrap_content,View的尺寸不超过MeasureSpec当中的Size值
- UNSPECIFIED 无限制模式:View设置多大就给多大。
ViewGroup中没有measure()也没有onMeasure()
自定义view的注意事项
在ViewGroup的子类中重写dispatchDraw以外的绘制方法是,可能需要调用setWillNotDra(false);
出于效率的考虑,ViewGroup默认会绕过draw()方法,直接执行dispatchDraw(),以此简化绘制流程。如果自定义了某个ViewGroup的子类(比如LinearLayout)并且需要在它的除dispatchDraw()以外的任何一个绘制方法内绘制内容,可能需要调佣setWillNotDraw(false)这行代码来切换到完整的绘制流程。
绘制代码优先写在onDraw方法内
当绘制代码可以写在onDraw方法内,也可以写在其他绘制方法里,那么优先写在onDraw里面,因为Android有相关优化,可以在不需要重绘的时候自动跳过onDraw()的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。
Activity、Window、DecorView之间关系
首先,来看一下Activity中setContentView的源代码。
public void setContentView(@LayoutRes int layoutResID) {
//将xml布局传递到Window当中
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
可以看到, Activity的 setContentView实质是将 View传递到 Window的 setContentView()方法中, Window的 setContenView会在内部调用 installDecor()方法创建 DecorView,代码如下。
public void setContentView(int layoutResID) {
if (mContentParent == null) {
//初始化DecorView以及其内部的content
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...............
} else {
//将contentView加载到DecorVoew当中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...............
}
private void installDecor() {
...............
if (mDecor == null) {
//实例化DecorView
mDecor = generateDecor(-1);
...............
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//获取Content
mContentParent = generateLayout(mDecor);
}
...............
}
protected DecorView generateDecor(int featureId) {
...............
return new DecorView(context, featureId, this, getAttributes());
}
通过 generateDecor()的new一个 DecorView,然后调用 generateLayout()获取 DecorView中 content,最终通过 inflate将 Activity视图添加到 DecorView中的 content中,但此时 DecorView还未被添加到 Window中。添加操作需要借助 ViewRootImpl。
ViewRootImpl的作用是用来衔接 WindowManager和 DecorView,在 Activity被创建后会通过 WindowManager将 DecorView添加到 PhoneWindow中并且创建 ViewRootImpl实例,随后将 DecorView与 ViewRootImpl进行关联,最终通过执行 ViewRootImpl的 performTraversals()开启整个View树的绘制。
Requestlayout,onlayout,onDraw,DrawChild区别与联系
- requestLayout() :会导致调用 measure()过程 和 layout()过程,将会根据标志位判断是否需要onDraw。
- onLayout() :如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局。
- onDraw() :绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)。
- drawChild() :去重新回调每个子视图的draw()方法。
invalidate() 和 postInvalidate()的区别
invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。