Android面试题自定义View之Window、ViewRootImpl和View的三大流程

本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点

View的三大流程指的是measure(测量)、layout(布局)、draw(绘制)。

下面我们来分别看看这三大流程

View的measure(测量)

MeasureSpec

MeasureSpec是View的一个内部静态类

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

    ...

    /**
     * 这种模式不用关心
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * 精确模式,对应的是match_parent和具体值,比如100dp
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * 最大模式,对应的就是wrap_content
     */
    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);
        }
    }

    /**
     * 获取测量的模式
     */
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    /**
     * 获取测量到的尺寸大小
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    ...
}

MeasureSpec总结起来就是:

  • 它由2部分数据组成,分别为定义了View测量的模式和View的测量尺寸大小
  • 其中EXACTLY精确模式表示的是match_parent和具体值;AT_MOST最大模式表示的是wrap_content的情况
View的measure过程

View的measure过程由其measure方法完成,在measure方法中会调用View的onMeasure方法

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

setMeasuredDimension方法会设置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;
    //对应的是wrap_content
    case MeasureSpec.AT_MOST:
    //对应的是match_parent和具体值,返回的是测量值
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

从getDefaultSize方法我们可以看到,无论是测量模式无论是AT_MOST还是EXACTLY,返回的结果都是specSize这个测量后的大小。当View的测量模式是AT_MOST,也就是我们在布局中给View设置的是wrap_content时,这个specSize实际上是父容器中的可用大小,也就相当于是和match_parent是一样的效果了。所以我们在通过继承View来自定义View时,就需要特别处理wrap_content的情况。

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的测量,还需要完成子元素的测量。ViewGroup是一个抽象类,为了测量子类,它提供了一个measureChildren方法:

//ViewGroup.class
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);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    
    //用子元素的LayoutParams构建MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看出ViewGroup的measureChildren方法最终会循环调用子元素的measure方法完成子元素的测量。

ViewGroup并没有定义自己的测量过程,因为它的测量过程要由子类自己完成,比如LinearLayout和RelativeLayout,显然测量过程是不同的。有兴趣的可以看看LinearLayout的onMearsure方法。

常见的在Activity中获取View的宽高的方法

View的measure过程和Activity生命周期方法是不同步的,需要用特殊的方法才能准确获取View的宽高

(1)onWindowFocusChanged方法中获取

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);

    if (hasFocus) {
        int width = myView.getMeasuredWidth();
        int height = myView.getMeasuredHeight();
    }
}

需要注意的是onWindowFocusChanged方法会被调用多次

(2)view.post(runnable)

通过view的post方法可以将一个runnable投递到消息队列的尾部,当Looper调用此runnable时,View已经初始化好了

myView.post(new Runnable() {
    @Override
    public void run() {
        int width = myView.getMeasuredWidth();
        int height = myView.getMeasuredHeight();
    }
});

(3)ViewTreeObserver

利用ViewTreeObserver的OnGlobalLayoutListener回调接口,当View树发生状态改变时会回调这个接口

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

View的layout(布局)

layout的作用就是ViewGroup用来确定子元素的位置,ViewGroup的位置被确定后,就会调用onLayout方法,遍历所有的子元素并调用其layout方法,在layout方法中又会调用onLayout方法。layout方法确定View本身的位置,而onLayout方法用来确定子元素的位置。

//View.class
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;

    //确定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);

        ...
    }

    ...
}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

layout方法主要就做了2件事,一个是调用setFrame方法确定自身的位置,另一个就是调用onLayout方法确定子元素的位置。

我们看到在View中并没有实现onLayout方法,同样的在ViewGroup中也没有实现onLayout方法,这是因为onLayout的具体实现同样和具体的布局有关,所以需要子类根据具体情况去实现。大家有兴趣可以看看LinearLayout的onLayout的实现。

需要注意的是默认情况下测量的宽高和最终的宽高是一样的,也就是getMeasuredWidth和getWidth是一样的。只不过一个获取的是measure过程后得到的宽高,一个是layout过程后的宽高。所以如果measure过程需要进行多次或是认为改变了layout方法,就有可能2者不相等。不过绝大多数都是一样的。


View的draw(绘制)

Draw说白了就是把View的内容绘制到屏幕上

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

    ...
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        //调用onDraw方法绘制内容
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        //调用dispatchDraw方法绘制子元素
        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;
    }

    ...
}

从上面的draw方法中,我们可以看出,绘制过程遵循如下几步:

(1)绘制背景:drawBackground(Canvas canvas)

(2)绘制自身内容:onDraw(canvas)

(3)绘制子元素:dispatchDraw(canvas)

(4) 绘制装饰:onDrawForeground(canvas)


View的三大流程开始的地方—ViewRootImpl

ViewRootImpl的performTraversals方法工作流程

上面这张图是一张很经典的图,很好的描述了View的绘制流程。ViewRootImpl中的performTraversals方法会调用performMeasure、performLayout、performDraw方法,开始View的测量、布局和绘制过程。那ViewRootImpl中的performTraversals方法又是在什么时候被调用的呢?这就需要理解一个窗口的概念,也就是Window。

Android中的Window

Window是一个抽象的概念,每一个Window都对应着一个View和ViewRootImpl,Window通过ViewRootImpl来和View建立联系。Android中所有的视图都是通过都是通过Window来呈现的,不管是Activity、Dialog、还是Toast,它们的View都是附加在Window上的,因此Window实际上是View的直接管理者。比如我们触摸屏幕的事件,就是通过Window传递给DecorView,然后再由DecorView传递给我们的View。我们在Activity、Dialog中设置视图内容的方法setContentView在底层也是通过Window来完成的

Window的添加过程

我们在启动一个Activity或是一个Dialog时,系统都会为我们创建一个Window,并把创建的Window注册到系统的WindowManagerService中。

Window的添加过程需要通过WindowManager的实现类WindowManagerImpl的addView方法来实现。只有在通过addView方法将View添加到Window中后,我们的View才和Window关联起来,才能接收通过Window传递的各种输入信息

//WindowManagerImpl.class
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

由WindowManagerImpl的源码我们发现,WindowManagerImpl把添加View的工作都交给了WindowManagerGlobal类处理。我们来简单看看WindowManagerGlobal类

//WindowManagerGlobal.class
//所有Window所对应的View
private final ArrayList<View> mViews = new ArrayList<View>();
//所有Window对应的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//多有Window对应的布局参数
private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
//正在被删除的View
private final ArraySet<View> mDyingViews = new ArraySet<View>();

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

    //创建ViewRootImpl
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);
    
    //将Window的一系列对象添加到对应的列表中
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

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

源码中已经做了相应的注释了。这里我们看到WindowManagerGlobal在addView方法中创建ViewRootImpl后,最后调用了ViewRootImpl的setView方法。下面我们来看看ViewRootImpl的setView方法

//ViewRootImpl.class
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            ...
            //调用了requestLayout进行View的绘制
            requestLayout();
            ...
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                //这里调用WindowSession的addToDisplay方法注册Window
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            } catch (RemoteException e) {
                mAdded = false;
                mView = null;
                mAttachInfo.mRootView = null;
                mInputChannel = null;
                mFallbackEventHandler.setView(null);
                unscheduleTraversals();
                setAccessibilityFocus(null, null);
                throw new RuntimeException("Adding window failed", e);
            } finally {
                if (restore) {
                    attrs.restore();
                }
            }

            ...

        }
    }
}

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

在ViewRootImpl的setView方法中主要是做了2件事,一个是调用requestLayout方法启动View的绘制流程;另一个是调用WindowSession的addToDisplay方法请求WindowManagerService添加Window,这是一次IPC调用。

至此,我们就分析完Window的添加过程了,总结如下:

(1)View的展示以及处理触摸点击事件离不开Window,2者通过ViewRootImpl进行关联

(2)我们在启动Activity、创建Dialog或是弹出Toast时都会创建一个Window,然后会通过WindowManagerGlobal类的addView方法来创建ViewRootImpl类,并将Window、View、ViewRootImpl关联起来,

(3)在创建完ViewRootImpl后,接着会调用ViewRootImpl的setView方法,在setView方法中通过requestLayout方法最终调用到performTraversals方法开启View的三大流程;通过WindowSession的addToDisplay方法向WindowManagerService发起远程IPC调用,完成Window的添加。

总结

(1)在通过继承View的方式自定义View时,需要特别处理wrap_content的情况,因为View中默认相当于没处理(和match_parent效果一样)

(2)在Activity中获取View的宽高需要用特殊的方式:onWindowFocusChanged、view.post(runnable)、ViewTreeObserver的OnGlobalLayoutListener

(3)我们的View的显示离不来Window,无论是Activity、Dialog还是Toast,都对应着一个Window。View和Window通过ViewRootImpl来建立关联。我们显示、更新、隐藏界面,比如Dialog的show和dismiss,说到底是Window中添加、更新和删除View的过程。

(4)我们通过setContentView方法添加View,其实是对应Window的添加View的过程,Window会创建ViewRootImpl来执行注册Window、开启View的绘制流程的操作。

(5)所以综上,我们显示一个界面的过程为:创建Window–>创建ViewRootImpl–>添加View–>绘制View、注册Window


欢迎关注我的公众号查看更多精彩文章!

AntDream

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/779060.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

React 省市查询组件完整代码

目录 一、地区文件 二、Antd配合使用 三、实现效果 一、地区文件 下载地址&#xff1a;全国省市区数据_JSON格式_SQL格式 export const chinaArea {0: {1: 北京,2: 天津,3: 河北省,4: 山西省,5: 内蒙古自治区,6: 辽宁省,7: 吉林省,8: 黑龙江省,9: 上海,10: 江苏省,11: 浙…

Linux之进程控制(下)

目录 进程替换的概念 进程替换的函数 execl​编辑 execlp execle execv execvp execve 上期&#xff0c;我们学习了进程创建&#xff0c;进程终止和进程等待&#xff0c;今天我们要学习的是进程控制中相对重要的板块------进程替换。 进程替换的概念 在进程创建时&…

微米级触觉感知的紧凑视触觉机器人皮肤

视触觉皮肤&#xff08;VTS&#xff09;分为涂层型、标记型和热致变色型。涂层的耐磨性和空间分辨率是涂层型VTS的核心问题。近期&#xff0c;北京邮电大学方斌教授联合中国地质大学&#xff08;北京&#xff09;杨义勇教授&#xff0c;在传感器领域Q1期刊IEEE Sensors Journal…

DHCP服务器

目录 网络传输原则&#xff1a; DHCP: DHCP作用&#xff1a; 优缺点&#xff1a; DHCP的原理&#xff1a; 用虚拟机模拟DHCP服务器​编辑​编辑 网络传输原则&#xff1a; 网络是双向的&#xff0c;网络是有方向的 解释&#xff1a;网络是双向的&#xff1a; …

轻松快速上手Thekey库,实现数据加密无忧

Thekey的概述&#xff1a; Thekey库是一个Python库,旨在简化数据加密、解密、签名和验证的过程。它提供了一套简洁易用的接口,用于处理各种加密任务,适合需要在应用程序中实现安全数据处理的开发人员. 安装Thekey库 pip install thekey使用Thekey库进行基本加密和解密操作的…

uniapp 在手机上导出excel

1.创建excelDev.js文件 export default {exportExcel(fileData, documentName excel) {plus.io.requestFileSystem(plus.io.PUBLIC_DOCUMENTS, function(fs) {let rootObj fs.rootlet fullPath rootObj.fullPathconsole.log("开始导出数据")// 创建文件夹rootObj…

Linux进程(1)(结构-操作系统-进程)

目录 1.体系结构 2.操作系统&#xff08;Operator System&#xff09; 1&#xff09;概念&#xff1a; 2&#xff09;结构 示意图&#xff08;不完整&#xff09; 3&#xff09;尝试理解操作系统 4&#xff09;系统调用和库函数概念 3.认识进程 1.启动 2.进程创建的代码…

11.x86游戏实战-汇编指令add sub inc dec

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 上一个内容&#xff1a;10.x86游戏实战-汇编指令lea 首先双击下图红框位置 然后在下图红框位置输入0 然…

Lock4j简单的支持不同方案的高性能分布式锁实现及源码解析

文章目录 1.Lock4j是什么?1.1简介1.2项目地址1.3 我之前手写的分布式锁和限流的实现 2.特性3.如何使用3.1引入相关依赖3.2 配置redis或zookeeper3.3 使用方式3.3.1 注解式自动式3.3.2 手动式 4.源码解析4.1项目目录4.2实现思路 5.总结 1.Lock4j是什么? 1.1简介 lock4j是苞米…

CorelDRAW2024设计师的神器,一试就爱上!

&#x1f3a8; CorelDRAW 2024&#xff1a;设计界的瑞士军刀&#xff0c;让创意不再受限&#xff01;&#x1f31f; 嗨&#xff0c;各位朋友们&#xff01;&#x1f44b;&#x1f3fb; 今天我要跟大家分享一个神奇的设计神器——CorelDRAW 2024。作为设计师的你&#xff0c;是否…

第10章 项目总结02:针对当前项目的面试题

1 项目概况 1.1 项目介绍 从以下几个方面进行项目介绍&#xff1a; 1、项目的背景&#xff1a;做什么业务、服务的客户群是谁、谁去运营、自研还是外包等问题。 2、项目的业务流程&#xff1a;课程发布流程、断点续传流程、视频处理流程、认证授权流程、支付流程、CI/CD流程…

three.js 后期处理,物体高亮

效果图 代码 引入资源文件&#xff0c;在初始化时创建后处理对象 // 用于边缘高亮的插件// 引入后处理扩展库EffectComposer.jsimport { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";// 引入渲染器通道RenderPassimport { RenderPass }…

接口自动化测试思路和实战(5):【推荐】混合测试自动化框架(关键字+数据驱动)

混合测试自动化框架(关键字数据驱动) 关键字驱动或表驱动的测试框架 这个框架需要开发数据表和关键字。这些数据表和关键字独立于执行它们的测试自动化工具&#xff0c;并可以用来“驱动&#xff02;待测应用程序和数据的测试脚本代码&#xff0c;关键字驱动测试看上去与手工测…

3.python

闯关 3作业 本节关卡&#xff1a; 学习 python 虚拟环境的安装 Python 的基本语法 学会 vscode 远程连接 internstudio 打断点调试 python 程序

揭秘“消费即收益”的循环购模式 商家智慧还是消费陷阱?

大家好&#xff0c;我是你们的电商策略顾问吴军。今天&#xff0c;我将带大家深入剖析一种新兴的商业模式——循环购模式&#xff0c;它以其独特的“消费赠礼、每日返利、提现自由”特性&#xff0c;在电商界掀起了不小的波澜。那么&#xff0c;这种模式究竟有何魅力&#xff1…

cs224n作业3 代码及运行结果

代码里要求用pytorch1.0.0版本&#xff0c;其实不用也可以的。 【删掉run.py里的assert(torch.version “1.0.0”)即可】 代码里面也有提示让你实现什么&#xff0c;弄懂代码什么意思基本就可以了&#xff0c;看多了感觉大框架都大差不差。多看多练慢慢来&#xff0c;加油&am…

厌倦了Nvim、vim等命令行编辑器?来看看新血脉....

是否厌倦了那几款烂大街的命令行风格编辑器&#xff1f;今天就来给各位换换血&#xff0c;介绍几个新成员。 让我们深入了解这些文本编辑器的主要功能和优点&#xff1a; 1. Ox Editor&#xff1a;优雅的新秀 Ox Editor是一款新兴的终端文本编辑器&#xff0c;以其简洁和优雅…

文档去重(TF-IDF,MinHash, SimHash)

2个doc有些相似有些不相似&#xff0c;如何衡量这个相似度&#xff1b; 直接用Jaccard距离&#xff0c;计算量太大 TF-IDF: TF*IDF TF&#xff1a;该词在该文档中的出现次数&#xff0c; IDF&#xff1a;该词在所有文档中的多少个文档出现是DF&#xff0c;lg(N/(1DF)) MinHash …

适合宠物饮水机的光电传感器有哪些

如今&#xff0c;随着越来越多的人选择养宠物&#xff0c;宠物饮水机作为一种便捷的饮水解决方案日益受到欢迎。为了确保宠物随时能够获得足够的水源&#xff0c;宠物饮水机通常配备了先进的光电液位传感器技术。 光电液位传感器在宠物饮水机中起着关键作用&#xff0c;主要用…

Java技术栈总结:kafka篇

一、# 基础知识 1、安装 部署一台ZooKeeper服务器&#xff1b;安装jdk&#xff1b;下载kafka安装包&#xff1b;上传安装包到kafka服务器上&#xff1a;/usr/local/kafka;解压缩压缩包&#xff1b;进入到config目录&#xff0c;修改server.properties配置信息&#xff1a; #…