Android中展示门类信息一般使用列表视图ListView或者网格视图GridView,特别是电商类APP的首页,除了顶部导航、底部标签、上方横幅外,主要页面都是展示各种商品和活动的网格视图。一般情况下GridView就够用了,不过GridView中规中矩,每个网格的大小都是一样的,有时显得有些死板。比如不同商品的外观尺寸很不一样,冰箱是高高的在纵向上长,空调则是在横向上长,所以若用一样规格的网格来展示,必然有的商品图片被压缩得很小。再比如像新闻摘要,每篇摘要的字数都不一样,为了把文字显示完全,也需要对每个网格自适应高度,字数多的网格分配较小的高度,字数较多的网格分配较大的高度。可惜GridView不支持自适配网格高度,所以我们得自己写个瀑布流网格控件来实现这样的效果了。 先来理下瀑布流控件的思路,因为GridView每个网格的宽和高都是一样的,所以无法基于GridView进行改造。如果是ListView,每行高度一样,一行内每个元素的长度是可以自定义的,但每列元素的长度必须一样,所以改造ListView的效果也很有限。改造GridView也不行,改造ListView也不行,看来得换个思路了,把复杂问题简单化试试。例如这个页面上只有四个视图:左上区块0、右上区块1、左下区块2、右下区块3,直接用布局文件xml编写的话也不难,可能大家多半会想到采用相对布局RelativeLayout来处理。
具体的说,就是布局文件的根节点用RelativeLayout,然后在页面上先放区块0,不指定位置时默认放在页面左上角。然后放区块1,位置在区块0的右边。然后放区块2,位置在区块0的下方。最后放区块3,位置在区块1的下方,同时位于区块0和区块2的右边。同理,我们也可以使用RelativeLayout来实现瀑布流网格,关键是指定每个网格的相对位置就好(在哪个区块右边,又在哪个区块下方)。
基于上述RelativeLayout的方案,下面博主给出一个具体的实现,完成瀑布流网格的简单功能。 首先建立一个自定义视图WaterfallGridView,传入两个自定义属性:column_num表示列数,item_gap表示单元间隔。在初始化视图时,我们需要初始化一个列高度的数组,用于存放每列最后一个视图的编号id,以及该列当前的总高度。保存每列末尾的视图id,是为了在它下方添加视图时可以指定位于哪个视图的下方;保存每列的总高度,是为了判断当前哪一列总高度最小,从而把新来的网格添加到该列末尾。 其次要实现一个适配器,用于决定网格总数getCount,以及每个网格存放的具体视图getView。并在WaterfallGridView中增加该适配器的对象,以及设置适配器setAdapter与获取适配器getAdapter两个方法。 再次在WaterfallGridView的onMeasure方法中测量该瀑布流视图的具体宽和高的尺寸,以及在onLayout方法中对每个网格进行排列堆放。前面我们在自定义视图章节中,已经提到尺寸测量以及视图绘制的相关知识,之所以在onLayout而不是在onDraw和dispatchDraw中排列视图,是因为onDraw和dispatchDraw都通过画布来绘制,可是瀑布流的各网格视图是已经明确的了,只需我们对它们重新组合排列即可,所以这里我们选择在onLayout方法中放置这些网格元素(网格元素从适配器中getView获得)。放置网格的算法便是一开始提到的RelativeLayout方案,在代码实现时要提供RelativeLayout.LayoutParams布局参数,然后调用该参数的addRule位置方法,常量RelativeLayout.RIGHT_OF表示在指定视图的右边,常量RelativeLayout.BELOW表示在指定视图的下方。当然需要对第一个视图先分配一个临时数字id,后面的视图编号依次累加;每次添加完毕一个视图,都要更新步骤一提到的列高度数组,后续才能根据这个数组来判断新的网格放在哪一列的哪个视图下方。 最后不要忘了实现瀑布流的元素单击和元素长按的监听器与调用方法,即OnItemClickListener的onItemClick,以及OnItemLongClickListener的onItemLongClick。为此我们需要重写dispatchTouchEvent方法,在按下事件时计算当前按下区域位于哪个控件中,具体算法就是获取该控件在屏幕上的位置getLocationOnScreen,然后根据宽和高得到该触摸点的归属控件。接着在弹起事件中判断要如何处理弹起事件,单击和长按可以通过按下的时间长短来区分,网格位置的position,可以用当前控件的编号id减去第一个视图的临时id,它们的差便是当前网格的序号。 下面是WaterfallGridView的效果图
StaggeredGridView是早期的一个瀑布流开源控件,在早期app上用的比较多。github上有多个该控件的开源项目,本文末尾也有给出示例代码的下载地址,所以这里就不贴出github的链接了。 该控件实现了瀑布流网格的所有常用功能,但在一些细节上处理地有问题。比如网格内容动态变化导致网格高度也随之变化时,StaggeredGridView在第一行网格的展示上就存在高度不对齐的情况,下面截图便反映了StaggeredGridView的这个问题。当然StaggeredGridView项目自从2014年之后就没有更新,所以无人解决问题使得用户越来越少了吧。
PinterestLikeAdapterView是新出来的一个瀑布流开源控件,它是韩国人写的,在github上也有该控件的开源项目,本文末尾也有给出该控件的示例代码下载地址。 该控件比StaggeredGridView要来的稳定,即使网格内容会动态变化,它也能重新计算各网格的高度并重新布局排列,不会出现StaggeredGridView那种首行布局错乱的问题。不过PinterestLikeAdapterView有个不足,就是还未实现长按事件的处理,博主看了它的代码,源码中只定义了监听器OnItemLongClickListener,却并未提供长按方法的调用,所以应该是不支持的。如果我们需要处理长按事件,就得自己实现每个网格的长按方法了。 本文给出的三种瀑布流网格的例子,在activity代码中调用都简单且相似,就不一一贴代码了。下面列出WaterfallGridView的代码调用例子:
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import com.example.exmwaterfall.adapter.WaterfallAdapter;
import com.example.exmwaterfall.view.WaterfallGridView;
public class WaterfallActivity extends Activity {
private static final String TAG = "WaterfallActivity";
private WaterfallGridView wgv_content;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_waterfall);
wgv_content = (WaterfallGridView) findViewById(R.id.wgv_content);
WaterfallAdapter adapter = new WaterfallAdapter(this);
wgv_content.setAdapter(adapter);
wgv_content.setOnItemClickListener(adapter);
wgv_content.setOnItemLongClickListener(adapter);
}
}