自定义View 篇一--------《自定义View流程分析》

Posted 杨道龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义View 篇一--------《自定义View流程分析》相关的知识,希望对你有一定的参考价值。

本文部分内容参考自掘金网:点击打开链接

坐标图解:


概述

android已经为我们提供了大量的View供我们使用,但是可能有时候这些组件不能满足我们的需求,这时候就需要自定义控件了。自定义控件对于初学者总是感觉是一种复杂的技术。因为里面涉及到的知识点会比较多。但是任何复杂的技术后面都是一点点简单知识的积累。通过对自定义控件的学习去可以更深入的掌握android的相关知识点,所以学习android自定义控件是很有必要的。所以,今天写的是怎么去自定义一个控件。而不是里面涉及到的细化知识点。一个东西我们先知道怎么用,再去问为什么。

自定义控件需要考虑的点

根据Android Developers官网的介绍,自定义控件你需要以下的步骤。(根据你的需要,某些步骤可以省略)

1、创建View

2、处理View的布局

3、绘制View

4、与用户进行交互

5、优化已定义的View

上面列出的五项就是android官方给出的自定义控件的步骤。每个步骤里面又包括了很多细小的知识点。我们可以记住这五个点,并且了解每个点里包含的小知识点。再加上一些自定义控件的练习。不断的将这些知识熟练于心,相信我们每个人都能够定义出优秀的自定义控件。接下来我们开始对上面列出的5个要点进行细化解说

自定义控件5个要点详细说明

1、创建View

继承View

对Android有一些了解的朋友都知道,android为我们提供的很多View都是继承于View的。所以我们自定义的View当然也是继承于View,当然如果你要自定义的View拥有某些android已经提供的控件的功能,你可以直接继承于已经提供的控件。

我们在使用android提供的控件的时候,我们在.xml文件中编辑了一个控件,在运行的时候就能够看到和获得这个控件。我们自定义的控件当然也要支持配置和一些自定义属性,所以下面的构造方法就必须有了。这个构造方法允许我们在.xml文件中创建和编辑我们自定义控件的实例。

上面说了那么多其实就是下面一段代码。

class PieChart extends View {
    public PieChart(Context context, AttributeSet attrs) {       
        super(context, attrs);    } }

定义自定义属性

自定义属性,参考自定义View专栏文章: 自定义View 篇二--------《自定义属性》

2、处理View的布局.

在开始写这一部分之前,穿插入个人之前总结的View的绘制原理的相关知识。

View的绘制原理:

1).测量-measure()---onMeasure();

2).指定在屏幕的位置--layout()--onLayout()

 子类只有建议权,父类才有决定权 

 一般view中不使用,并且源码中是空的方法;

 ViewGroup中该方法是抽象的,必须要实现,因为要指定位置孩子的位置

3).绘制控件到屏幕上--draw()---onDraw()

自定义View的时候一般重新onMeasure(int,int)和onDraw(canvas);

基本操作由三个函数完成:measure()、layout()、draw(),其内部又分别包含了onMeasure()、onLayout()、onDraw()三个子方法。具体操作如下:

1)、measure操作

     measure操作主要用于计算视图的大小,即视图的宽度和长度。在view中定义为final类型,要求子类不能修改。measure()函数中又会调用下面的函数:


    (1)onMeasure(),视图大小的将在这里最终确定,也就是说measure只是对onMeasure的一个包装,子类可以覆写onMeasure()方法实现自己的计算视图大小的方式,并通过setMeasuredDimension(width, height)保存计算结果。

2)、layout操作

     layout操作用于设置视图在屏幕中显示的位置。在view中定义为final类型,要求子类不能修改。layout()函数中有两个基本操作:

     (1)setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,该函数用于将这些参数保存起来;

     (2)onLayout(),在View中这个函数什么都不会做,提供该函数主要是为viewGroup类型布局子视图用的;

3)、draw操作

     draw操作利用前两部得到的参数,将视图显示在屏幕上,到这里也就完成了整个的视图绘制工作。子类也不应该修改该方法,因为其内部定义了绘图的基本操作:

     (1)绘制背景;

     (2)如果要视图显示渐变框,这里会做一些准备工作;

     (3)绘制视图本身,即调用onDraw()函数。在view中onDraw()是个空函数,也就是说具体的视图都要覆写该函数来实现自己的显示(比如TextView在这里实现了绘制文字的过程)。而对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的,其包含了多个子view,而子View已经实现了自己的绘制方法,因此只需要告诉子view绘制自己就可以了,也就是下面的dispatchDraw()方法;

     (4)绘制子视图,即dispatchDraw()函数。在view中这是个空函数,具体的视图不需要实现该方法,它是专门为容器类准备的,也就是容器类必须实现该方法;

     (5)如果需要(应用程序调用了setVerticalFadingEdge或者setHorizontalFadingEdge),开始绘制渐变框;

     (6)绘制滚动条;

      从上面可以看出自定义View需要最少覆写onMeasure()和onDraw()两个方法。

