Android自定义ViewGroup之CustomGridLayout(一)
之前写了两篇关于自定义view的文章,本篇讲讲自定义viewgroup的实现。
我们知道viewgroup就是view的容器类,我们经常用的linearlayout,relativelayout等都是viewgroup的子类。并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;于是乎,viewgroup的职能为:给childview计算出建议的宽和高和测量模式 ;决定childview的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childview宽和高可以设置为wrap_content,这样只有childview才能计算出自己的宽和高。
view的根据viewgroup传入的测量值和模式,对自己宽高进行确定(onmeasure中完成),然后在ondraw中完成对自己的绘制。viewgroup需要给view传入view的测量值和模式(onmeasure中完成),而且对于此viewgroup的父布局,自己也需要在onmeasure中完成对自己宽和高的确定。此外,需要在onlayout中完成对其childview的位置的指定。
因为viewgroup有很多子view,所以它的整个绘制过程相对于view会复杂一点,但是还是遵循三个步骤measure,layout,draw,我们依次说明。
本文我们来写一个类似于gridview的网格容器吧,姑且叫做customgridview。
自定义属性/获取属性值
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="customgridview"> <attr name="numcolumns" format="integer" /> <attr name="hspace" format="integer" /> <attr name="vspace" format="integer" /> </declare-styleable> </resources>
public customgridview(context context, attributeset attrs, int defstyle) { super(context, attrs, defstyle); if (attrs != null) { typedarray a = getcontext().obtainstyledattributes(attrs, r.styleable.customgridview); colums = a.getinteger(r.styleable.customgridlayout_numcolumns, 3); hspace = a.getinteger(r.styleable.customgridlayout_hspace, 10); vspace = a.getinteger(r.styleable.customgridlayout_vspace, 10); a.recycle(); } } public mygridlayout(context context, attributeset attrs) { this(context, attrs, 0); } public mygridlayout(context context) { this(context, null); }
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 viewgroup.layoutparams generatelayoutparams(attributeset attrs) { return new customgridlayout.layoutparams(getcontext(), attrs); } @override protected viewgroup.layoutparams generatedefaultlayoutparams() { return new layoutparams(layoutparams.wrap_content, layoutparams.wrap_content); } @override protected viewgroup.layoutparams generatelayoutparams(viewgroup.layoutparams p) { return new layoutparams(p); } @override protected boolean checklayoutparams(viewgroup.layoutparams p) { return p instanceof customgridlayout.layoutparams; }
measure
在onmeasure中需要做两件事:
•计算childview的测量值以及模式
measurechildren(widthmeasurespec, heightmeasurespec);
measurechild(child, widthmeasurespec, heightmeasurespec);
child.measure(widthmeasurespec, heightmeasurespec);
•设置viewgroup自己的宽和高
测量viewgroup的大小,如果layout_width和layout_height是match_parent或具体的xxxdp,就很简答了,直接调用setmeasureddimension()方法,设置viewgroup的宽高即可,如果是wrap_content,就比较麻烦了,我们需要遍历所有的子view,然后对每个子view进行测量,然后根据子view的排列规则,计算出最终viewgroup的大小。
注意:在自定义view第一篇讲specmode时,曾说到unspecified一般都是父控件是adapterview,通过measure方法传入的模式。在这里恰好就用到了。
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { int widthmode = measurespec.getmode(widthmeasurespec); int heightmode = measurespec.getmode(heightmeasurespec); int sizewidth = measurespec.getsize(widthmeasurespec); int sizeheight = measurespec.getsize(heightmeasurespec); //unspecified一般都是父控件是adapterview,通过measure方法传入的模式 final int childwidthmeasurespec = measurespec.makemeasurespec(sizewidth, measurespec.unspecified); final int childheightmeasurespec = measurespec.makemeasurespec(sizeheight, measurespec.unspecified); measurechildren(childwidthmeasurespec, childheightmeasurespec); int childcount = this.getchildcount(); int line = childcount % colums == 0 ? childcount / colums : (childcount + colums) / colums; //宽布局为wrap_content时,childwidth取childview宽的最大值,否则动态计算 if (widthmode == measurespec.at_most) { for (int i = 0; i < childcount; i++) { view child = this.getchildat(i); childwidth = math.max(childwidth, child.getmeasuredwidth()); } } else if (widthmode == measurespec.exactly) { childwidth = (sizewidth - (colums - 1) * hspace) / colums; } //高布局为wrap_content时,childheight取childview高的最大值,否则动态计算 if (heightmode == measurespec.at_most) { for (int i = 0; i < childcount; i++) { view child = this.getchildat(i); childheight = math.max(childheight, child.getmeasuredheight()); } } else if (heightmode == measurespec.exactly) { childheight = (sizeheight - (line - 1) * vspace) / line; } //遍历每个子view,将它们左上角坐标保存在它们的layoutparams中,为后面onlayout服务 for (int i = 0; i < childcount; i++) { view child = this.getchildat(i); layoutparams lparams = (layoutparams) child.getlayoutparams(); lparams.left = (i % colums) * (childwidth + hspace); lparams.top = (i / colums) * (childheight + vspace); } //当宽高为wrap_content时,分别计算出的viewgroup宽高 int wrapwidth; int wrapheight; if (childcount < colums) { wrapwidth = childcount * childwidth + (childcount - 1) * hspace; } else { wrapwidth = colums * childwidth + (colums - 1) * hspace; } wrapheight = line * childheight + (line - 1) * vspace; setmeasureddimension(widthmode == measurespec.at_most? wrapwidth:sizewidth,heightmode == measurespec.at_most? wrapheight:sizeheight); }
layout
最核心的就是调用layout方法,根据我们measure阶段获得的layoutparams中的left和top字段,也很好对每个子view进行位置排列。
@override protected void onlayout(boolean changed, int l, int t, int r, int b) { 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); } }
draw
viewgroup在draw阶段,其实就是按照子类的排列顺序,调用子类的ondraw方法,因为我们只是view的容器,本身一般不需要draw额外的修饰,所以往往在ondraw方法里面,只需要调用viewgroup的ondraw默认实现方法即可。不需要重写。
最后,在自定义viewgroup中定义gridadatper接口,以便在外部可以为viewgroup设置适配器。
public interface gridadatper { view getview(int index); int getcount(); } /** 设置适配器 */ public void setgridadapter(gridadatper adapter) { this.adapter = adapter; // 动态添加视图 int size = adapter.getcount(); for (int i = 0; i < size; i++) { addview(adapter.getview(i)); } }
并且在自定义viewgroup中定义onitemclicklistener接口,以便在外部可以获取到childview的点击事件。
public interface onitemclicklistener { void onitemclick(view v, int index); } public void setonitemclicklistener(final onitemclicklistener listener) { if (this.adapter == null) return; for (int i = 0; i < adapter.getcount(); i++) { final int index = i; view view = getchildat(i); view.setonclicklistener(new onclicklistener() { @override public void onclick(view v) { listener.onitemclick(v, index); } }); } }
使用自定义的customviewgroup
布局文件:
<?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/com.hx.customgridview" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#303030" android:orientation="vertical" > <com.hx.customgridview.customgridlayout android:id="@+id/gridview" android:layout_width="200dp" android:layout_height="300dp" android:background="#1e1d1d" app:hspace="10" app:vspace="10" app:numcolumns="3"/> </linearlayout>
grid_item:
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" > <imageview android:id="@+id/iv" android:layout_width="match_parent" android:layout_height="match_parent" android:scaletype="fitxy"/> </linearlayout>
java文件:
protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); grid = (customgridlayout) findviewbyid(r.id.gridview); grid.setgridadapter(new gridadatper() { @override public view getview(int index) { view view = getlayoutinflater().inflate(r.layout.grid_item, null); imageview iv = (imageview) view.findviewbyid(r.id.iv); iv.setimageresource(srcs[index]); return view; } @override public int getcount() { return srcs.length; } }); grid.setonitemclicklistener(new onitemclicklistener() { @override public void onitemclick(view v, int index) { toast.maketext(mainactivity.this, "item="+index, toast.length_short).show(); } }); } }
运行后效果图如下:
改变一下布局:
<com.hx.customgridview.customgridlayout android:id="@+id/gridview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#1e1d1d" app:hspace="10" app:vspace="10" app:numcolumns="3"/>
再改变
<com.hx.customgridview.customgridlayout android:id="@+id/gridview" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#1e1d1d" app:hspace="10" app:vspace="10" app:numcolumns="3"/>
再变
<com.hx.customgridview.customgridlayout android:id="@+id/gridview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#1e1d1d" app:hspace="10" app:vspace="10" app:numcolumns="4"/>
demo下载地址:http://xiazai.jb51.net/201609/yuanma/customgridlayout(jb51.net).rar
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
Android自定义ViewGroup之CustomGridLayout(一)
-
Android自定义ViewGroup之WaterfallLayout(二)
-
Android自定义ViewGroup实现标签流容器FlowLayout
-
Android自定义ViewGroup之FlowLayout(三)
-
Android开发之自定义控件用法详解
-
Android 让自定义TextView的drawableLeft与文本一起居中
-
Android开发之自定义CheckBox
-
Android自定义状态栏颜色与应用标题栏颜色一致
-
Android自定义view系列之99.99%实现QQ侧滑删除效果实例代码详解
-
安卓(Android)开发之自定义饼状图