【读书笔记】【Android 开发艺术探索】第4章 View 的工作原理

一、基础知识

1、ViewRoot 和 DecorView

ViewRoot 对应 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程都是通过 ViewRoot 来完成的。在ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView
添加到 Window 中,同时会创建 ViewRoot 对象。

DecorView 添加到窗口 Window 的过程。

图片来自https://yq.aliyun.com/articles/3005

View 的绘制流程从 ViewRootImpl 的 preformTraversals 开始,下面是它的伪码

 // ViewRootImple#performTraverals 的伪代码
    private void preformTraverals(){
        preformMeasure(...)     --------- View.measure(...);

        performLayout(...)        ---------  View.layout(...);

        performDraw(...)           --------   View.draw(...);
    }

2、 MeasureSpec

MeasureSpec 是一个32位的值,高2位是 SpecMode, 测量模式,低30位是 SpecSize,是在某种测量模式下的规格大小。

(1).SpecMode 的分类:

UNSPECIFIED: 表示开发人员可以将试图按照自己的意愿设置任意大小,没有任何限制。这种情况少见,不太会用到。比如 listView 一般自定义
View 用不到。

EXACTLY: 表示父视图希望子视图的大小应该由 SpeceSize 的值决定的。子元素将被限定在给定的边界里而忽略它本身的大小。对应的属性为 match_parent
或者指定大小。

WRAP_PARENT : 表示子视图最多只能是 SpecSize 中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不超过 SpeceSize.对应的属性是
wrap_content.

(2).MeasureSpece 和 LayoutParams 的对应关系

DecorView 的 MeasureSpec 由窗口尺寸和自身 LayoutParams 决定;

普通 View 的 MeasureSpec 由父容器的 MeasureSpce 和自身的 LayoutParams 决定。

由 ViewGroup 的 getChildMeasureSpece(...) 方法,可以得出下图中的结论。

二、View 的工作流程

1、measure 过程

(1). measure 的核心方法

.mensure(int widthMeasureSpec, int heightMeasureSpec)

该方法在 View 中定义为 final ,不能重写该方法。但是 measure 最终会调用 onMeasure (...)方法, 因此在自定义的View 的时候,只要重写onMeasure(...) 方法即可。

.onMeasure(int widthMeasureSpec, int heightMeasureSpec)

该方法就是我们只自定义 View 的时候需要实现测量绘制逻辑的方法,该方法的参数是父视图对子视图的 widht 和height 测量方法的要求。在自定义
View 时,需要做的就是更加 widthMeasureSpec 和 heightMeasureSpec 计算View 的width 和 height ,不同的处理模式不同。

.setMeasuredDimension(int measuredWidth, int measureHeith)

测量阶段的终极方法,在 onMeasure(int widthMeasureSpec, int heightMeasureSpece) 方法中调用,将计算的得到尺寸传递给该方法,测量阶段结束。该方法必须调用,否则会报异常。在自定义
View 的时候,不需要关系系统复杂的 Measure 过程,只需调用setMeasuredDimension(int measuredWith, int measuredHeith) 设置根据 MeasureSpec计算得到的尺寸即可。

(2)measure 过程

Measure 过程传递尺寸的两个参数

ViewGroup.LayoutParams View 自身的布局参数;

MeasureSpec 类, 父视图对子视图的测量要求。

View 的 measure 过程

View 的 getDefaultSize 方法

    // View#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;
    }

从 getDefaultSize 方法可以看出, View 的宽高是由 specSize 决定的。

直接继承 View 的自定义控件需要重写 onMeasure(...) 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 相当于使用 match_parent. 从 MeasureSpece 和 LayoutParams 关系表格中可看出。

解决方法,给 View 指定一个默认的内部宽高(mWith 和 mHeight),并在 wrap_content 时设置此宽高即可。

 protected  void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpeceMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpeceSize = View.MeasureSpec.getSize(widthMeasureSpec);
        int heightSepceSize = View.MeasureSpec.getSize(heightMeasureSpec);

        if (widthMeasureSpec == MeasureSpec.AT_MOST
                && heightMeasureSpec == MeasureSpec.AT_MOST){
            // 设置一个默认的宽高
             setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == View.MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth, heightSepceSize);
        } else if (heightSpeceMode == View.MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpeceSize, mHeight);
        }
    }

ViewGroup  的 measure 过程.

ViewGroup 除了完成自己的 measure 过程以为,还会遍历去调用所有子元素的 measure 方法。 ViewGroup 是一个抽象类,没有重写 View 的 onMeasure 方法。ViewGroup 也没有定义其测量的具体过程,其测量过程的 onMeasure 方法需要各个之类去实现。

