Android View绘制与自定义View

Android视图构成

在这里插入图片描述

如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是DecorView,这个DecorView就是存放布局文件,里面有TitleActionBar和我们setContentView传入进去的layout布局文件

  • Window类时一个抽象类,提供绘制窗口的API
  • PhoneWindow是继承Window的一个具体的类,该类内部包含了一个DecorView对象,该DecorView对象时所有应用窗口(Activity界面)的根View
  • DecorView继承FrameLayout,里面 id = content 就是我们传入的布局视图

更具面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装的位置。

setContentView流程

setContentView整个过程主要是如何把Activity布局文件或者java的View添加窗口里,重点概述如下:

  1. 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图
  2. 依据Feature等style
    theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)
  3. 将Activity的布局文件添加至id为content的FrameLayout内
  4. 当setContentView设置显示OK以后会回调Activity的onContentChange方法。Activity的各种View的findViewById方法等都可以放到该方法中,系统会帮忙回调

Andorid 的View绘制

一、起点:performTraversals() 方法

视图绘制的起点在_ViewRootImpl_ 类的 _performTraversals()_方法,该方法完成的工作主要是:根据之前的状态,判读是否重新计算测试视图的大小(measure),是否重新放置视图位置(layout)和是否重新重绘视图(draw),部分源码如下:

 1 private void performTraversals() {
 2         ......
 3         int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 4         int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
 5          ......
 6          // Ask host how big it wants to be
 7          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 8          ......
 9          performLayout(lp, desiredWindowWidth, desiredWindowHeight);
10          ......
11          performDraw();
12         ......
13      }

第2,3行代码调用getRootMeasureSpec方法生成对应宽高,下面是getRootMeasureSpec的源码:

 1   /**
 2      * Figures out the measure spec for the root view in a window based on it's
 3      * layout params.
 4      *
 5      * @param windowSize
 6      *            The available width or height of the window
 7      *
 8      * @param rootDimension
 9      *            The layout params for one dimension (width or height) of the
10      *            window.
11      *
12      * @return The measure spec to use to measure the root view.
13      */
14     private static int getRootMeasureSpec(int windowSize, int rootDimension) {
15         int measureSpec;
16         switch (rootDimension) {
17 
18         case ViewGroup.LayoutParams.MATCH_PARENT:
19             // Window can't resize. Force root view to be windowSize.
20             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
21             break;
22         case ViewGroup.LayoutParams.WRAP_CONTENT:
23             // Window can resize. Set max size for root view.
24             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
25             break;
26         default:
27             // Window wants to be an exact size. Force root view to be that size.
28             measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
29             break;
30         }
31         return measureSpec;
32     }

MeasureSpec 概念: 也叫测量规格,MeasureSpec是一个32位整数,有SpecMode和SpecSize两部分组成,其中,高2位为SpecMode,第30位为SpecSize。SpecMode为测量模式,SpecSize为相应测量模式下的测量尺寸。

View(包括View 和 ViewGroup)的SpecMode有本View的LayoutParams结合父View的MeasureSpec生成(普通View的MeasureSpec是由其父类ViewGroup生成的)。

SpecMode的取值有以下三种:

  • MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定
  • MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值
  • MeasureSpec.UNSPECIFIED //未指定模式,父View对子View的大小不做限制,完全由子View自己决定

getRootMeasureSpec() 方法就是生成根视图的MeasureSpec,生成的MeasureSpec中SpecMode为MeasureSpec.EXACTLY,SpecSize则为窗口的可用尺寸

回到performTraversals() 方法中:
7,9,11分别执行了performMeasure(childWidthMeasureSpec, childHeightMeasureSpec), performLayout(lp,desiredWindowWidth, desiredWindowWidth, desiredWindowHeight) , performDraw();

我们来看下这三个方法源码(都经过简化处理)

1 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
2         Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
3         try {
4             mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
5         } finally {
6             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
7         }
8     }

