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>