android View的绘制流程

View的绘制流程

    整个View的绘制流程时在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概括为根据之前设置的状态,判断是否需要重新计算视图的大小(measure),是否需要重新安置视图的位置(layout),以及是否需要重新绘制(draw).

  整个View树的结构,对每个View的操作相当于是一个递归的实现.

  流程一 : measure()过程

  主要作用 : 为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth),每个View的空间的实际宽高都是有父视图和本身视图决定的


  具体的调用链如下:
  ViewRoot根对象的属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:
    1.设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth);
    2.如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程.
      2.1对每个姿势突的measure()过程,是通过调用父类ViewGroup.java类李的measureChildWithMargins()方法去实现,该方法内部只是简单的调用了View对象的measure()方法.(由于measureChildWithMargins()方法只是一个过渡层,更简单的做法时直接调用View对象的measure()方法).

    整个measure调用流程就是个树形的递归过程
measure函数原型如下:
    public final void meassure(int widthMeasureSpec, iunt heightMeasureSpec) {
	//...
        //回调onMEasure()方法
	onMeasure(widthMeasureSpec, heightMeasureSpec);
	//more
}

    下面用伪代码来描述measure流程
    //回调View视图里面的onMeasure过程
    private void onMeasure(int heightm, int width){
	//设置view的实际宽高
	//1.该方法必须在onMeasure调用,否则会报异常
	setMeasuredDimension(h, 1);

	//2.如果该View时ViewGroup类型,则对它的每个子View进行measure()过程
	int childCOunt = getChildCount();
	for (int i = 0; i < childCount; i++) {
	//2.1 获得每个子View对象引用
	View child = getChildAt(i);
	
	//整个measure()过程就是个递归过程
	//该方法只是一个过滤器,最后会调用measure()过程;或者measureChild(child, h, i)方法
	measureChildWithMargins(child, h, i);
	//其实,对于我们自己写的应用来说,最好的办法就是去掉框架里面的该方法,直接调用view.measure(),如下:
        //child.measure(h, l)
}
}
  	//该方法具体实现在ViewGroup.java中
	protected void measureChildWithMargins(View v, int height, int width){
	v.measure(h, l);
}

    流程二. layout布局过程
    主要作用 : 将整个View树放到合适的位置上.
    具体的调用链如下:
    host.layout()开始View树的布局, 继而回调给View/ViewGroup类中的layout()方法,具体流程如下:
    1.layout方法会设置该View视图位于父视图的坐标轴位置,即,Left, mTop, mLeft, mBottom(调用setFrame()函数去实现).接下来回调onLayout()方法(如果该View对象是ViewGroup对象,需要实现该方法,对每个子视图进行布局);

    2.如果该View是个ViewGroup类型,需要遍历每个子视图childView,调用该子视图的layout()方法去设置它的坐标值
layout函数原型如下,位于View.java中:
    public final void layout(int l, int t, int r, int b) {
	boolean changed = setFrame(l, t, r, b);//设置每个视图位于父视图的坐标轴
	if (changed || (mPrivateFlags & LAY OUT_REQUIED) == LAYOUT_REQUIRED) {
	if (ViewDebug.TRACE_HIERARCHY) {
	ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}

 	onLayout(changed, l, t, r, b); //回调onLayout函数,设置每个子视图的布局
	mPrivateFlags &= ~LAYOUT_REQUIRED;	
}
	mPrivateFlags &= ~FORCE_LAYOUT;
}

同样的,将上面layout调用流程用伪代码描述:


    // layout()过程  ViewRoot.java  
    // 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout()  


    private void  performTraversals(){  


<span style="white-space:pre">	</span>    //...  


<span style="white-space:pre">	</span>    View mView  ;  
<span style="white-space:pre">	</span>    mView.layout(left,top,right,bottom) ;  


<span style="white-space:pre">	</span>    //....  
    }  