performMeasure方法最核心的是第4行 调用了mView的measure方法。

1    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
2             int desiredWindowHeight) {
3   
4         ...
5         final View host = mView;
6 
7         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
8        ...
9     }

performLayout 方法通过5,7 行代码发现其实也是调用了mView的layout方法

1     private void performDraw() {
2            ...
3             draw(fullRedrawNeeded);
4             ...
5     }
1 private void draw(boolean fullRedrawNeeded) {
2 
3           ...
4           if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
5              return;
6           }
7           ...
8     }
1     private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
2             boolean scalingRequired, Rect dirty) {
3         ...
4          mView.draw(canvas);
5         ...
6         return true;
7     }

performDraw最终调用的也是mView的draw方法,mView就是DecorView,我们知道DecorView是FrameLayout,FrameLayout继承自ViewGroup,ViewGrop继承自View,所以最终都会调用 View类中measure,layout,draw方法。

所以view绘制主要包括三个方面:

  • measure 测量组件本身的大小
  • layout 确定组件在视图中的位置
  • draw 根据位置和大小,将组件画下来

这三个子阶段可以用下图来描述:
在这里插入图片描述

二、View绘制流程第一步measure分析(API 23)
普通View的测量
 1    /**
 2      * <p>
 3      * This is called to find out how big a view should be. The parent
 4      * supplies constraint information in the width and height parameters.
 5      * </p>
 6      *
 7      * <p>
 8      * The actual measurement work of a view is performed in
 9      * {@link #onMeasure(int, int)}, called by this method. Therefore, only
10      * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
11      * </p>
12      *
13      *
14      * @param widthMeasureSpec Horizontal space requirements as imposed by the
15      *        parent
16      * @param heightMeasureSpec Vertical space requirements as imposed by the
17      *        parent
18      *
19      * @see #onMeasure(int, int)
20      */
21     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
22         ...
23         // measure ourselves, this should set the measured dimension flag back
24         onMeasure(widthMeasureSpec, heightMeasureSpec);
25          ...
26     }

注释已经给出大体描述:调用这个方法是为了测量出view的大小,并且其父类提供了约束信息widthMeasureSpec 与 heightMeasureSpec。

我们发现measure方法被final 修饰,所以这个方法不能被子类重写。
实际的测量是在 24行 onMeasure方法进行,所以在View的普通子类中需要重写onMeasure方法来实现自己的测量逻辑。
对于普通View,调用View类的onMeasure() 方法来进行实际的测量工作即可,当然我们也可以重载onMeasure并调用setMeasuredDimension 来设置任意大小的布局。
接下来我们来看看默认情况下 onMeasure方法都做了什么,源码如下:

1     /**
 2      * <p>
 3      * Measure the view and its content to determine the measured width and the
 4      * measured height. This method is invoked by {@link #measure(int, int)} and
 5      * should be overridden by subclasses to provide accurate and efficient
 6      * measurement of their contents.
 7      * </p>
 8      *
 9      * <p>
10      * <strong>CONTRACT:</strong> When overriding this method, you
11      * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
12      * measured width and height of this view. Failure to do so will trigger an
13      * <code>IllegalStateException</code>, thrown by
14      * {@link #measure(int, int)}. Calling the superclass'
15      * {@link #onMeasure(int, int)} is a valid use.
16      * </p>
17      *
18      * <p>
19      * The base class implementation of measure defaults to the background size,
20      * unless a larger size is allowed by the MeasureSpec. Subclasses should
21      * override {@link #onMeasure(int, int)} to provide better measurements of
22      * their content.
23      * </p>
24      *
25      * <p>
26      * If this method is overridden, it is the subclass's responsibility to make
27      * sure the measured height and width are at least the view's minimum height
28      * and width ({@link #getSuggestedMinimumHeight()} and
29      * {@link #getSuggestedMinimumWidth()}).
30      * </p>
31      *
32      * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
33      *                         The requirements are encoded with
34      *                         {@link android.view.View.MeasureSpec}.
35      * @param heightMeasureSpec vertical space requirements as imposed by the parent.
36      *                         The requirements are encoded with
37      *                         {@link android.view.View.MeasureSpec}.
38      *
39      * @see #getMeasuredWidth()
40      * @see #getMeasuredHeight()
41      * @see #setMeasuredDimension(int, int)
42      * @see #getSuggestedMinimumHeight()
43      * @see #getSuggestedMinimumWidth()
44      * @see android.view.View.MeasureSpec#getMode(int)
45      * @see android.view.View.MeasureSpec#getSize(int)
46      */
47     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
48         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
49                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
50     }