measure 完成以后,可以通过 getMeasureWidth / getMeasureHeight 获取 View 的测量宽高, 要在 onLayout 方法中去获取 View 的测量宽高或者最终宽高。

因为 View 的 measure 过程和 Activity 的生命周期方法不是同步的,因此无法保证 Activity 在 onCreate, onStart, onResume 方法中获取 View 的宽高信息。

解决办法:

1. 在 Activity/View # onWindowFoucsChanged 方法中

   @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

2. 使用 view.post(Runnable)

   @Override
    protected void onStart() {
        super.onStart();
        mTextView.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }

3.使用 ViewTreeObserver

@Override
    protected void onStart() {
        super.onStart();

        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressWarnings("deprecation")
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int widht = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }

4. 使用 View.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对 View 进行 measure 得到 View 的宽高。

View 的 LayoutParams 分:

match_parent:

无法测出;

具体数值(dp/px):

例如宽高都是 100px 时

int widthMesureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        view.measure(widthMesureSpec, heightMeasureSpec);

wrap_parent 时

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

2. layout 过程

子视图的具体位置是相对于父视图而言的。View 的 onLayout 方法时空方法,ViewGrop 的 onLayout 方法时 abstract .

如果自定义的 View 继承 ViewGroup ,需要实现 onLayout 方法。

 // View#layout
    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;

        // setOpticalFrame / setFrame 设定 View 的四个顶点
        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);

            ...
        }
        ...
    }

getMeasureWidth 和 getWidth 之间的区别:

getMeasureWidth 是 measure() 过程之后获取后,getWidth 是在 layout() 过程之后得到的。getMeasureWidth() 方法中的值是通过 setMeasureDimension() 方法类进行设置的,而 getWidth() 方法中的值是通过视图右边的坐标减去左边的坐标计算出来的。

3.draw 过程

.View.draw(Canvas canvas)

ViewGroup 没有重写该方法,所以所有的视图最终都会调用 View 的 draw(...) 方法进行绘制。在自定义视图时,不应该重
写该方法,而是应该重写 onDraw(Canvas cavas) 方法,进行绘制。如果自定义视图确实要重写该方法,先调用
super.draw(canvas) 完成系统的绘制,然后再进行自定义的绘制。

.View.onDraw(..)

View 的 onDraw(...) 方法默认是空方法,自定义视图时,需要重写该方法,绘制自身的内容。

.View.dispatchDraw(...)

View 中默认是空方法,ViewGroup 重写了该方法对子元素进行了绘制。在自定义 ViewGroup 是不应该对该方法进行重写。

   // View#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)
            // 第六步, 绘制 foreground, scrollbars
            onDrawForeground(canvas);

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

在不需要绘制 layer 的时候会跳过第二步和第五步。因此在绘制的时候,能不绘制 layer 就尽量不绘制 layer, 以提高绘制效率。

setWillNotDraw

   /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

如果一个 View 不需要绘制任何内容,则将这个标志位设置为 true, 系统会进行相应的优化。当我们在自定义控件继承 ViewGroup 并且本身不具备绘制功能时,可以开启这个标志位从而便于系统进行后续的优化。

三、自定义 View

1、分类

(1). 继承 View 重写 onDraw() 方法

用来实现一些不规则的视图,需要自己支持 wrap_content, 并且也需要自己处理 padding.

(2).继承 ViewGroup 派生特殊的 Layout

用于自定义布局,需要合适地处理 ViewGroup 的测量、布局的过程,并同时处理处理子元素的测量和布局过程。

(3). 继承特定的 View

用户扩展某种已有的 View 的特性。

(4).继承特定的 ViewGroup (例如 LinearLayout)

2、注意事项

(1).让 View 支持 wrap_content

(2).如有必要,让 View 支持 padding, 在 draw 方法中支持处理 padding.

(3).尽量不要在 View 中使用 Handler, 可使用其内部的 post 方法.

(4).View 中有线程或者动画时

在 onAttachToWindow 中启动线程和动画;

在 onDetachFromWindow 方法中要停止线程和动画.

(5). View 带有滑动嵌套情况时,需要处理好滑动冲突。

本文除了是《Android 开发艺术探索》 书中的知识,还有部分内容摘自这两个博客

https://github.com/android-cn/android-open-project-analysis/tree/master/tech/viewdrawflow

View 绘制流程

时间: 2024-10-23 23:19:44

【读书笔记】【Android 开发艺术探索】第4章 View 的工作原理的相关文章

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

Android开发艺术探索--第四章View的工作原理 4.1 (一)初识ViewToot和DecorView 基本概念 ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的.在ActivityThread中,当Activity对象被创建完成后,会将DecorView添加到View中.同时,会创建ViewRootImpl对象,并将ViewTootImpl对象和DecorView建立关联.

