View的测量和重绘

| 分类 android  sdk源码  | 标签 Android  Measrue  Invalidate  Draw 

首先,我们要了解android系统是怎样加载一个xml的layout文件,这个可以参考前面的章节:“加载xml布局文件原理”的内容。了解了这些内容之后,就可以继续研究了。

在inflate()方法和rInflate(parser, temp, attrs)方法中都会调用createViewFromTag(name, attrs)方法来创建新的View,我们可以看看这个创建View的方法的实现:

 1 View createViewFromTag(String name, AttributeSet attrs) {
 2         if (name.equals("view")) {
 3             name = attrs.getAttributeValue(null, "class");
 4         }
 5 
 6         if (DEBUG) System.out.println("******** Creating view: " + name);
 7 
 8         try {
 9             View view = (mFactory == null) ? null : mFactory.onCreateView(name,
10                     mContext, attrs);
11 
12             if (view == null) {
13                 if (-1 == name.indexOf('.')) {	
14                     view = onCreateView(name, attrs);
15                 } else {
16                     view = createView(name, null, attrs);
17                 }
18             }
19 
20             if (DEBUG) System.out.println("Created view is: " + view);
21             return view;
22 
23         } catch (InflateException e) {
24         //……
25         }
26     }

createViewFromTag()方法里面调用了Factory的onCreateView()或者LayoutInflater的onCreateView()方法或者createView()方法。事实上,createView()方法才是关键,因为它是正真创建一个View的方法。且看它的实现:

 1 public final View createView(String name, String prefix, AttributeSet attrs)
 2             throws ClassNotFoundException, InflateException {
 3         Constructor constructor = sConstructorMap.get(name);
 4         Class clazz = null;
 5 
 6         try {
 7             if (constructor == null) {
 8                 // Class not found in the cache, see if it's real, and try to add it
 9                 clazz = mContext.getClassLoader().loadClass(
10                         prefix != null ? (prefix + name) : name);
11                 
12                 if (mFilter != null && clazz != null) {
13                     boolean allowed = mFilter.onLoadClass(clazz);
14                     if (!allowed) {
15                         failNotAllowed(name, prefix, attrs);
16                     }
17                 }
18                 constructor = clazz.getConstructor(mConstructorSignature);
19                 sConstructorMap.put(name, constructor);
20             } else {
21                 // If we have a filter, apply it to cached constructor
22                 if (mFilter != null) {
23                     // Have we seen this name before?
24                     Boolean allowedState = mFilterMap.get(name);
25                     if (allowedState == null) {
26                         // New class -- remember whether it is allowed
27                         clazz = mContext.getClassLoader().loadClass(
28                                 prefix != null ? (prefix + name) : name);
29                         
30                         boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
31                         mFilterMap.put(name, allowed);
32                         if (!allowed) {
33                             failNotAllowed(name, prefix, attrs);
34                         }
35                     } else if (allowedState.equals(Boolean.FALSE)) {
36                         failNotAllowed(name, prefix, attrs);
37                     }
38                 }
39             }
40 
41             Object[] args = mConstructorArgs;
42             args[1] = attrs;
43             return (View) constructor.newInstance(args);
44 
45         } catch (NoSuchMethodException e) {
46             //……
47         }
48     }

createView()方法会根据name参数,生成或者从系统内存中获取一个构造器:

Constructor constructor = sConstructorMap.get(name);

或者constructor = clazz.getConstructor(mConstructorSignature);

然后通过构造器生成一个View对象,并且返回来:

1 return (View) constructor.newInstance(args);

构造器调用newInstance(args)方法,并传入args参数,这时候就会调用对应的View类的构造函数。例如,当createView()方法的参数name是LinearLayout,则就会调用LinearLayout的构造函数。并且args[1] = attrs,传入了相关的属性集合。我们可以看看LinearLayout的带有attrs参数的构造函数:

 1 public LinearLayout(Context context, AttributeSet attrs) {
 2         super(context, attrs);
 3 
 4         TypedArray a = 
 5             context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout);
 6 
 7         int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
 8         if (index >= 0) {
 9             setOrientation(index);
10         }
11 
12         index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
13         if (index >= 0) {
14             setGravity(index);
15         }
16 
17         boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
18         if (!baselineAligned) {
19             setBaselineAligned(baselineAligned);
20         }
21         //……
22         a.recycle();
23     }

