自定义View

来源:互联网 发布:js alert 编辑:程序博客网 时间:2024/06/06 03:43

前言

我们知道安卓中已经给我们提供了很多的控件,我们可以直接使用,但是很多时候我们也需要自己定义一个View来满足我们项目的需求。那我们怎么来自定义一个View呢,自定义一个View要经历那些过程,接下来,我们一点一点揭秘。

实现自定义View的基本步骤

首先,从大方向来讲,自定义View一共只需要以下三个步骤:

  • 测量(onMesure)
  • 布局(onLayout)
  • 绘制(onDraw)

然后就是实现自定义View的方法一共有三种:

  • 继承View
  • 继承ViewGroup
  • 继承已经定义好的View控件

在了解了这些东西以后,我们就要从源码的角度来揭开自定义View的面纱。内容不算多,跟着我的思路一步一步走就好了,大方向就是刚才说的三个步骤。

源码分析

首先,并不是所有的View都需要重新测量,所以,自定义view的绘制流程是从ViewRootImpl类(ViewRoot的实现类)中的performTraversals()方法开始的,这个方面主要是判断一下rootview本身是否需要重新测量,布局以及绘制,假设需要就就执行相应的动作。假设重新测量会有一个MeasureSpec,通常这个测量控件有的specMode为EXACTLY(已经精确测量),specSize为windowSize,所以我们的根视图通常是全屏的。

测量流程

这里有几个重要的概念:

  • 1.ViewGroup.LayoutParams :用来指定视图高度和宽度的参数,不包括Padding。
  • 2.MeasureSpec :表示测量规格。32位的Int值,高二位表示模式,后三十位表示测量规格的大小。
    • 测量模式有如下三种:
      • UNSPECIFIED:不限定子视图尺寸大小。
      • EXACTLY:父容器会为子视图确定一个尺寸大小,无论子视图要求多大,都要在父容器的限制内。
      • AT_MOST:父容器会为子视图指定一个最大的尺寸,子视图所有的大小都必须在这个尺寸范围内。对应wrap_content。此时父容器无法获取子视图的大小,只能子视图自己根据需求设定。

ViewGroup.LayoutParams最终会被封装成MeasureSpec。

  • ViewRoot根对象的属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:
    • 设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth) ;

onMeasure()方法:该方法会调用setMeasuredDimension()方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}

setMeasuredDimension():该方法完成整个的测量过程。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {        boolean optical = isLayoutModeOptical(this);        if (optical != isLayoutModeOptical(mParent)) {            Insets insets = getOpticalInsets();            int opticalWidth  = insets.left + insets.right;            int opticalHeight = insets.top  + insets.bottom;            measuredWidth  += optical ? opticalWidth  : -opticalWidth;            measuredHeight += optical ? opticalHeight : -opticalHeight;        }        setMeasuredDimensionRaw(measuredWidth, measuredHeight);}
  • 如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。 对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。整个measure调用流程就是个树形的递归过程。
  • Android中还有一个特殊的机制,就是当父视图认为子视图给它传递的宽高有异常,它会再次请求子视图去进行测量,如果子视图传递的宽高超过了父视图的约束范围,则父视图会使用一个确切的大小,给子视图设置成AT_MOST或者EXACTLY的形式,再次对子视图进行测量。

布局流程

  • layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局);
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {    }
  • 如果该View是个ViewGroup类型,需要遍历每个子视图childView,调用该子视图的layout()方法去设置它的坐标值。
@Overrideprotected abstract void onLayout(boolean changed, int l, int t, int r, int b);

绘制过程

源码中一共分了六步:

  • 第一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。
  • 第二步 一般跳过
  • 第一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的
  • 第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码
  • 第五步 一般跳过
  • 第六步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。
public void draw(Canvas canvas) {    final int privateFlags = mPrivateFlags;    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;    /*    * Draw traversal performs several drawing steps which must be executed    * in the appropriate order:    *    *      1. Draw the background    *      2. If necessary, save the canvas' layers to prepare for fading    *      3. Draw view's content    *      4. Draw children    *      5. If necessary, draw the fading edges and restore layers    *      6. Draw decorations (scrollbars for instance)    */    // Step 1, draw the background, if needed    int saveCount;    if (!dirtyOpaque) {        drawBackground(canvas);    }    // skip step 2 & 5 if possible (common case)    final int viewFlags = mViewFlags;    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;    if (!verticalEdges && !horizontalEdges) {        // Step 3, draw the content        if (!dirtyOpaque) onDraw(canvas);        // Step 4, draw the children        dispatchDraw(canvas);        // Overlay is part of the content and draws beneath Foreground        if (mOverlay != null && !mOverlay.isEmpty()) {            mOverlay.getOverlayView().dispatchDraw(canvas);        }        // Step 6, draw decorations (foreground, scrollbars)        onDrawForeground(canvas);        // we're done...        return;    }   ...

OK,以上就是自定义View的三大过程,面试的时候最好根据自己的项目来谈一谈。

原创粉丝点击