Android开发艺术探索——第七章:Android动画深入分析

Android开发艺术探索--第七章:Android动画深入分析 Android的动画可以分成三种,view动画,帧动画,还有属性动画,其实帧动画也是属于view动画的一种,,只不过他和传统的平移之类的动画不太一样的是表现形式上有点不一样,view动画是通过对场景的不断图像交换而产生的动画效果,而帧动画就是播放一大段图片,很显然,图片多了会OOM,属性动画通过动态的改变对象的属性达到动画效果,也是api11的新特性,在低版本无法使用属性动画,但是我们依旧有一些兼容库,OK,我们还是继续来看下详细

Android艺术开发探索第四章——View的工作原理(下)

Android艺术开发探索第四章--View的工作原理(下) 我们上篇BB了这么多,这篇就多多少少要来点实战了,上篇主席叫我多点自己的理解,那我就多点真诚,少点套路了,老司机,开车吧! 我们这一篇就扯一个内容,那就是自定义View 自定义View 自定义View的分类 自定义View的须知 自定义View的实例 自定义View的思想 一.自定义View的分类 自定义View百花齐放,没有什么具体的分类,不过可以从特性大致的分为4类,其实在我看来,就三类,继承原生View,继承View和继承Vie

Android艺术开发探索第四章——View的工作原理(上)

这章就比较好玩了,主要介绍一下View的工作原理,还有自定义View的实现方法,在Android中,View是一个很重要的角色,简单来说,View是Android中视觉的呈现,在界面上Android提供了一套完整的GUI库,里面有很多控件,但是有时候往往并不能满足于需求,所以只有自定义View了,我们会简单的说下流程,然后再去实践除了View的三大流程之外,View常见的回调方法也是必须掌握的,比如构造方法,onAttach,onVisibilityChanged,onDetach,另外对于一些

《android开发艺术探索》读书笔记(五)--动画

接上篇<android开发艺术探索>读书笔记(五)--Drawable No1: 自定义动画:派生一种新动画只需要继承Animation这个抽象类,然后重写它的initialize和applyTransformation方法,在initialize方法中做一些初始化工作,在applyTransformation中进行相应的矩阵变换即可,很多时候需要采用Camera来简化矩阵变换的过程. No2: 属性动画PropertyAnimation 补间动画TweenAnimation 帧动画Frame

《android开发艺术探索》读书笔记(十三)--综合技术

接上篇<android开发艺术探索>读书笔记(十二)--Bitmap的加载和Cache No1: 使用CrashHandler来获取应用的crash信息 No2: 在Android中单个dex文件所能够包含的最大方法数为65536,这包含Android FrameWork.依赖的jar包以及应用本身的代码中的所有方法. No3: 使用multidex来解决方法数越界 apply plugin: 'com.android.application' android { compileSdkVers

《android开发艺术探索》读书笔记(十五)--Android性能优化

接上篇<android开发艺术探索>读书笔记(十四)--JNI和NDK编程 No1: 如果<include>制定了这个id属性,同时被包含的布局文件的根元素也制定了id属性,那么以<include>指定的id属性为准 No2: 绘制优化 1)onDraw中不要创建新的局部对象 2)onDraw方法中不要做耗时的任务 No3: 内存泄露优化 场景一:静态变量导致的内存泄露: 如果静态变量持有了一个Activity,会导致Activity无法及时释放. 解决办法:1使用Ap

Android开发艺术探索读书笔记——进程间通信

1. 多进程使用场景 1) 应用某些模块因为特殊需求需要运行在单独进程中.如消息推送,使消息推送进程与应用进程能单独存活,消息推送进程不会因为应用程序进程crash而受影响. 2) 为加大一个应用可使用的内存,需要多进程来获取多份内存空间. 2. 如何开启多进程 给四大组件(Activity.Service.Receiver.ContentProvider)在AndroidMainfest中指定android:process属性指定. 如果进程以":"开头的进程,代表应用的私有进程,其

Android开发艺术探索(研读笔记)——03-Android中的IPC机制(一)

Android开发艺术探索(研读笔记) 作者:Dimon 微博:@Dimon-喰 GitHub:@Dimon94 LOFTER:@Dimon. 03-Android中的IPC机制(一) 1.Android IPC 简介 IPC(Inter-Process-Communication):含义为进程间通信,指两个进程之间进行数据交换的过程. 什么是进程:一般指一个执行单元,在PC和移动设备上的一个程序或者一个应用. 什么是线程:线程是CPU调度的最小单元,是一种有限的系统资源. 而一个进程可以包含多