注释:这个方法用来测量View以及自身内容来决定宽高,子类应该重写这个方法提供更精确更高效的测量内容。当重写这个方法的时候子类必须调用setMeasuredDimension(int,int)来存储已经测量出来的宽高。

我们看到系统默认的onMeasure方法只是直接调用了setMeasuredDImension,setMeasureDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth 和 mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWith 和 mMeasuredHeight 进行赋值,所以一旦这两个变量被赋值意味着该View的工作结束。

接下来我们看看设置的默认View宽高,默认宽高都是通过getDefaultSize方法来获取的,而getDefaultSize有调用了getSuggestedMinimumXXX方法,我们先看下getSuggestedMinimumXXX方法:

1 protected int getSuggestedMinimumHeight() {
2         return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
3 }
1 protected int getSuggestedMinimumWidth() {
2         return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
3 }

mMinHeight或mMinWidth 就是我们设置的android:minHeight 或 android:minWidth 参数。
如果我们没有设置背景则直接返回mMinHeight或mMinWidth,如果设置了背景,则取最大值。如果都没有设置,则返回0。

接下来看getDefault方法源码:

 1 public static int getDefaultSize(int size, int measureSpec) {
 2         int result = size;
 3         int specMode = MeasureSpec.getMode(measureSpec);
 4         int specSize = MeasureSpec.getSize(measureSpec);
 5 
 6         switch (specMode) {
 7         case MeasureSpec.UNSPECIFIED:
 8             result = size;
 9             break;
10         case MeasureSpec.AT_MOST:
11         case MeasureSpec.EXACTLY:
12             result = specSize;
13             break;
14         }
15         return result;
16 }

getDefault返回由 上面讲到的 getSuggestedMiniXXX方法获取到的size以及父类传递过来的measureSpec共同决定。可以看到如果specMode等于AT_MOST或EXACTLY 就返回specSize(父类传递过来的),这也是系统默认的规格。

到此为止,普通的View(非ViewGroup)的测量就基本讲解完了。但是ViewGroup这个容器类的布局是怎么测量其内部每个子View的呢?

ViewGroup测量所有子View

ViewGroup 容器类布局大部分情况下是用来嵌套具体子View的,所以需要负责其子View的测量,在ViewGroup中定义了:

  • measureChildren(int widthMeasureSpec, int heightMeasureSpec)
  • measureChild(View child, int parentWidthMeasureSpec, int
    parentHeightMeasureSpec)
  • measureChildWithMargins(View child, int parentWidthMeasureSpec, int
    widthUsed, int parentHeightMeasureSpec, int heightUsed)

三个方法来供其子类调用对具体子View进行测量。
measureChildren、measureChild 源码如下:

 1 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
 2         final int size = mChildrenCount;
 3         final View[] children = mChildren;
 4         for (int i = 0; i < size; ++i) {
 5             final View child = children[i];
 6             if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
 7                 measureChild(child, widthMeasureSpec, heightMeasureSpec);
 8             }
 9         }
