view的绘制

View的原理介绍

  • View表示的的屏幕上的某一块矩形的区域,而且所有的View都是矩形的;
  • View是不能添加子View的,而ViewGroup是可以添加子View的。ViewGroup之所以能够添加子View,是因为它实现了两个接口:ViewParent 和 ViewManager;
  • Activity之所以能加载并且控制View,是因为它包含了一个Window,所有的图形化界面都是由View显示的,而Service之所以称之为没有界面的activity是因为它不包含有Window,不能够加载View;
  • 一个View有且只能有一个父View;
  • 在Android中Window对象通常由PhoneWindow来实现的,PhoneWindow将一个DecorView设置为整个应用窗口的根View,即DecorView为整个Window界面的最顶层View。也可以说DecorView将要显示的具体内容呈现在了PhoneWindow上;
  •  DecorView是FrameLayout的子类,它继承了FrameLayout,即顶层的FrameLayout的实现类是Decorview,它是在phoneWindow里面创建的;
  • 顶层的FrameLayout的父view是Handler,Handler的作用除了线程之间的通讯以外,还可以跟WindowManagerService进行通讯;
  • windowManagerService是后台的一个服务,它控制并且管理者屏幕;
  • 一个应用可以有很多个window,其由windowManager来管理,而windowManager又由windowManagerService来管理;
  • 如果想要显示一个view那么他所要经历三个方法:1.测量measure, 2.布局layout, 3.绘制draw。

View的测量/布局/绘制过程

显示一个View主要进过以下三个步骤:
  • Measure测量一个View的大小:
    • 决定了View的测量宽度
    • getMeasureWidth/getMeasureHeight获取View测量后的宽/高
  • Layout摆放一个View的位置:
    • 决定了View的四个顶点坐标和实际View的宽/高
    • getTop、getBottom、getLeft、getRight获取View的四个顶点坐标
    • getWidth、getHeight获取View最终的宽高
  • Draw画出View的显示内容:
    • 决定了View的显示
其中measure和layout方法都是final的,无法重写,虽然draw不是final的,但是也不建议重写该方法。
这三个方法都已经写好了View的逻辑,如果我们想实现自身的逻辑,而又不破坏View的工作流程,可以重写onMeasure、onLayout、onDraw方法。

ViewRoot:

  • 对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot完成。
  • 在ActivityThread中,当Activity被创建完毕时,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象与DecorView建立关联。


DecorView:

  • 顶级View
  • 是一个FrameLayout
  • View所有事件必须经过DecorView才能传递给其他View

View的绘制是从ViewRoot的performTraversals


MeasureSpec:代表一个32位int值

系统给我们提供了一个设计小而强的工具类———MeasureSpec类
1、MeasureSpe描述了父View对子View大小的期望。里面包含了测量模式和大小。
2、MeasureSpe类把测量模式和大小组合到一个32位的int型的数值中,其中高2位表示模式,低30位表示大小而在计算中使用位运算的原因是为了提高并优化效率。
3、我们可以通过以下方式从MeasureSpec中提取模式和大小,该方法内部是采用位移计算。

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
也可以通过MeasureSpec的静态方法把大小和模式合成,该方法内部只是简单的相加。
MeasureSpec.makeMeasureSpec(specSize,specMode);

MeasureSpec与LayoutParams的关系

  • DecorView的MeasureSpec由窗口大小和自身的LayoutParams共同决定
  • 普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定
  • MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽/高

测量模式

在对View进行测量时,Android提供了三种测量模式:
1. EXACTLY
即精确值模式,当控件的layout_width属性或layout_height属性指定为具体数值时,例如android:layout_width="100dp",或者指定为match_parent属性时,系统使用的是EXACTLY 模式。
2. AT_MOST
即最大值模式,当控件的layout_width属性或layout_height属性指定为warp_content时,控件大小一般随着控件的子控件或者内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
3.UNSPECIFIED
这个属性很奇怪,因为它不指定其大小测量的模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。
View默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式,且控件只可以响应你指定的具体宽高值或者是match_parent属性。如果要让自定义的View支持wrap_content属性,那么就必须重写onMeasure()方法来指定wrap_content时的大小。
而通过上面介绍的MeasureSpec这个类,我们就可以获取View的测量模式和View想要绘制的大小。
MeasureSpec判定规则
在自定义View的时候要通过判断测量的模式,给出不同的测量值,下面的一张图表罗列了 MeasureSpec判定规则。

measure过程:

Vie的Measure

measure()—>onMeasure()—>setMeasuredDimension()—>getDefaultSize()—>getSuggestedMinimumWidth()—>getMinimumWidth()
其中getDefaultSize()中分两种情况
  1. AT_MOST和EXACTLY:返回onMeasure()方法中传进去的measureSpec的specSize
  2. UNSPECIFIED:getSuggestedMinimumWidth()的返回值,getSuggestedMinimumWidth()需要判断View的背景
    1. 背景为空:宽度为android:minWidth属性所指定的值(不指定为0)
    2. 背景非空:getMinimumWidth()的返回值
      1. 返回值是Drawable的原始宽/高
