zoukankan      html  css  js  c++  java
  • Android开发——View绘制过程源码解析(二)

    0. 前言  

    View的绘制流程从ViewRootperformTraversals开始,经过measurelayoutdraw三个流程,之后就可以在屏幕上看到View了。上一篇已经介绍了View和ViewGroup的measure的源码解析过程,本篇介绍measure后如何获得View的宽和高,以及layout和draw的过程

    本文原创,转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52742887


    1.  获取Measure后的宽高

    Meaure完成以后,可以通过getMeasuredWidth/Height()来获得View的测量宽高。但是在onMeasure()中拿到的宽高不像在onLayout()中拿到的那么可靠。因为有时View可能需要多次measure,虽然最终测量和最终结果相同,但是前几次measure可能会得到不可靠的宽高。

    因为measure的过程和Activity的生命周期没有任何关系,无法确定在哪个生命周期执行完毕以后Viewmeasure过程一定完成。因此在Activity的生命周期里可能无法获得测量宽高。可以尝试如下三种方法获取view的测量宽高。


    1.1  焦点相关回调

    //重写Activity的onWindowFocusChanged方法
    //该方法当Activity得到/失去焦点便会回调,表示View已经初始化完毕,可以保证Measure完成
    public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus) {
                int width = tv.getMeasuredWidth();
                int height = tv.getMeasuredHeight();
            }
    }
    


    1.2 消息队列

    //将Runnable置于消息队列尾部,延迟获取测量宽高的时间,保证View初始化完毕
    @Override
    protected void onStart() {
       super.onStart();
       view.post(new Runnable() {
          @Override
          public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
           }
        });
    }
    


    1.3  监听view树

    //当View树状态发生改变时回调onGlobalLayout
    @Override
    protected void onStart() {
    view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
         @Override
         public void onGlobalLayout() {
            //因为会调用多次,所以拿到测量结果后要remove掉监听
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
     });
    }
    


    2. Layout过程

    ViewRootperformTraversals()方法会在measure结束后继续执行,并调用Viewlayout()方法,layout方法是确定本身View的位置,而onLayout方法是确定所有子元素的位置。layout方法如下所示:

    //四个参数分别代表相对于父视图左上右下四个坐标值
    host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
    public void layout(int l, int t, int r, int b) {  
        int oldL = mLeft;  
        int oldT = mTop;  
        int oldB = mBottom;  
        int oldR = mRight;  
        boolean changed = setFrame(l, t, r, b);  
        if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
            if (ViewDebug.TRACE_HIERARCHY) {  
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
            }  
            onLayout(changed, l, t, r, b);  
            mPrivateFlags &= ~LAYOUT_REQUIRED;  
            if (mOnLayoutChangeListeners != null) {  
                ArrayList<OnLayoutChangeListener> listenersCopy =  
                        (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();  
                int numListeners = listenersCopy.size();  
                for (int i = 0; i < numListeners; ++i) {  
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);  
                }  
            }  
        }  
        mPrivateFlags &= ~FORCE_LAYOUT;  
    } 
    

    layout()方法中,第8行首先会调用setFrame()方法来判断视图的大小是否发生过变化,避免未发生变化时进行重绘操作。

    13行会调用onLayout()方法,用于父容器确定子元素的位置,这是一个空实现ViewViewGrope均没有实现它。

     

    我们尝试自定义一个布局,借此来理解onLayout()的过程。代码如下所示:

    public class MyLayout extends ViewGroup {  
        public MyLayout (Context context, AttributeSet attrs) {  
            super(context, attrs);  
        }  
        @Override  
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
            super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
            //调用measureChild()来测量出子元素的大小 
            measureChild(getChildAt(0), widthMeasureSpec, heightMeasureSpec);  
        }  
        @Override  
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //子元素可以在xml中任意添加一个控件
        //根据测量结果布置子元素在MyLayout中的位置
            childView.layout(0, 0, getChildAt(0).getMeasuredWidth(),getChildAt(0).getMeasuredHeight());  
        }  
    }
    

    onLayout()过程结束后,我们就可以调用getWidth/Height来获取View最终宽高了。之前也提到过, getMeasureWidth方法在measure过程结束后取到的是测量宽高,两者几乎在任何情况下都是相同的。但是也有非一般的情况,原因就是我们可以重写onLayout,并且对measure测量的结果"置之不理"。

    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //这样getWidth得到的值就是100减0,虽然这么做毫无意义  
        getChildAt(0).layout(0, 0, 100, 100);  
    }
    

    3. Draw过程

    measurelayout的过程都结束后,就进入到对View进行绘制的draw过程了。

    ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用Viewdraw()方法来执行具体的绘制工作。代码如下所示:

    public void draw(Canvas canvas) {  
        //…
        final int privateFlags = mPrivateFlags;  
        final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
        mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
        // 第一步,对背景进行绘制
        int saveCount;  
    if (!dirtyOpaque) {
        //得到android:background属性设置的图片或颜色
            final Drawable background = mBGDrawable;  
            if (background != null) {  
                final int scrollX = mScrollX;  
                final int scrollY = mScrollY;  
                if (mBackgroundSizeChanged) {
                    //根据layout的View位置结果确定背景绘制区域  
                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  
                    mBackgroundSizeChanged = false;  
                }  
                if ((scrollX | scrollY) == 0) {
                    //调用Drawable的draw()方法来完成背景的绘制工作
                    background.draw(canvas);  
                } else {  
                    canvas.translate(scrollX, scrollY);  
                    background.draw(canvas);  
                    canvas.translate(-scrollX, -scrollY);  
                }  
            }  
    } 
     
        final int viewFlags = mViewFlags;  
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  
        if (!verticalEdges && !horizontalEdges) {  
            //第三步,对内容进行绘制,onDraw为空实现,需要子类实现  
            if (!dirtyOpaque) onDraw(canvas);  
            //第四步,ViewGroup的dispatchDraw()方法遍历调用所有子元素的draw()
            dispatchDraw(canvas);  
            //第六步,显示控件的滚动条
            onDrawScrollBars(canvas);  
            // we're done...  
            return;  
        }  
    }
    

    从源码中可以看出,onDraw()是需要我们去重写实现的

    绘制主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,基本可以把它当成一块画布,在上面绘制任意的东西。


    4. setWillNotDraw方法

    /**
    * If this view doesn't do any drawing on its own, set this flag to allow further optimizations.
    * By default, this flag is not set on View, but could be set on some View subclasses such as ViewGroup.
    *Typically, if you override onDraw(Canvas),you should clear this flag.
    */
    public void setWillNotDraw(boolean willNotDraw) {
       setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
    

    如果你的自定义View不需要绘制出来的话,就可以设置这个方法为true,这样可以优化执行速度

    ViewGrope默认设置为true,因为ViewGrope多数都是只负责布局。如果我们继承自ViewGrope的自定义控件需要通过onDraw绘制内容时,需要手动关闭这个标记位。

    public class MyLayout extends LinearLayout {
        public MyLayout (Context context, intposition) {
             super(context);
             setWillNotDraw(false);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
             super.onDraw(canvas);
         }
    }
    

    至此关于View绘制的过程便总结完毕。

    转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52742887

          


  • 相关阅读:
    继承与多态,Instanceof关键字
    面向对象,单例模式
    方法
    数组
    流程控制
    基础语法
    连接linux四大远程工具
    MYSQL-索引的设计
    银行一类(Ⅰ类)、二类(Ⅱ类)、三类(Ⅲ类)账户区别是什么?
    真正有效的学习
  • 原文地址:https://www.cnblogs.com/qitian1/p/6461496.html
Copyright © 2011-2022 走看看