RxImagePicker:從零實(shí)現(xiàn)靈活且可高度定制的Android圖片選擇架構(gòu)

本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布

前言

RxImagePicker : 支持RxJava2響應(yīng)式流消痛、靈活可高度定制的Android圖片選擇器。

這是我花費(fèi)了數(shù)月閑暇時間從零開始寫的一個庫兆解,在這期間,我學(xué)習(xí)到了很多,我想把自己的一些所得所感是己,以及這期間的一些思路雳攘,能夠通過一篇文章的形式講述出來带兜,這就是本文的起源。

一.動機(jī)

在展開本文之前吨灭,我希望能夠占用一些篇幅先自我回答三個問題:

1. 為什么要"重復(fù)"造輪子刚照?
2. 要實(shí)現(xiàn)一個怎么樣的輪子?
3. 和網(wǎng)上成熟的三方圖片選擇庫相比喧兄,特點(diǎn)以及不足无畔?

接下來我要花費(fèi) 很大一部分的篇幅,根據(jù)上述的問題吠冤,闡述緣由浑彰,我堅信,相對于一個庫而言拯辙,設(shè)計的初衷和思想更為重要 郭变。

為什么“重復(fù)”造輪子

“Stop Trying to Reinvent the Wheel ( 不要重復(fù)造輪子 ) ”,可能是每個程序員入行被告知的第一條準(zhǔn)則。

我是一個Github的(偽)開源愛好者涯保,在公司的Android項(xiàng)目遇到APP圖片選擇的功能需求時诉濒,我花時間研究了Github上最受歡迎的那些圖片選擇庫,這些庫都是由行業(yè)各前輩花費(fèi)很大心血一點(diǎn)點(diǎn)寫出來的夕春,也經(jīng)過很多的項(xiàng)目和時間的檢驗(yàn)丢氢,一點(diǎn)點(diǎn)迭代過來遍希,從某種程度上講博投,我認(rèn)為這些庫已經(jīng)非常穩(wěn)定并且成熟朵诫。

我很快意識到了一個很“嚴(yán)重”的問題,這些庫對 圖片選擇功能 的實(shí)現(xiàn)速侈,其原理基本都是基于以下的思路實(shí)現(xiàn)的:

  • 開發(fā)者先配置好自己的需求划纽,比如,圖片最大可選的數(shù)量锌畸,是否可選擇視頻勇劣,或者是圖片縮略圖的加載引擎(GlideEngine/PicassoEngine)等等;
  • 通過某個方法,定向打開某個固定的Activity,這個Activity負(fù)責(zé)展示圖片選擇的UI;
  • 開發(fā)者在Activity的onActivityResult()方法中(或者其它的方式比默,比如回調(diào)方法)幻捏,對選擇結(jié)果的處理。

很合理的一種方式命咐,但是對于我個人而言篡九,還略顯不夠,我曾經(jīng)在我的 這篇文章 中闡述了我的一個觀點(diǎn):

事實(shí)上醋奠,現(xiàn)在網(wǎng)絡(luò)上越來越多出現(xiàn)別人封裝好的RecyclerViewAdapter或其他工具榛臼,很多都不可避免出現(xiàn)了 過度封裝 的情況:它也許能夠涵括大多數(shù)的需求,但是這也恰恰是它致命的弊端窜司,在涉及一些新的功能時沛善,它也許會突然無能為力——過度的封裝帶來了嚴(yán)重的耦合,這種問題是架構(gòu)級的塞祈。一個良好的設(shè)計需要更多的思考和嘗試金刁,更重要的也許是靈活,高度的可拓展性议薪。

試想這樣一個場景尤蛮,我在項(xiàng)目中集成了目前非常流行的 Matisse ,這是知乎開源出來的一個圖片選擇庫,擁有著非常 MaterialDesign 的UI設(shè)計(這也是我無比堅定擁護(hù)Matisse的原因)斯议,我們可以看一下它的UI效果:

Matisse.png

非常完美产捞,在大多數(shù)項(xiàng)目上,Matisse 是這樣的哼御,但是當(dāng)我嘗試深入它的原理時坯临,我有了一個困惑,那就是艇搀,我無法實(shí)現(xiàn) 更私有化的UI定制, 比如QQ這樣的設(shè)計:

qq.png

