剖析BannerViewPager中Indicator的設計思想

BannerViewPager系列文章共三篇,此文爲第三篇,其它兩篇參看下面連接:java

《打造一個絲滑般自動輪播無限循環Android庫》git

《BannerViewPager源碼剖析》github

很高興又和你們見面了,本篇文章是《BannerViewPager系列》的第三篇。就在不久前BannerViewPager發佈了2.5.0版本,在這個版本中針對Indicator部分的代碼進行了重構。本篇文章帶你們一塊兒來了解下本次重構Indicator中用到的設計思想,順便回顧及加深認識一下靜態代理模式。若是你還不瞭解BannerViewPager能夠先閱讀前兩篇文章:canvas

《打造一個絲滑般自動輪播無限循環Android庫》設計模式

《BannerViewPager源碼解析》ide

也能夠點此處到GitHub查看BannerViewPage源碼。post

1、爲何要重構

在BannerViewPager中針對IndicatorView已進行了兩次較大的重構。第一次重構在第二篇文章《BannerViewPager源碼解析》中也有提到。最初的Indicator是在BannerViewPager內部維護了一個icon圓點的List集合,在BannerViewPager內部會根據頁面size動態添加指示器圓點。顯然這種處理方式存在很大的弊端,即:不靈活、可擴展性低、性能相對較差等諸多問題。針對這一系列問題BannerViewPager在2.0.1中對Indicator進行了第一次重構。此次重構將Indicator改成自定義View,而且抽象出了IIndicator接口,極大的加強了Indicator的可擴展性。所以,在後續若干個版本迭代中Indicator逐漸支持了多種Style(CIRCLE/DASH)、多種滑動模式(SMOOTH/NORMAL)以及徹底自定義Indicator。相比最第一版本,無論在功能仍是性能上都有了很大的提高。可是,在後續版本的迭代中卻又暴露出許多問題。而這些問題很大程度上影響了開發和使用。碰到的最大問題以下:性能

多個IndicatorView不利於維護和使用

在2.5.0版本以前BannerViewPager已經支持了CIRCLE和DASH兩種Indicator樣式,與之對應的是CircleIndicatorView和DashIndicatorView。在BannerViewPager內部用簡單工廠模式根據IndicatorStyle來生成對應的IndicatorView。2.5.0版本以前的代碼以下:測試

# BannerViewPager
initIndicator(IndicatorFactory.createIndicatorView(getContext(), mIndicatorStyle));

# IndicatorFactory 
public class IndicatorFactory {
    public static BaseIndicatorView createIndicatorView(Context context, @AIndicatorStyle int indicatorStyle) {
        BaseIndicatorView indicatorView;
        if (indicatorStyle == IndicatorStyle.DASH) {
            indicatorView = new DashIndicatorView(context);
        } else {
            indicatorView = new CircleIndicatorView(context);
        }
        return indicatorView;
    }
}
複製代碼

這麼以來,每當添加一種Indicator Style時候都須要一個與之對應的IndicatorView類,而且須要修改IndicatorFactory 代碼生成對應的IndicatorView。當Indicator Style愈來愈多的時候維護成本和使用成本都會隨之增長。使用該庫的開發人員須要記住每種Indicator Style對應的IndicatorView,做爲該庫做者的我也要面對愈來愈臃腫的代碼結構,這是你們都不肯意看到的。所以,在這樣的背景下IndicatorView的第二次重構就勢在必行,不得不作。優化

若是關注該庫比較早的同窗很早以前就應該看到了項目源碼的README上添加了一條「優化重構Indicator」的需求。這個需求大概是在2.3.0時候就已經提出來的,但因爲當時BannerViewPager中還有不少功能性的需求未開發完畢,所以在後續的若干版本中開發重點仍是放在了BannerViewPager的功能和優化上。而在2.4.3.1以後,BannerViewPager的功能需求基本開發優化完畢,所以,重構Indicator的需求才在2.5.0版本展開了。

2、回顧靜態代理模式

