MainActivity如下:
package cc.testviewstudy2;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.app.Activity;
/**
* Demo描述:
* 关于自定义View的学习(二)
*
* View的绘制流程:onMeasure()-->onLayout()-->onDraw()
*
* 学习资料:
* 1 http://blog.csdn.net/guolin_blog/article/details/16330267
* 2 http://androidxref.com/2.3.6/xref/frameworks/base/core/java/android/view/View.java
* 3 http://blog.csdn.net/dawanganban/article/details/23953827
* 4 http://blog.csdn.net/xyz_lmn/article/details/20385049
* 5 http://blog.csdn.net/cskgnt/article/details/7988486
* Thank you very much
*
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
init();
}
//验证在onDraw()里面draw出来的东西并不是一个子View
private void init(){
LinearLayout linearLayout=(LinearLayout) findViewById(R.id.linearLayoutTest);
int childCount=linearLayout.getChildCount();
System.out.println("---> childCount="+childCount);
}
}
LinearLayoutTest如下:
package cc.testviewstudy2;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
/**
* View绘制的三个过程onMeasure()-->onLayout()-->onDraw()
*
*
*
* View系统的绘制流程
* 1 ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法.
* 2 完成View的measure()结束后继续执行,ViewRoot中调用View的layout()方法.
* 3 在layout()后ViewRoot中的代码会继续执行并创建出一个Canvas对象
* 然后调用View的draw()方法来执行具体的绘制工作
*
*
* private void performTraversals() {
* final View host = mView;
* ...
* host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
* ...
* host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
* ...
* draw(fullRedrawNeeded);
* }
*
*
* onMeasure():测量View的大小并且保存了测量的结果
* 我们首先来看该方法的定义:
* onMeasure(int widthMeasureSpec,int heightMeasureSpec)
* 首先搞明白这两个值输入参数是从哪里得到的?
* 通常情况下:
* 这两个值是由父视图经过计算后传递给子视图的,这说明父视图会在一定程度上决定子视图的大小
*
*
* 注意两个输入参数widthMeasureSpec和heightMeasureSpec不是一般的int而是MeasureSpec.
* MeasureSpec包含了specMode和specSize这两部分内容.
* 即specSize表示宽和高的大小,而specMode表示了宽和高的方式(规格).
* 其中,specMode有三种类型:
* 1 EXACTLY(具体的值为 1 << 30 即为:1073741824)
* 表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小
* 例如我们在xml文件中为控件的宽和高设置成一个具体的值或MATCH_PARENT时MeasureSpec的specMode就等于EXACTLY
* 2 AT_MOST(具体的值为 2 << 30)
* 表示子视图最多只能是specSize中指定的大小,开发人员应尽可能小得去设置这个视图,并且保证不会超过specSize
* 例如我们在xml文件中为控件的宽和高设置成WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST
* 3 UNSPECIFIED(具体的值为 0)
* 表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制.
* 这种情况极少用到
*
*
* 了解了该方法的定义,我们来梳理一下测量(measure)的流程
* 1 关于View的onMeasure()
* 为了更好的理解onMeasure(),我们从View的measure()方法说起.
* 比如有个View view;我们可调用view.measure(widthMeasureSpec, heightMeasureSpec)
* 请注意源码中View的measure()这个方法是final的,所以我们无法在子类中去重写这个方法.
* 那么这个方法到底干了些什么呢?假如我们要自己measure又该怎么办呢?
* 1.1 在view.measure()内,核心代码是调用了onMeasure()
* 1.2 我们看一下onMeasure()方法的源码:
* protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
* setMeasuredDimension(
* getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
* getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
* );
* }
* 在此继续看getDefaultSize()和setMeasuredDimension()方法的源码,可知,
* onMeasure()的执行过程:
* 1 利用getDefaultSize()获取到了view宽高的Mode,再依据Mode得到其大小size
* 2 利用setMeasuredDimension()方法保存了上一步中测量得到的该view的大小
* 注意setMeasuredDimension()也是final的,不可重写;可调用
* 所以:
* 1 我们要自己measure的时候可以重写onMeasure()
* 2 只有在调用了onMeasure()方法后调用getMeasuredWidth()和getMeasuredHeight()才可
* 获取到测量所得到的控件大小,在此之前调用这两个方法获取到的值均为0或者其他错误值.
*
* 这就是系统自动测量View的过程.当然我们要否定这个过程,自己修改测量的结果,可以怎么做呢?
* 挺简单的,调用:setMeasuredDimension()即可
*
* 综上所述,View大小的控制是由父视图,布局文件,以及视图本身共同完成的:
* 1 父视图会提供给子视图参考的大小
* 2 开发人员可在XML文件中指定视图的大小
* 3 最后视图本身会对最终的大小进行最终的拍板
*
*
* 2 关于ViewGroup的onMeasure()
* 看完了View的绘制流程,再看ViewGroup的绘制流程就简单多了.
* 我们去看ViewGroup的子类比如FrameLayout和LinearLayout的源码可知:
* 2.1 FrameLayout继承自ViewGroup而ViewGroup又继承自View
* 所以,在ViewGroup(XXXLayout).measure()时,还是会调用onMeasure()
* 2.2 在onMeasure()内调用了measureChildren()
* 从该方法名我们也可知,它要测量孩子们了.
* 2.2.1 针对每个子View调用measureChild()进行测量
* 2.2.1.1 综合ViewGroup的widthMeasureSpec和heightMeasureSpec以及自身的布局
* 得到每个子View的widthMeasureSpec和heightMeasureSpec.
* 然后才去调用子view.measure().
* 2.2.1.2 每个子View调用:view.measure()这就和上面描述的
* View的measure()过程一致了,在此不再赘述.
*
*
*
*
*
*
*
*
*
*
*
* onLayout():视图的布局,即确定视图的位置
* measure过程结束后,视图的大小就已经测量好了,接下来就该是layout的过程了.
* 在代码中是这样体现的:
* ViewRoot的performTraversals()方法会在measure()结束后调用View的layout()方法来执行此过程
* 如下所示:
* host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
* layout()四个输入参数:分别代表着左、上、右、下的坐标
* 当然这个坐标是相对于当前视图的父视图而言的.
* 从最后两个参数可以看到,把measure中测量出的宽度和高度传到了该layout()方法中.
* 在layout()中的核心代码是调用了onLayout()
*
* 1 View的onLayout()
* 源码中该方法为空,所以在View中该方法无实际用处.
* 因为布局和显示子控件是ViewGroup的工作.即父视图决定子视图的显示位置
* 2 ViewGroup的onLayout()
* 在ViewGroup中onLayout()是一个抽象方法:
* protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
* 所以我们在实现自定义ViewGroup时必须要重写该方法,否则其中的控件是无法显示的.
* 在重写onLayout()时:
* 2.1 得到每个子View
* 2.2 每个子View调用layout()方法.其中r-l=控件的宽,b-t=控件的高.
* 所以说该方法决定了控件的摆放也决定了控件的大小.
* 2.2.1 常用childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
* 注意最后两个参数就是在onMeasure()阶段测量得到的宽和高.
* 以前有个错误的认识:
* 在看到设置childView.measure(300, 300)后界面上View的大小改变了就觉得
* measure()方法除了测量的作用还可以设置View的大小.
* 其实这样的理解是片面和误读的.
* 是因为系统默认的情况下在onLayout()阶段会用到onMeasure()阶段测量的控件大小.
* 2.2.2 也可不用 在onMeasure()阶段测量得到的宽和高作为参数,而按照需求给定其他值也行.
*
*
* 在onMeasure()和onLayout()结束之后,在此比较一下getMeasureWidth()和getWidth()的区别
* 1 获取的时机不一样
* getMeasureWidth()方法在onMeasure()结束后就可获取到
* getWidth()方法要在layout()过程结束后才能获取到
* 2 计算方式不一样.
* getMeasureWidth()的值是在setMeasuredDimension()方法已经设置好的.
* getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算得出的.
*
*
*
*
*
*
*
*
*
*
*
* onDraw():
* 绘制图形界面.
* ViewRoot的performTraversals()方法会在layout()结束后调用View的draw()方法来执行此过程
* draw()一共有六步:
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*
* 第三步核心是onDraw()绘制内容
* 第四步的核心就是dispatchDraw()绘制子View
*
*
*
*
*
*
*
*
* 分析重绘:
* invalidate()或者postInvalidate()都会导致整个View树重绘
* 而且是自上而下的重绘,比如:从最外层的Layout到里层的Layout,直到每个子View.
* 在重绘时又会调用draw()方法
* draw()一共有六步:
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
* 其中最重要的是第三步和第四步
* 第三步会去调用onDraw()绘制内容
* 第四步会去调用dispatchDraw()绘制子View
*
* 在这个从上而下的重绘过程中,ViewGroup和View的重绘流程差不多.
* 最重要的差别在于View不会执行dispatchDraw()因为它没有子View.
*
* 在重绘View树时ViewGroup和View时按理都会经过onMeasure()和onLayout()以及
* onDraw()方法.当然系统会判断这三个方法是否都必须执行,如果没有必要就不会调用.
*
*
*
* 注意事项:
* 1 该示例继承自XXXXLayout不要继承自ViewGroup
* 假如继承自ViewGroup那么xml的cc.testviewstudy2.ViewGroupLayout中
* 设置layout_width和layout_height不论是wrap_content还是fill_parent
* 这个父视图总是充满屏幕的.
* 现象是如此,原因暂不明.待以后调查
*
* 2 在本LinearLayoutTest我们只放入一个ImageView作为测试
*
* 3 在LinearLayoutTest中有个ImageView,所以其子View只有一个.
* 注意:在onDraw()里面绘制出来的东西并不算作子View
*/
public class LinearLayoutTest extends LinearLayout {
private Paint mPaint;
public LinearLayoutTest(Context context) {
super(context);
}
public LinearLayoutTest(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 再次补充说明measureChild()方法,解释如下:
* 综合ViewGroup的widthMeasureSpec和heightMeasureSpec以及自身的布局
* 从而得到每个子View的widthMeasureSpec和heightMeasureSpec.
* 然后才去调用子view.measure()
*
* 源码如下:
* protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
* final LayoutParams lp = child.getLayoutParams();
* final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
* mPaddingLeft + mPaddingRight, lp.width);
* final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
* mPaddingTop + mPaddingBottom, lp.height);
* child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
* }
*
* 所以该方法不是用来修改子View经过测量所得的大小.
*
* 可用measure()方法
* childView.measure(MeasureSpec.EXACTLY+200, MeasureSpec.EXACTLY+200);
* 修改系统默认测量的子view的大小.
* 有的人会说:直接调用childView.setMeasuredDimension()就行了嘛,何必调用
* childView.measure()呢?反正measure()最后悔调用setMeasuredDimension()的.
* 可是View的对象不能直接调用setMeasuredDimension()方法.
* 1 注意源码 该方法的定义
* protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
* 它是protected修饰的
* 2 关于protected关键字可以参考
* http://blog.csdn.net/cskgnt/article/details/7988486
*
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
System.out.println("父视图给出的该ViewGroup的参考值:");
System.out.println("widthSize="+widthSize+",widthMode="+widthMode);
System.out.println("heightSize="+heightSize+",heightMode="+heightMode);
System.out.println(" ");
//我们可以不依照该参考值,而通过以下代码自己设置
//设置了整个ViewGroup(即此处的LinearLayoutTest)的大小
this.setMeasuredDimension(250, 250);
if (getChildCount()>0) {
View childView=getChildAt(0);
//测量子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//不能简单粗暴地这写:
//measureChild(childView, (MeasureSpec.EXACTLY+150), (MeasureSpec.EXACTLY+150));
//以为这样就可以最终修改测量值.原因参见上面的解释
//但我们可以这样来设置子View的测量大小
//childView.measure(MeasureSpec.EXACTLY+200, MeasureSpec.EXACTLY+200);
}
}
/**
*注意事项:
*1 layout(int l, int t, int r, int b)
* 该方法的值都是相对于父元素的左上角而言
*
*2 若要想把子View的X,Y位置均平移50
* 错误写法--->
* childView.layout(50, 50, childView.getMeasuredWidth(), childView.getMeasuredHeight());
* 导致图片显示出来很小
* 正确写法--->
* childView.layout(50, 50, childView.getMeasuredWidth()+50, childView.getMeasuredHeight()+50);
*
*/
@Override
protected void onLayout(boolean arg0, int left, int top, int right, int bottom) {
if (getChildCount()>0) {
View childView=getChildAt(0);
System.out.println("子视图childView.getMeasuredWidth()="+childView.getMeasuredWidth()+
",子视图childView.getMeasuredHeight()="+ childView.getMeasuredHeight());
//摆放子View
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
/**
*注意事项:
*在onDraw()里面draw出来的东西并不是一个子View
*参见MainActivity里的验证性代码
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint = new Paint();
mPaint.setColor(Color.YELLOW);
mPaint.setTextSize(20);
String content = "Hello World";
canvas.drawText(content, 150, 100, mPaint);
}
}
main.xml如下:
<cc.testviewstudy2.LinearLayoutTest
android:id="@+id/linearLayoutTest"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ff0033"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher"
/>
</cc.testviewstudy2.LinearLayoutTest>