不可否認(rèn)尿扯,當(dāng)我點(diǎn)擊【相冊】按鈕時進(jìn)入到的相冊界面的UI求晶,Matisse應(yīng)該可以勝任(實(shí)際上焰雕,這也大概率需要修改Matisse的源碼才能完全符合公司UI的設(shè)計),但是芳杏,在那之前矩屁,這個界面上的圖片選擇的UI需求,我需要自己去實(shí)現(xiàn)爵赵。

困惑就是吝秕,即使我實(shí)現(xiàn)了這個需求(簡單分析來看,我需要自己實(shí)現(xiàn)一個Fragment來作為容器)空幻,這個界面的圖片選擇和打開相冊界面的圖片選擇烁峭,完全是不相干的兩個功能——它們都是圖片選擇,但一個是由我實(shí)現(xiàn)的,一個由借助三方的圖片選擇庫實(shí)現(xiàn)约郁,這是不合理的設(shè)計缩挑。

我更希望的是,能夠有這樣的一個架構(gòu)鬓梅,它能夠?qū)?圖片選擇的【業(yè)務(wù)功能】和【UI設(shè)計】分離開 供置,在穩(wěn)定成熟的基礎(chǔ)上,讓開發(fā)者根據(jù)需求的變更绽快,靈活的實(shí)現(xiàn)圖片選擇的功能—— 當(dāng)我需要對圖片選擇的需求進(jìn)行修改或者增加時芥丧,我不需要替換框架,而是僅僅替換或者修改一個接口的實(shí)現(xiàn)坊罢。

遺憾的是续担,我并沒有找到這樣的輪子,于是我在18年年初給自己設(shè)定了一個目標(biāo)艘绍,嘗試設(shè)計出這樣一個輪子赤拒。

上文中,我以 Matisse 為例進(jìn)行了說明诱鞠,但這并不是選擇性地針對它挎挖,事實(shí)上, Matisse 是我認(rèn)為非常優(yōu)秀的庫航夺,同樣蕉朵,在我設(shè)計的框架中, Matisse 起到了至關(guān)重要的作用阳掐,這個后文會提到始衅。

要實(shí)現(xiàn)一個怎么樣的輪子

在我接觸 RxJava 的這段時間里,這是一個需要一定學(xué)習(xí)成本的工具庫缭保,但我完全愛上了它汛闸,我希望我能實(shí)現(xiàn)的是一個 支持RxJava響應(yīng)式流、靈活且可高度定制的Android圖片選擇器艺骂,因此我命名為 RxImagePicker诸老。

業(yè)務(wù)層

在業(yè)務(wù)層的設(shè)計上,它應(yīng)該支持這些:

  • Android 拍照
  • Android 圖片選擇
  • 以響應(yīng)式數(shù)據(jù)流的格式返回數(shù)據(jù)(支持Observable/Flowable/Single/Maybe钳恕,對别伏,就是RxJava)
  • 動態(tài)配置響應(yīng)式數(shù)據(jù)流的數(shù)據(jù)類型(就是返回值的類型,開發(fā)者不應(yīng)該為它憂慮忧额,而只需要調(diào)用一個接口):File,Bitmap,或是Uri
  • 控制UI展示的邏輯厘肮,比如是直接打開一個Activity,還是作為一個Fragment被放入一個指定Id的容器中進(jìn)行展示(就像QQ那樣)睦番。

我喜歡 Retrofit 的設(shè)計 ,它將復(fù)雜的網(wǎng)絡(luò)請求需求轉(zhuǎn)換為一個接口進(jìn)行配置类茂,圖片選擇框架也許也可以這樣做,比如這樣:

public interface MyImagePicker {

    @Gallery    //打開相冊選擇圖片
    @AsFile     //返回值為File類型
    Observable<File> openGallery();

    @Camera    //打開相機(jī)拍照
    @AsBitmap  //返回值為Bitmap類型
    Observable<Bitmap> openCamera();
}

接下來,我想要拍照或者圖片選擇巩检,只需要通過注解配置好一個自定義的接口恬涧,然后通過代理實(shí)現(xiàn)即可:

//打開系統(tǒng)默認(rèn)的圖片選擇器
private void onButtonClick() {
        new RxImagePicker.Builder()
                .with(this)
                .build()
                .create(MyImagePicker.class)   
                .openGallery()
                .subscribe(new Consumer<File>() {
                    @Override
                    public void accept(File file) throws Exception {
                        //做你想做的
                    }
                });
}