LinearLayout的构造函数首先会调用父类的构造函数super(context, attrs),传入了相同的参数,然后再对LinearLayout特有的属性进行设置,如:

设置子控件排版方向的setOrientation(index);

设置子控件的对齐位置的setGravity(index);

设置基线对齐方式的setBaselineAligned(baselineAligned)。

我们要寻找的是设置一个View的高度和宽度,而LinearLayout是继承于View的,故高度和宽度的属性应该是在View的构造函数当中。那么不如一步步的跟踪LinearLayout的父类的构造函数,以及父类的的父类的构造函数……最终找到了View的构造函数:

 1 public View(Context context, AttributeSet attrs, int defStyle) {
 2         this(context);
 3 
 4         TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,
 5                 defStyle, 0);
 6 
 7         Drawable background = null;
 8 
 9         int leftPadding = -1;
10         int topPadding = -1;
11         int rightPadding = -1;
12         int bottomPadding = -1;
13 
14         int padding = -1;
15 
16         int viewFlagValues = 0;
17         int viewFlagMasks = 0;
18 
19         boolean setScrollContainer = false;
20 
21         int x = 0;
22         int y = 0;
23 
24         int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY;
25 
26         final int N = a.getIndexCount();
27         for (int i = 0; i < N; i++) {
28             int attr = a.getIndex(i);
29             switch (attr) {
30                 case com.android.internal.R.styleable.View_background:
31                     background = a.getDrawable(attr);
32                     break;
33                 case com.android.internal.R.styleable.View_padding:
34                     padding = a.getDimensionPixelSize(attr, -1);
35                     break;
36                  case com.android.internal.R.styleable.View_paddingLeft:
37                     leftPadding = a.getDimensionPixelSize(attr, -1);
38                     break;
39         //……

在View的构造函数中,首先通过context.obtainStyledAttributes()方法返回一个TypedArray的对象数组,这个数组记录了相应的属性的值。然后使用for循环遍历这个数组,把这个数组里面的值取出来,并使用switch-case语句进行分派处理。例如处理background、padding等属性。

事实上在这个switch-case语句的分派处理中,并没有对View的高度和宽度的属性进行处理。这是为什么呢?找了半天了啊!还是没有找到这个东东,这是一个严峻的问题呢!

那我们好好回想一下怎样从xml文件中加载一个View呢?也就是那个rInflate(parser, temp, attrs)方法。

rInflate()方法的过程如下:

  • parser.getName();
  • createViewFromTag(name, attrs);
  • generateLayoutParams(attrs);
  • rInflate(parser, view, attrs);
  • addView(view, params)。

可以看到,调用了createViewFromTag(name, attrs)方法创建了一个View之后,还生成相应的LayoutParams参数,而在addView(view, params)的时候就把参数传进去。

那么我们好好看看LayoutParams这个类是怎样的。这个类其实就是ViewGroup类里面的LayoutParams类。

 1 public static class LayoutParams {
 2         @Deprecated
 3         public static final int FILL_PARENT = -1;
 4 
 5         public static final int MATCH_PARENT = -1;
 6 
 7         public static final int WRAP_CONTENT = -2;
 8 
 9         //……
10         public int width;
11         //……
12         public int height;
13 
14         /**
15          * Used to animate layouts.
16          */
17         public LayoutAnimationController.AnimationParameters layoutAnimationParameters;
18 
19         //……
20 
21         public LayoutParams(Context c, AttributeSet attrs) {
22             TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
23             setBaseAttributes(a,
24                     R.styleable.ViewGroup_Layout_layout_width,
25                     R.styleable.ViewGroup_Layout_layout_height);
26             a.recycle();
27         }
28         //……
29         public LayoutParams(int width, int height) {
30             this.width = width;
31             this.height = height;
32         }
33 
34         public LayoutParams(LayoutParams source) {
35             this.width = source.width;
36             this.height = source.height;
37         }
38 
39         //……
40         protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
41             width = a.getLayoutDimension(widthAttr, "layout_width");
42             height = a.getLayoutDimension(heightAttr, "layout_height");
43         }
44 
45         //……
46     }

嗯嗯!似乎可以看到了想看的的了,在LayoutParams类中就是定义了width和height两个属性,这两个属性就是用来标志View的宽度和高度的。无论是LinearLayout、RelativeLayout、ImageView、EditText都是由这两个属性来标志它们的宽度和高度的。

在rInflate()方法中调用createViewFromTag()方法创建了一个View之后,就调用viewGroup类的generateLayoutParams(attrs)方法来生成一个LayoutParams参数,这个方法的实现是这样的:

1 public LayoutParams generateLayoutParams(AttributeSet attrs) {
2         return new LayoutParams(getContext(), attrs);
3     }

generateLayoutParams()方法调用LayoutParams构造函数生成一个LayoutParams对象;而LayoutParams构造函数构造函数会调用setBaseAttributes()方法来设置LayoutParams的width和height属性。

在每一个View类中有一个私有的成员变量:

1 /**
2      * The layout parameters associated with this view and used by the parent
3      * {@link android.view.ViewGroup} to determine how this view should be
4      * laid out.
5      * {@hide}
6      */
7     protected ViewGroup.LayoutParams mLayoutParams;

故View的派生类,如LinearLayout、ImageView等都存在这样的变量。在rInflate()方法中,通过addView(view, params)方法来设置这个View的mLayoutParams成员的值。而addView()方法的设置mLayoutParams参数主要调用过程是:

  • addView(view, params);
  • addView(child, -1, params);
  • addViewInner(child, index, params, false);
  • child.setLayoutParams(params);

最后会通过View类的setLayoutParams(params)方法来设置这个mLayoutParams成员的值。其中child是View类的对象。

上面创建一个View的过程,都只是初始化这个View的各个属性而已,还没有真正绘制这个View。那么在绘制这个View之前对这个mLayoutParams变量的width和height属性进行重新设置就可以达到了重新调整这个View的size的目的了!不是吗?

绘制这个View的方法是在draw()方法中,而draw()方法会调用onDraw()方法。那么我们可以写一个类继承于View,并且重写draw()方法或者onDraw()方法,在重写的方法中要调用父类的对应方法,并在父类方法的前或后加入一些打印信息,例如:

1 @Override
2     public void draw(Canvas canvas) {
3         Log.i(TAG, "->draw()");
4         super.draw(canvas);
5     }

然后,再重写一些有可能是draw()方法之前调用的一些方法,例如:

 1 @Override
 2     public void setLayoutParams(android.view.ViewGroup.LayoutParams params) {
 3         Log.i(TAG, "->setLayoutParams()");
 4         super.setLayoutParams(params);
 5     }
 6 
 7     @Override
 8     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 9         Log.i(TAG, "->onMeasure()");
10         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
11     }
12 
13     @Override
14     protected void onLayout(boolean changed, int l, int t, int r, int b) {
15         Log.i(TAG, "->onLayout()");
16         super.onLayout(changed, l, t, r, b);
17     }