10}
 1 protected void measureChild(View child, int parentWidthMeasureSpec,
 2             int parentHeightMeasureSpec) {
 3         final LayoutParams lp = child.getLayoutParams();
 4 
 5         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
 6                 mPaddingLeft + mPaddingRight, lp.width);
 7         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
 8                 mPaddingTop + mPaddingBottom, lp.height);
 9 
10         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
11}

可以看到,measureChildren 只是循环调用 measureChild方法,而measureChild方法中会根据父类提供的测量规格parentXXXMeasureSpec 以及子类child调用getLayoutParams() 方法生成子类自己具体的测量规格。(getChildMeasureSpec 稍后会具体分析)

接下来我们看下measureChildWithMargins方法源码:

1 protected void measureChildWithMargins(View child,
 2             int parentWidthMeasureSpec, int widthUsed,
 3             int parentHeightMeasureSpec, int heightUsed) {
 4         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
 5 
 6         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
 7                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
 8                         + widthUsed, lp.width);
 9         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
10                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
11                         + heightUsed, lp.height);
12 
13         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
14 }

与measureChild相比最重要的区别是 measureChildWithWargins 额外将具体子View的LayoutParams参数的margin也作为参数来生成测量规格。

measureChild 与 measureChildWithWargins 都调用了 getChildMeasureSpec方法来生成具体测量规格,我们来看看这个方法源码:

 1     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
 2         int specMode = MeasureSpec.getMode(spec);//获取父View的mode 3         int specSize = MeasureSpec.getSize(spec);//获取父View的size
 4      //父View的size减去padding与0比较取其大,specSize - padding得到的值是父View可以用来盛放子View的空间大小
 5         int size = Math.max(0, specSize - padding);
 6 
 7         int resultSize = 0;
 8         int resultMode = 0;
 9 
10         switch (specMode) {
11         // Parent has imposed an exact size on us
12         case MeasureSpec.EXACTLY://父View希望子View是明确大小
13             if (childDimension >= 0) {//子View设置了明确的大小:如 10dp,20dp
14                 resultSize = childDimension;//设置子View测量规格大小为其本身设置的大小
15                 resultMode = MeasureSpec.EXACTLY;//mode设置为EXACTLY
16             } else if (childDimension == LayoutParams.MATCH_PARENT) {//子VIEW的宽或者高设置为MATCH_PARENT,表明子View想和父View一样大小
17                 // Child wants to be our size. So be it.
18                 resultSize = size;//设置子View测量规格大小为父View可用空间的大小
19                 resultMode = MeasureSpec.EXACTLY;//mode设置为EXACTLY
20             } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子VIEW的宽或者高设置为WRAP_CONTENT,表明子View大小是动态的
21                 // Child wants to determine its own size. It can't be
22                 // bigger than us.
23                 resultSize = size;//设置子View测量规格大小为父View可用空间的大小
24                 resultMode = MeasureSpec.AT_MOST;//mode设置为AT_MOST,表明子View宽高最大值不能超过resultSize 
25             }
26             break;27      //其余情况请自行分析
28         ......
29         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
30     }

上面的方法展示根据父View的MeasureSpec 与 子View的LayoutPaarmas生成了View的MeasureSpec的过程,子View的LayoutParams表示子View的期待大小。这个产生的MeasureSpec用于指导子View自身的测量。

在我们自定义View的时候一般会重写onMeasure(int widthMeasureSpec,int heightMeasureSpec)方法,其中的widthMeasureSpec 和 heightMeasureSpec参数就是父类通过getChildMeasureSpec方法生成的。一个好的自定义View会根据父类传递过来的测量规格动态设置大小,而不是直接写死其大小。

总结