UI層

同時,它的UI層的設(shè)計上碴巾,我希望至少提供這些支持:

  • 系統(tǒng)級別的拍照溯捆,相冊選擇圖片功能
  • 知乎主題圖片選擇器UI (可選的,包括日間和夜間的兩種主題)
  • 微信主題圖片選擇器UI (可選的)
  • 自定義的圖片選擇器UI (可選的)

下圖就是庫的sample所展示的效果:

系統(tǒng)圖片選擇
system.png
知乎主題
zhihu.png
微信主題
wechat.png
展示到界面上
result.png

這其中的知乎主題的UI效果看起來和 Matisse 非常相似厦瓢,事實(shí)上提揍,UI層的架構(gòu)就是基于 Matisse 進(jìn)行的設(shè)計和修改——包括后面微信主題的圖片選擇UI,可以說煮仇,Matisse庫的源碼劳跃,是UI層的核心。

和其他庫相比浙垫,差異在哪刨仑?

1.靈活

上文花了巨幅的筆墨,說明了我為什么要設(shè)計一個這樣的框架夹姥,無論是UI層還是業(yè)務(wù)層杉武,一旦和目前項(xiàng)目的需求有了沖突(修改或者添加),開發(fā)者考慮的不應(yīng)該是【這個庫實(shí)現(xiàn)不了辙售,干脆換一個庫吧】或者【不管這個庫轻抱,我去再單獨(dú)實(shí)現(xiàn)一個】,而是旦部,基于同一個圖片選擇框架祈搜,修改或者添加對應(yīng)配置的接口

2.可定制

對于一個單獨(dú)的依賴庫士八,我添加了它的依賴容燕,意味著我需要依賴它的所有。

如果 RxImagePicker 只提供了一個依賴婚度,依賴它開發(fā)者可以隨便使用知乎/微信的主題蘸秘,也可以自定義UI,但是我無論選擇哪一種實(shí)現(xiàn)方式陕见,都意味著其他的主題沒有用到秘血,這些沒有用到的類和文件資源占用了apk的體積味抖,站在開發(fā)者的角度而言也許問題不大评甜,但對用戶有限的流量來說,這是毀滅性的災(zāi)難仔涩。

RxImagePicker中忍坷, 類似于知乎/微信主題的UI,對于開發(fā)者是可選的:

//下面的版本號是筆者寫本文時最新的版本號,使用時佩研,請以github上最新的版本作為參考

//【1】最基礎(chǔ)的架構(gòu)柑肴,僅提供了系統(tǒng)默認(rèn)的圖片選擇器和拍照功能
compile 'com.github.qingmei2:rximagepicker:0.2.0'

//【2】提供了自定義UI圖片選擇器的基本組件,自定義UI的需求需要添加該依賴
compile 'com.github.qingmei2:rximagepicker_support:0.2.0'

//如果需要額外的UI支持旬薯,請選擇依賴對應(yīng)的UI拓展庫
compile 'com.github.qingmei2:rximagepicker_support_zhihu:0.2.0'     //【3】知乎圖片選擇器
compile 'com.github.qingmei2:rximagepicker_support_wechat:0.2.0'    //【4】微信圖片選擇器

這樣分層設(shè)計的原因是晰骑,開發(fā)者可以根據(jù)不同的需求選擇添加不同的依賴:

  • 當(dāng)僅僅需要集成系統(tǒng)默認(rèn)的功能時,可以添加最基礎(chǔ)的依賴 compile 'com.github.qingmei2:rximagepicker:0.2.0' 绊序,讓自己項(xiàng)目中的圖片選擇功能能夠通過RxJava的數(shù)據(jù)流觀察到用戶的行為硕舆。

  • 當(dāng)需要實(shí)現(xiàn)仿微信/知乎的圖片選擇功能時,可以添加對應(yīng)主題的依賴骤公,并進(jìn)行對應(yīng)的UI展示抚官。

  • 當(dāng)需要實(shí)現(xiàn)私有化的UI定制時,開發(fā)者可以選擇依賴 compile 'com.github.qingmei2:rximagepicker_support:0.2.0', 其提供了最基礎(chǔ)的UI組件(基于Matisse源碼上阶捆,進(jìn)行了一些修改以適應(yīng)架構(gòu))凌节,以方便開發(fā)者進(jìn)行私有化的UI設(shè)計,只需要實(shí)現(xiàn)RxImagePicker提供的ICustomPickerView接口洒试,然后一切都不需要再管倍奢,交給RxImagePicker就行了±萜澹——事實(shí)上娱挨,【知乎】/【微信】主題圖片選擇器也是基于這些UI組件進(jìn)行了UI的調(diào)整和封裝。

