您好,欢迎访问一九零五行业门户网

Android自定义View

前言
android自定义view的详细步骤是我们每一个android开发人员都必须掌握的技能,因为在开发中总会遇到自定义view的需求。为了提高自己的技术水平,自己就系统的去研究了一下,在这里写下一点心得,有不足之处希望大家及时指出。
流程
在android中对于布局的请求绘制是在android framework层开始处理的。绘制是从根节点开始,对布局树进行measure与draw。在rootviewimpl中的performtraversals展开。它所做的就是对需要的视图进行measure(测量视图大小)、layout(确定视图的位置)与draw(绘制视图)。下面的图能很好的展现视图的绘制流程:
当用户调用requestlayout时,只会触发measure与layout,但系统开始调用时还会触发draw
下面来详细介绍这几个流程。
measure
measure是view中的final型方法不可以进行重写。它是对视图的大小进行测量计算,但它会回调onmeasure方法,所以我们在自定义view的时候可以重写onmeasure方法来对view进行我们所需要的测量。它有两个参数widthmeasurespec与heightmeasurespec。其实这两个参数都包含两部分,分别为size与mode。size为测量的大小而mode为视图布局的模式
我们可以通过以下代码分别获取:
int widthsize = measurespec.getsize(widthmeasurespec); int heightsize = measurespec.getsize(heightmeasurespec); int widthmode = measurespec.getmode(widthmeasurespec); int heightmode = measurespec.getmode(heightmeasurespec);
获取到的mode种类分为以下三种:
setmeasureddimension
通过以上逻辑获取视图的宽高,最后要调用setmeasureddimension方法将测量好的宽高进行传递出去。其实最终是调用setmeasureddimensionraw方法对传过来的值进行属性赋值。调用super.onmeasure()的调用逻辑也是一样的。
下面以自定义一个验证码的view为例,它的onmeasure方法如下:
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { int widthsize = measurespec.getsize(widthmeasurespec); int heightsize = measurespec.getsize(heightmeasurespec); int widthmode = measurespec.getmode(widthmeasurespec); int heightmode = measurespec.getmode(heightmeasurespec); if (widthmode == measurespec.exactly) { //直接获取精确的宽度 width = widthsize; } else if (widthmode == measurespec.at_most) { //计算出宽度(文本的宽度+padding的大小) width = bounds.width() + getpaddingleft() + getpaddingright(); } if (heightmode == measurespec.exactly) { //直接获取精确的高度 height = heightsize; } else if (heightmode == measurespec.at_most) { //计算出高度(文本的高度+padding的大小) height = bounds.height() + getpaddingbottom() + getpaddingtop(); } //设置获取的宽高 setmeasureddimension(width, height); }
可以对自定义view的layout_width与layout_height进行设置不同的属性,达到不同的mode类型,就可以看到不同的效果
measurechildren
如果你是对继承viewgroup的自定义view那么在进行测量自身的大小时还要测量子视图的大小。一般通过measurechildren(int widthmeasurespec, int heightmeasurespec)方法来测量子视图的大小。
protected void measurechildren(int widthmeasurespec, int heightmeasurespec) { final int size = mchildrencount; final view[] children = mchildren; for (int i = 0; i < size; ++i) { final view child = children[i]; if ((child.mviewflags & visibility_mask) != gone) { measurechild(child, widthmeasurespec, heightmeasurespec); } } }
通过上面的源码会发现,它其实是遍历每一个子视图,如果该子视图不是隐藏的就调用measurechild方法,那么来看下measurechild源码:
protected void measurechild(view child, int parentwidthmeasurespec, int parentheightmeasurespec) { final layoutparams lp = child.getlayoutparams(); final int childwidthmeasurespec = getchildmeasurespec(parentwidthmeasurespec, mpaddingleft + mpaddingright, lp.width); final int childheightmeasurespec = getchildmeasurespec(parentheightmeasurespec, mpaddingtop + mpaddingbottom, lp.height); child.measure(childwidthmeasurespec, childheightmeasurespec); }
会发现它首先调用了getchildmeasurespec方法来分别获取宽高,最后再调用的就是view的measure方法,而通过前面的分析我们已经知道它做的就是对视图大小的计算。而对于measure中的参数是通过getchildmeasurespec获取,再来看下其源码:
public static int getchildmeasurespec(int spec, int padding, int childdimension) { int specmode = measurespec.getmode(spec); int specsize = measurespec.getsize(spec); int size = math.max(0, specsize - padding); int resultsize = 0; int resultmode = 0; switch (specmode) { // parent has imposed an exact size on us case measurespec.exactly: if (childdimension >= 0) { resultsize = childdimension; resultmode = measurespec.exactly; } else if (childdimension == layoutparams.match_parent) { // child wants to be our size. so be it. resultsize = size; resultmode = measurespec.exactly; } else if (childdimension == layoutparams.wrap_content) { // child wants to determine its own size. it can't be // bigger than us. resultsize = size; resultmode = measurespec.at_most; } break; // parent has imposed a maximum size on us case measurespec.at_most: if (childdimension >= 0) { // child wants a specific size... so be it resultsize = childdimension; resultmode = measurespec.exactly; } else if (childdimension == layoutparams.match_parent) { // child wants to be our size, but our size is not fixed. // constrain child to not be bigger than us. resultsize = size; resultmode = measurespec.at_most; } else if (childdimension == layoutparams.wrap_content) { // child wants to determine its own size. it can't be // bigger than us. resultsize = size; resultmode = measurespec.at_most; } break; // parent asked to see how big we want to be case measurespec.unspecified: if (childdimension >= 0) { // child wants a specific size... let him have it resultsize = childdimension; resultmode = measurespec.exactly; } else if (childdimension == layoutparams.match_parent) { // child wants to be our size... find out how big it should // be resultsize = view.susezerounspecifiedmeasurespec ? 0 : size; resultmode = measurespec.unspecified; } else if (childdimension == layoutparams.wrap_content) { // child wants to determine its own size.... find out how // big it should be resultsize = view.susezerounspecifiedmeasurespec ? 0 : size; resultmode = measurespec.unspecified; } break; } //noinspection resourcetype return measurespec.makemeasurespec(resultsize, resultmode); }
是不是容易理解了点呢。它做的就是前面所说的根据mode的类型,获取相应的size。根据父视图的mode类型与子视图的layoutparams类型来决定子视图所属的mode,最后再将获取的size与mode通过measurespec.makemeasurespec方法整合返回。最后传递到measure中,这就是前面所说的widthmeasurespec与heightmeasurespec中包含的两部分的值。整个过程为measurechildren->measurechild->getchildmeasurespec->measure->onmeasure->setmeasureddimension,所以通过measurechildren就可以对子视图进行测量计算。
layout
layout也是一样的内部会回调onlayout方法,该方法是用来确定子视图的绘制位置,但这个方法在viewgroup中是个抽象方法,所以如果要自定义的view是继承viewgroup的话就必须实现该方法。但如果是继承view的话就不需要了,view中有一个空实现。而对子视图位置的设置是通过view的layout方法通过传递计算出来的left、top、right与bottom值,而这些值一般都要借助view的宽高来计算,视图的宽高则可以通过getmeasurewidth与getmeasureheight方法获取,这两个方法获取的值就是上面onmeasure中setmeasureddimension传递的值,即子视图测量的宽高。
getwidth、getheight与getmeasurewidth、getmeasureheight是不同的,前者是在onlayout之后才能获取到的值,分别为left-right与top-bottom;而后者是在onmeasure之后才能获取到的值。只不过这两种获取的值一般都是相同的,所以要注意调用的时机。
下面以定义一个把子视图放置于父视图的四个角的view为例:
@override protected void onlayout(boolean changed, int l, int t, int r, int b) { int count = getchildcount(); marginlayoutparams params; int cl; int ct; int cr; int cb; for (int i = 0; i < count; i++) { view child = getchildat(i); params = (marginlayoutparams) child.getlayoutparams(); if (i == 0) { //左上角 cl = params.leftmargin; ct = params.topmargin; } else if (i == 1) { //右上角 cl = getmeasuredwidth() - params.rightmargin - child.getmeasuredwidth(); ct = params.topmargin; } else if (i == 2) { //左下角 cl = params.leftmargin; ct = getmeasuredheight() - params.bottommargin - child.getmeasuredheight() - params.topmargin; } else { //右下角 cl = getmeasuredwidth() - params.rightmargin - child.getmeasuredwidth(); ct = getmeasuredheight() - params.bottommargin - child.getmeasuredheight() - params.topmargin; } cr = cl + child.getmeasuredwidth(); cb = ct + child.getmeasuredheight(); //确定子视图在父视图中放置的位置 child.layout(cl, ct, cr, cb); } }
至于onmeasure的实现源码我后面会给链接,如果要看效果图的话,我后面也会贴出来,前面的那个验证码的也是一样
draw
draw是由dispatchdraw发动的,dispatchdraw是viewgroup中的方法,在view是空实现。自定义view时不需要去管理该方法。而draw方法只在view中存在,viewgoup做的只是在dispatchdraw中调用drawchild方法,而drawchild中调用的就是view的draw方法。那么我们来看下draw的源码:
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; } //省略2&5的情况 .... }
源码已经非常清晰了draw总共分为6步;
绘制背景
如果需要的话,保存layers
绘制自身文本
绘制子视图
如果需要的话,绘制fading edges
绘制scrollbars
其中 第2步与第5步不是必须的。在第3步调用了ondraw方法来绘制自身的内容,在view中是空实现,这就是我们为什么在自定义view时必须要重写该方法。而第4步调用了dispatchdraw对子视图进行绘制。还是以验证码为例:
@override protected void ondraw(canvas canvas) { //绘制背景 mpaint.setcolor(getresources().getcolor(r.color.autocodebg)); canvas.drawrect(0, 0, getmeasuredwidth(), getmeasuredheight(), mpaint); mpaint.gettextbounds(autotext, 0, autotext.length(), bounds); //绘制文本 for (int i = 0; i < autotext.length(); i++) { mpaint.setcolor(getresources().getcolor(colorres[random.nextint(6)])); canvas.drawtext(autotext, i, i + 1, getwidth() / 2 - bounds.width() / 2 + i * bounds.width() / autonum , bounds.height() + random.nextint(getheight() - bounds.height()) , mpaint); } //绘制干扰点 for (int j = 0; j < 250; j++) { canvas.drawpoint(random.nextint(getwidth()), random.nextint(getheight()), pointpaint); } //绘制干扰线 for (int k = 0; k < 20; k++) { int startx = random.nextint(getwidth()); int starty = random.nextint(getheight()); int stopx = startx + random.nextint(getwidth() - startx); int stopy = starty + random.nextint(getheight() - starty); linepaint.setcolor(getresources().getcolor(colorres[random.nextint(6)])); canvas.drawline(startx, starty, stopx, stopy, linepaint); } }
图,与源码链接
示例图
其它类似信息

推荐信息