Android 开发艺术探索笔记 第四章 View的工作原理

一、初识ViewRoot和DecorView

ViewRoot类对应ViewRootImpl类,它是连接WindowManage和DecorView的纽带。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecoView建立关联。
View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure、layout、draw三个过程将一个View绘制出来。
Measure过程决定了View的宽和高,Measure完成后,一般情况下可以通过getMeasureWidth和getMeasureHeight获取View测量后的高,特殊情况除外;Layout过程决定了View四个顶点的坐标和实际的View的宽高;Draw过程则决定了View的显示。
DecorView作为顶级View,当我们setContentView时,布局添加到了id为content的FrameLayout中,以下方式可以得到content和我们设置的view:

ViewGroup content  = (ViewGroup)findViewById(android.R.id.content);
View view = content.getChildAt(0);

二、理解MeasureSpec

  1. MeasureSpec
    MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表specSize,前者指测量模式,后者指某种测量模式下的规格大小。

    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
         if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }   
         public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
         public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

    MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,并提供了打包解包方法。一组SpecMode和SpecSize可以打包为一个MeasureSpec,而一个MeasureSpec可以通过解包的形式来得出其原始的SpecMode和SpecSize。

    SpecMode有三类:

    1. UNSPECIFIED
      父容器不对View有任何限制,要多大给多大,一般用于系统内部,表示一种测量的状态。
    2. EXACTLY
      父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体的数值两种模式。
    3. AT_MOST
      指定一个可用大小即SpecSize,View的大小不能大于这个值,对应LayoutParams中的wrap_content。
  2. MeasureSpec和LayoutParams的对应关系
    对于DecorView,其MeasureSpec由其窗口的尺寸和其自身的LayoutParams来共同确定;对于普通view,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽/高。
    DecorView的MeasureSpec产生过程根据LayoutParams中的宽和高来划分:

    • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
    • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
    • 固定大小:精确模式,大小为LayoutParams指定的大小;

    普通View来说,针对不同的父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec。

    1. 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循LayoutParams的大小。
    2. 当View的宽高是match_parent时,如果父容器的模式是精确模式,那么view也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
    3. 当view的宽高是wrap_content时,不管父容器的模式是精确还是最大化吗,View的模式总是最大化并且大小不能超过父容器的剩余空间。