其依賴結(jié)構(gòu)為:

【3】知乎 /【4】微信 → 【2】UI support → 【1】基礎(chǔ)組件

wechat_dependencies.png
support_dependencies.png
rxiamgepicker_dependencies.png

3.需要考慮的成本

RxImagePicker 是一個支持RxJava2響應(yīng)式流捕犬、靈活可高度定制的Android圖片選擇器跷坝,因此,添加它的依賴碉碉,默認(rèn)就會自動添加 RxJavaRxAndroid 的依賴柴钻,因此,對于項(xiàng)目中并未使用RxJava的項(xiàng)目來說垢粮,所需要的成本是需要去認(rèn)真考量的贴届。

看到這里,如果您對于我個人的淺見有所認(rèn)同蜡吧,或者對這個庫有一些興趣毫蚓,請不妨繼續(xù)慢慢看下去,并嘗試去使用它昔善。

二. 在項(xiàng)目中使用它

本文不闡述如何使用RxImagePicker元潘,因?yàn)樗鼞?yīng)該詳細(xì)地展示在項(xiàng)目的Github頁面上。

RxImagePicker 項(xiàng)目的Github地址君仆,內(nèi)含各種UI主題(系統(tǒng)默認(rèn)/微信/知乎)的使用Sample:

https://github.com/qingmei2/RxImagePicker

在閱讀接下來的章節(jié)前翩概,能夠進(jìn)入Github的項(xiàng)目主頁牲距,并花費(fèi)一些時間下載嘗試運(yùn)行RxImagePicker的Sample也許是一個不錯的選擇。

我不能保證你會喜歡它钥庇,如果真的如此牍鞠,花費(fèi)時間閱讀接下來的內(nèi)容同樣沒有意義(雖然此時我還沒有寫接下來的內(nèi)容,但我不認(rèn)為它會比上面開頭的內(nèi)容少)评姨。

三.從零實(shí)現(xiàn)RxImagePicker

3.1 業(yè)務(wù)層架構(gòu)設(shè)計

靈感來源于 RetrofitRxCache 的設(shè)計 ,這兩個庫都通過將復(fù)雜的【網(wǎng)絡(luò)請求】/【數(shù)據(jù)緩存】的需求轉(zhuǎn)換為一個接口進(jìn)行配置难述。

這種設(shè)計的優(yōu)勢很明顯,開發(fā)者不需要真正了解底層的實(shí)現(xiàn)吐句,通過 注解參數(shù)的注入 對接口進(jìn)行配置龄广,同時通過動態(tài)代理完成對功能的實(shí)現(xiàn)。

于是在編碼之前蕴侧,我決定使用上述的這種方式實(shí)現(xiàn) RxImagePicker 底部的搭建择同,開發(fā)者通過 @Camera 或者 @Gallery 注解來標(biāo)記對應(yīng)的行為(是相機(jī)拍照還是打開相冊)。

同時净宵,作為數(shù)據(jù)返回值的類型敲才,我暫時提供了 File/Bitmap/Uri 三種可選項(xiàng),其中Uri是默認(rèn)的返回類型择葡,開發(fā)者也不應(yīng)該對返回值類型進(jìn)行多余 的操作紧武,最好是使用框架提供的接口,于是我添加了對應(yīng)的注解 @AsFile , @AsBitmap@AsUri 敏储。

于是你能看到RxImagePicker 中自定義接口標(biāo)準(zhǔn)的使用方式:

public interface MyImagePicker {

    @Gallery    //打開相冊選擇圖片
    @AsFile     //返回值為File類型
    Observable<File> openGallery();

    @Camera    //打開相機(jī)拍照
    @AsBitmap  //返回值為Bitmap類型
    Observable<Bitmap> openCamera();
}

同時阻星,除了Observable,庫的本身還提供了對RxJava其他響應(yīng)式類型的支持已添,包括Observable/Flowable/Single/Maybe妥箕。

3.2 實(shí)現(xiàn)業(yè)務(wù)層