好了,到此位置View的测量过程就差不多讲完了。总结下关键部分:

  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑
  • 最顶层DecorView测量时的MeasureSpec是由 ViewRootImpl中getRootMeasureSpec方法确定的
  • ViewGroup类提供了measureChildren、measureChild和measureChildWithMargins方法,以供容器类布局测量自身子View使用
  • 使用View的getMeasuredWidth() 和 getMeasureHeight()
    方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值,只有onMeasure流程完成后mMeasuredWidth
    与 mMeasureHeight 才会被赋值
  • View的布局大小是由父View和子View共同决定的。我们平时设置的宽高可以理解为期望的大小,具体大小还要结合父类大小来确定。

最后附上View绘制流程图:
在这里插入图片描述

三、View绘制流程第二步layout过程分析(API 23)

performMeasure 执行完毕后,接着就会执行performLayout;

1    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
2             int desiredWindowHeight) {
3   
4         ...
5         final View host = mView;
6 
7         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
8        ...
9     } 

mView 是根View ,即DecorView,DecorView是FrameLayout的子类,最终会调用ViewGroup中的layout方法。
接下来我们看下ViewGroup中layout方法源码:

1 @Override
 2     public final void layout(int l, int t, int r, int b) {
 3         if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
 4             if (mTransition != null) {
 5                 mTransition.layoutChange(this);
 6             }
 7             super.layout(l, t, r, b);
 8         } else {
 9             // record the fact that we noop'd it; request layout when transition finishes
10             mLayoutCalledWhileSuppressed = true;
11         }
12     }

第7行表明 又调用了父类View的layout方法,所以我们看下View的layout源码,如下:

1     public void layout(int l, int t, int r, int b) {
2     // l为本View左边缘与父View左边缘的距离
           // t为本View上边缘与父View上边缘的距离
       // r为本View右边缘与父View左边缘的距离
      // b为本View下边缘与父View上边缘的距离

3     ...
4         boolean changed = isLayoutModeOptical(mParent) ?
5                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
6 
7         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
8             onLayout(changed, l, t, r, b);
9      ...
10     }

4、5行代码主要判断View的位置是否发生了变化,发生变化则changed 会为true,并且setOpticalFrame也是调用的setFrame方法
我们看下setFrame方法:

 1 protected boolean setFrame(int left, int top, int right, int bottom) {
 2         boolean changed = false;
 3 
 4 
 5      ...
 6         if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
 7             changed = true;
 8     
 9             ...
10             mLeft = left;
11             mTop = top;
12             mRight = right;
13             mBottom = bottom;
14      ...
15         }
16         return changed;
17     } 

第6行代码分别比较之前的记录的mLeft,mRight,mTop,mBottom 与新传入的参数如果有一个不同则进入判断,将changed变量置为true,并且将新传入的参数分别重新赋值给mLeft,mRight,mTop,mBottom,最后返回changed。

这里还有一点要说,getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别,先看一下源码;

 1    public final int getMeasuredWidth() {
 2         return mMeasuredWidth & MEASURED_SIZE_MASK;
 3     }
 4 
 5     public final int getMeasuredHeight() {
 6         return mMeasuredHeight & MEASURED_SIZE_MASK;
 7     }
 8 
 9     public final int getWidth() {
10         return mRight - mLeft;
11     }
12 
13     public final int getHeight() {
14         return mBottom - mTop;
15     }

在讨论View时候说过mMeasuredHeight 与 mMeasuredWidth 只有测量过程完成才会被赋值,所以只有测量过程完成调用getMeasuredWidth() 、getMeasuredHeight() 才会获取正确的值。
而 getWidth()、getHeight() 只有在Layout过程完成时mLeft、mRight、mTop、mBottom才会被赋值,调用才会获取正确返回值,所以二者一个在view时调用,一个在layout时调用。

继续看view中layout源码第7行,如果changed为true,也就是说view的位置发生了变化,或者标记为PFLAG_LAYOUT_REQUIRED 则进入判断执行onLayout方法。

我们继续看View中onLayout方法源码:

protected void onLayout(boolean changed, int left, int top, int right, int bottom){
}