三、View的工作流程

  1. measure过程

    View的measure过程

    View的measure方法是一个final类型的方法,意味着子类不能重写这个方法。View的measure方法中总会去调用View的onMeasure方法。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    我们只需要看这个getDefaultSize方法:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
    
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

    从上面源码可以看出,一般我们只需要分析AT_MOST和EXACTLY两种情况,getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,View的最终大小是在layout阶段确定的,几乎所有情况下View的测量大小和最终大小是相等的。

    UNSPECIFIED这种情况,一般用于系统内部的测量过程,View的大小就是getDefaultSize第一个参数size,即宽高分别为getSuggestedMinimumWidth()和
    getSuggestedMinimumHeight()这两个方法的返回值,源码如下:

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

    从上面方法可以看出,getSuggestedMinimumWidth和getSuggestedMinimumHeight方法实现原理一样。从getSuggestedMinimumWidth方法里面可以看出,如果没有设置背景那么view的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此view的宽度即为android:minWidth属性所指定的值,如果没有指定则默认为0;如果指定了背景,那么View的宽度就是max(mMinWidth, mBackground.getMinimumWidth()),我们看一下mBackground.getMinimumWidth(),Drawable的getMinimumWidth方法,如下所示:

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

    这个方法返回的就是Drawable的原始宽度,前提是有原始宽度,否则返回0。
    getSuggestedMinimumWidth这个方法逻辑如下:如果view没有设置了背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景最小宽度中的最大值。getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽高。

    从getDefaultSize方法的实现来看,View的宽高由specSize决定,所以我们一般自定义控件的时候需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content相当于match_parent。

    ViewGroup的measure过程

    ViewGroup除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素在递归去执行这个过程。ViewGroup是一个抽象类,提供了一个叫measureChildren的方法,如下:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    上面方法会对每一个子元素进行measure,measureChild这个方法如下:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
    
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    上面主要是取出子元素的LayoutParams,然后在通过getChildMeasureSpec来创建子元素的MeasureSpec,将MeasureSpec直接传递给View的measure方法来进行测量。在ViewGroup没有定义测量的具体过程,不同的ViewGroup子类有不同的布局特性,需要各个子类去具体实现,下面分析LinearLayout的onMeasure的具体实现。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

    上述代码主要针对不同方向实现不同的测量,看一下竖直方向上的布局:

    // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            ...
    // Determine how big this child would like to be. If this or
    // previous children have given a weight, then we allow it to
    // use all available space (and we will shrink things later
    // if needed).
    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
    final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));

    系统会遍历子元素对每个子元素执行measureChildBeforeLayout这个方法,这个方法内部还是会调用子元素的measure方法,这样各个子元素就依次开始进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向上的初步高度。每测量一个元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。子元素测量完毕,LinearLayout会测量自己的大小,如下:

    // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
    
        int heightSize = mTotalLength;
    
        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

    当子元素测量完毕后,LinearLayout会根据子元素的情况来测量自己的大小。如果在布局中高度采用的match_parent或者具体的数值,那么则和View的测量过程一致,即高度为specSize;如果高度采用的是wrap_content,高度就是所有子元素所占用的高度总和,但是不能超过它的父容器的剩余空间,最终高度还要考虑其在竖直方向的padding,如下源码所示:

    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

    View的measure完成以后,通过getMeasureWidth/Height方法就可以正确的获取到view的测量宽高。但是在某些极端情况下,系统可能需要多次measure才能获取到最终的测量宽高,最好在onLayout方法中获取View的测量宽高或者最终宽高。

    在Activity里面获取某一个View的宽高:
    1. Activity/View#onWindowFocusChanged
      View初始化完毕,可以获取宽高,不过会被调用多次,在Activity的窗口得到焦点和失去焦点的时候均会被调用一次,当Activity继续执行和暂停执行的时候,onWindowFocusChanged均会被调用。

      public void onWindowFocusChanged(boolean hasFocus) {
          super.onWindowFocusChanged(hasFocus);
          if(hasFocus){
              int width = view.getMeasuredWidth();
              int height = view.getMeasuredHeight();
          }
      }
    2. view.post(runnable)
      通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnabe的时候,View已经初始化好了。

      protected void onStart(){
          super.onStart();
          view.post(new Runnable() {
              @Override
              public void run() {
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          });
      }
    3. ViewTreeObserver
      使用ViewTreeObserver的众多回调也可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,随着View树的状态发生改变,onGloballayout会被调用多次,如下:

      ViewTreeObserver observer = view.getViewTreeObserver();
          observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
              @Override
              public void onGlobalLayout() {
                  view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          });

      view.measure(int widthMeasureSpec, int heightMeasureSpec)
      通过手动对view进行measure来得到具体View的宽高,需要根据LayoutParams来分:

      • match_parent
        无法测出具体宽高,无法知道父容器的剩余空间。
      • 具体数值(dp/px)
        比如宽和高都是100px,如下measure

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
            int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
            view.measure(widthMeasureSpec,heightMeasureSpec);
      • wrap_content
        如下measure

         int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
            int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
            view.measure(widthMeasureSpec,heightMeasureSpec);

        注意(1 << 30)-1,View的尺寸使用30位二进制表示,最大是30个1(2^30-1),在最大化模式下,用View理论上能支持的最大值去构造MeasureSpec是合理的。

  2. layout过程
    Layout的作用是ViewGroup用来确定子元素的位置,当viewGroup的位置被确定后,在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout又会被调用。layout确定view本身位置,onLayout方法确定所有子元素的位置,源码如下:

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
    
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
    
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.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 &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

    首先通过setFrame方法来设定View的四个顶点的位置,初始化mLeft、mRight、mTop、mBottom四个值,四个顶点确定,View在父容器中的位置也就确定了;接着调用onLayout方法,父容器用来确定子元素位置,onLayout的实现和布局有关,我们看一下LinearLayout的onLayout源码:

    void layoutVertical(int left, int top, int right, int bottom) {
    ...
    final int count = getVirtualChildCount();
    for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
    
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();   
         ...
         if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
    
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                i += getChildrenSkipCount(child, i);

    我们这里还是分析在竖直方向上的布局,这里会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,非常符合LinearLayout在竖直方向上的特性。setChildFrame调用子元素的layout方法而已,这样当父元素在layout方法中完成自己的定位后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,一层一层地传递下去就完成了整个view树的layout过程。setChildFrame源码如下:

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

    方法中的width和height实际上就是子元素的测量宽高。
    而在layout方法中会通过setFrame去设置子元素的四个顶点的位置,有如下赋值语句:

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
    View的测量宽高和最终宽高有什么不同?
    public final int getWidth() {
        return mRight - mLeft;
    }
    public final int getHeight() {
        return mBottom - mTop;
    }

    从上面getWidth和getHeight的源码结合mLeft、mRight、mTop、mBottom这四个变量的赋值过程来看,getWidth的返回值就是View的测量宽度,getHeight同理。在View的默认实现中,View的测量宽高和最终宽高是相等的,测量宽高是形成于View的measure过程,而最终宽高形成于View的layout过程,赋值时机不一样,一般情况下两者是相等的,但在有些情况下不一致,举个例子:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.layout(l, t, r + 100, b + 100);
    }

    上述代码则会导致在任何情况下View的最终宽高总是比测量宽高大100px;另外一种情况则是View需要多次measure才能确定自己的测量宽高,在前几次的测量过程中,得出的测量宽高和最终宽高有可能不一致,但是最终来说,测量宽高和最终宽高还是一样的。

  3. draw过程
    Draw的作用是将View绘制到屏幕上面,View的绘制过程遵循如下几步:

    • 绘制背景background.draw(canvas)
    • 绘制自己(onDraw)
    • 绘制children(dispatchDraw)
    • 绘制装饰(onDrawScrollBars)

    可以通过draw方法的源码看出:

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
    
        // Step 1, draw the background, if needed
        int saveCount;
    
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
    
            // Step 4, draw the children
            dispatchDraw(canvas);
    
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
    
            // we're done...
            return;
        }

    View的绘制过程是通过dispatchDraw来实现的,会遍历所有元素的draw方法,draw事件就一层一层传递下去。View有一个特殊的方法setWillNotDraw:

    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

    如果一个view不需要绘制任何内容,那么设置这个标记为true后,系统会进行相应的优化。默认情况下,View没有启用这个标记位,但是ViewGroup会启用这个标记位。一般我们的自定义控件继承于ViewGroup本身并不具备绘制功能时,就可以开启这个标记位便于系统进行后续优化。当知道ViewGroup需要通过onDraw来绘制内容时,我们需要显式的关闭WILL_NOT_DRAW 这个标记位。

