Android自定义View(一)--基础知识

一:自定义View绘制流程函数调用链


二.几个重要的函数

1.构造函数

构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性

View的构造函数有四种重载分别如下:

public void SloopView(Context context) {}

public void SloopView(Context context,AttributeSet attrs) {}

public void SloopView(Context context,AttributeSet attrs,int defStyleAttr) {}

public void SloopView(Context context,AttributeSet attrs,int defStyleAttr,int defStyleRes) {}

可以看出,关于View构造函数的参数有多有少,先排除几个不常用的,留下常用的再研究。

有四个参数的构造函数在API21的时候才添加上,暂不考虑。

有三个参数的构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style,且只有在明确调用的时候才会生效,以系统中的ImageButton为例说明:

public ImageButton(Context context,AttributeSet attrs) {//调用了三个参数的构造函数,明确指定第三个参数this(context, attrs,com.android.internal.R.attr.imageButtonStyle);    }

publicImageButton(Context context,AttributeSet attrs, int defStyleAttr) {//此处调了四个参数的构造函数,无视即可this(context, attrs, defStyleAttr,0);    }

注意:即使你在View中使用了Style这个属性也不会调用三个参数的构造函数,所调用的依旧是两个参数的构造函数。

由于三个参数的构造函数第三个参数一般不用,暂不考虑,第三个参数的具体用法会在以后用到的时候详细介绍。

排除了两个之后,只剩下一个参数和两个参数的构造函数,他们的详情如下:

//一般在直接New一个View的时候调用。

public void SloopView(Context context) {}

//一般在layout文件中使用的时候会调用,关于它的所有属性(包括自定义属性)都会包含在attrs中传递进来。

public void SloopView(Context context,AttributeSet attrs) {}

以下方法调用的是一个参数的构造函数:

//在Avtivity中SloopViewview=newSloopView(this);

以下方法调用的是两个参数的构造函数:

//在layout文件中 - 格式为: <包名.View名 />

关于构造函数先讲这么多,关于如何自定义属性和使用attrs中的内容,在后面会详细讲解,目前只需要知道这两个构造函数在何时调用即可。

2.onAttachedToWindow():

运行在onResume()之后;此函数会调用Activity的onResume()生命周期,所以在onResume之后可以设置窗体尺寸。

3.Measure


具体分析

measure 过程由measure(int, int)方法发起,从上到下有序的测量 View,在 measure 过程的最后,每个视图存储了自己的尺寸大小和测量规格。 layout 过程由layout(int, int, int, int)方法发起,也是自上而下进行遍历。在该过程中,每个父视图会根据 measure 过程得到的尺寸来摆放自己的子视图。

measure 过程会为一个 View 及所有子节点的 mMeasuredWidth 和 mMeasuredHeight 变量赋值,该值可以通过getMeasuredWidth()和getMeasuredHeight()方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于 Measure 得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次 measure。比如,父视图可以先根据未给定的 dimension 去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure。

measure 过程传递尺寸的两个类

ViewGroup.LayoutParams (View 自身的布局参数)

MeasureSpecs 类(父视图对子视图的测量要求)

ViewGroup.LayoutParams

这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 height 和 width,你有以下选择:

具体值

MATCH_PARENT 表示子视图希望和父视图一样大(不包含 padding 值)

WRAP_CONTENT 表示视图为正好能包裹其内容大小(包含 padding 值)

ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的 ViewGroup.LayoutParams 的子类 RelativeLayoutParams。

有时我们需要使用 view.getLayoutParams() 方法获取一个视图 LayoutParams,然后进行强转,但由于不知道其具体类型,可能会导致强转错误。其实该方法得到的就是其所在父视图类型的 LayoutParams,比如 View 的父控件为 RelativeLayout,那么得到的 LayoutParams 类型就为 RelativeLayoutParams。

measure 核心方法

measure(int widthMeasureSpec, int heightMeasureSpec)

该方法定义在View.java类中,为 final 类型,不可被复写,但 measure 调用链最终会回调 View/ViewGroup 对象的onMeasure()方法,因此自定义视图时,只需要复写onMeasure()方法即可。

4.onMeasure(int widthMeasureSpec, int heightMeasureSpec)

该方法就是我们自定义视图中实现测量逻辑的方法,该方法的参数是父视图对子视图的 width 和 height 的测量要求。在我们自身的自定义视图中,要做的就是根据该 widthMeasureSpec 和 heightMeasureSpec 计算视图的 width 和 height,不同的模式处理方式不同。

Q: 为什么要测量View大小?

A: View的大小不仅由自身所决定,同时也会受到父控件的影响,为了我们的控件能更好的适应各种情况,一般会自己进行测量。

测量View大小使用的是onMeasure函数,我们可以从onMeasure的两个参数中取出宽高的相关数据:

@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec{

int widthsize=MeasureSpec.getSize(widthMeasureSpec);//取出宽度的确切数值

int widthmode=MeasureSpec.getMode(widthMeasureSpec);//取出宽度的测量模式

int heightsize=MeasureSpec.getSize(heightMeasureSpec);//取出高度的确切数值

int heightmode=MeasureSpec.getMode(heightMeasureSpec);//取出高度的测量模式

}

setMeasuredDimension()

测量阶段终极方法,在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中调用,将计算得到的尺寸,传递给该方法,测量阶段即结束。该方法也是必须要调用的方法,否则会报异常。在我们在自定义视图的时候,不需要关心系统复杂的 Measure 过程的,只需调用setMeasuredDimension()设置根据 MeasureSpec 计算得到的尺寸即可,你可以参考ViewPagerIndicator的 onMeasure 方法。

View.MeasureSpec中:

模式二进制数值描述

UNSPECIFIED00默认值,父控件没有给子view任何限制,子View可以设置为任意大小。

EXACTLY01表示父控件已经确切的指定了子View的大小。

AT_MOST10表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。

源码解析:ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解

5.确定View大小(onSizeChanged)

这个函数在视图大小发生改变时调用。

Q: 在测量完View并使用setMeasuredDimension函数之后View的大小基本上已经确定了,那么为什么还要再次确定View的大小呢?

A: 这是因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。

onSizeChanged如下:

@OverrideprotectedvoidonSizeChanged(intw,inth,intoldw,intoldh) {super.onSizeChanged(w, h, oldw, oldh);    }

可以看出,它又四个参数,分别为 宽度,高度,上一次宽度,上一次高度。

这个函数比较简单,我们只需关注 宽度(w), 高度(h) 即可,这两个参数就是View最终的大小。


6.确定子View布局位置(onLayout)

首先要明确的是,子视图的具体位置都是相对于父视图而言的。View 的 onLayout 方法为空实现,而 ViewGroup 的 onLayout 为 abstract 的,因此,如果自定义的 View 要继承 ViewGroup 时,必须实现 onLayout 函数。

在 layout 过程中,子视图会调用getMeasuredWidth()和getMeasuredHeight()方法获取到 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight,作为自己的 width 和 height。然后调用每一个子视图的layout(l, t, r, b)函数,来确定每个子视图在父视图中的位置。

确定布局的函数是onLayout,它用于确定子View的位置,在自定义ViewGroup中会用到,他调用的是子View的layout函数。

在自定义ViewGroup中,onLayout一般是循环取出子View,然后经过计算得出各个子View位置的坐标值,然后用以下函数设置子View位置。

child.layout(l, t, r, b);

四个参数分别为:

名称说明对应的函数

lView左侧距父View左侧的距离getLeft();

tView顶部距父View顶部的距离getTop();

rView右侧距父View左侧的距离getRight();

bView底部距父View顶部的距离getBottom();

具体可以参考坐标系这篇文章。

来自GcsSloop

7.draw绘制流程相关概念及核心方法

View.draw(Canvas canvas): 由于 ViewGroup 并没有复写此方法,因此,所有的视图最终都是调用 View 的 draw 方法进行绘制的。在自定义的视图中,也不应该复写该方法,而是复写onDraw(Canvas)方法进行绘制,如果自定义的视图确实要复写该方法,那么请先调用super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。

View.onDraw():

View 的onDraw(Canvas)默认是空实现,自定义绘制过程需要复写的方法,绘制自身的内容。

drawChild(canvas, this, drawingTime)

直接调用了 View 的child.draw(canvas, this,drawingTime)方法,文档中也说明了,除了被ViewGroup.drawChild()方法外,你不应该在其它任何地方去复写或调用该方法,它属于 ViewGroup。而View.draw(Canvas)方法是我们自定义控件中可以复写的方法,具体可以参考上述对view.draw(Canvas)的说明。从参数中可以看到,child.draw(canvas, this, drawingTime)肯定是处理了和父视图相关的逻辑,但 View 的最终绘制,还是View.draw(Canvas)方法。

invalidate()

请求重绘 View 树,即 draw 过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些调用了invalidate()方法的 View。

requestLayout()

当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法,在自定义的视图中,如果某些情况下希望重新测量尺寸大小,应该手动去调用该方法,它会触发measure()和layout()过程,但不会进行 draw。

dispatchDraw() 发起对子视图的绘制。View 中默认是空实现,ViewGroup 复写了dispatchDraw()来对其子视图进行绘制。该方法我们不用去管,自定义的 ViewGroup 不应该对dispatchDraw()进行复写。

Android的view组件显示主要经过mesure, layout和draw这三个过程。在mesure阶段里调用mesure(int widthSpec, int heightSpec)方法,这个方法是final不能被重写,在这个过程里会调用onMesure(int widthSpec, int heightSpec)方法。当组件设置好大小后,调用final layout(int l, int t, int r, int b)方法进行布局,在这个过程里会调用onLayout(boolean changed, int l, int t, int r, int b)方法,所以处理组件的布局通常要重写onMesure和onLayout这两个方法。

View组件的绘制会调用draw(Canvas canvas)方法,这个方法在源代码里看不到在哪里调用...draw过程中主要是先画Drawable背景,对drawable调用setBounds()然后是draw(Canvas c)方法.有点注意的是背景drawable的实际大小会影响view组件的大小,drawable的实际大小通过getIntrinsicWidth()和getIntrinsicHeight()获取,当背景比较大时view组件大小等于背景drawable的大小,不过俺没有在源代码里找到布局时调用过getIntrinsicWidth()和getIntrinsicHeight()方法...

画完背景后,draw过程会调用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,或者自定制一个Drawable,重写它的draw(Canvas c)和getIntrinsicWidth(),

getIntrinsicHeight()方法,然后设为背景。


参考文档:

公共技术点之 View 绘制流程

自定义View分类与流程

自定义View总结

推荐阅读更多精彩内容