静态代理这么用?--聊一聊ViewPagerIndicator重构的一些经验

ViewPagerIndicator的代码可谓一波三折,在不久前ViewPagerIndicator作为一个单独的仓库从BannerViewPager中拆分了出来。拆分后的indicator已经不仅仅适用于BannerViewPager,还可以用于ViewPager和ViewPager2。现在,经历了几次代码重构后,总算可以拿得出手了。本篇文章就来写一写关于重构indicator的一些经验,了解下该库是如何通过静态代理模式来实现多种多样的indicator样式的。

先贴上ViewPagerIndicator源码链接以及预览图,使用方式可以参考GitHub主页README,同时有兴趣的同学欢迎到GitHub star项目。

Attrs CIRCLE DASH ROUND_RECT
NORMAL CIRCLE_NORMAL DASH_NORMAL ROUND_RECT_NORMAL
SMOOTH CIRCLE_SMOOTH DASH_SMOOTH ROUND_RECT_SMOOTH
WORM CIRCLE_WORM DASH_WORM ROUND_WORM
COLOR CIRCLE_COLOR DASH_COLOR ROUND_COLOR
SCALE CIRCLE_SCALE DASH_SCALE ROUND_SCALE

一、为什么要重构

在Indicator未拆分之前针对IndicatorView进行了两次较大的重构。第一次重构在上篇文章中也有提到。最初的Indicator是在BannerViewPager内部维护了一个指示器ImageView的List集合,在BannerViewPager内部会根据页面size动态添加指示器的Image。显然这种处理方式存在很大的弊端,即:不灵活、可扩展性低、性能相对较差等诸多问题。针对这一系列问题,在后续版本中对Indicator进行了第一次重构。这次重构将Indicator改为自定义View,并且抽象出了IIndicator接口,极大的增强了Indicator的可扩展性。因此,在后续若干个版本迭代中Indicator逐渐支持了多种样式(CIRCLE/DASH/ROUND_RECT)和多种滑动模式(SMOOTH/NORMAL)并且支持自定义Indicator。相比最初版本,不管在功能还是性能上都有了很大的提升。但是,在后续版本的迭代中却又暴露出许多问题。而这些问题很大程度上影响了开发和使用。列举其中一个最大问题如下:

多个IndicatorView不利于维护和使用

在BannerViewPager早期版本中indicator已经支持了CIRCLE和DASH两种样式,与之对应的是CircleIndicatorView和DashIndicatorView。在BannerViewPager内部用简单工厂模式根据IndicatorStyle来生成对应的IndicatorView。这么以来就出现了一个弊端,即每添加一种Indicator样式都需要一个与之对应的IndicatorView类,当Indicator 样式越来越多的时候维护成本和使用成本都会随之增加–使用该库的开发人员需要记住每种样式对应的IndicatorView名字,作为该库维护者也要面对越来越臃肿的代码结构,这是大家都不愿意看到的。因此,在这样的背景下IndicatorView的第二次重构就势在必行,不得不做了。针对这一问题,在第二次重构中使用了静态代理模式对代码结构进行了优化。

二、回顾静态代理模式

不知道现在大家对代理模式还记得多少,也不知道是否经常会在项目种用到代理模式。不管怎样,我们先来回顾以下静态代理模式吧:

代理模式即为其它对象提供一种代理控制对这个对象的访问。在代理模式中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。

代理模式的结构图如下:
这里写图片描述
注:图片来源《大话设计模式》

看定义总是那么的晦涩难懂,我们还是来举一个代理模式的场景:

Ryan想在上海买一套房子,但是他又不懂房地产的行情,于是委托了中介(Proxy)来帮助他买房子。

我们把这个场景通过Java代码来实现一下:

1.抽象出接口

首先我们把买房子的一类人抽象出来一个接口,接口中有一个buyHouse的方法:

1
2
3
public interface IPersonBuyHouse {
void buyHouse();
}

