网上对自定义View总结的文章都不少,可是本身仍是写一篇,好记性不如多敲字!
其实自定义View就是三大流程,onMeasure、onLayout、onDraw。看名字就知道,onMeasure是用来测量,onLayout布局,onDraw进行绘制。
那么什么时候开始进行View的绘制流程,这就要从ViewRoot和DecorView的概念提及。java
ViewRoot对应于ViewRootImpl类,是链接WindowManager和DecorView的纽带,View的三大绘制流程都是经过ViewRoot来完成的。在ActivityThread中,当Activity被建立时,会将DecorView添加到Window中,同时建立一个ViewRootImpl对象,并将ViewRootImpl对象和DecorView对象创建关联。android
以上摘自《Android开发艺术探索》第4章View的工做原理
咱们一般开发时,更新UI通常都是不能在子线程中进行,假如在子线程中更新,会抛出异常。这并非由于只有UI线程才能更新UI,而是ViewRootImpl对象是在UI线程中建立。
View的绘制就是从ViewRoot的performTraversals方法开始的。
DecorView是一个顶级View,通常是一个竖直方向的LinearLayout,包含一个titlebar和内容区域。咱们在Activity中setContentView中设置的布局文件就是加载到内容区域。内容区域是个FrameLayout。web
大多数状况下,咱们若是在布局文件中,对自定义View的layout_width和layout_height不设置wrap_content,咱们通常都是不须要进行处理的,可是若是要设置为wrap_content,咱们须要在测量时,对宽高进行测量。ide
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
重写onMeasure方法,咱们能够看到两个传入的int值widthMeasureSpec和heightMeasureSpec。Java中int类型是4个字节,也就是32位,这两个int值中的高2位表明SpecMode,也就是测量模式,低32位则是表明SpecSize也就是在某个测量模式下的大小。
咱们不须要本身写代码进行位运算获得SpecMode和SpecSize,Android内置了MeasureSpec类来处理。函数
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
那SpecMode测量模式占2位,二进制2位能够表达最多4种状况,还好,测量模式只有三种状况,每一种状况有其特殊的意思。oop
SpecMode | 含义 |
---|---|
UNSPECIFIED | 父容器不对当前View有任何限制,就是说View能够取任意大小。 |
EXACTLY | 父容器测量出View须要的精确大小,对于match_parent和具体数值状况xxdp |
AT_MOST | 当前View所能取的最大尺寸,通常是给定一个大小,View的尺寸不能超过该大小,通常用于warp_content |
如下摘自实验室小伙伴的总结,《自定义View,这一篇就够了》。对于咱们在布局中定义的尺寸和测量模式的对应关系,看了下面的总结,就不会有任何疑惑了。布局
match_parent:EXACTLY。怎么理解呢?match_parent就是要利用父View给咱们提供的所剩余空间,而父View剩余空间是肯定的,也就是这个测量模式的整数里面存放的尺寸。
wrap_content:AT_MOST。怎么理解?就是咱们想要将大小设置为包裹咱们View内容,那么尺寸大小就是父View给咱们做为参考的尺寸,只要不超过这个尺寸就能够了,具体尺寸就根据咱们的需求去设定。
固定尺寸(如100dp):EXACTLY。怎么理解呢?用户本身指定了大小,咱们就不用再去干涉了,固然是以指定的大小为主啦。post
经过前文的描述,咱们已经能够动手重写onMeasure函数了。spa
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(WRAP_WIDTH, WRAP_HEIGHT);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(WRAP_WIDTH, height);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, WRAP_HEIGHT);
}
}
只处理AT_MOST状况也就是wrap_content,其余状况则沿用系统的测量值便可。setMeasuredDimension会设置View宽高的测量值,只有setMeasuredDimension调用以后,才能使用getMeasureWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此调用这两个方法获得的值都会是0。
上述是一个通用的些烦,咱们实现一个自定义View,画一个圆。
xml布局以下:线程
<com.zhu.testview.MyView android:id="@+id/my_view" android:layout_width="match_parent" android:layout_height="100dp" android:background="#44ff0000" />
咱们将其中的宽改成wrap_content,并设置默认的宽高为200;
private final int WRAP_WIDTH = 200;
private final int WRAP_HEIGHT = 200;
<com.zhu.testview.MyView android:id="@+id/my_view" android:layout_width="wrap_content" android:layout_height="100dp" android:background="#44ff0000" />
注意
若是咱们不处理AT_MOST状况,那么即便设置了wrap_content,最终的效果也和match_parent同样,这是由于这种状况下,View的SpecSize就是父容器测量出来可用的大小。
若是咱们设置了margin会有什么效果呢?咱们来看看。
<com.zhu.testview.MyView android:id="@+id/my_view" android:layout_width="wrap_content" android:layout_height="100dp" android:layout_margin="20dp" android:background="#44ff0000" />
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
到这里整个onMeasure过程就基本差很少了。
注意
一、某些极端状况下,系统可能要屡次measure才能肯定最终测量的宽高,这时onMeasure中拿到的不必定是准确的,因此onLayout或onSizeChanged中获取宽高。
protected void onSizeChanged(int w, int h, int oldw, int oldh)
Activity中在onWindowFocusChanged中获取。这时View已经初始化完了,能够获取宽高。当Activity窗口得到焦点和失去焦点时均会被调用,所以该函数会被调用屡次。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = myView.getWidth();
int height = myView.getHeight();
Log.d(TAG, "width: " + width);
Log.d(TAG, "height: " + height);
Log.d(TAG, "measuredWidth: " + myView.getMeasuredWidth());
Log.d(TAG, "measuredHeight: " + myView.getMeasuredHeight());
}
}
view.post(runnable)
经过post将一个runnable放到消息队列尾部,等到looper调用此runnable,这时View也已经初始化好了。
myView.post(new Runnable() {
@Override public void run() {
Log.d(TAG, "measuredWidth: " + myView.getMeasuredWidth());
Log.d(TAG, "measuredHeight: " + myView.getMeasuredHeight());
}
});
能够在onCreate、onStart和onResume中调用view.post(runnable)方法。
ViewTreeObserver
使用ViewTreeObserver的回调能够完成获取View的宽高。
ViewTreeObserver observer = myView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override public void onGlobalLayout() {
Log.d(TAG, "observer measuredWidth: " + myView.getMeasuredWidth());
Log.d(TAG, "observer measuredHeight: " + myView.getMeasuredHeight());
}
});
这里使用了onGlobalLayoutListener接口,当View树的状态发生改变或View树内部的View可见性发生改变时,onGlobalLayout会被回调,这也说明onGlobalLayout会被调用屡次。
做者:拿头撞鸡
连接:http://www.jianshu.com/p/1695988095a5