ViewGroup中的扩展操作:

     首先Viewgroup是一个抽象类。

1)、对子视图的measure过程

     (1)measureChildren(),内部使用一个for循环对子视图进行遍历,分别调用子视图的measure()方法;

     (2)measureChild(),为指定的子视图measure,会被 measureChildren调用;

     (3)measureChildWithMargins(),为指定子视图考虑了margin和padding的measure;

      以上三个方法是ViewGroup提供的3个对子view进行测量的参考方法,设计者需要在实际中首先覆写onMeasure(),之后再对子view进行遍历measure,这时候就可以使用以上三个方法,当然也可以自定义方法进行遍历。

2)、对子视图的layout过程

     在ViewGroup中onLayout()被定义为abstract类型,也就是具体的容器必须实现此方法来安排子视图的布局位置,实现中主要考虑的是视图的大小及视图间的相对位置关系,如gravity、layout_gravity。

3、对子视图的draw过程

   (1)dispatchDraw(),该方法用于对子视图进行遍历然后分别让子视图分别draw,方法内部会首先处理布局动画(也就是说布局动画是在这里处理的),如果有布局动画则会为每个子视图产生一个绘制时间,之后再有一个for循环对子视图进行遍历,来调用子视图的draw方法(实际为下边的drawChild());

    (2)drawChild(),该方法用于具体调用子视图的draw方法,内部首先会处理视图动画(也就是说视图动画是在这里处理的),之后调用子视图的draw()。

    从上面分析可以看出自定义viewGroup的时候需要最少覆写onMeasure()和onLayout()方法,其中onMeasure方法中可以直接调用measureChildren等已有的方法,而onLayout方法就需要设计者进行完整的定义;一般不需要覆写以dispatchDraw()和drawChild()这两个方法,因为上面两个方法已经完成了基本的事情。但是可以通过覆写在该基础之上做一些特殊的效果,比如

其他

      从以上分析可以看出View树的绘制是一个递归的过程,从ViewGroup一直向下遍历,直到所有的子view都完成绘制,那这一切的源头在什么地方(是谁最发起measure、layout和draw的)?当然就是在View树的源头了——ViewRoot!,ViewRoot中包含了窗口的总容器DecorView,ViewRoot中的performTraversal()方法会依次调用decorView的measure、layout、draw方法,从而完成view树的绘制。

     invalidate()方法

     invalidate()方法会导致View树的重新绘制,而且view中的状态标志mPrivateFlags中有一个关于当前视图是否需要重绘的标志位DRAWN,也就是说只有标志位DRAWN置位的视图才需要进行重绘。当视图调用invalidate()方法时,首先会将当前视图的DRAWN标志置位,之后有一个循环调用parent.invalidateChildinParent(),这样会导致从当前视图依次向上遍历直到根视图ViewRoot,这个过程会将需要重绘的视图标记DRAWN置位,之后ViewRoot调用performTraversals()方法,完成视图的绘制过程。

测量

一个View是在展示时总是有它的宽和高,我画的View是一个大象大小还是一个蚂蚁大小,因此必须先确定下来。测量View就是为了能够让自定义的控件能够根据各种不同的情况以合适的宽高去展示。提到测量就必须要提到onMeasure方法了。onMeasure方法是一个view确定它的宽高的地方。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    }

onMeasure方法里有两个重要的参数, widthMeasureSpec, heightMeasureSpec。在这里你只需要记住它们包含了两个信息:mode和size 
我们可以通过以下代码拿到mode和size

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

那么获取到的mode和size又代表了什么呢? 
mode代表了我们当前控件的父控件告诉我们控件,你应该按怎样的方式来布局。
mode有三个可选值:EXACTLY, AT_MOST, UNSPECIFIED。它们的含义是:

EXACTLY:父控件告诉我们子控件了一个确定的大小,你就按这个大小来布局。比如我们指定了确定的dp值和macth_parent的情况。 
AT_MOST:当前控件不能超过一个固定的最大值,一般是wrap_content的情况。 
UNSPECIFIED:当前控件没有限制,要多大就有多大,这种情况很少出现。