使用動態(tài)代理

研究過 Retrofit 或者 RxCache 的源碼的同學(xué)都知道,其底部原理都是基于Java的動態(tài)代理更舞,通過反射的方式解析接口方法中的配置(比如參數(shù)/返回值/注解)畦幢,生成對應(yīng)的proxy對象。

RxImagePicker 也不例外缆蝉,我仿照 Retrofit 和 RxCache,通過實(shí)現(xiàn)一個InvocationHandler的實(shí)例宇葱,負(fù)責(zé)管理proxy對象的生成,并在invoke()方法中對接口方法解析刊头,并將返回值返回黍瞧。

public final class ProxyProviders implements InvocationHandler {
    //......
    //省略其他代碼
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        return Observable.defer(new Callable<ObservableSource<?>>() {
            @Override
            public ObservableSource<?> call() throws Exception {
                //解析接口方法,獲取配置的Bean對象
                ImagePickerConfigProvider configProvider = proxyTranslator.processMethod(method, args);
             
               //實(shí)例化UI組件的Projector原杂,display()方法意味著展示UI
                proxyTranslator.instanceProjector(configProvider, fragmentActivity)
                        .display(customPickConfigurations);

                //這個返回值就是用戶選擇結(jié)果的流
                Observable<?> observable = rxImagePickerProcessor.process(configProvider);

                //根據(jù)返回值類型印颤,進(jìn)行數(shù)據(jù)轉(zhuǎn)換
                Class<?> methodType = method.getReturnType();

                if (methodType == Observable.class) return Observable.just(observable);

                if (methodType == Single.class)
                    return Observable.just(Single.fromObservable(observable));

                if (methodType == Maybe.class)
                    return Observable.just(Maybe.fromSingle(Single.fromObservable(observable)));

                if (methodType == Flowable.class)
                    return Observable.just(observable.toFlowable(BackpressureStrategy.MISSING));

                throw new RuntimeException(method.getName() + " needs to return one of the next reactive types: observable, single, maybe or flowable");
            }
        }).blockingFirst();
    }
}

最終所有配置都會匯聚在一個實(shí)例化的 ImagePickerConfigProvider Bean對象中,這個類包含所有的個性化配置:

/**
 * Entity class for user config.
 */
public final class ImagePickerConfigProvider {

    private final String viewKey;
    private final boolean singleActivity;  //是否作為一個activity打開
    private final Class<? extends Activity> activityClass;

    private final SourcesFrom sourcesFrom;//是打開相冊還是拍照
    private final ObserverAs observerAs;  //返回值的類型污尉,F(xiàn)ile,Uri還是Bitmap

    private final int containerViewId;  //若作為一個View(比如Fragment)膀哲,承載它容器的Id
    private final ICustomPickerView pickerView;  //View組件的實(shí)例(比如Fragment)

    public ImagePickerConfigProvider(boolean singleActivity,
                                     String viewKey,
                                     SourcesFrom sourcesFrom,
                                     ObserverAs observerAs,
                                     ICustomPickerView pickerView,
                                     @IdRes int containerViewId,
                                     Class<? extends Activity> activityClass
    ) {
        this.sourcesFrom = sourcesFrom;
        this.observerAs = observerAs;
        this.pickerView = pickerView;
        this.containerViewId = containerViewId;
        this.viewKey = viewKey;
        this.singleActivity = singleActivity;
        this.activityClass = activityClass;
    }
    //省略get()方法
}

這樣的好處顯而易見,匯聚不同地方的配置被碗,最終生成一個對象某宪,它的職責(zé)是單一的,它保證了:在實(shí)現(xiàn)業(yè)務(wù)功能時锐朴,只需要持有一個對象兴喂,根據(jù)對應(yīng)的屬性進(jìn)行對應(yīng)的配置。

在這里焚志,我們似乎并沒有看到一些常見的配置衣迷,比如可選擇圖片的最大數(shù)量,UI的主題等等酱酬。原因是壶谒,這個對象只是管理業(yè)務(wù)功能的配置,這些UI的配置應(yīng)該交給對應(yīng)的UI層去存儲并管理膳沽。

使用Dagger2依賴注入

