View的绘制原理 - 从起源到实践

目录

一、起源
  1、从ActivityThread开始
  2、WindowManagerImpl
  3、WindowManagerGlobal
  4、ViewRootImpl
二、概念
三、绘制流程
  1、measure
    1)解析
    2)demo
  2、layout
    1)解析
    2)demo
  3、draw
    1)解析
四、实例

===================================================================

在梳理原理的过程中,穿插实例解析,更易理解;最后还有一个简单的自定义view实践

一、起源

先简单说下View的起源,有助于我们后续的分析理解。
1、从ActivityThread.java开始

下面只贴出源码中的关键代码,重点是vm.addView(decor, l)这句,那么有三个对象需要理解:

  • vm:a.getWindowManager()即通过Activity.java的getWindowManager方法得到的对象;继续跟踪源码,发现此对象由Window.java的getWindowManager方法获得,即是((WindowManagerImpl)wm).createLocalWindowManager(this)。
  • decor:通过r.activity.getWindow()可知r.window是PhoneWindow对象,在PhoneWindow中找到getDecorView方法,得知decor即DecorView对象。
  • l:通过下面源码得知,l通过r.window.getAttributes获取到,由于PhoneWindow中没有getAttributes方法,故从他的父类Window中获取,得知宽高均为LayoutParams.MATCH_PARENT
ActivityThread.java

    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason)  {   
            ...
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            ...
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                   ...
                }
            }
     }

2、接上面wm.addView(decor, l),通过上面分析wm是WindowManagerImpl,则走进此类的addView中

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

3、WindowManagerGlobal

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        ...
        root = new ViewRootImpl(view.getContext(), display);
        ...
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            ...
        }
    }
}

4、有第三步可知,最终走到了ViewRootImpl.java,曙光在前方

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                ...
                requestLayout();
                ...
    }
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            ...
            scheduleTraversals();
        }
    }
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
        }
    }
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    void doTraversal() {
            ...
            performTraversals();
            ...
    }

  重点来了,performMeasure、performLayout、performDraw则是对根view的测量、布局
、draw;先说下mWidth、mHeight和lp,其中mWidth、mHeight是window的宽高,lp就是前面提到的WindowManager中的LayoutParam,跟踪代码发现均为LayoutParams.MATCH_PARENT

    private void performTraversals() {
            ...
            WindowManager.LayoutParams lp = mWindowAttributes;
            ...
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            ...
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            performLayout(lp, mWidth, mHeight);
            ...
            performDraw();
            ...
    }

  根据以下源码,那么childWidthMeasureSpec、childHeightMeasureSpec的值就是MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);

