Android App开发中自定义View和ViewGroup的实例教程
view
android所有的控件都是view或者view的子类,它其实表示的就是屏幕上的一块矩形区域,用一个rect来表示,left,top表示view相对于它的parent view的起点,width,height表示view自己的宽高,通过这4个字段就能确定view在屏幕上的位置,确定位置后就可以开始绘制view的内容了。
view绘制过程
view的绘制可以分为下面三个过程:
measure
view会先做一次测量,算出自己需要占用多大的面积。view的measure过程给我们暴露了一个接口onmeasure,方法的定义是这样的,
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {}
view类已经提供了一个基本的onmeasure实现,
protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { setmeasureddimension(getdefaultsize(getsuggestedminimumwidth(), widthmeasurespec), getdefaultsize(getsuggestedminimumheight(), heightmeasurespec)); } public static int getdefaultsize(int size, int measurespec) { int result = size; int specmode = measurespec.getmode(measurespec); int specsize = measurespec.getsize(measurespec); switch (specmode) { case measurespec.unspecified: result = size; break; case measurespec.at_most: case measurespec.exactly: result = specsize; break; } return result; }
其中invoke了setmeasureddimension()方法,设置了measure过程中view的宽高,getsuggestedminimumwidth()返回view的最小width,height也有对应的方法。插几句,measurespec类是view类的一个内部静态类,它定义了三个常量unspecified、at_most、exactly,其实我们可以这样理解它,它们分别对应layoutparams中match_parent、wrap_content、xxxdp。我们可以重写onmeasure来重新定义view的宽高。
layout
layout过程对于view类非常简单,同样view给我们暴露了onlayout方法
protected void onlayout(boolean changed, int left, int top, int right, int bottom) { }
因为我们现在讨论的是view,没有子view需要排列,所以这一步其实我们不需要做额外的工作。插一句,对viewgroup类,onlayout方法中,我们需要将所有子view的大小宽高设置好,这个我们下一篇会详细说。
draw
draw过程,就是在canvas上画出我们需要的view样式。同样view给我们暴露了ondraw方法
protected void ondraw(canvas canvas) { }
默认view类的ondraw没有一行代码,但是提供给我们了一张空白的画布,举个例子,就像一张画卷一样,我们就是画家,能画出什么样的效果,完全取决我们。
view中还有三个比较重要的方法
requestlayout
view重新调用一次layout过程。
invalidate
view重新调用一次draw过程
forcelayout
标识view在下一次重绘,需要重新调用layout过程。
自定义属性
整个view的绘制流程我们已经介绍完了,还有一个很重要的知识,自定义控件属性,我们都知道view已经有一些基本的属性,比如layout_width,layout_height,background等,我们往往需要定义自己的属性,那么具体可以这么做。
1.在values文件夹下,打开attrs.xml,其实这个文件名称可以是任意的,写在这里更规范一点,表示里面放的全是view的属性。
2.因为我们下面的实例会用到2个长度,一个颜色值的属性,所以我们这里先创建3个属性。
<declare-styleable name="rainbowbar"> <attr name="rainbowbar_hspace" format="dimension"></attr> <attr name="rainbowbar_vspace" format="dimension"></attr> <attr name="rainbowbar_color" format="color"></attr> </declare-styleable>
那么到底怎么用呢,我们会看一个实例。
实现一个比较简单的google彩虹进度条。
为了简单起见,这里我只用一种颜色,多种颜色就留给大家了,我们直接上代码。
public class rainbowbar extends view { //progress bar color int barcolor = color.parsecolor("#1e88e5"); //every bar segment width int hspace = utils.dptopx(80, getresources()); //every bar segment height int vspace = utils.dptopx(4, getresources()); //space among bars int space = utils.dptopx(10, getresources()); float startx = 0; float delta = 10f; paint mpaint; public rainbowbar(context context) { super(context); } public rainbowbar(context context, attributeset attrs) { this(context, attrs, 0); } public rainbowbar(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); //read custom attrs typedarray t = context.obtainstyledattributes(attrs, r.styleable.rainbowbar, 0, 0); hspace = t.getdimensionpixelsize(r.styleable.rainbowbar_rainbowbar_hspace, hspace); vspace = t.getdimensionpixeloffset(r.styleable.rainbowbar_rainbowbar_vspace, vspace); barcolor = t.getcolor(r.styleable.rainbowbar_rainbowbar_color, barcolor); t.recycle(); // we should always recycle after used mpaint = new paint(); mpaint.setantialias(true); mpaint.setcolor(barcolor); mpaint.setstrokewidth(vspace); } ....... }
view有了三个构造方法需要我们重写,这里介绍下三个方法会被调用的场景,
第一个方法,一般我们这样使用时会被调用,view view = new view(context);
第二个方法,当我们在xml布局文件中使用view时,会在inflate布局时被调用,
<view layout_width="match_parent" layout_height="match_parent"/>。
第三个方法,跟第二种类似,但是增加style属性设置,这时inflater布局时会调用第三个构造方法。
<view style="@styles/mycustomstyle" layout_width="match_parent" layout_height="match_parent"/>。
上面大家可能会感觉到有点困惑的是,我把初始化读取自定义属性hspace,vspace,和barcolor的代码写在第三个构造方法里面,但是我rainbowbar在线性布局中没有加style属性(),那按照我们上面的解释,inflate布局时应该会invoke第二个构造方法啊,但是我们在第二个构造方法里面调用了第三个构造方法,this(context, attrs, 0); 所以在第三个构造方法中读取自定义属性,没有问题,这是一点小细节,避免代码冗余-,-
draw
因为我们这里不用关注measrue和layout过程,直接重写ondraw方法即可。
//draw be invoke numbers. int index = 0; @override protected void ondraw(canvas canvas) { super.ondraw(canvas); //get screen width float sw = this.getmeasuredwidth(); if (startx >= sw + (hspace + space) - (sw % (hspace + space))) { startx = 0; } else { startx += delta; } float start = startx; // draw latter parse while (start < sw) { canvas.drawline(start, 5, start + hspace, 5, mpaint); start += (hspace + space); } start = startx - space - hspace; // draw front parse while (start >= -hspace) { canvas.drawline(start, 5, start + hspace, 5, mpaint); start -= (hspace + space); } if (index >= 700000) { index = 0; } invalidate(); }
布局文件:
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:layout_margintop="40dp" android:orientation="vertical" > <com.sw.demo.widget.rainbowbar android:layout_width="match_parent" android:layout_height="wrap_content" app:rainbowbar_color="@android:color/holo_blue_bright" app:rainbowbar_hspace="80dp" app:rainbowbar_vspace="10dp" ></com.sw.demo.widget.rainbowbar> </linearlayout>
其实就是调用canvas的drawline方法,然后每次将draw的起点向前推进,在方法的结尾,我们调用了invalidate方法,上面我们已经说明了,这个方法会让view重新调用ondraw方法,所以就达到我们的进度条一直在向前绘制的效果。下面是最后的显示效果,制作成gif时好像有色差,但是真实效果是蓝色的。我们只写了短短的几十行代码,自定义view并不是我们想象中那么难,下一篇我们会继续viewgroup的绘制流程学习。
自定义viewgroup
viewgroup
我们知道viewgroup就是view的容器类,我们经常用的linearlayout,relativelayout等都是viewgroup的子类,因为viewgroup有很多子view,所以它的整个绘制过程相对于view会复杂一点,但是还是三个步骤measure,layout,draw,我们一次说明。
measure
measure过程还是测量viewgroup的大小,如果layout_widht和layout_height是match_parent或具体的xxxdp,就很简答了,直接调用setmeasureddimension()方法,设置viewgroup的宽高即可,如果是wrap_content,就比较麻烦了,我们需要遍历所有的子view,然后对每个子view进行测量,然后根据子view的排列规则,计算出最终viewgroup的大小。
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { int childcount = this.getchildcount(); for (int i = 0; i < childcount; i++) { view child = this.getchildat(i); this.measurechild(child, widthmeasurespec, heightmeasurespec); int cw = child.getmeasuredwidth(); // int ch = child.getmeasuredheight(); } }
你可能需要类似上面的代码,其中getchildcount()方法,返回子view的数量,measurechild()方法,调用子view的测量方法。
layout
上面view的自定义中,我们稍微提到了,layout过程其实就是对子view的位置进行排列,onlayout方法给我一个机会,来按照我们想要的规则自定义子view排列。
@override protected void onlayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) { int childcount = this.getchildcount(); for (int i = 0; i < childcount; i++) { view child = this.getchildat(i); layoutparams lparams = (layoutparams) child.getlayoutparams(); child.layout(lparams.left, lparams.top, lparams.left + childwidth, lparams.top + childheight); } }
你同样可能需要类似上面的代码,其中child.layout(left,top,right,bottom)方法可以对子view的位置进行设置,四个参数的意思大家通过变量名都应该清楚了。
draw
viewgroup在draw阶段,其实就是按照子类的排列顺序,调用子类的ondraw方法,因为我们只是view的容器, 本身一般不需要draw额外的修饰,所以往往在ondraw方法里面,只需要调用viewgroup的ondraw默认实现方法即可。
layoutparams
viewgroup还有一个很重要的知识layoutparams,layoutparams存储了子view在加入viewgroup中时的一些参数信息,在继承viewgroup类时,一般也需要新建一个新的layoutparams类,就像sdk中我们熟悉的linearlayout.layoutparams,relativelayout.layoutparams类等一样,那么可以这样做,在你定义的viewgroup子类中,新建一个layoutparams类继承与viewgroup.layoutparams。
public static class layoutparams extends viewgroup.layoutparams { public int left = 0; public int top = 0; public layoutparams(context arg0, attributeset arg1) { super(arg0, arg1); } public layoutparams(int arg0, int arg1) { super(arg0, arg1); } public layoutparams(android.view.viewgroup.layoutparams arg0) { super(arg0); } }
那么现在新的layoutparams类已经有了,如何让我们自定义的viewgroup使用我们自定义的layoutparams类来添加子view呢,viewgroup同样提供了下面这几个方法供我们重写,我们重写返回我们自定义的layoutparams对象即可。
@override public android.view.viewgroup.layoutparams generatelayoutparams( attributeset attrs) { return new ninephotoview.layoutparams(getcontext(), attrs); } @override protected android.view.viewgroup.layoutparams generatedefaultlayoutparams() { return new layoutparams(layoutparams.wrap_content, layoutparams.wrap_content); } @override protected android.view.viewgroup.layoutparams generatelayoutparams( android.view.viewgroup.layoutparams p) { return new layoutparams(p); } @override protected boolean checklayoutparams(android.view.viewgroup.layoutparams p) { return p instanceof ninephotoview.layoutparams; }
实例
我们还是做一个实例来说明,我们今天做一个类似微信朋友圈 存储要发送图片的控件,点击+号图片,可以一直加图片,最多9张。那么微信是4个一排,我们这里是3个一排,因为一般常规都是三个一排,这些都是细节不要在意(另外偷偷告诉大家,微信的实现是用tablelayout,-.-)。
public class ninephotoview extends viewgroup { public static final int max_photo_number = 9; private int[] constimageids = { r.drawable.girl_0, r.drawable.girl_1, r.drawable.girl_2, r.drawable.girl_3, r.drawable.girl_4, r.drawable.girl_5, r.drawable.girl_6, r.drawable.girl_7, r.drawable.girl_8 }; // horizontal space among children views int hspace = utils.dptopx(10, getresources()); // vertical space among children views int vspace = utils.dptopx(10, getresources()); // every child view width and height. int childwidth = 0; int childheight = 0; // store images res id arraylist<integer> mimageresarraylist = new arraylist<integer>(9); private view addphotoview; public ninephotoview(context context) { super(context); } public ninephotoview(context context, attributeset attrs) { this(context, attrs, 0); } public ninephotoview(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); typedarray t = context.obtainstyledattributes(attrs, r.styleable.ninephotoview, 0, 0); hspace = t.getdimensionpixelsize( r.styleable.ninephotoview_ninephoto_hspace, hspace); vspace = t.getdimensionpixelsize( r.styleable.ninephotoview_ninephoto_vspace, vspace); t.recycle(); addphotoview = new view(context); addview(addphotoview); mimageresarraylist.add(new integer()); }
目前为止,都跟上一篇说的大致差不多,另外拍照和从相册选择图片不是我们这一篇的重点,所以我们把图片硬编码到代码中(全是美女...),viewgroup初始化时我们添加了一个+号按钮,给用户点击添加新的图片。
measure
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { int rw = measurespec.getsize(widthmeasurespec); int rh = measurespec.getsize(heightmeasurespec); childwidth = (rw - 2 * hspace) / 3; childheight = childwidth; int childcount = this.getchildcount(); for (int i = 0; i < childcount; i++) { view child = this.getchildat(i); //this.measurechild(child, widthmeasurespec, heightmeasurespec); layoutparams lparams = (layoutparams) child.getlayoutparams(); lparams.left = (i % 3) * (childwidth + hspace); lparams.top = (i / 3) * (childwidth + vspace); } int vw = rw; int vh = rh; if (childcount < 3) { vw = childcount * (childwidth + hspace); } vh = ((childcount + 3) / 3) * (childwidth + vspace); setmeasureddimension(vw, vh); }
我们的子view三个一排,而且都是正方形,所以我们上面通过循环很好去得到所有子view的位置,注意我们上面把子view的左上角坐标存储到我们自定义的layoutparams 的left和top二个字段中,layout阶段会使用,最后我们算得整个viewgroup的宽高,调用setmeasureddimension设置。
layout
@override protected void onlayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) { int childcount = this.getchildcount(); for (int i = 0; i < childcount; i++) { view child = this.getchildat(i); layoutparams lparams = (layoutparams) child.getlayoutparams(); child.layout(lparams.left, lparams.top, lparams.left + childwidth, lparams.top + childheight); if (i == mimageresarraylist.size() - 1 && mimageresarraylist.size() != max_photo_number) { child.setbackgroundresource(r.drawable.add_photo); child.setonclicklistener(new view.onclicklistener() { @override public void onclick(view arg0) { addphotobtnclick(); } }); }else { child.setbackgroundresource(constimageids[i]); child.setonclicklistener(null); } } } public void addphoto() { if (mimageresarraylist.size() < max_photo_number) { view newchild = new view(getcontext()); addview(newchild); mimageresarraylist.add(new integer()); requestlayout(); invalidate(); } } public void addphotobtnclick() { final charsequence[] items = { "take photo", "photo from gallery" }; alertdialog.builder builder = new alertdialog.builder(getcontext()); builder.setitems(items, new dialoginterface.onclicklistener() { @override public void onclick(dialoginterface arg0, int arg1) { addphoto(); } }); builder.show(); }
最核心的就是调用layout方法,根据我们measure阶段获得的layoutparams中的left和top字段,也很好对每个子view进行位置排列。然后判断在图片未达到最大值9张时,默认最后一张是+号图片,然后设置点击事件,弹出对话框供用户选择操作。
draw
不需要重写,使用viewgroup默认实现即可。
附上布局文件
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margintop="40dp" android:orientation="vertical" > <com.sw.demo.widget.ninephotoview android:id="@+id/photoview" android:layout_width="match_parent" android:layout_height="wrap_content" app:ninephoto_hspace="10dp" app:ninephoto_vspace="10dp" app:rainbowbar_color="@android:color/holo_blue_bright" > </com.sw.demo.widget.ninephotoview> </linearlayout>
最后还是加上程序运行的效果图,今天自定义viewgroup的讲解就这么多了,祝大家每天都有新收获,每天都有好心情~~~
上一篇: PHP7.1方括号数组符号多值复制及指定键值赋值用法分析
下一篇: Deepin下安装nginx