Android的控件架构与自定义控件原理

android控件架构

Android中的每个控件都会在界面上得到一块矩形的区域,而在Android中,控件大致被分为两类,即ViewGroup 控件和View控件。ViewGroup控件作为父控件可以包含多个View控件,并管理其包含的View控件,通过ViewGroup,界面上的控件形成一个树形图,如下图.
这里写图片描述
每颗控件树的顶部,都有一个ViewParent对象,作为控制核心,来统一调度和分配所有的交互管理事件.
通过activity中的findViewById()方法,在控件树中以树的深度优先遍历来查找对应元素.并通过activity中的setContentView()来设置一个布局并将其内容显示出来.对于setContentView()具体做了什么?首先看看android的界面架构 如下图:
这里写图片描述

每个Activity都包含一个Window对象,在Android中Window对象通常由PhoneWindow来实现的,PhoneWindow将一个DecorView设置为整个应用窗口的根View,即DecorView为整个Window界面的最顶层View。也可以说DecorView将要显示的具体内容呈现在了PhoneWindow上,这里所有的view的监听事件,都由WindowManagerService来接收,并通过activity对象来回调相应的onClickListener.
上图中的ContentView是一个ID为content的FrameLayout,通过DecorView可以创建一个视图树,如下图
这里写图片描述
在视图树中的第二层装载了一个LinearLayout的ViewGroup,这一层的布局结构根据不同的参数设置不同的布局,比如图中的布局:上面显示TitalBar 下面显示Content,如果通过设置requestWindowFeature(Window.FEATURE_NO_TITLE)来全屏显示,视图树中就只显示Content了,这就是requestWindowFeature(Window.FEATURE_NO_TITLE)必须在setContentView之前调用才会生效的原因.在代码中,当程序调用onCreate中setContentView后,ActivityManagerService会回调onResume方法,此时系统才将整个DecorView添加到PhoneWindow中,并让其显示出来,最终完成界面的绘制.
总结:
1 View表示的的屏幕上的某一块矩形的区域,而且所有的View都是矩形的;
2 View是不能添加子View的,而ViewGroup是可以添加子View的。ViewGroup之所以能够添加子View,是因为它实现了两个接口:ViewParent 和 ViewManager;
3 Activity之所以能加载并且控制View,是因为它包含了一个Window,所有的图形化界面都是由View显示的而Service之所以称之为没有界面的activity是因为它不包含有Window,不能够加载View;
4 一个View有且只能有一个父View;
5 在Android中Window对象通常由PhoneWindow来实现的,PhoneWindow将一个DecorView设置为整个应用窗口的根View,即DecorView为整个Window界面的最顶层View。也可以说DecorView将要显示的具体内容呈现在了PhoneWindow上;
6 DecorView是FrameLayout的子类,它继承了FrameLayout,即顶层的FrameLayout的实现类是Decorview,它是在phoneWindow里面创建的;
7 顶层的FrameLayout的父view是Handler,Handler的作用除了线程之间的通讯以外,还可以跟WindowManagerService进行通讯;
8 windowManagerService是后台的一个服务,它控制并且管理者屏幕;
9 一个应用可以有很多个window,其由windowManager来管理,而windowManager又由windowManagerService来管理;

自定义控件的绘制原理

绘制自定义控件的三步骤:1.测量measure, 2.布局layout, 3.绘制draw。
这里写图片描述
1、Measure测量一个View的大小
2、Layout摆放一个View的位置
3、Draw画出View的显示内容
其中measure和layout方法都是final的,无法重写,虽然draw不是final的,但是也不建议重写该方法。
这三个方法都已经写好了View的逻辑,如果我们想实现自身的逻辑,而又不破坏View的工作流程,可以重写onMeasure、onLayout、onDraw方法。下面来一一介绍这三个方法。
#View的测量
Android系统在绘制View之前,必须对View进行测量,即告诉系统该画一个多大的View,这个过程在onMeasure()方法中进行。测量过程如下图所示:
这里写图片描述
主要方法有:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

measure调用onMeasure来测量宽度、高度,然后调用setMeasureDimension()保存测量结果,其中measure,setMeasureDimension是final类型,view的子类不需要重写,onMeasure在view的子类中重写。

关于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);

其中测量模式有以下三种:
(1) UPSPECIFIED :父容器对于子容器没有任何限制,因为它不指定其大小测量的模式,子容器想要多大就多大.通常情况下在绘制自定义View时才会使用。
(2) EXACTLY父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间.
即精确值模式,当控件的layout_width属性或layout_height属性指定为具体数值时,例如android:layout_width=”100dp”,或者指定为match_parent属性时,系统使用的是EXACTLY 模式。
(3) AT_MOST 子容器可以是声明大小内的任意大小.
即最大值模式,当控件的layout_width属性或layout_height属性指定为warp_content时,控件大小一般随着控件的子控件或者内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。