四、自定义View

  1. 自定义view的分类

    • 继承View重写onDraw方法
      重写onDraw方法实现一些不顾则的图形
    • 继承ViewGroup派生特殊的Layout
      主要用于实现自定义布局
    • 继承特定的View(比如TextView)
      一般用于扩展某种已有的View的功能
    • 集成特定的ViewGroup(比如LinearLayout)
      当某种效果看起来像几种view组合在一起的时候,可以采用这种方法。
  2. 自定义view须知

    • 让View支持wrap_content
    • 如果有必要,View支持padding
      直接继承View的控件,如果不在draw方法中处理padding,那么padding属性不起作用的;直接继承ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然导致padding和子元素的margin失效。
    • 尽量不要在View中使用Handler,没必要
      View内部本身提供了post系列的方法,完全可以替代Handler
    • View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
      有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被Remove时,View的onDetachedFromWindow方法会被调用;相对应的一个方法是onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow会被调用,当view变得不可见时我们也需要停止线程和动画。
    • View带有滑动嵌套情形时,需要处理好滑动冲突
  3. 自定义View示例
    自定义view的示例可以在这里找到:
    自定义CricleView
    继承ViewGroup派生的特殊layout

关于View事件体系,有几篇比较好的博文,值得一读:

Android应用层View绘制流程与源码分析
Android中View的量算、布局及绘图机制
源码解析Android中View的measure量算过程
源码解析Android中View的layout布局过程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值