因为View中的measure()方法和layout()方法是final方法,故不能重载,只能重载被measure()方法和layout()方法调用的onMeasure()方法和onLayout()方法。

然后把这个新定义的类运用在xml文件中。然后运行程序,就可以看到下面的输出信息: img

那么,从上面的输出信息可以看出,setLayoutParams()方法先被调用,然后是onMeasrue()方法,接着是onLayout()方法,组后才到draw()方法。

我们再研究一下这个setLayoutParams()方法是在什么时候被调用。可以在重载的setLayoutParams()方法中加入如下的出错语句:

1 @Override
2 	public void setLayoutParams(android.view.ViewGroup.LayoutParams params) {
3 		((String) null).toString();
4 		Log.i(TAG, "->setLayoutParams()");
5 		super.setLayoutParams(params);
6 	}

运行程序的时候,就会得到如下的出错信息: img

从上面的输出信息可以看出来,setLayoutParams(),是在加载xml文件的时候在addView()的时候进行设置的。

然后再看看onMeasure()方法是什么时候被调用的。同理在onMeasure()方法中加入出错语句((String) null).toString(),运行程序得到如下信息: img

从上面的输出信息可以看出来,ScaleImageView的父View先调用View的measure()方法,然后再调用父View的onMeasure()方法,接着在onMeasure()方法中ScaleImageView就会调用View的measure()方法和ScaleImageView的onMeasure()方法。但是,这也并不能清楚地了解到这个onMeasure()方法是怎么无端端地被调用呢!哈!