我希望我能夠?qū)ψ约簬焖?額外添加的依賴汗菜,管理更加嚴(yán)苛一些,在最基礎(chǔ)的組件中挑社,我甚至沒有添加 Glide 或者 picasso 陨界,因?yàn)槲艺J(rèn)為它們更應(yīng)該在UI層的拓展library中被添加或者管理,這樣能夠進(jìn)一步減少基礎(chǔ)業(yè)務(wù)組件的體積痛阻。

使用Dagger2是一個艱難的決定菌瘪,大家都知道,一個庫的設(shè)計阱当,依賴越少越好俏扩,這樣就不會因?yàn)槠渌蕾噹斓纳墸鴮?dǎo)致庫本身的bug弊添。

比如說动猬,我的一個庫底層依賴了Glide,那么Glide在大版本升級時表箭,API的改變有可能影響到我自己庫的使用赁咙,這是很難避免的。

同時免钻,依賴的越多彼水,意味著庫本身的體積增加,這本身就是一種局限性极舔。

但我還是選擇了Dagger2凤覆,因?yàn)樵谖铱磥磉@樣利大于弊,因此拆魏,在選擇使用 RxImagePicker 的時候盯桦,默認(rèn)除了 RxJavaRxAndroid 的依賴,還額外依賴了 dagger——這是 google 基于 square 的dagger上慈俯,fork并自行拓展的依賴注入庫

dagger的使用讓我對架構(gòu)中配置的管理游刃有余拥峦,這也要感謝 VictorAlbertos 贴膘,通過他的 RxCache ,讓我知道了略号,依賴注入還可以這樣用刑峡。

到這里,庫本身的所有額外依賴都已經(jīng)寫清楚了:

image.png

當(dāng)然玄柠,對于項(xiàng)目中使用到了dagger和RxJava的同行來說突梦,這種行為幾乎沒有影響。

圖片選擇功能的實(shí)現(xiàn)

現(xiàn)在羽利,通過動態(tài)代理和Dagger宫患,我已經(jīng)獲取到了接口方法對應(yīng)的所有配置,接下來就是要將UI和業(yè)務(wù)進(jìn)行綁定了这弧。

和 Retrofit 略微不同的是撮奏,前者并未涉及UI,因此我還需要尋求其它庫的一些幫助当宴。

MLSDev:RxImagePicker畜吊,感謝這個庫,讓我看到了曙光户矢。

這是和我名字相同的一個庫(實(shí)際上我的基礎(chǔ)組件的部分設(shè)計參考了它玲献,給我?guī)砹撕艽蟮膸椭孕母兄xL堇恕)捌年,它也是用來提供Android設(shè)備上圖片選擇的功能,但是有一個缺陷挂洛,就是它使用的是系統(tǒng)默認(rèn)的拍照和相冊功能礼预,如果你想要自定義UI,你需要拉他的源碼進(jìn)行修改虏劲,并自己實(shí)現(xiàn)UI托酸。

雖然有局限性,但是它的原理是柒巫,創(chuàng)建一個不可見的Fragment励堡,通過這個Fragment,控制Intent打開系統(tǒng)的相冊或者相機(jī)堡掏,在onActiviyResult()方法中將數(shù)據(jù)交給Subject發(fā)射应结,開發(fā)者就能在代碼中通過subscribe()訂閱獲取對應(yīng)的數(shù)據(jù)流了。

除此之外,我還研究參考了其它一些RxJava的拓展庫鹅龄,比如:

RxLifecycle: Lifecycle handling APIs for Android apps using RxJava
RxPermissions: Android runtime permissions powered by RxJava2

他們的原理都很相似揩慕,通過在生命周期組件中添加一個Subject,用來接受并發(fā)射數(shù)據(jù)扮休,然后將Subject返回給開發(fā)者迎卤,開發(fā)者訂閱后,就能獲取對應(yīng)的數(shù)據(jù)肛炮。

我也實(shí)現(xiàn)了一個Fragment:

