项目重构的Git地址:
https://github.com/razerdp/FriendCircle
项目同步更新的文集:
http://www.jianshu.com/notebooks/3224048/latest
本文控件Git地址:
https://github.com/razerdp/PhotoContents
上集:欢乐的票圈重构——九宫格控件(上)
下集:欢乐的票圈重构——九宫格控件(中下)
上集概述
在上篇文章中,我们选择了继承FlowLayout来实现我们的九宫格,选择它的原因很简单,就是因为其layout过程实现很优秀,可扩展性也很强,所以这就是选择它的原因(我打死也不会说是为了偷懒)
OK,前言说完,本篇将会开始搭建这个控件的基本要素,可能涉及较多的代码方面的东西,也许略微枯燥【于是我这次选择了代码截图,弄得色彩斑斓点,看起来应该会爽很多】,我尽量述说的清晰一点
设计
撸一个控件并不是说随便继承个什么,然后就是一阵狂敲,在撸代码之前我们需要做的就是设计。
设计一个控件,往往都会从以下几个方面入手:
-
对外的有:
- 视觉样式
- 交互方式
- 数据绑定
-
对内的有:
- 逻辑处理
- 灵活性、耦合性、扩展性、稳定性、可维护性等
如果具体到细节,还可以细到初始化方式、布局时机、绘制性能、touch传递响应、边界裁定、内存控制等等。。。。
当然,我们这里肯定不会有那么细致的描述了,否则感觉都可以写一本书了QAQ;
在这里我们就针对上面的内部和外部进行设计就好了。
1、视觉样式
用惯了朋友圈的我们肯定都知道朋友圈的九宫格是有几个明显的特征的:
- 单张图片可大可小
- 4张图片田字摆放
- 最多九张图片
- 多图的时候图片皆为边长一样的正方形
因此,针对这几个特征,我们可以根据FlowLayout
的特性来进行初步的设想:
- 单张图片的情况下,我们仅仅控制图片大小的上限和下限,至于具体数值随图片大小而自适应。
- 4张图片需要田字摆放,对于这个的处理我们可以覆写
onLayout()
,也可以通过设置LayoutParam的newLine参数,让其换行(FlowLayout的LayoutParam有该参数) - 最9张图片,这个应该是最好处理的
- 多图的图片显示为正方形:同第二点,针对
LayoutParam
做操作,限制宽高即可。
2、交互方式
朋友圈的图片交互是点击图片的时候从图片的位置放大到整屏,退出的时候则是缩回到原来的位置。
在设计的时候,按照我的思路是这个控件顺便把图片浏览实现了,也顺便把动画给实现了,但考虑到后面的数据绑定等,以及单一职责准则,最终还是决定本控件仅仅负责图片的渲染和缓存处理,其余不管。
3、数据绑定
数据绑定其实跟外面使用者挂钩,也跟内部逻辑,扩展性等挂钩,所以这个需要慎重处理。
按照目前的思路,我们的控件所负责的东西是十分明确的:拿到View
→得到数据(图片地址)
→加载图片
→摆放控件
→渲染或缓存
也就是说,得到数据
和加载图片
这两部分不应该由控件来限定死方式,而是应该暴露给使用者自行处理,因此,适配器模式是一个非常好的选择,而且大家也用的习惯。
对内设计部分主要看大家的码代码水平,,,这里就不着重讨论了。
初步搭建
很多人在写自定义控件的时候往往很久都下不了手,依我的经验来看(以我踩过的坑来看),很多时候都是顾虑的太多,,,换句话说,就是想得太多从而导致第一步都还没迈出,就想着第一百步的情景。
对此,我的解决方法很简单,首先不管三七二十一,先把包弄出来,以及把控件的类给弄出来。。。。至少在没方向的时候给自己一个方向也好。
于是我们就有了下面初步结构:
接下来,既然选择了适配器模式,我们就首先搭建我们的adapter吧。
1、Adapter的设计:
适配器模式简单地说其实就是对客户端统一接口(统一为设计者希望最终得到对应类的接口),使复杂的使用场景或不兼容的类或接口都能正常的工作在一起。
但是,在数据和View
的刷新间,我们总得需要一个桥梁,用来沟通View
和Bean
说到通知,我们首当其冲想到的是。。。。EventBus对吧,既然想到了EventBus,也就不难想到观察者
这货了。
所以在写Adapter前,我们先把桥搭好。
1.1、Observer
在adapter包下新建一个observer包,我们将在这里完成我们的观察者的搭建。
首先我们需要一个观察者(Observer)
这个观察者只要做的只有两件事:
- 通知requestLayout()【为了触发measure()/layout()】
- 通知invalidate()【为了重绘】
因为系统本来就已经有相关的观察者,所以我们直接使用就好,毕竟咱们这个观察者的功能实在有点简单。。。
OK,观察者有了,接下来我们需要的是被观察的对象,一般被观察的都是 xxx -able结尾(able就是“可以..XXOO..的”)
当然,被观察的对象可能有好多个,也可能好多种实现,所以我们先抽象出一个被观察对象作为以后其他被观察对象的父类。
最后就实现我们的具体观察对象就行了
到目前为止,看起来还是很轻松愉快的嘛~
1.2、Adapter
Adapter的设计,我们只需要认准一个东西就行:
不管具体实现类怎么干,反正我只想要我需要的东西,最终实现类必须返回这个东西给我
所以与其说定制一个Adapter,倒不如说我们是在定义一个协议,当然,换到Java说法就是定义一系列的接口。。。
在开干之前,我们可以思考一下,我们需要的是什么。。。
想了大概30秒,几乎可以确定,我们似乎需要的东西不多,就一个:ImageView
,其他的似乎都不太需要。。。(真好满足)
目标很明确,所以我们可以动手了。
不过因为是一个初步的搭建,所以就没有抽象为接口,而是以一个抽象类来制定这个Adapter。
代码量很少,我们只做了几件事:
- 注册观察者(当然,观察者在控件实现)
- 制定接口:
onCreateView(),很明显,跟
ListView
非常类似,这也是为了方便使用这个控件的人进行开发,这个方法返回的就是ImageView
,至于返回的这个View怎么摆放,是我们控件该干的事情了。onBindData(),这个方法是用来加载数据用的,上一个方法目的用来创建View,这一个方法则是用来加载图片
getCount(),获取数据(或者说图片)的数量
Adapter的设计就这样轻松愉悦的搞定了,当然这只是一个初步的,我们其实还可以进一步的抽象。。。
比如:
接口化,而不用抽象类,因为对于Java而言,接口简直就是各种干爹啊,我们不能多继承,但接口提供了另一种的“多继承”
泛型:如您所见,我们的
onCreateView
指定返回ImageView
,但实际项目中,我们可能很多自定义的ImageView
,如果实现类里强转什么的,既不方便于静态检查,又不那么爽,所以我们其实可以采取泛型,比如:
public abstract class PhotoContentsBaseAdapter {
...注册观察者,略
public abstract V onCreateView(V convertView, ViewGroup parent, int position);
public abstract void onBindData(int position, @NonNull V convertView);
public abstract int getCount();
}
嘛,其实还有好多好多了啦,喜欢就折腾一下咯-V- 这个项目是16年底才粗略的撸了一个,所以就不怎么维护了(基本满足我朋友圈项目使用就好~毕竟懒。。。。)
2、控件的设计(因为篇幅,这里仅讲述部分内容,剩余内容放到【中下】篇):
嘿嘿,终于到了重头戏了,前面干了那么多东东,为的就是养这个亲儿子啊。。。
这个控件代码量不多。。。嗯。。。。至今码了560多行,嘛~看起来这个控件还是很年轻嘛。。。
不过对于一篇文章来说,500多行还是太多太多了,所以接下来就针对几个地方着重讲述吧
整理了一下代码,着重讲述的地方我已经标注了起来(如果图片模糊,PC上可以先放大图片,然后右键新窗口打开再放大就可以看完整了,手机上的话,经过我的小手机测试,直接放大也是可以看得挺清晰的):
因为篇幅,所以我们这里着重说一下Adapter相关吧(毕竟上面那么多空间都在描述adapter)
2.1、观察者
首先我们搭桥,在上面的文字里,我们把桥的桥墩搭好了,所以这里我们就直接铺桥。
要使控件跟Adapter的观察者联合起来,要做的就是把我们的控件作为观察者给塞进去。
我们可选择两种方式:
控件实现观察者的接口,然后把自己塞进去
控件内部类实现观察者接口,然后把它塞进去
这里我采取后者。
首先实现一个内部类:
在onChanged里面我们执行requestLayout()
,也就是要求控件重新布局以触发measure和layout,毕竟setAdapter的时候可能是set了不同的Adapter,所以我们需要做这两步操作以适应新的需求。
在控件里我们直接new一个观察者:
private PhotoImageAdapterObserver mAdapterObserver = new PhotoImageAdapterObserver();
这个观察者就是我们和adapter沟通的桥梁了,以后需要换adapter的时候,我们先取消注册然后再重新new一个并注册就好了。
接下来就是setAdapter方法
在setAdapter前,我们需要先注销掉原来的观察者,然后执行控件的初始化,接着重新new一个观察者并注册,最后才requestLayout(),也许有人问为何这里需要requestLayout()
,上面的观察者不是应该实现了么,这里需要注明的是,我们的观察者的触发条件是外部执行adapter.notifyDataChanged()
,平时我们用刀setAdapter也不会说下一行立刻就执行adapter.notifyDataChanged()
吧,所以在这个方法里直接就提醒控件进行测量,重绘。
那么,写到这里,关于这个控件的一些细节已经开始渗透了,本篇主要讲的是控件的搭建和Adapter的搭建,下一篇我们将会着重说明一下关于缓存池和View的创建时机以及方法这几个部分。
然后就可以进入下篇了。