那么就研究研究setLayoutParams()是怎样的一个过程呢?

在View中setLayoutParams()的实现是这样的:

1 public void setLayoutParams(ViewGroup.LayoutParams params) {
2         if (params == null) {
3             throw new NullPointerException("params == null");
4         }
5         mLayoutParams = params;
6         requestLayout();
7     }

其中requestLayout()方法是最关键的,调用了requestLayout(),就是要请求重新来绘画View的layout,且看这个方法的实现:

 1 public void requestLayout() {
 2         if (ViewDebug.TRACE_HIERARCHY) {
 3             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.REQUEST_LAYOUT);
 4         }
 5 
 6         mPrivateFlags |= FORCE_LAYOUT;
 7 
 8         if (mParent != null && !mParent.isLayoutRequested()) {
 9             mParent.requestLayout();
10         }
11     }

这个方法把这个View中的mPrivateFlags变量添加了FORCE_LAYOUT的特性,然后再调用parent的requestLayout()方法。想到这里,大家是不是很奇怪,一个View的parent不也是一个View吗?如果也是View,那么还不是会调用这个requestLayout()方法?这是一种可能,但是如果是这样的话,那么只是给每个View的mPrivateFlags变量设置一些特性而已,并没有其他实质性的引发重绘layout的操作啊!所以这不科学啊!那么View派生的类呢?例如最外层的layout,如LinearLayout等,重写了requestLayout()方法,是不是这样呢?

事不宜迟,那就看看吧!看过才知道,原来LinearLayout并没有重写requestLayout()方法呢!对PhoneWindow有了解的都知道(如果不了解,可以查看前面的内容),LinearLayout是最外层的layout,FrameLayout次之,然后才是我们通过xml文件定义的View。现在连最外层的LinearLayout都没有重写这个requestLayout()方法,那么哪里去做这个真正的重绘layout的请求呢?

难题还真是多啊……不过不管怎样,难题肯定可以解决的!那么接下来就好好理解android系统的事件分派机制了。

可以在任意地方使程序报错,然后输出打印信息,例如在一个构造函数中报错:

1 public ScaleImageView(Context context, AttributeSet attrs) {
2         super(context, attrs);
3         ((String) null).toString();
4         Log.i(TAG, "->ScaleImageView()-after super()");
5     }

之后输出的打印信息为: img

在错误信息中,最后一行的阴影信息就是运行的虚拟机dalvik输出的信息,然后这样顺着这些输出信息从下往上的顺序层层地调用。如果你想好好的了解这个过程是怎样的,你可以根据这些输出信息去寻找对应的class文件或者java文件,然后打开文件找到相应的行,然后一步步地跟踪这个调用的过程。

例如android.app.ActivityThread.main(ActivityThread.java:4627),这就需要找到ActivityThread这个类,或者找到这个类的源文件,源文件在源代码目录下去找,也就是在sources目录去找“android/app/”目录,然后再找ActivityThread.java这个文件。然后打开这个类,找到4627行,这一行就是出错的那一行。找到这一行之后,还可以继续跟踪下去,找到最终出错的那一行代码。

事实上,这里面比较重要的一个类就是ViewRoot了,这个类是干什么的呢?它的父类是Handler,实现的接口类是ViewParent,详细的了解可以看下面的连接:

事实上,当启动一个Activity时,会创建一个PhoneWindow类的Window对象,这个对象是Activity和整个View系统交互的接口。另外Activity还有一个WindowManager对象,因为WindowManager是一个接口,需要一个实现类来生成这样的一个对象,那就是WindowManagerImpl类,这个类管理着所在应用进程的窗口。另外WindowManagerImpl,创建一个ViewRoot来管理该窗口的根View。并通过ViewRoot.setView方法把该View传给ViewRoot。ViewRoot用于管理窗口的根View,并和global window manger进行交互。

所以ViewRoot是在整个控件树的最顶端,是一个逻辑的树顶。ViewRoot实现了ViewParent的各个方法,包括了requestLayout()方法,那我们来看看这个方法的实现:

1 public void requestLayout() {
2         checkThread();
3         mLayoutRequested = true;
4         scheduleTraversals();
5     }

而requestLayout()方法调用了ViewRoot 的scheduleTraversals()方法,那么就来看看scheduleTraversals()方法的实现:

1 public void scheduleTraversals() {
2         if (!mTraversalScheduled) {
3             mTraversalScheduled = true;
4             sendEmptyMessage(DO_TRAVERSAL);
5         }
6     }

然后scheduleTraversals()方法会调用sendEmptyMessage(DO_TRAVERSAL)发送DO_TRAVERSAL消息给ViewRoot的消息队列那里去。再申明一下,ViewRoot的父类是Handler呢!

到这里,似乎很明了了。

View中setLayoutParams()会调用View的requestLayout()方法;

View的requestLayout()方法会调用parent的requestLayout()方法;

如果parent是一个View,那么会继续调用parent的requestLayout()方法;

直到获取到最顶端的parent,这时候,这个parent不是一个View,而是一个ViewRoot,那么调用parent的requestLayout()方法,就是调用ViewRoot的requestLayout()方法;

继而调用ViewRoot的scheduleTraversals()方法;

继而ViewRoot的sendEmptyMessage(DO_TRAVERSAL)方法,发送DO_TRAVERSAL消息给ViewRoot的消息队列。

那么接下来的就是系统的Looper来对这个消息队列进行处理了。Looper按顺序从消息队列里面取出消息,然后交由ViewRoot来进行消息的分派处理,也就是重载Handler的handleMessage()方法:

 1 @Override
 2     public void handleMessage(Message msg) {
 3         switch (msg.what) {
 4         case View.AttachInfo.INVALIDATE_MSG:
 5             ((View) msg.obj).invalidate();
 6             break;
 7         case View.AttachInfo.INVALIDATE_RECT_MSG:
 8             final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
 9             info.target.invalidate(info.left, info.top, info.right, info.bottom);
10             info.release();
11             break;
12         case DO_TRAVERSAL:
13             if (mProfile) {
14                 Debug.startMethodTracing("ViewRoot");
15             }
16 
17             performTraversals();
18 
19             if (mProfile) {
20                 Debug.stopMethodTracing();
21                 mProfile = false;
22             }
23         //...

在handleMessage()方法中,使用switch-case语句来进行分派处理,其中就有一项是针对DO_TRAVERSAL消息进行处理的,处理这个消息时,调用了performTraversals()方法,那么再看看这个方法的实现:

1 private void performTraversals() {
2         // cache mView since it is used so much below...
3         final View host = mView;
4     //……
5     host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
6     //……

在performTraversals()方法中,获取mView的引用,mView就是在ViewRoot.setView()方法把Activity的根View传递进来的那个View。这个根View调用了measure()方法来自己进行size的测量。因为根View是一个GroupView的派生类的对象,所以measure()方法又会调用GroupView的onMeasure()方法,然后这个onMeasure()方法又会调用各个子View的measure()方法,如此类推,直到调用最末端的View的onMeasure()方法。

这就是onMeasure()方法的从头到尾的一个调用过程。明白了吗,哈?

同理可以研究这个onLayout()方法。类似地,在performTraversals()方法中,同样调用了根View的layout()方法,这个方法,也是这样调用onLayout()方法,然后调用各个子View的layout()方法,直到最末端的View。

而draw()函数竟然也是在performTraversals()方法里面被调用。这跟我一开始的想法是相符的,并且这几个方法被调用的顺序是:

  • measure();
  • layout();
  • draw();

事实上,在View的layout()方法中会调用setFrame()方法,这个方法又会调用invalidate()方法,这个方法就是请求进行重绘的一个方法。而invalidate()方法最后又会循环往parent那里去进行重绘请求,调用的方法是nvalidateChild(),最终到达ViewRoot。在ViewRoot中调用的重绘请求的方法是也是invalidateChild(),这个方法会调用ViewRoot的scheduleTraversals()方法,发送DO_TRAVERSAL消息。这样子最终就会调用到performTraversals()方法来对View进行重绘了。


上一篇     下一篇