//略微進(jìn)行了調(diào)整止吐,并省略大量代碼宝踪,這里僅僅闡述自己的思路
public final class SystemGalleryPickerView extends Fragment 
                  implements ICustomPickerView {

       //這個Subject發(fā)射的數(shù)據(jù)就是用戶選擇的圖片
       private PublishSubject<Uri> publishSubject;
       //是否要終止本次訂閱
       private PublishSubject<Integer> canceledSubject;
       
       //當(dāng)用戶調(diào)用接口的方法打開相冊或者拍照時侨糟,代理對象實(shí)際會調(diào)用該方法
       //并將publishSubject強(qiáng)轉(zhuǎn)成Obervable<T>返回給開發(fā)者
       //同時規(guī)定,當(dāng)canceledSubject發(fā)射數(shù)據(jù)時瘩燥,publishSubject停止訂閱
       public Observable <Uri> pickImage() {
          publishSubject = PublishSubject.create();
          canceledSubject = PublishSubject.create();

          requestPickImage();
          return publishSubject.takeUntil(canceledSubject);
      }
        
        //實(shí)際上就是這個隱藏的Fragment打開相冊秕重,然后再onActivityResult()中監(jiān)聽
        //當(dāng)用戶選擇的圖片返回,將圖片的Uri從intent中取出并交給PublishSubject發(fā)射
       @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK) {
            if (publishSubject != null) {
            publishSubject.onNext(getActivityResultUri(data));
            publishSubject.onComplete();
            }
        } else {
            canceledSubject.onNext(requestCode);
        }
    }
}

業(yè)務(wù)層和UI層的隔離嘗試:ICustomPickerView

上文中厉膀,我的Fragment實(shí)現(xiàn)了ICustomPickerView溶耘,這是一個接口,用于定義UI層的一些行為:

public interface ICustomPickerView {
    //展示UI界面
    void display(FragmentActivity fragmentActivity,
                 @IdRes int viewContainer,
                 String tag,
                 ICustomPickerConfiguration configuration);
    //要返回的數(shù)據(jù)
    Observable<Uri> pickImage();
}

它的意義是服鹅,將用戶的圖片選擇或者拍照行為凳兵,所需要展示的界面,抽象成一個接口企软,供RxImagePicker調(diào)用庐扫。

很好理解,我依賴了知乎主題的圖片選擇器仗哨,其中的ZhihuImagePickerFragment類負(fù)責(zé)展示知乎主題的UI:

compile 'com.github.qingmei2:rximagepicker_support_zhihu:0.2.0' 

實(shí)際上形庭,因?yàn)橐蕾囮P(guān)系是RxImagePicker被依賴于知乎主題的UI庫,底層的RxImagePicker基礎(chǔ)組件無法獲取上層的 ZhihuImagePickerFragment類的引用厌漂,因此我設(shè)計了一個接口萨醒,就是ICustomPickerView

這之后苇倡,再讓ZhihuImagePickerFragment實(shí)現(xiàn)ICustomPickerView接口:

public class ZhihuImagePickerFragment extends Fragment implements
        IGalleryCustomPickerView {
        //...
}

這樣富纸,底層的組件只需要負(fù)責(zé)調(diào)用兩個方法,什么時候展示UI旨椒,以及什么時候返回數(shù)據(jù)胜嗓,至于是如何實(shí)現(xiàn)的,底層組件并不關(guān)心钩乍。

3.3 UI層的設(shè)計和實(shí)現(xiàn)

上文中辞州,我已經(jīng)定義好了UI層接口,對于一些拓展的UI選擇器寥粹,我只需要按照UI層接口的規(guī)范变过,實(shí)現(xiàn)對應(yīng)的邏輯即可埃元。

UI層的設(shè)計我借鑒了 Matisse ,它的設(shè)計已經(jīng)足夠好媚狰,穩(wěn)定性也一定比我自己實(shí)現(xiàn)好得多岛杀,拓展性功能的API也已經(jīng)涵括了大多數(shù)的需求。

我花了一些時間基于 Matisse 的源碼進(jìn)行了部分的修改崭孤。

我把 Matisse 的代碼分成了兩層:

//提供了自定義UI圖片選擇器的基本組件类嗤,自定義UI的需求需要添加該依賴
compile 'com.github.qingmei2:rximagepicker_support:0.2.0'

compile 'com.github.qingmei2:rximagepicker_support_zhihu:0.2.0'     //知乎圖片選擇器

其中我將公共的放入了 rximagepicker_support包中,作為UI層的基本組件辨宠,而詳細(xì)的UI實(shí)現(xiàn)界面遗锣,我放入了rximagepicker_support_zhihu包中:

image.png
image.png

這之后就是長時間的UI界面代碼的修改和編寫,相比之前熱情飽滿的業(yè)務(wù)設(shè)計嗤形,UI層代碼寫起來真的有點(diǎn)枯燥(其間我加深了關(guān)于自定義控件的理解精偿,收獲頗多,但是我還是要說赋兵,很枯燥...)笔咽。