回想初學Java時你們都應該學過Java的23種設計模式。看完設計模式發現也沒什麼難的,可是在項目中使用的時候就犯了難,每寫一個需求都在想着是否是能夠用某一種設計模式來實現呢?但當着手寫時卻又不知道該選取哪種。相信你們都經歷過這樣的迷茫期。對於這樣的狀況我以爲順其天然就好,只要平時多寫代碼,多思考,加之多看優秀的開源代碼,這種設計思想逐漸的就會被積澱下來,可能在寫代碼的時候不經意間就發現本身使用了某一種設計模式。就像我在重構Indicator以前我並無考慮該用怎樣的設計模式去寫,可是在着手重構的時候就以爲我應該這麼作啊。等到寫完以後回顧本身代碼的時候發現這不就是一個標準的靜態代理模式嗎?

不知道如今你們對代理模式還記得多少,也不知道是否常常會在項目種用到代理模式。無論怎樣,咱們先來回顧如下靜態代理模式吧:

代理模式即爲其它對象提供一種代理控制對這個對象的訪問。在代理模式中,一個類表明另外一個類的功能。這種類型的設計模式屬於結構型模式。在代理模式中,咱們建立具備現有對象的對象,以便向外界提供功能接口。

代理模式的結構圖以下:

這裏寫圖片描述
注:圖片來源《大話設計模式》

看定義老是那麼的晦澀難懂,咱們仍是來舉一個代理模式的場景:

Ryan想在上海買一套房子,可是他又不懂得房地產的行情,因而委託了中介(Proxy)來幫助他買房子。

咱們把這個場景經過Java代碼來實現實現一下:

1.抽象出接口

首先咱們把買房子的人抽象出來一個接口,接口中有一個buyHouse的方法:

public interface IPersonBuyHouse {
	void buyHouse();
}
複製代碼

2.明確被代理的對象

Ryan想要買房子,因而他就須要實現這個IPersonBuyHouse接口:

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代碼以下:

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類:

public class ProxyTest {

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

輸出結果:

在這裏插入圖片描述
經過上面的例子能夠看到靜態代理是一個很簡單的設計模式。那麼接下來咱們看下如何經過靜態代理模式來完成對IndicatorView的重構吧。

3、用靜態代理模式重構Indicator

在第一章節中咱們就已經提到了當前Indicator的弊端:要維護多個IndicatorView,不利於開發也不利於使用。咱們當前的目的就是要將IndicatorView統一成一個。而咱們如今面臨的困境是如何讓一個IndicatorView承載多個Indicator Style?由於它既要繪製CIRCLE Style又要繪製DASH Style,以及之後可能還會增長更多的Style樣式。在這種場景下咱們就能夠想到代理模式來解決問題。

上一個章節中咱們舉了一個靜態代理的例子是正向思惟寫下來的,那麼本章中咱們就採用反向思惟,看下是如何倒逼出來靜態代理模式的。

1.初步設想

首先,咱們想要一個IndicatorView承接全部Style的繪製,那麼正常來講咱們就須要在IndicatorView中經過IndicatorStyle判斷是哪一種樣式,而後在IndicatorView中進行繪製,可是若是IndicatorStyle樣式很是多的狀況下,IndicatorView必然會變得很是龐大且臃腫。所以,咱們天然而然的就會想到將View的measure和draw的邏輯抽出來單獨給一個類來完成,那麼這個類中呢至少應該有measure和draw兩個方法。所以,咱們將這個類的僞代碼寫出來大概應該是這樣子的:

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的接口:

public interface IDrawer {

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

    void onDraw(Canvas canvas);
}
複製代碼

同時CircleDrawer和DashDrawer都應該實現該接口:

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類就成了這個樣子:

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

public class IndicatorView extends BaseIndicatorView implements IIndicator {

    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來完成。這樣,全部的類都各司其職,代碼簡單明瞭!開發和維護起來也就變得更加駕輕就熟了!

最後,咱們來看下IndicatorView在BannerViewPager中的使用:

initIndicator(new IndicatorView(getContext()));
複製代碼

4、總結

本篇文章分享了對BannerViewPager的Indicator重構的一些經驗。經過本篇文章相信你們對於靜態代理模式也會有了更深的認識。重構後的代碼在維護和使用上相比之前顯然有了更明顯的提高。可是並不等於如今的Indicator已經無懈可擊了。相反,它還有很長的路要走。就目前而言,Indicator的SlideMode部分仍是又至關大的優化空間的,那麼咱們就在後面的版本中拭目以吧。

好了,本篇文章到此就結束了,最後到github順手點個Star唄,項目連接見文章末尾。

BannerViewPager源碼戳此處

相關文章
相關標籤/搜索