android-----View工作原理系列(一)

来源:互联网 发布:男士护肤推荐 知乎 编辑:程序博客网 时间:2024/06/08 16:50

        这几天开始了View工作原理的学习,当然最初肯定是从View的绘制过程开始的,至于其中的源码分析网上挺多的,我只会在随后的博客中做些总结,并不从代码层面进行分析,毕竟网上资料已经很多了,这篇博客我主要涉及的是invalidate以及与之有关的postInvalidate最后讲解下requestLayout,他们三个都是用于视图重绘的,同样我也不会从代码层面进行分析,首先会通过实例来提出一个问题,之后把invalidate源码层面的执行过程通过总结的方式呈现出来;

        本文涉及到的源码是android4.3版本的;

        好了,我们开始吧!!!

        我们查看下测试的布局文件:

<com.hzw.viewshow.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/myRelativeLayout"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="#4400B3">    <com.hzw.viewshow.MyLinearLayout         android:id="@+id/myLinearLayout"         android:layout_width="100dip"         android:layout_height="100dip"         android:background="#77FF00"         android:layout_centerInParent = "true">         <com.hzw.viewshow.MyTextView            android:id="@+id/myTextView"        android:layout_width="50dp"        android:layout_height="50dp"        android:layout_gravity="center"        android:background="#FF0000"        android:layout_marginLeft="20dip"/>    </com.hzw.viewshow.MyLinearLayout></com.hzw.viewshow.MyRelativeLayout>
        比较简单就是RelativeLayout嵌套LinearLayout,LinearLayout嵌套TextView,只不过这里我们都是自定义的而已,主要是为了能够打印出我们想要的Log信息,在MyRelativeLayout和MyLinearLayout中我们重写了invalidate()、invalidateDrawable(Drawable drawable)、invalidate(int l, int t, int r, int b)、invalidateChildInParent(int[] location, Rect dirty),在MyTextView中我们重写了invalidate()、invalidateDrawable(Drawable drawable)、invalidate(int l, int t, int r, int b);

        点击下载具体测试代码!!!!!

        先来看看测试效果:

                                                           

        可以看到我们的测试样例是在点击红色区域(表示MyTextView)的时候会将该区域颜色设置为黑色,调用的方法是setBackgroundColor,而这个方法会去调用MyTextView的invalidateDrawable,invalidateDrawable方法会去调用invalidate(int l, int t, int r, int b),那么我们就可以在这个方法里面获取到布局的四个点的值啦!

        下面我们查看Logcat输出:

           可以看到首先调用的是我们MyTextView的invalidateDrawable,随后获取到他的四个点的值,下面我们对输出一一来进行解释:

        首先需要明确的是Logcat这里的输出数字是像素值,也就是pix指,而我们布局中设置的是dip值,当然因为dp=dip,所以设置dp也一样,需要知道的是这里1dp/dip=1.5pix;

        先来看这5行输出:

07-04 05:48:05.936: I/System.out(2239): MyTextView-->invalidate-->l: 0 t: 0 r: 75 b: 75
07-04 05:48:05.945: I/System.out(2239): MyLinearLayout-->invalidateChildInParent-->before-->l: 0 t: 0 r: 75 b: 75
07-04 05:48:05.945: I/System.out(2239): MyLinearLayout-->invalidateChildInParent-->location[0]: 30
07-04 05:48:05.945: I/System.out(2239): MyLinearLayout-->invalidateChildInParent-->location[1]: 37

07-04 06:01:11.445: I/System.out(2303): MyLinearLayout-->invalidateChildInParent-->after-->l: 30 t: 37 r: 105 b: 112

                                                                    

        从上面的图中你会发现,我们最里面的正方形也就是MyTextView的宽度和高度为75pix,所以在点击MyTextView之后输出的(0,0,75,75)是相对于黑色框的左上角而言的,也就是把黑色框的左上角作为原点;接着会对MyTextView的父布局MyLinearLayout进行查看是否需要重新绘制,其实就是设置一些标志啦,随后的重绘工作会根据这些标志来判断是否需要重新绘制,可以发现这里调用的invalidateChildInParent方法,此处的30表示MyTextView的左边缘距离MyLinearLayout左边缘的像素距离,37值的是MyTextView的上边缘距离MyLinearLayout上边缘的像素距离,那么在invalidateChildInParent执行结束之后输出的位置(30,37,105,112)指的是黑色框的左,上,右,下,四个位置相对于绿色框左上角的位置,也就是此时把绿色框的左上角作为了原点;

        在将MyLinearLayout标志设置结束之后,接下就是MyLinearLayout的父布局MyRelativeLayout了,我们来看下随后的输出:

07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->before-->l: 30 t: 37 r: 105 b: 112
07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->location[0]: 165
07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->location[1]: 270
07-04 06:01:11.445: I/System.out(2303): MyRelativeLayout-->invalidateChildInParent-->after-->l: 195 t: 307 r: 270 b: 382

        在上面的MyLinearLayout结束之后,黑色框的位置变成了(30,37,105,112),当然这也就是开始执行MyRelativeLayout的位置了,此处的165,270代表的含义见下图:

                                                                          

        即165表示MyLinearLayout左边缘和MyRelativeLayout左边缘的距离,270表示MyLinearLayout上边缘和MyRelativeLayout上边缘的距离,执行完MyRelativeLayout的invalidateChildInParent之后,MyTextView的位置变成了(195,307,270,382),也就是l和r分别加了165,t和b分别加了270,这时候MyTextView的位置是相对于MyRelativeLayout左上角的;

        这样,我们对上面测试的输出解释完了,你会发现在MyTextView进行重绘的过程中,首先执行的是MyTextView的invalidate,接着执行的是MyTextView父布局的invalidateChildInParent方法,这个过程和我们之前的事件分发过程以及View的measure、layout、draw过程正好相反,原因在于他是我们measure、layout、draw过程的前一步操作,也就是设置一些需不需要重绘的标志信息,随后的重绘过程会根据这些标志信息进行是否重绘的操作,在这里有一点需要说明一下,其实你调用View的invalidate方法进行重绘,实际上重绘的时候是只有draw过程的,并不涉及到measure以及layout过程,这一点和通过requestLayout重绘有点区别

        下面我用叙述的方式讲解下invalidate的执行流程:

        我们平常使用invalidate的方式是:view.invalidate();

        (1):这里的invalidate()实际上调用的是invalidate(true),这里的参数true表示进行的是否是完全重绘,完全与否就是是否使用绘制缓存绘制的问题;

        (2):进入invalidate之后首先会判断是否要跳过绘制,接着分硬件加速可用与不可用两种情况来调用ViewParent的invalidateChild方法,这里的ViewParent一般来说指的就是ViewGroup,因为ViewGroup实现了ViewParent接口,ViewGroup中的invalidateChild方法需要两个参数,第一个参数就是我们当前的View了,第2个参数是我们想要重绘区域的矩阵(实际上就是左、上、右、下四个位置),如果是硬件加速可用的话,传入的第2个参数是null,表示绘制整个View的层级,硬件加速不可用的话,则绘制我们传进来Rect对象的局域;

        (3):接下来进行到ViewGroup的invalidateChild方法里面了,这个方法里面存在一个do while循环,这个循环的主要作用是将View的父View以及祖先View的可显矩阵和当前View的矩阵做运算,计算出当前View的Rect相对于不同祖先View的坐标值而已,在do while循环里面可以看到最关键的是执行ViewParent的invalidateChildInParent方法了,这个方法在ViewGroup以及ViewRootImpl都有实现,如果parent类型是ViewGroup的话,执行的是ViewGroup的invalidateChildInParent方法,ViewGroup里面的invalidateChildInParent主要进行的也是一些矩阵相交的运算,代码有点复杂,只需要了解流程就可以了,因为我们的每一个窗体的最上层肯定就是ViewRootImpl,而他是不属于ViewGroup类型的,那么到最后执行的将是ViewRootImpl的invalidateChildInParent方法了;

        (4):ViewRootImpl里面的invalidateChildInParent代码比较关键,他首先会检查当前线程是否是UI线程,这点很容易理解,如果不是UI线程,你都没权利更新UI的,更别说View的绘制了,随后你会看到一个scheduleTraversals方法,而这个方法最终会通过postCallback来调用TraversalRunnable对象的run方法,而在run方法里面会执行doTraversal方法,在doTraversal方法里面就会执行我们很熟悉的performTraversals方法了,也就是我们常见的分析View绘制过程博客最开始执行的方法了;

        (5):接下来的过程就是一般View的绘制过程了,执行measure、layout、draw三个步骤,先测量父View接着测量子View,绘制父View,绘制子View,但是对于调用View的invalidate方法进行重绘来说是不会执行measure、layout这两个过程的,具体是怎么屏蔽这两个过程的呢?是通过在从子View到父View的计算矩阵的过程中没有设置与measure、layout过程有关的标志实现的;

        介绍完invalidate的过程,那postInvalidate又是怎么执行的呢?其实从上面的分析中你也看出来了,invalidate是只适用于UI线程的,非UI线程中执行invalidate方法是会抛异常的,那么怎么样才能在非UI线程中进行View视图重绘呢?就是我们这里的postInvalidate了,其实了解Handler机制的一下子就会明白这里的post是什么意思了,因为从postInvalidate调用invalidate源码比较少,所以我贴出来分析一下:

        首先是postInvalidate方法,他有两个重载实现:

  public void postInvalidate() {        postInvalidateDelayed(0);    }
 public void postInvalidate(int left, int top, int right, int bottom) {        postInvalidateDelayed(0, left, top, right, bottom);    }
        第1个是我们经常用的形式,第2个是对指定区域进行重绘,一般我们自己的话很少使用;     

        我们这里只分析第一个postInvalidate方法,可以看到他执行的是postInvalidateDelayed,传入的参数是0,来看postInvalidateDelayed方法:

    public void postInvalidateDelayed(long delayMilliseconds) {        // We try only with the AttachInfo because there's no point in invalidating        // if we are not attached to our window        final AttachInfo attachInfo = mAttachInfo;        if (attachInfo != null) {            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);        }    }
        可以看到调用的是ViewRootImpl对象的dispatchInvalidateDelayed方法,将当前View作为参数传递进去;

 public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);        mHandler.sendMessageDelayed(msg, delayMilliseconds);    }
        在dispatchInvalidateDelayed方法中我们将View封装成Message对象,通过handler对象将他发送出去,这里的mHandler是ViewRootHandler类型的对象,所以我们需要查看mHandler的handleMessage中case为MSG_INVALIDATE的执行语句:

          case MSG_INVALIDATE:                ((View) msg.obj).invalidate();                break;
        可以看到他执行的就是View里面的invalidate方法了;

        至此,我们知道了postInvalidate实际上就是在非UI线程下的invalidate而已啦;

        最后就是讲下requestLayout了,有时候我们在进行View绘制的时候也会通过调用view.requestLayout来实现,那么requestLayout和invalidate方式有什么区别呢?requestLayout是怎么实现的呢?鉴于他的源码比较短,在这里我从源码角度讲解下:

        先来看看View的requestLayout方法:

public void requestLayout() {        if (mMeasureCache != null) mMeasureCache.clear();        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {            // Only trigger request-during-layout logic if this is the view requesting it,            // not the views in its parent hierarchy            ViewRootImpl viewRoot = getViewRootImpl();            if (viewRoot != null && viewRoot.isInLayout()) {                if (!viewRoot.requestLayoutDuringLayout(this)) {                    return;                }            }            mAttachInfo.mViewRequestingLayout = this;        }        mPrivateFlags |= PFLAG_FORCE_LAYOUT;        mPrivateFlags |= PFLAG_INVALIDATED;        if (mParent != null && !mParent.isLayoutRequested()) {            mParent.requestLayout();        }        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {            mAttachInfo.mViewRequestingLayout = null;        }    }

       代码16/17行会进行一些标志的设置,这个和invalidate的过程是一样的,在invalidate里面,我们同样也会进行一些标志的设定,接着有父布局的话执行第20行父布局的requestLayout方法,如果父布局还有父布局的话,还会继续调用父布局的requestLayout方法,也就是requestLayout层层向上传递,这点和invalidate的过程是类似的,知道DecorView,即根View,而跟View又会传给他的父布局ViewRootImpl也就是最终会执行ViewRootImpl的requestLayout方法,我们快来看看:

@Override    public void requestLayout() {        if (!mHandlingLayoutInLayoutRequest) {            checkThread();            mLayoutRequested = true;            scheduleTraversals();        }    }
        在这个方法里我们看到了scheduleTraversals,这个方法在invalidate里面出现过,接下来的过程就和上面的过程一样了;

        至此,invalidate、postInvalidate以及requestLayout分析结束了,带着为什么既要有invalidate还要有requestLayout的疑问,我们来进行总结下:

        (1):postInvalidate是在非UI线程中通过handler调用invalidate的方式实现视图重绘的;

        (2):invalidate是在UI线程中执行的,如果在非UI线程中执行会抛异常;

        (3):requestLayout的执行流程和invalidate很类似,都是从当前需要重绘的view开始层层向上传递,在这个过程中设置一些标志用来在随后的重绘过程中根据这些标志查看是否需要重绘;

        (4):最后就是为什么有invalidate还要requestLayout了,原因就是加入我们只需要重绘View,也就是View自身的LayoutParams属性值并没有发生变化的话,这时候我们只需要invalidate方法来刷新View即可,他并不会执行View绘制中的measure和layout这两个过程;但是当我们View的LayoutParams属性发生变化的时候,invalidate显然不能达到要求,因为我们需要measure和layout过程,那么这时候就可以调用requestLayout来实现了,这是两者的最大区别;




       



                                                          

1 0
原创粉丝点击