首页 > 代码库 > 自定义控件浅析

自定义控件浅析

1、View、Activity、ViewGroup关系

1.1、View概念

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

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

1.2、View和Activity的区别

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

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

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

1.3、View的种类

android提供了种类丰富的View来应对各种需求,例如提供文字显示的TextView,提供点击事件的Button,提供图片显示的ImageView,还有各种布局文件,例如Relativilayout,Linearlayout等等。他们都是继承自View。详见下图(请自行想办法放大):

 技术分享

 

1.4、ViewGroup

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

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

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

1.5、为什么要有ViewGroup?

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

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

1.6、View树

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

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

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

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

1.7、View树示意图

 技术分享

 

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

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

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

 技术分享

 

在android系统里面所有所有的view展示到界面上。都必须有如下步骤:

 技术分享

 

相关源码:

1、画多大

 技术分享

技术分享

 

 

2、画在什么位置

 技术分享

技术分享

技术分享

 

 

 

3、如何画?

在draw方法中有如下代码:

 技术分享

 

2.1、Measure测量流程

 技术分享

 

onMeasure()方法主要是测量子View的大小。

measure是一个final方法,用来测量View自身的大小,View类该方法体逻辑比较简单,只是根据判断条件决定是否需要调用onMeasure。

方法接受两个参数,分别就是通过MeasureSpec类合成测量模式和大小的宽与高

2.1.1、MeasureSpec

MeasureSpe描述了父View对子View大小的期望。里面包含了测量模式和大小。

MeasureSpe类把测量模式和大小组合到一个int型的数值中,其中高2位表示模式,低30位表示大小。

我们可以通过以下方式从MeasureSpec中提取模式和大小,该方法内部是采用位移计算。

int mode= MeasureSpec.getMode(measureSpec);

int size = MeasureSpec.getSize(measureSpec);

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

 技术分享

 技术分享

 

 

2.2、View的onMeasure方法

onMeasure通过父View传递过来的大小和模式,以及自身的背景图片的大小得出自身最终的大小,通过setMeasuredDimension()方法设置给mMeasuredWidth和mMeasuredHeight。

普通View的onMeasure逻辑大同小异,基本都是测量自身内容和背景,然后根据父View传递过来的MeasureSpec进行最终的大小判定,例如TextView会根据文字的长度,文字的大小,文字行高,文字的行宽,显示方式,背景图片,以及父View传递过来的模式和大小最终确定自身的大小。

2.3、ViewGroup的onMeasure

ViewGroup是个抽象类,本身没有实现onMeasure,但是他的子类都有各自的实现,通常他们都是通过measureChildWithMargins函数或者其他类似于measureChild的函数来遍历测量子View,被GONE的子View将不参与测量,当所有的子View都测量完毕后,才根据父View传递过来的模式和大小来最终决定自身的大小。

在测量子View时,会先获取子View的LayoutParams,从中取出宽高,如果是大于0,将会以精确的模式加上其值组合成MeasureSpec传递子View,如果是小于0,将会把自身的大小或者剩余的大小传递给子View,其模式判定在前面已经讲过。

ViewGroup一般都在测量完所有子View后才会调用setMeasuredDimension()设置自身大小。

2.4、layout布局流程

 技术分享

 

onLayout()方法主要是摆放子View的位置

Layout方法中接受四个参数,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置时通常会根据子View在measure中测量的大小来决定。

子View的位置通常还受有其他属性左右,例如父View的orientation,gravity,自身的margin等等,特别是RelativeLayout,影响布局的因素非常多。

2.4.1、setFrame

setFrame方法是一个隐藏方法,所以作为应用层程序员来说,无法重写该方法。该方法体内部通过比对本次的l、t、r、b四个值与上次是否相同来判断自身的位置和大小是否发生了改变,如果发生了改变,将会调用invalidate请求重绘。

如果大小发生了变化,则调用onSizeChanged方法,该方法在大多数View中都是空实现,程序员可以重写该方法用于监听View大小发生变化的事件,在可以滚动的视图中重载了该方法,用于重新根据大小计算出需要滚动的值,以便显示之前显示的区域。

2.4.2、onLayout

onLayout是ViewGroup用来决定子View摆放位置的,各种布局的差异都在该方法中得到了体现。

onLayout比layout多一个参数,changed,该参数是在setFrame通过比对上次的位置得出是否发生了变化,通常该参数没有被使用的意义,因为父View位置和大小不变,并 改变。

2.5、Draw绘制流程

 技术分享

 

2.5.1、draw

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

draw的流程:

      1.绘制背景

      2.绘制渐变效果(通常不绘制)

      3.调用onDraw

      4.调用dispatchDraw

      5.调用onDrawScrollBars

2.5.2、onDraw

View用来绘制自身的实现方法,如果我们想要自定义View,通常需要重载该方法。

TextView中在该方法中绘制文字、光标和CompoundDrawable

ImageView中相对简单,只是绘制了图片

2.5.3、dispatchDraw

先根据自身的padding剪裁画布,所有的子View都将在画布剪裁后的区域绘制。

遍历所有子View,调用子View的computeScroll对子View的滚动值进行计算。

根据滚动值和子View在父View中的坐标进行画布原点坐标的移动,根据子在父View中的坐标计算出子View的视图大小,然后对画布进行剪裁,请看下面的示意图。

dispatchDraw的逻辑其实比较复杂,但是幸运的是对子View流程都采用该方式,而ViewGroup已经处理好了,我们不必要重载该方法对子View进行绘制事件的派遣分发。

2.6、动画的绘制

动画就是让画面“动”起来,其原理就是不断的绘制,但是每次绘制都有区别。

在ViewGroup的drawChild方法中会判断child是否包含动画,如果包含,则根据动画类计算出动画执行的区域矩形,判断动画是否启动了,启动了就获取动画当前的值,例如位移值等等。然后根据值对画布进行剪裁调整,执行子View的draw进行绘制。

判断动画是否结束,如果没有,则调用invalidate再次请求绘制。

 

自定义控件浅析