View默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式,且控件只可以响应你指定的具体宽高值或者是match_parent属性。如果要让自定义的View支持wrap_content属性,那么就必须重写onMeasure()方法来指定wrap_content时的大小。
而通过上面介绍的MeasureSpec这个类,我们就可以获取View的测量模式和View想要绘制的大小。
下图是MeasureSpec判定规则,不同的测量模式,自定义View时会给出不同的测量值.
这里写图片描述

#View布局
布局是用于设置视图在屏幕中显示的位置,其流程图如下:
这里写图片描述

View layout过程相关方法主要要三个:
public void layout(int l, int t, int r, int b)
protected boolean setFrame(int left, int top, int right, int bottom)
protected void onLayout(boolean changed, int left, int top, int right, int bottom)

layout通过调用setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,onLayout一般只会在自定义ViewGroup中才会使用
  • Layout

    ① Layout方法中接受四个参数l,t,r,b,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置时通常会根据子View在measure中测量的大小来决定。
    ② 子View的位置通常还受有其他属性左右,例如父View的orientation,gravity,自身的margin等等,特别是RelativeLayout,影响布局的因素非常多。
    ③ layout方法虽然可以被复写,但是不建议去复写,我们可以直接调用layout方法去确定自身的位置, 而且可以去复写onLayout方法去确定子view的位置
    
  • setFrame

    ① setFrame方法是一个隐藏方法,该方法体内部通过比对本次的l、t、r、b四个值与上次是否相同来判断自身的位置和大小是否发生了改变。如果发生了改变,将会调用invalidate请求重绘。
    ② 记录本次的l、t、r、b,用于下次比对。
    ③  如果大小发生了变化,onSizeChanged方法,该方法在大多数View中都是空实现,我们可以重写该方法用于监听View大小发生变化的事件。
    
  • onLayout

    ① onLayout是ViewGroup用来决定子View摆放位置的,各种布局的差异都在该方法中得到了体现。
    ② onLayout比layout多一个参数,changed,该参数是在setFrame通过比对上次的位置得出是否发生了变化,通常该参数没有被使用的意义,因为父View位置和大小不变,并不能代表子View的位置和大小没有发生改变。
    

    View的绘制

    draw过程主要用于利用前两步得到的参数,将视图显示在屏幕上,到这里也就完成了整个的视图绘制工作。流程图如下:
    这里写图片描述

public void draw(Canvas canvas)
protected void onDraw(Canvas canvas)

通过调用draw函数进行视图绘制,在View类中onDraw函数是个空函数,最终的绘制需求需要在自定义的onDraw函数中进行实现,比如ImageView完成图片的绘制,如果自定义ViewGroup这个函数则不需要重载。
  • draw

    由ViewRoot的performTraversals方法发起,调用DecorView的draw方法,并把成员变量canvas传给给draw方法。而在后面draw遍历中,传递的都是同一个canvas。所以android的绘制是同一个window中的所有View都绘制在同一个画布上。等绘制完成,将会通知WMS把canvas上的内容绘制到屏幕上。自定义View时一般不重写该方法.
    
  • onDraw
    View用来绘制自身的实现方法,自定义View,通常需要重载该方法。比如TextView中在该方法中绘制文字、光标和CompoundDrawable;ImageView中在该方法中绘制图片,
    所以当我们测量好了一个View之后,最终的绘制要在onDraw函数中进行实现,重写onDraw()这个方法,并在指定的Canvas对象上来绘制所需要的图形。在onDraw()中就有一个参数,该参数就是Canvas canvas对象,使用这个对象进行绘图操作,并显示出来.;
    而如果在其他地方想绘图并通过onDraw方法显示出来,通常需要使用代码创建一个Canvas对象,并传入一个bitmap对象,

Canvas canvas = new Canvas(bitmap);

之所以要传入一个bitmap,是因为传进来的bitmap与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程称之为装载画布。这个bitmap用来存储所有绘制在Canvas上的像素信息,通过这种方式创建了Canvas对象后,后面调用的Canvas.drawXXX方法都发生在这个bitmap上.
例如:在View类的onDraw()方法中,通过下面的代码,让canvas与bitmap发生直接的联系:

canvas.drawBitmap(bitmap, 0, 0, null);

然后将bitmap装载到另外一个Canvas对象中:

Canvas mCanvas = new Canvas(bitmap);

通过mCanvas.drawXXX方法在装载了bitmap的mCanvas对象上进行绘图,从而mCanvas将绘制效果作用在了bitmap上,再通过invalidate()刷新的时候,我们就会发现通过onDraw()方法画出来的bitmap已经发生了改变。这是因为bitmap承载了mCanvas上所进行的绘图操作.所以虽然没有直接将图形绘制在onDraw()方法指定的画布上,但是通过改变bitmap.让View重绘,从而显示改变之后的bitmap.

  • dispatchDraw

    dispatchDraw的逻辑其实比较复杂,主要ViewGroup对子View进行绘制事件的派遣分发,ViewGroup已经处理好了,我们不必要重载该方法对。重写时,不要注释super.方法
    

猜你喜欢

转载自blog.csdn.net/dakaniu/article/details/78612922