美文网首页
Android UI绘制分析(二)-测量、布局、绘制

Android UI绘制分析(二)-测量、布局、绘制

作者: 张小凡凡 | 来源:发表于2019-07-09 17:45 被阅读0次

    本文源码基于 Android sdk 26, 为了逻辑清晰,省略了无关代码,不排除后期重新加上相关代码

    系统发送Message 绘制布局, 启动 requestLayout( )

    系统发送消息开始绘制API调用流程图如下:

    系统发送消息开始绘制API调用流程图.jpg

    这个要从应用程序启动开始, 因为Android sdk使用Java写的, 而Java程序运行是从main函数开始,所以我们要先看 ActivityThread 。

    public static void main(String[] args) {
            SamplingProfilerIntegration.start();
            CloseGuard.setEnabled(false);
            Environment.initForCurrentUser();
            Looper.prepareMainLooper();
    
            ActivityThread thread = new ActivityThread();
            thread.attach(false);
    
            if (sMainThreadHandler == null) {
                sMainThreadHandler = thread.getHandler();
            }
            Looper.loop();
    
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    

    该方法最后调用 Looper.loop() 表示主线程进入消息队列循环,接下来的所有操作 需要 系统通过Binder机制主动向程序进程发送通知,子线程接受到信息后 向主线程发送Message,从而控制应用程序行为方式。

    比如说当Activity显示的时候,系统发送RESUME_ACTIVITY 消息,控制activity开始绘制。

     private class H extends Handler {
            ·····
            public void handleMessage(Message msg) {
                switch (msg.what) {
                     ······
                    case RESUME_ACTIVITY:  
                        SomeArgs args = (SomeArgs) msg.obj;
                        handleResumeActivity((IBinder) args.arg1, true, args.argi1 != 0, true,args.argi3, "RESUME_ACTIVITY");
                        break;
                        ······
                    }
            }
    

    在handleResumeActivity()方法中,主要做两件事:
    第一,调用Activity的生命周期函数onResume();
    第二,将之前创建的DecorView添加到 ViewRootImp中,开始测量、布局、绘制。

     final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
            // 该方法会调用到Activity的生命周期函数 onResume()
            r = performResumeActivity(token, clearHide, reason);
    
            if (r != null) {
                boolean willBeVisible = !a.mStartedActivity;
                if (r.window == null && !a.mFinished && willBeVisible) {
                    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;
      
                    if (a.mVisibleFromClient) {
                        if (!a.mWindowAdded) {
                            a.mWindowAdded = true;
                            //根据源码可以知道该方法的具体实现在WindowManagerImpl中
                            wm.addView(decor, l);
                        } 
                    }
                  ......
    

    WindowManagerImpl中调用

        //WindowManagerImpl中方法
        @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            //mGlobal 即 WindowManagerGlobal
            mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
        }
    

    WindowManagerGlobal 中调用

        //WindowManagerGlobal 中方法
        public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
            ......
            synchronized (mLock) {
                root = new ViewRootImpl(view.getContext(), display);
                view.setLayoutParams(wparams);
                // do this last because it fires off messages to start doing things
                root.setView(view, wparams, panelParentView);
            }
        }
    

    ViewRootImpl中调用

         //ViewRootImpl中方法
        public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            synchronized (this) {
                    ......
                    requestLayout();
                    ......
                }
            }
        }
    
       @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                scheduleTraversals();
            }
        }
    

    经过一连串的调用,最后方法走到这里,方法主要做一件事,发送一个Runnable 开始View的测量、布局
    、绘制,然后通知系统开始下一帧。

       void scheduleTraversals() {
            if (!mTraversalScheduled) {
               //mTraversalRunnable  中开始View的测量、布局、绘制
                mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                //通知系统 绘制新一帧画面, 即将View显示到屏幕上
                notifyRendererOfFramePending();
            }
        }
    
       final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
    
     void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
                performTraversals();
                if (mProfile) {
                    Debug.stopMethodTracing();
    
                    mProfile = false;
                }
            }
        }
    
    private void performTraversals() {
        ......
        Rect frame = mWinFrame;
        ......
                
        // !!FIXME!! This next section handles the case where we did not get the
        // window size we asked for. We should avoid this by getting a maximum size from
        // the window session beforehand.
        if (mWidth != frame.width() || mHeight != frame.height()) {
           mWidth = frame.width();
           mHeight = frame.height();
        }
        ...... 
        //开始测量
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
         //布局
        performLayout(lp, mWidth, mHeight);
        ......
        //绘制
        performDraw();
        ......    
    

    测量

    测量模式 MeasureSpec 介绍

    程序使用一个32位 的 int型的整数 来表示view的尺寸信息, 其中该数据 高2位 表示测量模式, 低30位表示具体的大小数据。

    EXACTLY: 精确模式,父控件 已经为子控制确定具体尺寸, 大小即为低30位数值
    AT_MOST : 最大值模式,子控件自己确定自己的尺寸, 但不能超过父控件指定的最大值
    UNSPECIFIED : 未确定模式,父控件没有做任何约束限制,子控件可以自己指定任意大小

    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 {}
    
            /**
             * Measure specification mode: The parent has not imposed any constraint
             * on the child. It can be whatever size it wants.
             */
            public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
            /**
             * Measure specification mode: The parent has determined an exact size
             * for the child. The child is going to be given those bounds regardless
             * of how big it wants to be.
             */
            public static final int EXACTLY     = 1 << MODE_SHIFT;
    
            /**
             * Measure specification mode: The child can be as large as it wants up
             * to the specified size.
             */
            public static final int AT_MOST     = 2 << MODE_SHIFT;
            ......
    }
    
    
    父控件 确认子View的 MeasureSpec

    开始测量子控件时, 父控件会根据自身的尺寸 和子控件的LayoutParam 确定子控件的MeasureSepc

    如下源码可知,规则如下:
    当前控件自身测量模式为

    1. MeasureSpec.EXACTLY
      当子控件LayoutParams 为 具体值:设置子控件 MeasureSpec.EXACTLY, 尺寸为当前设置的尺寸
      当子控件LayoutParams 为 MATCH_PARENT : 设置子控件 MeasureSpec.EXACTLY, 尺寸为父控件尺寸
      当子控件LayoutParams 为 WRAP_CONTENT:设置子控件MeasureSpec.AT_MOST,尺寸不超过父控件尺寸

    2. MeasureSpec.AT_MOST
      当子控件LayoutParams 为 具体值:设置子控件 MeasureSpec.EXACTLY, 尺寸为当前设置的尺寸
      当子控件LayoutParams 为 MATCH_PARENTWRAP_CONTENT :设置子控件MeasureSpec.AT_MOST,尺寸不超过父控件尺寸

    3. MeasureSpec.UNSPECIFIED
      当子控件LayoutParams 为 具体值:设置子控件 MeasureSpec.EXACTLY, 尺寸为当前设置的尺寸
      当子控件LayoutParams 为 MATCH_PARENTWRAP_CONTENT : 设置子控件 MeasureSpec.UNSPECIFIED,尺寸根据当前配置 设置为0 或当前父控件尺寸

        /**
         *
         * @param spec The requirements for this view
         * @param padding The padding of this view for the current dimension and
         *        margins, if applicable
         * @param childDimension How big the child wants to be in the current
         *        dimension
         * @return a MeasureSpec integer for the child
         */
        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.MeasureSpec中的方法
         */
          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);
                }
            }
    
    子view测量自己

    通过父控件 为自己指定的 MeasureSpec 和自身需要的尺寸, 计算出自己最后的大小。规则如下

    /**
         *
         * @param size 自身需要的尺寸
         * @param measureSpec 父控件为自己指定的MeasureSpec
         * @return  控件最后的尺寸
         */
        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:
                //使用xml中设置的尺寸,但最大值不超过父控件规定的尺寸
                result =  Math.min(size, specSize);
               break;
            case MeasureSpec.EXACTLY:
                //精确模式,直接使用具体的值
                result = specSize;
                break;
            }
            return result;
        }
    

    布局

    通过ViewRootImpl中的performLayout(...)开始当前界面的布局。

    使用getValidLayoutRequesters(...)方法 得到当前控件内所有需要进行布局的子View, (过滤掉状态为View.Gone的view)

        private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
            final View host = mView;
            ......
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());  
            ......
            ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,false);
            int numValidRequests = validLayoutRequesters.size();
            for (int i = 0; i < numValidRequests; ++i) {
                final View view = validLayoutRequesters.get(i);
                view.requestLayout();
            }
          ......
        }
    

    View.layout(...)方法 完成当前控件位置的设定,在该方法中会回调onLayout,继承view的ViewGroup会重写该方法,实现自己子控件 布局的逻辑。

        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;
            ......
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                ......
            }
         ......
        }
    

    比如以下代码为 LinearLayout中的代码

        @Override
        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);
            }
        }
    

    绘制

    通过ViewRootImpl中的performDraw()开始当前界面的绘制。api调用流程图如下:

    绘制API调用流程图.png

    在View.draw()开始当前控件的绘制, 绘制步骤如注释

    1. 绘制背景
    2. 绘制当前控件内容
    3. 绘制子控件
    4. 绘制前景,滚动条装饰等
    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;
            }
    

    View子类控件重写 onDraw()方法 绘制自身, 如LineaLayout中:

        @Override
        protected void onDraw(Canvas canvas) {
            if (mOrientation == VERTICAL) {
                drawDividersVertical(canvas);
            } else {
                drawDividersHorizontal(canvas);
            }
        }
    

    viewGroup控件 重写dispatchDraw()方法 绘制 子控件

        @Override
        protected void dispatchDraw(Canvas canvas) {
           ......
           drawChild(canvas, child, drawingTime
           ......
        }
    
        protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
            return child.draw(canvas, this, drawingTime);
        }
    

    完~
    (如有不足,欢迎指出,共同学习,共同进步)

    相关文章

      网友评论

          本文标题:Android UI绘制分析(二)-测量、布局、绘制

          本文链接:https://www.haomeiwen.com/subject/sxvahctx.html