ViewGroup的measure过程
  • 首先遍历调用子元素的measure方法测量子元素的大小,然后再测量自己的大小
  • ViewGroup是抽象类,没有重写View的onMeasure方法,它的onMeasure方法需要各个子类去实现
  • ViewGroup有一个measureChildren方法用于测量子元素
  • measureChildren会遍历每一个子元素并调用measureChild方法
  • measureChild会获取子元素的LayoutParams参数,然后通过getChildMeasureSpec创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法进行测量

layout过程:


  • Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup位置被确定之后,它在onLayout中遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用
  • layout确定View本身的位置
    • 通过setFrame方法设置view的四个位置(上下左右)
  • onLayout确定所有子元素的位置
    • 通过setChildFrame方法为子view设置位置,setChildFrame最终还是遍历调用了子view
  • getWidth、getHeight得到的是最终的宽高;getMeasureWidth、getMeasureHeight得到的是测量的宽高,可能出现不一样的情况:
    • View在需要经过多次measure的情况
  • 用户重写了View的layout并在此处对View进行了设置


draw过程:

  • 绘制背景:background.draw(canvas)
  • 绘制自己:(onDraw)
  • 绘制child:(dispatchDraw)
  • 绘制装饰:(onDrawScrollBars)
  • View绘制过程的传递是通过dispatchDraw实现的,dispatchDraw会遍历调用所有的子元素的draw方法
  • 特殊方法:setWillNotDraw:如果一个View不需要绘制任何内容将该标志位设置为true,ViewGroup默认关闭此功能



draw
draw是由ViewRoot的performTraversals方法发起,它将调用DecorView的draw方法,并把成员变量canvas传给draw方法。而在后面draw遍历中,传递的都是同一个canvas。所以android的绘制是同一个window中的所有View都绘制在同一个画布上。等绘制完成,将会通知WMS把canvas上的内容绘制到屏幕上。自定义View时一般不重写该方法。

onDraw
①View用来绘制自身的实现方法,如果我们想要自定义View,通常需要重载该方法。
②TextView中在该方法中绘制文字、光标和CompoundDrawable
③ImageView中相对简单,只是绘制了图片
因为我们的目的就是自定义View,所以当我们测量好了一个View之后,我们就可以间的重写onDraw()这个方法,并在Canvas对象上来绘制所需要的图形。在onDraw()中就有一个参数,该参数就是Canvas canvas对象,使用这个对象即可进行绘图操作;而如果在其他地方,通常需要使用代码创建一个Canvas对象:
Canvas canvas = new Canvas(bitmap);
之所以要传入一个bitmap,是因为传进来的bitmap与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程称之为装载画布。
在View类的onDraw()方法中,我们通过下面的代码,让canvas与bitmap发生直接的联系:
canvas.drawBitmap(bitmap, 0, 0, null);
然后将bitmap装载到另外一个Canvas对象中:
Canvas mCanvas = new Canvas(bitmap);
通过mCanvas将绘制效果作用在了bitmap上,再通过invalidate()刷新的时候,我们就会发现通过onDraw()方法画出来的bitmap已经发生了改变。
dispatchDraw
先根据自身的padding剪裁画布,所有的子View都将在画布剪裁后的区域绘制。
遍历所有子View,调用子View的computeScroll对子View的滚动值进行计算。
根据滚动值和子View在父View中的坐标进行画布原点坐标的移动,根据子在父View中的坐标计算出子View的视图大小,然后对画布进行剪裁。
dispatchDraw的逻辑其实比较复杂,但是幸运的是对子View流程都采用该方式,而ViewGroup已经处理好了,我们不必要重载该方法对子View进行绘制事件的派遣分发。
重写时,千万千万不要注释了super.方法

如何获取View的宽高(防止出现在Activity中获取View宽高不正确的情况)

1、onWindowFocusChanged
此方法的含义是View已经初始化完成此时去获取View的宽高就没有问题了
该方法在得到焦点与失去焦点时都会调用,即该方法会执行多次

public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}
2、view.post(runnable)
通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View已经初始化完成
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}
3、ViewTreeObserver
ViewTreeObserver的众多回调都能完成该功能(OnGlobalLayoutListener)
View树的状态改变或者View树内部的View可见性改变都会回调(onGlobalLayout会调用多次)
protected void onStart() {
    super.onStart();
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}
4、view.measure(int widthMeasureSpec, int heightMeasureSpec ),通过手动对View进行measure得到View的宽/高
View的LayoutParams参数是match_parent:直接放弃
View的LayoutParams参数是具体的数值
//比如宽高都为100dp
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
View的LayoutParams参数是wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1,MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值