//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现  
private void onLayout(int left , int top , right , bottom){  


<span style="white-space:pre">	</span>//如果该View不是ViewGroup类型  
<span style="white-space:pre">	</span>//调用setFrame()方法设置该控件的在父视图上的坐标轴  


<span style="white-space:pre">	</span>setFrame(l ,t , r ,b) ;  


<span style="white-space:pre">	</span>//--------------------------  


<span style="white-space:pre">	</span>//如果该View是ViewGroup类型,则对它的每个子View进行layout()过程  
<span style="white-space:pre">	</span>int childCount = getChildCount() ;  


<span style="white-space:pre">	</span>for(int i=0 ;i<childCount ;i++){  
<span style="white-space:pre">		</span>//2.1、获得每个子View对象引用  
<span style="white-space:pre">		</span>View child = getChildAt(i) ;  
<span style="white-space:pre">		</span>//整个layout()过程就是个递归过程  
<span style="white-space:pre">		</span>child.layout(l, t, r, b) ;  
<span style="white-space:pre">	</span>}  
}  

    流程三 . draw()绘制过程
  由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不需要绘制每个View树的视图,而只会重新绘制需要绘制的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重新绘制的时候,就会为View添加该标志位.

  调用流程 : 
  mView.draw()开始绘制,draw()方法实现功能如下:
    1. 绘制该View的背景
    2. 为显示渐变框做一些准备操作(大多情况下不需要改变渐变框) 
    3. 调用onDraw()方法绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)
    4. 调用dispatchDraw()方法绘制姿势突(如果View类型不为ViewGroup,即不包含子视图,不需要重载该方法)
      4.1 dispatchDraw()方法内部会遍历每个子视图,调用每个子视图的draw()方法(注意,这个地方"需要重新绘制的视图才会调用draw()方法).值得说明的时,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一不需要重写该方法但可以重载富勒函数实现具体的功能.
    5. 绘制滚动条

  强调一点就是,在上述的三个流程,measure(),layout()和draw()中,骨骼已经帮忙把dispatchDraw()过程框架写好了,自定义的ViewGroup只需要实现measure()过程和layout()过程即可.

  这三种情况最终会直接或者简介调用三个函数,分别时invalidate(),requestLayout()以requestFocus(),接着这三个函数最终会调用刀ViewRoot中的scheduleTraversale()方法,该函数接着会发起一个异步消息,消息处理中调用performTraverser()方法对整个View进行遍历.

  invalidate()方法:
  说明: 请求重绘View树,即draw()过程,假如视图大小没有发生变化,则不会调用layout()过程,并且只绘制那些"需要绘制的视图",即谁调用invalidate(),就绘制该视图(如果是View调用,则重绘该View,如果时ViewGroup调用该方法,则重绘整个ViewGroup)请求invalidate()方法).

  一般引起invalidate()操作的函数如下:
    1. 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身.
    2. 调用setSelection()方法,请求重新draw(),但只会绘制调用者本身.
    3. 调用setVisibility()方法,当View可忽视状态在INVISIBLE转换VISIBLE时,会简介调用invalidate()方法,继而绘制该View.
    4. 调用setEnabled()方法,请求重新draw(),但不会重新绘制任何视图包括调用者.

  requestLayout()方法:会导致调用measure()过程和layout()过程.
  说明: 只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括调用者本身.

  一般引起invalidate()操作的函数如下:
    1. 调用setVisibility()方法,当视图状态在INVISIBLE/VISIBLE转换为GONE状态时,会间接调用requestLayout()和invalidate方法.同时,由于整个View树大小发生了变化,会请求measure()过程以及draw()过程,同样的只绘制需要重新绘制的视图.

  requestFocus()函数说明:
  说明: 请求View树的draw()过程,但只绘制需要重绘的视图.
