基于源码分析 Android View 绘制机制

栏目: Android · 发布时间: 7年前

内容简介:在 Android 的知识体系中,上面的代码说明,在

在 Android 的知识体系中, View 扮演者很重要的角色。 View 是 Android 在视觉上的呈现。本文结合 android-28 的源码来分析 View 的绘制过程。

ViewRootImpl

ViewRootImpl 类是连接 WindowManager 和 DecorView 的纽带, View 的绘制流程均是通过 ViewRootImpl 来完成的。

// ActivityThread.java

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        
        ...
        
        if (r.window == null && !a.mFinished && willBeVisible) {
            // 获取 WindowManager 及 DecorView
            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 (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    // 将 DecorView 添加到当前 Window 中
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }

        ...
    }
复制代码

上面的代码说明,在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 通过 WindowManager 添加到 Window 中。

// WindowManagerImpl.java

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

可以知道最终是通过 WindowManagerGlobal 的 addView 方法来将 DecorView 添加到 Window 中

// WindowManagerGlobal

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
       ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

            // 初始化 ViewRootImpl 并将 ViewRootImpl 对象和  DecorView 建立关联
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }
复制代码

上述代码创建了 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。最终在 setView 方法中,会执行 ViewRootImpl 的 requestLayout 方法来执行 View 的绘制流程

// ViewRootImpl.java

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
复制代码

scheduleTraversals 方法最终会调用 performTraversals 方法,经过 measure 、 layout 和 draw 三个过程才能最终将一个 View 绘制出来

View
View
View
基于源码分析 Android View 绘制机制

如图所示, performTraversals 会依次调用 performMeasure 、 performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure 、 layout 和 draw 这三大流程。其中在 performMeasure 中会调用 measure 方法,在 measure 方法中又会去调用 onMeasure 方法,在 onMeasure 方法中又会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到了子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。通过 performLayout 和 performDraw 的传递流程跟 performMeasure 类似

MeasureSpec

为了更好地理解 View 的测量过程,我们还需要理解 MeasureSpec 。 MeasureSpec 参与了 View 的 measure 过程,在很大程度上决定了一个 View 的尺寸规格,但父容器也会影响 View 的 MeasureSpec 的创建过程。 在测量过程中,系统会将 View 的 LayoutParams 根据父容器所试驾的规则转换成对应的 MeasureSpec ,然后再根据这个 MeasureSpec 来测量出 View 的宽和高。

// View.java

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        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 makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }
        
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }
复制代码

MeasureSpec 代表一个 32 位的 int 值,高 2 位代表 SpecMode , 低 30 位代表 SpecSize

  • SpecMode: 测量模式
  • SpecSize: 在某种测量模式下的规格大小

MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配 。 makeMeasureSpec 是打包方法, getMode 和 getSize 则为解包方法。

SpecMode 有三类,每一类都标识特殊的含义

UNSPECIFIED

父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,标识一种测量的状态

EXACTLY

父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。 它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式

AT_MOST

父容器指定了一个可用大小即 SpecSize , View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content

MeasupreSpec 和 LayoutParams

MeasureSpec 不是唯一由 LayoutParams 决定的, LayoutParams 需要和父容器一起才能决定 View 的 MeasureSpec ,从而进一步决定 View 的宽和高。

对于 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定;对于普通的 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同局诶的那个, MeasureSpec 一旦确定后, onMeasure 中就可以确定 View 的测量宽和高

DecorView 创建 MeasureSpec

对于 DecorView 来说,它的 MeasureSpec 创建过程是由 getRootMeasureSpec 方法来完成的

// ViewRootImpl.java

    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;
        case ViewGroup.LayoutParams.WRAP_CONTENT: // 最大模式,大小不定,但是不能超过窗口的大小
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default: // 精确模式,大小为 LayoutParams 中指定的大小
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
复制代码

通过上述代码, DecorView 的 MeasureSpec 的产生过程就很明确了,具体来说其遵守如下规则,根据它的 LayoutParams 中的宽和高参数来划分

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

ViewRootImpl 在 performTraversals 方法中调用 getRootMeasureSpec 获取到 childWidthMeasureSpec 和 childHeightMeasureSpec 后,会传给 performMeasure 方法,最终调用 DecorView 的 measure 方法

普通 View 创建 MeasureSpec

对于普通 View 来说,即布局中的 View , View 的 measure 过程由 ViewGroup 传递而来,在 ViewGroup 的 measureChildWithMargins 方法

// ViewGroup.java

    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);
    }
复制代码

measureChildWithMargins 方法一般会在自定义 Layout 组件的 onMeasure 方法中调用(如 FrameLayout, LinearLayout),来测量子元素的规格。在调用子元素的 measure 方法之前会先通过 getChildMeasureSpec 方法来得到子元素的 MeasureSpec 。通过上面代码可知,子元素的 MeasureSpec 的创建和父容器的 MeasureSpec 和子元素本身的 LayoutParams 有关,此外还和 View 的 margin 及 padding 有关

// ViewGroup.java

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 父容器的 mode 和 size
        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);
    }
复制代码

getChildMeasureSpec 函数主要的作用是根据父容器的 MeasureSpec 同时结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec 。

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

基于源码分析 Android View 绘制机制

以上所述就是小编给大家介绍的《基于源码分析 Android View 绘制机制》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

How Tomcat Works

How Tomcat Works

Budi Kurniawan、Paul Deck / BrainySoftware / 2004-4-1 / USD 54.95

A Guide to Developing Your Own Java Servlet Container一起来看看 《How Tomcat Works》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换