发现是个空方法。
对比View的layout和ViewGroup的layout方法发现,View的layout方法是可以重写的,而ViewGroup的layout方法是不能重写的,那么容器类View是如何对子View进行摆放的呢?看看ViewGroup 中的onLayout方法源码:

1 /**
2      * {@inheritDoc}
3      */
4     @Override
5     protected abstract void onLayout(boolean changed,
6             int l, int t, int r, int b);

是个抽象方法,因为具体ViewGroup摆放规则不同,所以其具体子类需要重写这个方法来实现对其子View的摆放逻辑。
我们来看看具体子类,我们选取FrameLayout,其onLayout源码如下:

1     @Override
 2     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 3         layoutChildren(left, top, right, bottom, false /* no force left gravity */);
 4     }
 5 
 6     void layoutChildren(int left, int top, int right, int bottom,
 7                                   boolean forceLeftGravity) {
 8         final int count = getChildCount();
 9 
10         ......
11 
12         for (int i = 0; i < count; i++) {
13             final View child = getChildAt(i);
14             if (child.getVisibility() != GONE) {
15                .....
16 
17                 child.layout(childLeft, childTop, childLeft + width, childTop + height);
18             }
19         }
20     }

onLayout方法调用layoutChildren方法,在layoutChildren方法中遍历每个子View调用其layout方法。

好了,到此layout过程就讨论的差不多了,相比measure过程还是简单不少,其也是递归调用逻辑,如图:
在这里插入图片描述

总结
  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,具体ViewGroup子类必须重载来按照自己规则对子View进行摆放。
  • measure操作完成后得到的是对每个View经测量过的measuredWidth 和
    measuredHeight,layout操作完成后得到的是对每个View进行位置分配后的mLeft,mRight,mTop,mBottom,这些值都是相对于父View来说的。
  • 使用View的getWidth() 和 getHeight()
    方法来获取View测量的宽高,必须保持这两个方法在onLayout流程之后被调用才能返回有效值。
四、View绘制流程第三步draw过程分析(API 23)

performMeasure、performLayout 过程执行完,接下来就执行performDraw() 逻辑了,ViewGroup没有重新View的draw方法,最终调用的是View中的draw方法,源码如下:

1    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
 2    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
     (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
 3 
 4         /*
 5          * Draw traversal performs several drawing steps which must be executed
 6          * in the appropriate order:
 7          *
 8          *      1. Draw the background
 9          *      2. If necessary, save the canvas' layers to prepare for fading
10          *      3. Draw view's content
11          *      4. Draw children
12          *      5. If necessary, draw the fading edges and restore layers
13          *      6. Draw decorations (scrollbars for instance)
14          */
15 
16         // Step 1, draw the background, if needed
17         int saveCount;
18 
19         if (!dirtyOpaque) {
20             drawBackground(canvas);
21         }
22         
23         // skip step 2 & 5 if possible (common case)
24         final int viewFlags = mViewFlags;
25         boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
26         boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
27         if (!verticalEdges && !horizontalEdges) {
28             // Step 3, draw the content
29             if (!dirtyOpaque) onDraw(canvas);
30 
31             // Step 4, draw the children
32             dispatchDraw(canvas);
33 
34             // Overlay is part of the content and draws beneath Foreground
35             if (mOverlay != null && !mOverlay.isEmpty()) {
36                 mOverlay.getOverlayView().dispatchDraw(canvas);
37             }
38 
39             // Step 6, draw decorations (foreground, scrollbars)
40             onDrawForeground(canvas);
41 
42             // we're done...
43             return;
44         }
45         ...
46         // Step 2, save the canvas' layers
47         ....
48         // Step 3, draw the content
49         if (!dirtyOpaque) onDraw(canvas);
50 
51         // Step 4, draw the children
52         dispatchDraw(canvas);
53 
54         // Step 5, draw the fade effect and restore layers
55         ....
56         // Step 6, draw decorations (foreground, scrollbars)
57         onDrawForeground(canvas);
58     }

