欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义ViewGroup之CustomGridLayout(一)

程序员文章站 2024-03-07 10:07:20
之前写了两篇关于自定义view的文章,本篇讲讲自定义viewgroup的实现。 我们知道viewgroup就是view的容器类,我们经常用的linearlayout,re...

之前写了两篇关于自定义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();
  }
 });
 }
}

运行后效果图如下:

Android自定义ViewGroup之CustomGridLayout(一)

改变一下布局:

<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"/>

Android自定义ViewGroup之CustomGridLayout(一)

再改变

<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"/>

Android自定义ViewGroup之CustomGridLayout(一)

再变

<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"/>

Android自定义ViewGroup之CustomGridLayout(一)

demo下载地址:http://xiazai.jb51.net/201609/yuanma/customgridlayout(jb51.net).rar

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