/**
     * Figures out the measure spec for the root view in a window based on it's
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
   private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        ..
        }
        return measureSpec;
    }

  下面的mView上面已分析过,就是DecorView,也就是根view

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        ...
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            ...
        }
    }

二、概念

  首先需要理解MeasureSpec的概念,详见另一篇文章 https://www.jianshu.com/p/cecd0de7ec27

三、绘制流程

1、measure

(1)解析
  接上面说到了performMeasure,即走到了DecorView的measure,而DecorView实际是FrameLayout,FrameLayout的父类是ViewGroup,而ViewGroup的父类是View,所以直接走 到了View的measure里面。

  主角登场,接下来分析View的measure,measure是final的,也就是不能不能重写此方法,但是里面有一个onMeasure方法,到这里应该很熟悉了,我们自定义控件都会重写这个方法;

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
           ...
           onMeasure(widthMeasureSpec, heightMeasureSpec);
           ...
    }

  由于DecorView的父类的FrameLayout,那么我们来看FrameLayout的onMeasure方法;可以看到会测量所有的子View,最后测量自己。所以有两点:测量子View;测量自己。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            ...
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                ...
            }
        }
        ...
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                                               resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        ...
        }
    }

  我们先来关注测量子View即measureChildWithMargins,大概的意思是:
父View的MeasureSpec+当前View的LayoutParams=得出当前View的MeasureSpec
  parentWidthMeasureSpec、parentHeightMeasureSpec还记得是什么吗?就是之前说过的ViewRootImpl中的performTraversals方法里面的performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)传过去的,可以回顾下前面的分析;

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

  那么具体看下getChildMeasureSpec做了什么,在这篇文章有过详细分析https://www.jianshu.com/p/cecd0de7ec27,特别提一下,size是父view的size与padding的差值,而padding是父view的padding与子view的margin的总和,最终算出了当前View的MeasureSpec的int值;接着调用子View的measure方法传入计算出的宽高MeasureSpec,层层递归直到无子View为止。

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

  分析完测量子View,接下来看测量自己,即上面源码中提到的setMeasuredDimension

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        ...
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        ...
    }

  最后为mMeasureWidth、mMeasureSpec赋值,测量完毕。

  我们来看下View类中的onMeasure(即若不覆写onMeasure的默认逻辑),同上调用了setMeasureSpec为测量结果赋值;这里有需要注意的地方,当我们自定义View覆写onMeasure时,最后一定要为测量结果赋值(setMeasuredDimension),否则会报错。

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

  看下getSuggestedMinimumWidth,mMinWidth就是我们在layout中设置的android:minWidth,也就是有background的话则取background的宽度,否则去minWidth;高度也一样。

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

  getDefaultSize获取结果,首先通过之前算好的MeasureSpec取出mode、size,若mod是AT_MOST、WXACTLY则结果为specSize,若是UNSPECIFIED则结果为getSuggestedMinimumWidth的默认值。

    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;
    }

  重点源码分析完了,接下来通过一个例子串起来。

(2)demo

  写了一个简单的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    android:background="@android:color/holo_blue_light"
    android:orientation="vertical"
    android:paddingTop="50dp">

    <TextView
        android:id="@+id/textview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_orange_light"
        android:text="Hello World!"
        android:textSize="20sp" />

    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginBottom="30dp"
        android:background="@android:color/holo_green_light" />

</LinearLayout>

  首先需要弄清楚布局的整体结构,通过工具Tools > Layout Inspector可以看到,顶层是我们之前提到的DecorView(是一个FrameLayout);我们直接看重点部分,即content(ContentFrameLayout可知是FrameLayout),我们平时开发中调用setContentView设置我们的布局,就是放置到这里面,由下图也可知content下面包含的布局正是id为layout的LinearLayout。


layout_inspector.png

  接下来一步步测量(measure)所有控件,我们从R.id.content(ContentFrameLayout)这里开始说,ContentFrameLayout是FrameLayout,走到了FrameLayout的onMeasure中,这里会循环子View,也就是会计算出子View(即我们写的布局 R.id.layout)的MeasureSpec后调用子View的measure测量宽高;

parent_MeasureSpec.png

child_LayoutParams.png

  父view(即R.id.content ContentFrameLayout)的MeasureSpec和子View(即我们写的布局R.id.layout LinearLayout)的LayoutParams共同决定 子View的MeasureSpec;

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                ...
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        ...
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

  如之前的分析,当父View是MeasureSpec.EXACTLY,子View的宽为LayoutParams.MATCH_PARENT,则size的结果为父View的Size,即1080,mode为MeasureSpec.EXACTLY;子View的高为LayoutParams.WRAP_CONTENT,则size为父view与padding的差值,padding是父view的padding与子view的margin的总和,即1710 - 53(marginTop值为20dp 换算值与手机像素有关) = 1657,mode为MeasureSpec.AT_MOST,表示高度最大值为1657。

child_MeasureSpec.png

  此时走到了LinearLayout的onMeasure,我们的布局是android:orientation="vertical",所以我们看measureVertical

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

  循环所有子view测量出宽高,最后再测量自己。

  void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    if (skippedMeasure
            || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
      ...
      for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null || child.getVisibility() == View.GONE) {
          continue;
        }
        ...
        final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
        final float childWeight = lp.weight;
        if (childWeight > 0) {
          ...
          final int childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
                  Math.max(0, childHeight), View.MeasureSpec.EXACTLY);
          final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                  mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                  lp.width);
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
          ...
        }
        ...
      }
      ...
      mTotalLength += mPaddingTop + mPaddingBottom;
    } else {
       ...
    }
    ...
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
    ...
  }

  再来看我们layout中的两个子view,TextView和View,先来看TextView
其父View的MeasureSpec与TextView的LayoutParams如下,两者共同决定TextView的MeasureSpec,来到TextView的onMeasure中,为了更关注整体的把控,这里不做详细拓展解析TextView了

parent_MeasureSpec.png

child_LayoutParams.png

算出TextView的MeasureSpec,如下


child_MeasureSpec.png

  接下来看另一个子view,View,父View的MeasureSpec与子View的LayoutParams如下图,通过源码getChildMeasureSpec可知,当父view的宽是EXACTLY时,子view的width的值是match_parent,那么子view的size为父view的size,即1080,mode是MeasureSpec.EXACTLY;当父view的高是AT_MOST时,子view的height值是确切数值263,左移子view的size为263,mode是MeasureSpec.EXACTLY。


parent_MeasureSpec.png

child_LayoutParams.png
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                ...
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                ...
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                ...
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                ...
            }
            break;
            ...
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
child_MeasureSpec.png

  所有子view测量完毕,最后测量父view的MeasureSpec,宽是1080,高是TextView的height+View的height+父view的paddingTop+View的margin_bottom = 544。

2、layout

(1)解析
  上面说过的view的入口是ViewRootImpl

    private void performTraversals() {
            ...
            WindowManager.LayoutParams lp = mWindowAttributes;
            ...
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            ...
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            performLayout(lp, mWidth, mHeight);
            ...
            performDraw();
            ...
    }

  performMeasureSpec说完了,接下来看performLayout,measure负责测量、layout负责位置摆放,performLayout里面调用了View.java的layout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ...

        final View host = mView;
        if (host == null) {
            return;
        }
        ...
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            ...
            }
        } finally {
            ...
        }
        ...
    }

  这里根据传入的left、top、right、bottom值来确定摆放位置,right和bottom一般分别为left、top的值加上当前view的宽、高。

    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 = 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);
            ...
        }
        ...
    }

  通过setFrame来设置上下左右四个值,setOpticalFrame最终也会走到setFrame,这里就不贴源码了

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        ...
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            ...
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            ...
            // Invalidate our old position
            invalidate(sizeChanged);
            ...
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            ...
        }
        return changed;
    }

  我们发现view.java的onLayout是空方法,但是在viewgroup中是抽象方法,也就是父类均需要重写该方法,说明该方法是用来摆放子view位置的。

View.java
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
ViewGroup.java
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

(2)demo
  我们用LinearLayout来举例,只看核心代码,通过不同的gravity值算出其left,top值递增:下一个子view的top值为 当前子view的高度+其bottomMargin( childHeight + lp.bottomMargin + getNextLocationOffset(child) ),所以我们在使用android:orientation="vertical"的时候,控件在LinearLayout中竖直排列。
  那么后两个值宽高是什么?就是我们在measure阶段算出的测量值,通过getMeasuredWidth、getMeasureSpecHeight获得,和我们之前的分析一致。

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        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();
                
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                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,接着调用子类的layout方法,上面说过如果子View无子类,则在layout中直接通过setFrame算出位置,若有子View,则在layout中算出自己的位置后,通过重写onLayout测量其子view。

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

3、draw

(1)解析
  上面说过的view的入口是ViewRootImpl

    private void performTraversals() {
            ...
            WindowManager.LayoutParams lp = mWindowAttributes;
            ...
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            ...
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            performLayout(lp, mWidth, mHeight);
            ...
            performDraw();
            ...
    }

  performMeasureSpec、performLayout说完了,measure负责测量、layout负责位置摆放,draw负责绘制。下面只贴出了关键代码,最后走到了mView的draw里面

private void performDraw() {
        ...
        try {
            boolean canUseAsync = draw(fullRedrawNeeded);
            ...
        } 
        ...
    }
    private boolean draw(boolean fullRedrawNeeded) {
         ...
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
        ...
        return useAsyncReport;
    }
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
            ...
                mView.draw(canvas);
            ...
        return true;
    }

View中的draw方法注释和清晰,分为6步,其中2、5可以跳过:
1、绘制背景
2、跳过
3、绘制view内容
4、绘制子view
5、跳过
6、绘制

真正的绘制工作()在view中进行,

View.java
    public void draw(Canvas canvas) {
        ...

        /*
         * 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);

            drawAutofilledHighlight(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);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
    }

四、实例

shili.gif

  这是实现效果,是比较常见的一个控件,文字轮播。

  用自定义view实现(其实有个现成的控件TextSwitcher也可以实现);如果用自定义view方案的话,继承FrameLayout会更简单些,但是为了能将前面的知识点串起来的话,这里自定义了viewgroup,代码逻辑不复杂。我们来看下,下面是使用方式,还是比较简单的。

    <com.example.textswitcherdemo.SwitcherViewGroup
        android:id="@+id/text_switcher_view"
        android:layout_width="match_parent"
        android:layout_height="35dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
        val contents = ArrayList<String>()
        contents.add("草帽一伙抵达新世界")
        contents.add("机器猫一伙乘坐时光机来到侏罗纪")
        contents.add("蜡笔小新一家吃到霸王餐")
        val switcherView : SwitcherViewGroup = findViewById(R.id.text_switcher_view)
        switcherView.addContents(contents)

  先说下思路:自定义viewgroup其实就是个简易版的framelayout,这个我们后面具体说,使用者传过来arraylist字符串,字符串new出自己的textview添加到viewgroup,与动画结合,使用timer定时器发送消息给Handler,Handler接到消息后,为当前子view(TextView)设置向上移出动画,为next子View(TextView)设置移入动画,如此循环可实现文字滚动效果(要算好index以防数组越界)。

  接下来看下SwitcherViewGroup,重写了onMeasure、onLayout,之前我们说过,onMeasure最后一定要调用setMeasuredDimension设置测量的宽高,否则报错,所以onMeasure中调用super.onMeasure走到了view.java的onMeasure里,目的是为了为测量宽高赋值;接下来通过for循环测量所有子view的宽高值,ViewGroup中的measureChild我们之前有说过,这里就不再赘述啦。

SwitcherViewGroup.java

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChild(child, child.measuredWidth, child.measuredHeight)
        }
    }
View.java

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

  接下来计算放置位置,用刚刚所有子view的测量宽高值计算子view的位置,这里简化的将top、left值设置为0,bottom、right值分别设置为top+子view测量高,left+子view测量宽。onDraw在View中实现,即TextView,至此完成自定义View。

SwitcherViewGroup.java

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        for (i in 0 until childCount) {
            val view = getChildAt(i)
            setChildFrame(view, 0, 0, view.measuredWidth, view.measuredHeight)
            view.visibility = View.INVISIBLE
        }
        startTimer()
    }

    private fun setChildFrame(view: View, left: Int, top: Int, width: Int, height: Int) {
        view.layout(left, top, left + width, top + height)
    }

  接下来看下外部使用自定义view的addContents,将每个字符串创建一个TextView加入自定义view中。

    fun addContents(contents: ArrayList<String>) {
        for (i in 0 until contents.size) {
            val textView = TextView(context)
            textView.textSize = 25f
            textView.setTextColor(context.resources.getColor(android.R.color.holo_blue_light))
            textView.text = contents[i]
            textView.gravity = Gravity.CENTER
            this.addView(textView)
        }
    }

  与动画结合,使用timer定时器发送消息给Handler,Handler接到消息后,为当前子view(TextView)设置向上移出动画,为next子View(TextView)设置移入动画

    private val mHandler: Handler = object : Handler() {
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            when (msg?.what) {
                UPDATE_VIEW -> {
                    val view = getChildAt(getContentIndex(mIndex))
                    outAnimation(view)
                    val nextView = getChildAt(getContentIndex(mIndex + 1))
                    inAnimation(nextView)
                    mIndex++
                }
            }
        }
    }

    private fun startTimer() {
        if (mTimer == null) {
            mTimer = Timer()
        }
        mTimer!!.schedule(object : TimerTask() {
            override fun run() {
                mHandler.sendEmptyMessage(UPDATE_VIEW)
            }
        }, 0, mDuration)
    }

    private fun outAnimation(view: View) {
        view.visibility = View.VISIBLE
        val animator = ObjectAnimator.ofFloat(view, "translationY", 0f, -view.height.toFloat())
        val set = AnimatorSet()
        set.play(animator)
        set.duration = mAnimationDuration
        set.start()
    }

    private fun inAnimation(view: View) {
        view.visibility = View.VISIBLE
        val animator = ObjectAnimator.ofFloat(view, "translationY", view.height.toFloat(), 0f)
        val set = AnimatorSet()
        set.play(animator)
        set.duration = mAnimationDuration
        set.start()
    } 

  计算出实际index

    private fun getContentIndex(index: Int): Int {
        var realIndex = index % childCount
        if (realIndex > childCount - 1) {
            realIndex -= childCount
            return realIndex
        }
        return realIndex
    }

  当然,循环时间间隔、动画效果、都可以开放一个函数,外面可以改变,也更加灵活。

  原理包括例子都已经说完了,原理只是开始,最重要的还是实践,实践,实践,任务来了,不要急着在网上找资料,自己尝试手动写,之后再看实现方案,对比自己的思路及代码差在哪,就能发现自己薄弱点在哪,通过不断的实践才会真正掌握它。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343