5到14行注释可以看到draw过程分为6步:
1、画背景
2、必要时,保存画布的层以准备渐变。
3、绘制视图内容
4、画孩子
5、如果需要,绘制渐变边缘并恢复图层。
6、绘制装饰(例如滚动条)

再由23 行提示大部分情况下跳过第2步,第5步,所以我们着重分析其余四步。

第一步 画背景(Draw the background)

19-21行执行第一步,绘制背景源码如下:

1 private void drawBackground(Canvas canvas) {
 2         final Drawable background = mBackground;
 3         if (background == null) {
 4             return;
 5         }
 6      ....
 7         setBackgroundBounds();
 8         ....
 9         background.draw(canvas);
10        
11     }
12 
13 
14     void setBackgroundBounds() {
15         if (mBackgroundSizeChanged && mBackground != null) {
16             mBackground.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
17             mBackgroundSizeChanged = false;
18             rebuildOutline();
19         }
20     }

主要逻辑就是获取我们在xml文件或者代码中设置的背景,然后根据layout过程摆放的位置绘制出来

第三步 绘制视图内容(Draw view’s content)

第29行执行绘制内容逻辑,源码如下:

1     /**
2      * Implement this to do your drawing.
3      *
4      * @param canvas the canvas on which the background will be drawn
5      */
6     protected void onDraw(Canvas canvas) {
7     }

一个空方法,需要具体子类自己去实现,因为每个具体View要绘制的内容是不同的,所以子类需要实现这个方法来绘制自身的内容。

第四步 绘制子View(Draw children)

第32行 执行绘制子View逻辑,源码如下:

1 /**
2      * Called by draw to draw the child views. This may be overridden
3      * by derived classes to gain control just before its children are drawn
4      * (but after its own view has been drawn).
5      * @param canvas the canvas on which to draw the view
6      */
7     protected void dispatchDraw(Canvas canvas) {
8 
9     }

也是个空方法,这个方法被用来绘制子View的,如果有子View则需要调用这个方法去绘制,我们一般知道只有容器类View才可以盛放子View,所以我们看下VIewGroup中有没有实现这个方法,源码如下:

1  @Override
 2     protected void dispatchDraw(Canvas canvas) {
 3         boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
 4         final int childrenCount = mChildrenCount;
 5         final View[] children = mChildren;
 6         .......
 7         for (int i = 0; i < childrenCount; i++) {
 8             while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
 9                 final View transientChild = mTransientViews.get(transientIndex);
10                 if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
11                         transientChild.getAnimation() != null) {
12                     more |= drawChild(canvas, transientChild, drawingTime);
13                 }
14                 .......
15             }
16           ......
17         }
18         ......   
19     }

在dispatchDraw方法中遍历每个子View并且调用drawChild方法,接下来我们看下drawChild源码:

1 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
2         return child.draw(canvas, this, drawingTime);
3 }

可以看到,最终是调用每个子View的draw方法来完成自身的绘制。

第六步 绘制装饰(例如滚动条)(Draw decorations (scrollbars for instance))

40行 执行绘制装饰逻辑,这一部分只是绘制一些装饰物,比如 ScrollBar,这部分就不分析了,也不是重点.

到这里,View的主要绘制流程我们就分析完了,也不复杂。

但是,我们在执行第一步和第三步时,都有个if判断(if(!dirtyOpaque)),也就是说只有判断成立才会执行绘制背景和自身内容。ViewGroup子类默认情况下是不执行onDraw方法的,在ViewGroup源码中的initViewGroup() 方法中设置了一个标记,源码如下:

1 private void initViewGroup() {
2         // ViewGroup doesn't draw by default
3         if (!debugDraw()) {
4             setFlags(WILL_NOT_DRAW, DRAW_MASK);
5         }
6         ......
7 }    

