自定义九宫格控件(附源码地址)
最近公司在做个类似朋友圈的功能,需要一个九宫格控件,因为算是个常用控件,所以自己撸了一个。
职责分解
九宫格控件因为其需要承载其内部的 View,所以应该是一个 ViewGroup。每个子 View 基本上是相同的,但显示的内容不同,正好最近在看 RecyclerView,因此也就想到了可以使用 Adapter 来个性化每个子 View,正好通过 Adapter 模式来适配了子 View 与 ViewGroup。最后为了防止在创建、显示子 View 时的冗余操作,应该给子 View 增加一些属性,选用 ViewHolder 正好可以完成这个要求,这样也更方便以后的扩展。
ViewGroup、Adapter、ViewHolder 三者的具体职责
功能 | ||
---|---|---|
ViewGroup | Adapter | ViewHolder |
Measure 子 View | 创建子 View 布局 | 子 View 的获取 |
Layout 子 View | 子 View 的内容 display | display 子 View 标志 |
add 子 View | 子 view 数量 | |
回收缓存 | 数据变化通知 |
ChildView 的测量
ChildView 测量所需要的数据主要来自于 ParentView 的测量数据,这里就是我们之前说的 ViewGroup 的 onMeasure
方法。
/**
* 在这之前已经对 childView 的数量小于等于 0 或大于 9 的情况进行了处理
* 这里讲显示情况分成三种进行测量:
* 1、ChildView 数量为 1
* 2、ChildView 数量为 2 或 4
* 3、ChildView 数量的剩余类型
*/
if (adapter.getItemCount() > 1) {
childSize = (width - border * 2) / 3;
height = (int) (childSize * (int) Math.ceil(adapter.getItemCount() / 3.0) + border * (int) Math.ceil(adapter.getItemCount() / 3.0
if (adapter.getItemCount() == 4 || adapter.getItemCount() == 2) {
int currentWidth = childSize*2 + border;
setMeasuredDimension(currentWidth + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom());
}else {
int currentWidth = childSize*3 + border*2;
setMeasuredDimension(currentWidth + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom());
}
} else {
childSize = width/3;
height = width/3;
setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom());
}
ChildView 的添加
在测量 ChildView 之后,将使用 addView
方法将其添加到 ViewGroup 中。
/**
* 在增加 ChildView 之前,需要先行清除已经添加的所有 ChildView
*/
removeAllViews();
for (int i = 0; i < adapter.getItemCount(); i++) {
addView(generateViewHolder(i).getItemView(),generateDefaultLayoutParams());
}
ChildView 的回收
generateViewHolder 是用于获取 ChildView 的方法,其中有一个简单的缓存列表
/**
* 当需要添加的 ChildView 的 position 小于缓存列表大小时,直接从缓存列表中获取 ChildView
* 否则,则调用 adapter.createView 方法,创建一个新的 ViewHolder,同时将其添加到缓存列表中
*/
private IKNinePhotoViewHolder generateViewHolder(int position){
if (position < mRecyclerList.size()) {
return mRecyclerList.get(position);
} else {
if (adapter != null){
IKNinePhotoViewHolder holder = adapter.createView(IKNinePhotoView.this);
if (holder == null){
return null;
}
mRecyclerList.add(holder);
return holder;
} else
return null;
}
}
ChildView 的布局
在完成 ChildView 的测量和添加之后,需要对 ChildView 的位置进行确定。而这些数值来自 ParentView 的 onLayout
方法。
/**
* 先对 ChildView 数量为 4 的特殊情况,进行处理,将其的列数确定为 2。
*/
int count = adapter.getItemCount();
int colNum = 3;
if (count == 4){
colNum = 2;
}
/**
* 便利每个 ChildView,对其进行布局
*/
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
if (childView == null){
return;
}
if (adapter != null && mRecyclerList.get(i) != null &&!mRecyclerList.get(i).getFlag()) {
adapter.displayView(generateViewHolder(i), i);
// 设置这个标志,表示此 ViewHolder 中包含的 ChildView 已经完成了布局,防止多余的操作
mRecyclerList.get(i).setFlag(true);
}
// 之前设置的列数
int rows = i / colNum;
int cols = i % colNum;
int childLeft = getPaddingLeft() + (childSize + border) * (cols);
int childTop = getPaddingTop() + (childSize + border) * (rows);
int childRight = childLeft + childSize;
int childBottom = childTop + childSize;
childView.layout(childLeft, childTop, childRight, childBottom);
}
ChildView 数据的更新
到这里已经基本上完成了九宫格控件的编写,最后需要再添加一个数据更新的功能,以完备其功能。这里使用观察者模式的来实现这个功能,Adapter 继承 Observable 类、ViewGroup 实现 Observer 接口。
// 在 Adapter 中
public void notifyChanged(){
setChanged();
notifyObservers();
}
// 在 ViewGroup 中
@Override
public void update(Observable o, Object arg) {
if (o instanceof IKNinePhotoViewAdapter){
this.adapter = (IKNinePhotoViewAdapter) o;
adapter.addObserver(this);
for(IKNinePhotoViewHolder holder: mRecyclerList){
holder.setFlag(false);
}
requestLayout();
invalidate();
}
}
总结
本文详细说明了九宫格控件的实现方法,并对其功能进行了分解。分解成多个组件之后,不仅符合程序的单一性原则,更加易于程序的功能扩展,比如添加监听事件,使用不同的图片库去显示图片,同时九宫格的 ChildView 也不仅限于 ImageView,而可以扩展到全部的 View。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论