当前位置: 移动技术网 > 移动技术>移动开发>Android > Android自定义ViewGroup之CustomGridLayout(一)

Android自定义ViewGroup之CustomGridLayout(一)

2019年07月24日  | 移动技术网移动技术  | 我要评论

之前写了两篇关于自定义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下载地址:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网