看第二行注释知道,ViewGroup默认情况下是不会draw的。第四行调用setFlags方法设置标记为 WILL_NOT_DRAW.
我们回到View中draw方法看第2行代码:

1  final int privateFlags = mPrivateFlags;
2  final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
3      (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

setFlags方法就是对View中的mPrivateFlags值进行相应改变,我们设置标记WILL_NOT_DRAW,那么dirtyOpaque得到的值就是true,从而if(!dirtyOpaque)不成立,也就不会执行onDraw方法了。

为什么容器类(ViewGroup子类)默认情况下不绘制背景和自身内容呢?答案是为了性能。

总结
  • 容器类布局需要递归绘制其所有包的所有子View
  • View中 onDraw默认是空方法,需要子类自己实现来完成自身内容的绘制
  • 容器类布局默认是不会调用onDraw方法的,我们可以为其设置背景或者调用setWillNotDraw(flase) 方法来使其主动调用onDraw方法

流程图:
在这里插入图片描述

onAttachedToWindow() 和 onDetachedFromWindow()

onAttachedToWindow() 是第一次onDraw前调用。也就是我们写的View还没有绘制出来时调用,但只会调用一次。
onDetachedFromWinodow() 销毁资源(即销毁View)之后调用
View状态的保持:onSaveInstanceState() 方法

手动转载:https://www.cnblogs.com/leipDao/p/7573803.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数字乡村和智慧农业的数字化转型是当前农业发展的新趋势,旨在通过应用数字技术,实现农业全流程的再造和全生命周期的管理服务。中国政府高度重视这一领域的发展,提出“数字中国”和“乡村振兴”战略,以提升国家治理能力,推动城乡融合发展。 数字乡村的建设面临乡村治理、基础设施、产业链条和公共服务等方面的问题,需要分阶段实施《数字乡村发展战略纲要》来解决。农业数字化转型的需求包括满足市民对优质农产品的需求、解决产销对接问题、形成优质优价机制、提高农业劳动力素质、打破信息孤岛、提高农业政策服务的精准度和有效性,以及解决农业融资难的问题。 数字乡村建设的关键在于构建“1+3+4+1”工程,即以新技术、新要素、新商业、新农民、新文化、新农村为核心,推进数据融合,强化农业大数据的汇集功能。数字农业大数据解决方案以农业数字底图和数据资源为基础,通过可视化监管,实现区域农业的全面数字化管理。 数字农业大数据架构基于大数据、区块链、GIS和物联网技术,构建农业大数据中心、农业物联网平台和农村综合服务指挥决策平台三大基础平台。农业大数据中心汇聚各类涉农信息资源和业务数据,支持大数据应用。信息采集系统覆盖市、县、乡、村多级,形成高效的农业大数据信息采集体系。 农业物联网平台包括环境监测系统、视频监控系统、预警预报系统和智能控制系统,通过收集和监测数据,实现对农业环境和生产过程的智能化管理。综合服务指挥决策平台利用数据分析和GIS技术,为农业决策提供支持。 数字乡村建设包括三大服务平台:治理服务平台、民生服务平台和产业服务平台。治理服务平台通过大数据和AI技术,实现乡村治理的数字化;民生服务平台利用互联网技术,提供各类民生服务;产业服务平台融合政企关系,支持农业产业发展。 数字乡村的应用场景广泛,包括农业生产过程、农产品流通、农业管理和农村社会服务。农业生产管理系统利用AIoT技术,实现农业生产的标准化和智能化。农产品智慧流通管理系统和溯源管理系统提高流通效率和产品追溯能力。智慧农业管理通过互联网+农业,提升农业管理的科学性和效率。农村社会服务则通过数字化手段,提高农村地区的公共服务水平。 总体而言,数字乡村和智慧农业的建设,不仅能够提升农业生产效率和管理水平,还能够促进农村地区的社会经济发展,实现城乡融合发展,是推动中国农业现代化的重要途径。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值