2.明确被代理的对象

Ryan想要买房子,于是他就需要实现这个IPersonBuyHouse接口:

1
2
3
4
5
6
7
public class Ryan implements IPersonBuyHouse{

@Override
public void buyHouse() {
System.out.println("Ryan:I want buy a House...");
}
}

3.寻找代理

由于Ryan不了解房地产行情,于是将买房子的事情委托给了中介(Proxy),因此中介(Proxy)也需要实现IPersonBuyHouse的接口。但是中介不是给自己买房子的,而是买给其它有购房需求者的,所以他应该持有一个IPersonBuyHouse。而此处的购房需求者就是Ryan.于是Proxy代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Proxy implements IPersonBuyHouse{

private IPersonBuyHouse mIPerson;

public Proxy() {
mIPerson=new Ryan();
}

@Override
public void buyHouse() {
System.out.println("Proxy:I can help you to buy house");
mIPerson.buyHouse();
}
}

接下来我们在Main方法种测试一下Proxy类:

1
2
3
4
5
6
public class ProxyTest {

public static void main(String[] args) {
new Proxy().buyHouse();
}
}

输出结果:

在这里插入图片描述
通过上面的例子可以看到静态代理是一个很简单的设计模式。那么接下来我们看下如何通过静态代理模式来完成对IndicatorView的重构吧。

三、用静态代理模式重构Indicator

在第一章节中我们就已经提到了当前Indicator的弊端:要维护多个IndicatorView,不利于开发也不利于使用。我们当前的目的就是要将IndicatorView统一成一个。而我们现在面临的困境是如何让一个IndicatorView承载多个Indicator Style?因为它既要绘制CIRCLE Style又要绘制DASH Style,以及以后可能还会增加更多的Style样式。在这种场景下我们就可以想到代理模式来解决问题。

上一个章节中我们举了一个静态代理的例子是正向思维写下来的,那么本章中我们就采用逆向思维,看下如何倒推出来静态代理模式。

1.初步设想

首先,我们想要一个IndicatorView承接所有Style的绘制,那么正常来说我们就需要在IndicatorView中通过IndicatorStyle判断是哪种样式,然后在IndicatorView中进行绘制。其伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IndicatorView  {

public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(Style==CIRCLE) {
setMeasuredDimension(measurCircleWidth(), measurCircleHeight());
} else {
setMeasuredDimension(measurDashWidth(), measurDashHeight());
}
}

public void onDraw(Canvas canvas) {
if(Style==CIRCLE) {
drawCircleIndicator(canvas);
} else {
drawDashleIndicator(canvas);
}
}
}

但是如果IndicatorStyle样式非常多的情况下,IndicatorView必然会变得非常庞大且臃肿。因此,我们自然而然的就会想到将View的measure和draw的逻辑抽出来单独给一个类来完成,那么这个类中呢至少应该有measure和draw两个方法。因此,我们将这个类的伪代码写出来大概应该是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DrawerProxy  {

public BaseDrawer.MeasureResult onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(Style==CIRCLE) {
return measureCircleIndicator(int widthMeasureSpec, int heightMeasureSpec);
} else {
return measureDashIndicator(int widthMeasureSpec, int heightMeasureSpec);
}
}

public void onDraw(Canvas canvas) {
if(Style==CIRCLE) {
drawCircleIndicator(canvas);
} else {
drawDashleIndicator(canvas);
}
}
}

2.抽象接口

通过上一小节的操作我们虽然将测量和绘制逻辑从IndicatorView中剥离了出来,但是DrawerProxy 这个类却承载了所有的测量和绘制逻辑。当Style样式多的时候同样会使DrawerProxy类变得臃肿不堪。因此,我们又很自然的想到了应该把不同Style的绘制逻辑单独抽出来,于是就有了CircleDrawer和DashDrawer两个类来分别处理各自的逻辑。但因为这两个类又要同时被放在DrawerProxy类中,且这两个类都又共同的方法。因此可以抽出一个CircleDrawer和DashDrawer的共同接口。于是就有了这样的一个IDrawer的接口:

1
2
3
4
5
6
public interface IDrawer {

BaseDrawer.MeasureResult onMeasure(int widthMeasureSpec, int heightMeasureSpec);

void onDraw(Canvas canvas);
}

同时CircleDrawer和DashDrawer都应该实现该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CircleDrawer implements IDrawer {

@Override
public MeasureResult onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// ... 省略measure逻辑
return mMeasureResult;
}

@Override
public void onDraw(Canvas canvas) {
drawIndicator(canvas);
}

private void drawIndicator(Canvas canvas) {
// ... 省略draw逻辑
}
}
// DashDrawer与此类似,不再贴出

3.回眸一看,静态代理?

到了这里我们在再来看DrawerProxy,发现这个类中同样需要onMeasure和onDraw,那他实现IDrawer接口顺理成章,同时它应该持有一个IDrawer类以便完成真实的测量和绘制任务。于是乎,完善之后的DrawerProxy类就成了这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DrawerProxy implements IDrawer {

private IDrawer mIDrawer;

public DrawerProxy(IndicatorOptions indicatorOptions) {
init(indicatorOptions);
}

private void init(IndicatorOptions indicatorOptions) {
mIDrawer = DrawerFactory.createDrawer(indicatorOptions);
}

public void setIndicatorOptions(IndicatorOptions indicatorOptions) {
init(indicatorOptions);
}

@Override
public BaseDrawer.MeasureResult onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
return mIDrawer.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
public void onDraw(Canvas canvas) {
mIDrawer.onDraw(canvas);
}
}

到这里,我们回过神来看一下,这不就是一个非常标准的静态代理模式吗?当然,这里也结合了简单工厂模式来生成对应的Drawer。
接着我们来看下重构后的IndicatorView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class IndicatorView extends BaseIndicatorView {

private DrawerProxy mDrawerProxy;

public IndicatorView(Context context) {
this(context, null);
}

public IndicatorView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public IndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDrawerProxy = new DrawerProxy(getIndicatorOptions());
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
BaseDrawer.MeasureResult measureResult = mDrawerProxy.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureResult.getMeasureWidth(), measureResult.getMeasureHeight());
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDrawerProxy.onDraw(canvas);
}

@Override
public void setIndicatorOptions(IndicatorOptions indicatorOptions) {
super.setIndicatorOptions(indicatorOptions);
mDrawerProxy.setIndicatorOptions(indicatorOptions);
}
}

可以看到通过静态代理模式简化完后的IndicatorView仅仅剩下了三十多行的代码,所有的测量和绘制逻辑都交给代理类DrawerProxy来处理,而DrawerProxy又将逻辑移交给对应的Drawer来完成。这样,所有的类都各司其职,代码简单明了!开发和维护起来也就变得更加得心应手了!

最后,我们来看下如何使用Indicator:

1
2
3
4
5
    indicatorView
.setSlideMode(IndicatorSlideMode.WORM)
.setIndicatorStyle(IndicatorStyle.CIRCLE)
.setSliderColor(getResColor(R.color.red_normal_color), getResColor(R.color.red_checked_color))
.setupWithViewPager(viewPager)

通过一个简单的链式调用传入不同的参数便实现了多种多样的指示器样式。

四、总结

本篇文章分享了对ViewPagerIndicator重构的一些经验。通过本篇文章相信大家对于静态代理模式也会有了更深的认识。重构后的代码在维护和使用上相比以前显然有了更明显的提升。但是并不等于现在的Indicator已经无懈可击了。相反,它还有很长的路要走。就目前而言,Indicator的SlideMode部分还是又相当大的优化空间的,那么我们就在后面的版本中拭目以吧。

ViewPagerIndicator源码戳此处

同时,欢迎关注用ViewPagerIndicator实现的BannerViewPager


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!