最終,除了zhihu主題的圖片選擇器霹期,我還基于 Matisse 修改后的 UI組件叶组,設(shè)計出了Wechat主題的圖片選擇器以供參考——就像文章開篇時的 sample 展示效果一樣。

總結(jié)

經(jīng)過幾個月的學(xué)習(xí)和嘗試历造,我最終實(shí)現(xiàn)出來了 RxImagePicker 甩十,坦白來講,其中的設(shè)計并非如我預(yù)期的那般完美帕膜,但是它依然是我目前比較滿意的作品枣氧。它達(dá)到了我想要的樣子:靈活可高度定制 ,并且支持 RxJava 垮刹。

這不是終點(diǎn)达吞,因?yàn)橐粋€工具是依靠不斷的嘗試和迭代才能慢慢完善的,我已經(jīng)在公司的項(xiàng)目中嘗試使用了它荒典,至少目前為止酪劫,它正在經(jīng)歷Production級別的檢驗(yàn)。

這幾個月的經(jīng)歷寺董,RxImagePicker并不是一個很重的庫覆糟,它僅僅是一個工具,便讓我更加體會到了開源的艱難遮咖,而這才僅僅開始滩字。

在此要特別感謝一位前輩, JessYan ,有幸和他在數(shù)次交流中,讓我深刻理解了開源的意義麦箍,開源重要的不僅僅是 方便快速實(shí)現(xiàn)便于裝X 漓藕,更重要的是 思想的交流責(zé)任感

我會堅持維護(hù)下去挟裂,也希望有朋友能夠嘗試使用它享钞,并在 issues 中提供反饋,我將第一時間進(jìn)行回復(fù)诀蓉。

最后留下RxImagePicker的Github地址:

RxImagePicker : 支持RxJava2響應(yīng)式流栗竖、靈活可高度定制的Android圖片選擇器。
https://github.com/qingmei2/RxImagePicker

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渠啤,一起剝皮案震驚了整個濱河市狐肢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌埃篓,老刑警劉巖处坪,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件根资,死亡現(xiàn)場離奇詭異架专,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)玄帕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門部脚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人裤纹,你說我怎么就攤上這事委刘。” “怎么了鹰椒?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵锡移,是天一觀的道長。 經(jīng)常有香客問我漆际,道長淆珊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任奸汇,我火速辦了婚禮施符,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘擂找。我一直安慰自己戳吝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布贯涎。 她就那樣靜靜地躺著听哭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上陆盘,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天且警,我揣著相機(jī)與錄音,去河邊找鬼礁遣。 笑死斑芜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的祟霍。 我是一名探鬼主播杏头,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沸呐!你這毒婦竟也來了醇王?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤崭添,失蹤者是張志新(化名)和其女友劉穎寓娩,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡盏檐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年忌傻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焊夸。...
    茶點(diǎn)故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蓝角,靈堂內(nèi)的尸體忽然破棺而出阱穗,到底是詐尸還是另有隱情,我是刑警寧澤使鹅,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布揪阶,位于F島的核電站,受9級特大地震影響患朱,放射性物質(zhì)發(fā)生泄漏鲁僚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一麦乞、第九天 我趴在偏房一處隱蔽的房頂上張望蕴茴。 院中可真熱鬧,春花似錦姐直、人聲如沸倦淀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撞叽。三九已至姻成,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間愿棋,已是汗流浹背科展。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留糠雨,地道東北人才睹。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像甘邀,于是被迫代替她去往敵國和親琅攘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,283評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理松邪,服務(wù)發(fā)現(xiàn)坞琴,斷路器,智...
    卡卡羅2017閱讀 134,697評論 18 139
  • 原文鏈接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影閱讀 32,942評論 6 472
  • 轉(zhuǎn)眼已過秋分時節(jié)逗抑,可今年的冬天似乎就這么早早的來了剧辐,前幾天還是艷陽高照,今天卻已經(jīng)冷的讓人窒息邮府,一陣寒風(fēng)吹過...
    娟子時光閱讀 256評論 0 0
  • 我國改革開放近三十年荧关,人民生活水平正在不斷的提高,衣食住行都在升級之中挟纱,古語云:日求三餐羞酗,夜求一宿腐宋。這個是...
    bd3d0f7c4454閱讀 223評論 0 0