自定义View:测量measure,布局layout,绘制draw

本文详细介绍了Android中自定义View的相关知识,包括View的测量、布局、绘制过程,以及与此相关的measure、layout、draw方法。内容涵盖了ViewGroup的测量策略、MeasureSpec的使用、onMeasure、onLayout、onDraw等关键方法,还有画布的移动和剪裁,以及动画的绘制原理。通过对这些概念和方法的理解,开发者可以更好地定制自己的Android UI组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 什么是View

在Android的官方文档中是这样描述的:表示了用户界面的基本构建模块。一个View占用了屏幕上的一个矩形区域并且负责界面绘制和事件处理。

手机屏幕上所有看得见摸得着的都是View。这一点对所有图形系统来说都一样,例如ios的UIView。

2. View和Activity的区别

我们之前学习过android的四大组件,Activity是四大组件中唯一一个用来和用户进行交互的组件。可以说Activity就是android的视图层。

如果再细化,Activity相当于视图层中的控制层,是用来控制和管理View的,真正用来显示和处理事件的实际上是View。

每个Activity内部都有一个Window对象, Window对象包含了一个DecorView(实际上就是FrameLayout),我们通过setContentView给Activity设置显示的View实际上都是加到了DecorView中。

3. View种类

android提供了种类丰富的View来应对各种需求,例如提供文字显示的TextView,提供点击事件的Button,提供图片显示的ImageView,还有各种布局文件,例如Relativilayout,Linearlayout等等。他们都是继承自View。

view

4. ViewGroup

ViewGroup继承自View,并实现了两个接口ViewParent和ViewManager。

ViewManager提供了三个抽象方法addView,removeView,updateViewLayout。用来添加、删除、更新布局。

ViewParent主要提供了一系列操作子View的方法例如焦点的切换,显示区域的控制等等。

5. 为什么要有ViewGroup?

实际上所有的事情View都能做,包括显示复杂的界面,我们只需要设计一个复杂的View即可。例如短信通知的icon,一个可以显示图片又可以显示文字的View,我们后期学习了View的draw方法后,可以轻松的设计一个View来达到这个效果,但是这样不仅复杂,而且重用性较差,还会因为一点小改动而重复的创造轮子,这显然不符合程序员偷懒的原则,所以我们可以完全把ImageView和TextView组合到一起就可以了,这个时候我们就需要一个容器,ViewGroup,来装这两个View。

ViewGroup和View最大的不同是可以组合多个View,那么多个View在一起,该如何摆放,这就是ViewGroup需要解决的问题。

6. View树

我们看到的界面,都是以一个ViewGroup作为根View,通过往ViewGroup中添加子View(可以是View,也可以是ViewGroup),来组合出各具特色的界面。

这种从根到叶的组合方式,我们可以看做成一个View树。(类似于XML),而View的显示和事件处理,都是依赖于这个View树。

绘制和事件处理的起始点,都是从根View开始一级一级的往下传递。我们从任意一层发起绘制,都将反馈到根View,然后再从上往下传递。

之前我们说过根View就是Window中的DecorView,也就是一个FrameLayout。

6.1 View树示意图

view

对SystemUI,也就是我们常说的StatusBar显示在哪儿呢,其实SystemUI是一个单独的App,随着系统启动而启动,将会启动一个系统级服务,接收我们提交的通知,该应用也会有一个window,并且级别比我们普通应用的window要高,所以会显示在我们的应用的外面,只不过该window的高度比较小。

7. View的测量、布局、绘制过程

整个android系统 CS架构,view被展示到界面上需要经过3个步骤

  • 需要花多大:measure –> onMeasure –> setMeasuredDimension
  • 画在什么地方:layout –> setFrame –> onLayout
  • 怎么画:draw –> > onDraw –> dispatchDraw

7.1 显示一个View需要经过哪些步骤

  • Measure测量一个View的大小
  • Layout摆放一个View的位置
  • Draw画出View的显示内容

其中measure和layout方法都是final的,无法重写,虽然draw不是final的,但是也不建议重写该方法。这三个方法都已经写好了View的逻辑,如果我们想实现自身的逻辑,而又不破坏View的工作流程,可以重写onMeasure、onLayout、onDraw方法。

7.2 如何发起一个View树的测量/布局/绘制流程

通过调用requestLayout/requestFocus都将发起一个View树的测量。测量完毕后会进行布局,布局完毕后就会绘制。

如果View的大小没有发生改变,布局也没有变化,只是显示的内容发生了变化,则可以通过invalidate来请求绘制,此时将不会测量和布局,直接从绘制开始。

7.3 View内部的mPrivateFlags变量

View中有一个私有int变量mPrivateFlags,用于保存View的状态,int型32位,通过0/1可以保存32个状态的true或者false,采用这种方式可以有效的减少内存占用,提高运算效率。

当某一个View发起了测量请求时,将会把mPrivateFlags中的某一位从0变为1,同时请求父View,父View也会把自身的该值从0变为1,同时也将会把其他子View的值从0变为1。这样一层一层传递,最终传到到DecorView,DecorView的父View是ViewRoot,所以最终都将由ViewRoot来进行处理。