size其实就是父布局传递过来的一个大小,父布局希望当前布局的大小。

下面是一个重写onMeasure的固定伪代码写法:

if mode is EXACTLY{
     父布局已经告诉了我们当前布局应该是多大的宽高, 
    所以我们直接返回从measureSpec中获取到的size }else{     计算出希望的desiredSize    
    if mode is AT_MOST          返回desireSize和specSize当中的最小值    
    else:          返回计算出的desireSize }

上面的代码虽然基本都是固定的,但是需要写的步骤还是有点多,如果你不想自己写,你也可以用android为我们提供的工具方法:resolveSizeAndState,该方法需要传入两个参数:我们测量的大小和父布局希望的大小,它会返回根据各种情况返回正确的大小。这样我们就可以不需要实现上面的模版,只需要计算出想要的大小然后调用resolveSizeAndState。之后在做自定义View的时候我会展示用这个方法来确定view的大小。

计算出height和width之后在onMeasure中别忘记调用setMeasuredDimension()方法。否则会出现运行时异常。

计算一些自定义控件需要的值 onSizeChange()

onSizeChange() 方法在view第一次被指定了大小值、或者view的大小发生改变时会被调用。所以一般用来计算一些位置和与view的size有关的值。

3、绘制View(Draw)

一旦自定义控件被创建并且测量代码写好之后,接下来你就可以实现onDraw()来绘制View了,onDraw方法包含了一个Canvas叫做画布的参数,onDraw()简单来说就两点: 
Canvas决定要去画什么 
Paint决定怎么画

比如,Canvas提供了画线方法,Paint就来决定线的颜色。Canvas提供了画矩形,Paint又可以决定让矩形是空心还是实心。

在onDraw方法中开始绘制之前,你应该让画笔Paint对象的信息初始化完毕。是因为View的这重新绘制是比较频繁的,这就可能多次调用onDraw,所以初始化的代码不应该放在onDraw方法里。

Canvas和Paint提供的很多方法在本文中就不一一列举了。大家可以自己去查看api,之后的文章中我们也会用到,现在你只需要理解定义的大体步骤,然后再慢慢锻炼加深理解。

4、与用户进行交互

也许某些情况你的自定义控件不仅仅只是展示一个漂亮的内容,还需要支持用户点击,拖动等等操作,这时候我们的自定义控件就需要做用户交互这一步骤了。

在android系统中最常见的事件就是触摸事件了,它会调用view的onTouchEvent(android.view.MotionEvent).重写这个方法去处理我们的事件逻辑

  @Override
   public boolean onTouchEvent(MotionEvent event) {    
       return super.onTouchEvent(event);   }

对与onTouchEvent方法相信大家都有一定了解,如果不了解的话,你就先记住这是处理Touch的地方。

现在的触控有了更多的手势,比如轻点,快速滑动等等,所以在支持特殊用户交互的时候你需要用到android提供的GestureDetector.你只需要实现GestureDetector中相对应的接口,并且处理相应的回调方法。

除了手势之外,如果有移动之类的情况我们还需要让滑动的动画显示得比较平滑。动画应该是平滑的开始和结束,而不是突然消失突然开始。在这种情况下,我们需要用到属性动画 property animation framework

由于与用户进行交互中涉及到的知识举例子会比较多,所以我在之后的自定义控件文章中再讲解。

5、优化你的自定义View

在上面的步骤结束之后,其实一个完善的自定义控件已经出来了。接下来你要做的只是确保自定义控件运行得流畅,官方的说法是:为了避免你的控件看得来迟缓,确保动画始终保持每秒60帧.

下面是官网给出的优化建议:

1、避免不必要的代码 
2、在onDraw()方法中不应该有会导致垃圾回收的代码。 
3、尽可能少让onDraw()方法调用,大多数onDraw()方法调用都是手动调用了invalidate()的结果,所以如果不是必须,不要调用invalidate()方法。

总结

到这里基本上自定义控件的大致步骤和可能涉及到的知识点都说完了。看一张图。


图片基本描述了自定义控件的大致流程,右边是相对应的流程所涉及到的一些知识点。可以看到自定义控件包括了很多android知识。网上还有一张自定义View的图片比较清晰,如下:

详解Android主流框架不可或缺的基石

探究drawable图片的加载原理和缩放规律

自定义View实现跟随手指的小球

Android自定义View(三深入解析控件测量onMeasure)

自定义View第一篇(view生命周期的简介)

自定义View第一篇(view生命周期的简介)