前言
一、自定义View介绍
二、View绘制流程
2.1 Measure()
2.2 Layout()
2.2 Draw()
注意
我们经常使用TextView、button等控件,但是有些同学对于它们是如何显示和扩展的却并不那么熟悉。而这一块的知识也进阶高手必备的,写这一篇文章是想把view绘制这块的技术全面总结一下。我们知道,,Activity作为应用程序的载体负责向用户展现界面并提供了窗口进行视图绘制。
Android View绘制及事件(二)setContentView()源码,LayoutInflater加载View的过程
上一篇讲解了,当调用 Activity 的setContentView 方法后会调用PhoneWindow 类的setContentView方法,最终会生成一个继承FrameLayout的PhoneWindow的内部类DecorView对象。DecorView容器中包含根布局,通过findViewById()找到一个id为content的FrameLayout的根布局,Activity加载布局的xml最后通过 LayoutInflater.inflate() 将xml文件中的内容解析成View层级体系,最后填加到id为content的FrameLayout根布局中。LayoutInflater.inflate() 会调用 createViewFromTag解析该元素拿到View类型的temp对象实例,再调用rInflate采用递归解析temp中的子View,并将这些子View添加到temp中。
好了,上面大致认识了一下View被加载显示的原理,那么接下来一起看看View类内部是什么样子的?
1.1 实现方式:
类型 | 定义 |
---|---|
自定义组合控件 | 多个控件组合成为一个新的控件,方便多处复用 |
继承系统控件 | 继承自TextView等系统控件,在系统控件的基础功能上进行扩展 |
继承View、ViewGroup | 不复用系统控件逻辑,继承View、ViewGroup进行功能定义 |
查看本篇,代码实例教程:Android View绘制及事件(四)自定义组合控件+约束布局ConstraintLayout+自定义控件属性
1.2 构造函数:
继承系统View或直接继承View,都需要对构造函数进行重写,构造函数有多个,区别在于方法参数:
public class TestView extends View {
/**
* 在java代码里new的时候会用到
* @param context
*/
public TestView(Context context) {
super(context);
}
/**
* 在xml布局文件中使用时自动调用
* @param context
*/
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
* @param context
* @param attrs
* @param defStyleAttr
*/
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 只有在API版本>21时才会用到
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
1.3 绘制流程
函数 | 定义 |
---|---|
measure() | 测量View的宽高 |
layout() | 计算当前View以及子View位置 |
draw() | 绘制视图的工作 |
1.4 View的屏幕坐标
函数 | 定义 |
---|---|
getTop() | 获取View到其父布局顶边的距离 |
getLeft() | 获取View到其父布局左边的距离 |
getBottom() | 获取View到其父布局底边的距离 |
getRight() |
获取View到其父布局右边的距离 |
ViewGroup的测量过程与View有一点点区别,其本身是继承自View。ViewGroup除了要测量自身宽高外还需要测量各个子View的大小,因此它提供了测量子View的方法measureChildren()以及measureChild()帮助我们对子View进行测量。measureChildren()以及measureChild()的源码这里不再分析,大致流程就是遍历所有的子View,然后调用View的measure()方法,让子View测量自身大小。
1、 onMeasure()方法
测量视图大小,整个测量过程的入口位于View的measure方法当中,measure方法又回调OnMeasure。从顶层父View到子View递归调用measure()方法。measure()方法当中做了一些参数的初始化之后调用了onMeasure方法。
源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//该方法用来设置View的宽高,在我们自定义View时也会经常用到。
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
//该方法用来获取View默认的宽高,结合源码来看。
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
先看getDefautSize()方法的参数 getSuggestedMinimumHeight() 的源码:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
再看getDefaultSize(int size , int measureSpec) 源码:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//从这里我们看出,对于AT_MOST和EXACTLY在View当中的处理是完全相同的。所以在我们自定义View时要对这两种模式做出处理。
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
有两个参数size和measureSpec
2、 MeasureSpec类
在调用onMeasure()时,会根据MeasureSpec类的封装View尺寸的值来确定View的宽高,MeasureSpec = mode+size ,mode三种类型:UNSPECIFIED、EXACTLY 和AT_MOST。如下:
父mode | 作用 | 对应子View |
---|---|---|
EXACTLY | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size. | 父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0. |
AT_MOST | 最大模式,View尺寸有一个最大值,不可以超过MeasureSpec当中的Size值. | 父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围. |
UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大. | 父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围. |
在View当中,MeasureSpece的测量代码:(为子View设置MeasureSpec参数,子View大小需要在View中具体设置)
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) {
//当父View要求一个精确值时,为子View赋值
case MeasureSpec.EXACTLY:
//如果子view有自己的尺寸,则使用自己的尺寸
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//当子View是match_parent,将父View的大小赋值给子View
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//如果子View是wrap_content,设置子View的最大尺寸为父View
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局给子View了一个最大界限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局对子View没有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
子View的测量模式是由自身LayoutParam和父View的MeasureSpec来决定的!
对于View来说用来计算View的位置参数,进行页面布局。对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置,即从顶层父View向子View的递归调用view.layout()方法的过程,父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。
源码:
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;
//这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置。
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现。
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
小结
- 在layout()方法中的四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。
- 在layout()方法中通过setOpticalFrame(l, t, r, b)或 setFrame(l, t, r, b)方法对View自身的位置进行了设置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup对子View的位置进行计算。
绘制视图。draw过程也就是View绘制到屏幕上的过程。ViewRoot创建一个Canvas对象,然后调用OnDraw()。整个过程可以分为6个步骤。
源码:
public void draw(Canvas canvas) {
int saveCount;
// 1. 如果需要,绘制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 如果有必要,保存当前canvas。
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 绘制View的内容。
if (!dirtyOpaque) onDraw(canvas);
// 4. 绘制子View。
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// 覆盖是内容的一部分,在前景下绘制
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 绘制前景,如滚动条等等。
onDrawForeground(canvas);
return;
}
}
private void drawBackground(Canvas canvas) {
//获取背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
在onDraw(Canvas canvas)方法中,可以绘制图片,通过使用canvas、paint、matrix等。
参考链接https://www.jianshu.com/p/705a6cb6bfee
一、invalidate()和requestLayout()
invalidate()和requestLayout(),常用于View重绘和更新,其主要区别如下
所以,当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法。
如果,View的宽高及位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法
二、invalidate()和postInvalidate()