ViewRoot收到请求后,将会从上至下开始遍历,检查标记,只要有相对应的标记就执行测量/布局/绘制

当Activity被创建时,会相应的创建一个Window对象,Window对象创建时会获取应用的WindowManager(注意,这是应用的窗口管理者,不是系统的)。

Activity被创建后,会调用Activity的onCreate方法。我们通过设置setContentView就会调用到Window中的setContextView,从而初始化DecorView。

所以我们需要隐藏标题栏什么的,都需要在DecorView初始化之前进行设置。

DecorView初始化之后将会被添加到WindowManager中,同时WindowManager中会为新添加的DecorView创建一个对应的ViewRoot,并把DecorView设置给ViewRoot。

所以根View就是DecorView,因为DecorView的父亲是ViewRoot,实现自ViewParent接口,但是没有继承自View,所以根本不是一个View。

从系统的命名来看,WindowManger继承自ViewManager,而添加到WindowManager中的是DecorView,不是Window,都说明了其实真正意义上的window就是View。

在ViewRoot的构造方法中会通过getWindowSession来获取WindowManagerService系统服务的远程对象(这才是系统级的)。

当ViewRoot的setView方法中将会调用requestLayout进行第一次视图测量请求。同时sWindowSession.add自身内部的一个W对象,以此达到和WindowManagerService的关联。

W是一个Binder对象。可以实现跨进程的通信了,并且是一个双方都掌握着主动调用的跨进程通信方式。

7.4 常用的标记位

  • FORCE_LAYOUT 请求绘制,将从measure开始,,并增加LAYOUT_REQUIRED标记
  • 持有LAYOUT_REQUIRED标记的View将会被执行layout,完毕后会去掉LAYOUT_REQUIRED和FORCE_LAYOUT
  • DRAWN带有该标签的将不会被draw,注意,这和上面两个不一致,当draw完毕后会加上该标签,当没有该标签才会被draw。

还有一些其他的标记位,大家可以自行阅读源码。

7.5 测量/布局/绘制流程

view

测量事件最终传递到decorView的父亲ViewRoot那里,由它的函数performTraversals来执行,听名字就知道是执行遍历了。

首先它会检测之前设置的标记为来确定是否需要测量大小,是,就会直接执行decorView的measture方法,该方法内部会测量完自身后,将会继续遍历所有子View,直到每一个设置有标记的子View都测量完。

然后它会检测是否需要布局,是,将会执行decorView的layout方法进行,该方法内部也会遍历所有设置有标记位子View。

8. measure 测量

8.1 测量流程

measure

测量View是在measure()方法中,而measure()方法是final修饰的,不允许重写,但是在measure()方法中回调了onMeasure()方法,所以我们自定义View的时候需要重写onMeasure()方法,在该方法中实现测量的逻辑

  • 如果是普通View,则直接通过setMeasureDimension()方法设置大小即可
  • 如果是ViewGroup,则需要循环遍历所有子View,调用子View的measure()方法,测量每个子View的大小,等所有的子View都测量完毕,最后通过setMeasureDimension()设置ViewGroup自身的大小

8.2 LayoutParams

每个View都包含一个ViewGroup.LayoutParams类或者其派生类,LayoutParams中包含了View和它的父View之间的关系,而View大小正是View和它的父View共同决定的。

我们设置View的大小,有match_parent、wrap_content和具体的dip值。

match_parent对应值为-1、wrap_conten对应值为-2,具体dip对应其设定的值。在测量时,View的父类从Layout中读出宽高值,根据不同的值设置不同的计算模式。

布局文件中所有layout_开头的在代码中都是需要通过LayoutParams来设置。

当我们通过addView添加一个子View时,如果它没有LayoutParams或者是LayoutParams的类型不匹配,那么将会创建一个默认的LayoutParams。

通过布局文件进行layout_width,layout_height进行设定。通过代码设置,需要一个LayoutParams来描述。

View view = new View(this);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
view.setLayoutParams(lp);

8.3 measure

/**
  * This is called to find out how big a view should be. The parent
  * supplies constraint information in the width and height parameters.
  * The actual measurement work of a view is performed in
  * {@link #onMeasure(int, int)}, called by this method. Therefore, only
  * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
  * @param widthMeasureSpec Horizontal space requirements as imposed by the
  *        parent
  * @param heightMeasureSpec Vertical space requirements as imposed by the
  *        parent
  *
  * @see #onMeasure(int, int)
  */
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
 }

measure是final修饰的方法,不可被重写。在外部调用时,直接调用view.measure(int wSpec, int hSpec)。measure中调用了onMeasure。自定义view时,重写onMeasure即可

8.4 onMeasure

measure是一个final方法,用来测量View自身的大小,View类该方法体逻辑比较简单,只是根据判断条件决定是否需要调用onMeasure。方法接受两个参数,分别就是通过MeasureSpec类合成测量模式和大小的宽与高。

实际上View的大小是无限大

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值