下面有一个小Demo:

  1. MyViewGroup.java自定义ViewGroup类型
  public class MyViewGroup extends ViewGroup{
    private static String TAG = "MyViewGroup";
    private Context mContext;

    public MyViewGroup(Context context) {
      super(context);
      mContext = context;
      init();
  }

  //xml定义的属性,需要该构造函数
  public MyViewGroup(Context context, AttributeSet attrs) {
    super(context);
    mContext = context;
    init();
  }
 
  //为MyViewGroup添加三个子View 
  private void init() {
    //调用ViewGroup父类addView()方法添加子View

    //child 对象一: Button
    Button btn = new Button(mContext);
    btn.setTest("I am Button");
    this.addView(btn);

    //child 对象二: ImageView
    ImageView img = new ImageView(mConetxt);
    img.setBackgroundResource(R.drawable.icon);
    this.addView(img);

    //child 对象三: TextView
    TextView txt = new TextView(mContext);
    txt.setText("Only text");
    this.addView(txt);

    //child 对象四: 自定义View
    MyView myView = new MyView(mContext);
    this.addView(myView); 
  }

  @Override
  //对每个子View进行measure():设置每个子View的大小,即实际宽和高
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //通过init()方法,我们为该ViewGroup对象添加了三个视图,Button,ImageView,TextView
    int childCount = getChildCount();
    Log.i(TAG, "the size of this ViewGroup is ----> " + childCount);
    Log.i(TAG, "**** onMeasure start ****");

    //获取该ViewGroup的实际长和宽 设计刀MeasureSpec类的使用
    int specSize_Width = MeasureSpec.getSize(widthMeasureSpec);
    int SpecSize_Height = MeasureSpec.getSize(htightMeasureSpec);
    Log.i(TAG, "**** specSize_Width" + specSize_Width + " * specSize_Height * ";

    //設置本ViewGroup的寬高
    setMeasuredDimension(specSize_Width, specSize_Height);

    for (int i=0;i<childCount;i++) {
      View child = getChildAt(i);
      child.measure(50, 50);  //简单的设置每个子View对象的宽高为50px, 50px
      //或者可以调用ViewGroup父类方法measureChild()或者measureChildWithMargins()方法
      //this.measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
  }

  @Override
  //对每个子View视图进行布局
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //TODO Auto-generated method stub 
    //通过init()方法,我们为该ViewGroup对象添加了三个视图
    int childCount = getChildCount();
    int startLeft = 0; //设置每个子View的起始横坐标
    int startTop = 10; //每个子View距离父视图的位置, 简单设置为10px,可以理解为android:margin = 10px;

    Log.i(TAG, "**** onLayout start ****");
    for (int i=0; i<childCount; i++) {
      View child = getChildAt(i);
      child.layout(startLeft, startTop, startLeft.child.getMeasuredWidth(), startTop+child.getMeasuredHeight());
      startLeft = startLeft + child.getMeasuredWidth() + 10;  //校准startLeft值,View之间的间距设为10px;
      Log.i(TAG, "**** onLayout startLeft ****" + startLeft);  
    }
  }

  //绘制过程Android已经为我们封装好了,在这里只打出Log
  protected void dispatchDraw(Canvas canvas) {
    Log.i(TAG, "**** dispatchDraw start ****");
    super.dispatchDraw(canvas);
  }  
  
  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    Log.i(TAG, "*** drawChild start****");
    return super.drawChild(canvas, child, drawingTime);
  }
}


   2、MyView.java 自定义View类型,重写onDraw()方法
//自定义View对象  
    public class MyView extends View{  
  
        private Paint paint  = new Paint() ;  
          
        public MyView(Context context) {  
            super(context);  
            // TODO Auto-generated constructor stub  
        }  
        public MyView(Context context , AttributeSet attrs){  
            super(context,attrs);  
        }  
          
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){  
            //设置该View大小为 80 80  
            setMeasuredDimension(50 , 50) ;  
        }  
          
          
          
        //存在canvas对象,即存在默认的显示区域  
        @Override  
        public void onDraw(Canvas canvas) {  
            // TODO Auto-generated method stub  
            super.onDraw(canvas);  
              
            Log.i("MyViewGroup", "MyView is onDraw ") ;  
            //加粗  
            paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));  
            paint.setColor(Color.RED);  
            canvas.drawColor(Color.BLUE) ;  
            canvas.drawRect(0, 0, 30, 30, paint);  
            canvas.drawText("MyView", 10, 40, paint);  
        }  
    } 
    主Activity只是显示了该xml文件,在此也不罗嗦了。 大家可以查看该ViewGroup的Log仔细分析下View的绘制流程以及
相关方法的使用。第一次启动后捕获的Log如下,网上找了些资料,第一次View树绘制过程会走几遍,具体原因可能是某些
View 发生了改变,请求重新绘制,但这根本不影响我们的界面显示效果 。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值