聊一聊ViewPagerIndicator重構的一些經驗

一包純牛奶的博客地址:

https://juejin.im/user/5aab9dcef265da239c7b1e91

ViewPagerIndicator 的代碼可謂一波三折,在不久前ViewPagerIndicator作爲一個單獨的倉庫從BannerViewPager中拆分了出來。拆分後的indicator已經不僅僅適用於BannerViewPager,還可以用於 ViewPager 和 ViewPager2。

現在,經歷了幾次代碼重構後,總算可以拿得出手了。本篇文章就來寫一寫關於重構indicator的一些經驗,瞭解下該庫是如何通過靜態代理模式來實現多種多樣的indicator樣式的。

先貼上ViewPagerIndicator源碼鏈接(https://github.com/zhpanvip/viewpagerindicator)以及預覽圖,使用方式可以參考GitHub主頁README

/   爲什麼要重構   /

在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的方法:

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的重構吧。

/   重構   /

在第一章節中我們就已經提到了當前Indicator的弊端:要維護多個IndicatorView,不利於開發也不利於使用。我們當前的目的就是要將IndicatorView統一成一個。

而我們現在面臨的困境是如何讓一個IndicatorView承載多個Indicator Style?因爲它既要繪製CIRCLE Style又要繪製DASH Style,以及以後可能還會增加更多的Style樣式。在這種場景下我們就可以想到代理模式來解決問題。

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

1. 初步設想

首先,我們想要一個IndicatorView承接所有Style的繪製,那麼正常來說我們就需要在IndicatorView中通過IndicatorStyle判斷是哪種樣式,然後在IndicatorView中進行繪製。其僞代碼如下:

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兩個方法。因此,我們將這個類的僞代碼寫出來大概應該是這樣子的:

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 {

    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:

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部分還是有相當大的優化空間的,那麼我們就在後面的版本中拭目以待吧。

關注我獲取更多知識或者投稿

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章