致敬Glide-借用其思想設計一個拍照選圖控件

內容提要

本文內容較長,包含一個功能整個重構從想法到設計以及落地的完整過程,經過閱讀本文你能夠收穫:java

  • Glide 幾個關鍵特性的設計原理以及對它們的思考(面試可用)
  • 編碼從拓展性層面的考慮到面向對象編程的實踐
  • 相似於RxJava的工做流的設計思想以及實踐
  • 一些kotlin和Java 互相調用的小技巧
  • 一個功能強大的調用系統相機、系統相冊的庫以及它是如何被設計出來的

背景

鑑於最近對原有項目進行了老的代碼的重構,其中的調用系統拍照選圖模塊就是咱們平常遇到一個痛點,須要在調用系統相機(相冊)的部分寫Intent :android

//示例代碼
  Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
  intent.setType("image/*");
  activity.startActivityForResult(intent, getRequestCode());
複製代碼

在onActivityResult中得到照片之後、異步處理(方向糾正、壓縮等)得到的數據、若是有業務須要還需實現上傳到後端的邏輯:git

@Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable final Intent data) {
   	  //示例代碼
      //異步處理圖片、壓縮、旋轉等
      File result =  compressPhotoFromAlbumAsy(data);
      // 上傳給後端、等業務處理
      uploadAsy(result);      
    }
複製代碼

因此梳理下來有幾點痛點:github

  • 處理入口分散、觸發拍照行爲和接收不在一個地方、依賴重寫onActivityResult方法,很是不利於後期模塊化組件化拆分。
  • 圖片須要異步處理以提升用戶體驗,因此此時還須要考慮容器的生命週期,須要添加不少判斷代碼,從而致使可讀性比較差,每次梳理圖片的邏輯處理曾和業務上的圖片上傳層都須要很多時間

重構結果:

故鑑於以上幾個痛點,我借鑑Glide中的幾個思想對此模塊進行了重構,設計了CoCo 庫->一行代碼靈活完成原生系統提供的拍照、選圖、壓縮、裁剪等脫離業務層的操做。先隨便貼兩個功能:面試

  • 調用系統相機拍張照:
CoCo.with(this@MainActivity)
	    .take(createSDCardFile())
	    .start(object : CoCoCallBack<TakeResult> {
	
	            override fun onSuccess(data: TakeResult) {
	            	iv_image.setImageBitmap(Utils.getBitmapFromFile(data.savedFile!!.absolutePath))
	            }
	        })
複製代碼

上述即可自動拿到原圖去展現咱們的結果,固然若是你須要壓縮或者處理一下原圖的話,只需切換一個操做符便可:編程

CoCo.with(this@TakePictureActivity)
                .take(createSDCardFile())
                //切換操做符執行下一步功能
                .then()
                //處理圖片 
                .dispose()
                .start(object : CoCoCallBack<DisposeResult> {

                    override fun onSuccess(data: DisposeResult) {
                        iv_image.setImageBitmap(data.compressBitmap)
                    }
                })
複製代碼

用dispose 操做符能夠壓縮並糾正原圖的轉向,而且異步操做自動像Glide同樣綁定with()中傳入容器的生命週期,並且dispose 對於圖片文件的處理方式能夠自定義,按需修改策略。後端

固然、CoCo還提供了 系統相冊選圖、系統裁剪等的功能、而且這一系列的操做符能夠組合使用,你能夠選圖、壓縮並裁剪、選圖裁剪等,按需本身組合便可。緩存

原理與設計思想:

對Glide 的致敬:

Glide 是咱們Android 經常使用的圖片庫,它有幾個特性是咱們所熟知的:markdown

  • 一行代碼完成圖片加載、緩存、展現過程、而且異步操做自動綁定容器生命週期
  • 執行加載操做load以後、拓展了不少不一樣的方法可靈活選擇,好比圖片大小、緩存類型等、 最終經過into 完成全部操做

1:上述使人舒服特性的原理在於在Glide 內部,本身經過添加Fragment的方式 在Fragment的生命週期中維護了個LifeCycle,所以能夠感知調用容器的生命週期,從而在相關的時候作一些異步的解綁操做。app

public class RequestManagerFragment extends Fragment {
    @Override
    public void onStart() {
        super.onStart();
        lifecycle.onStart();
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        lifecycle.onDestroy();
    }
}
複製代碼

2:而經過load操做符以後返回的對象實際上是一個 Builder對象,根據load入參數的不一樣產生不一樣的子類Builder對象,不一樣子類封裝本身特有的方法,好比load以後返回的是:DrawableTypeRequest 對象,本質也是一個Builder:

//簡化了繼承關係,只爲了展現清楚核心代碼
public class DrawableTypeRequest<ModelType> extends GenericRequestBuilder<ModelType> implements DownloadOptions {
    /** * 本身特有的方法。。。。。 */
    public <Y extends Target<File>> Y downloadOnly(Y target) {
        ///
    }
}
複製代碼

調用asBitmap 以後又返回 BitmapTypeRequest 對象,一樣也是Builder:

//簡化了繼承關係,只爲了展現清楚核心代碼
public class BitmapTypeRequest<ModelType> extends GenericRequestBuilder<ModelType, Bitmap> {
 /** * 本身特有的方法。。。。。 */
  public BitmapRequestBuilder<ModelType, byte[]> toBytes() {
       ///
    }
}
複製代碼

可是它們都繼承自最終基類 GenericRequestBuilder,這個類中就包含了咱們經常使用的into 方法:

public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable { /** * 基類共有的方法。。。。。 */
  public <Y extends Target<TranscodeType>> Y into(Y target) {
  ////
    }

}
複製代碼

經過上述設計便可以保證每一個子功能模塊在Builder中維護本身特有的方法,又能夠最終調用到父類的公用方法,一來比較符合面向對象的單一職責的設計原則,二來能夠提升代碼可讀性,讓使用者在使用的時候便於理解。

總結一下 Glide的核心思想就一行代碼完成

  • 在哪裏?
  • 作什麼?
  • 結果是?
//在哪裏
    Glide.with(this@MainActivity)
        //幹嗎
    .load("https://xxxxx.com.sxx.jpg")
        //結果是
    .into(iv_image)
複製代碼

需求分析

  1. 我須要實現調用系統的相機、相冊、裁剪、無非都須要經過intent來啓動Activity,若是我採用Fragment的方式一來能夠將其做爲載體去啓動Activity和重寫onActivityResult接收返回的數據的方法,從而無須在業務層重寫,從而減小代碼入侵解決第一個痛點、而來能夠借鑑Glide模擬生命週期的方式在後期處理圖片好比壓縮、旋轉等異步操做的時候自動感知外部容器生命週期,從而減小業務代碼

  2. Glide的 功能對應基類Builder提供基本方法、而後在子類拓展其特有功能的方法能夠借鑑到這個場景下:執行每一個功能產生相關的功能特性Builder、而後在父類方法中收攏,最終拿到結果,因此設計了以下UML圖:

在這裏插入圖片描述

UML 分析:
  1. CoCo.with 方法傳入當前容器,指定當前的工做場地[在哪裏 Where] ,返回一個FunctionManager 對象,這個對象提供了咱們當前場地能提供的基本方法,也就是咱們接下來的事情[作什麼 whate]拍照(take)、選圖(pick)、裁剪(crop)、處理(dispose) 等功能均在此類提供和拓展。
  2. 當FunctionManager調用其中某一個操做符時候,產生其對應的Builder對象,好比調用了take功能,咱們返回一個 TakerBuilder對象,由於是拍照功能,咱們能夠在這裏提供攝像頭面向(cameraFace)、拍照結果寫入文件(fileToSave)等參數、同理若是你選擇了pick 方法,則能夠在這裏拓展圖片選擇範圍(range)等方法、而最終不管最終是哪一種子功能Builder、它們都繼承自BaseFunctionBuilder、而在這個父類中提供了start的方法,不管經過哪一種子類拓展其方法來拼裝參數、均可以經過父類的start 方法執行到下一步,即保證了一行代碼的整潔性、又保證了子類之間方法的隔離,符合面向對象設計思想,便於後續拓展和維護 。
  3. 經過一、2步 咱們完成了在哪裏、幹什麼的封裝,最終經過start的方法最終執行,拿到咱們想要的結果,也就是一系列的Result

詳細源碼的設計與分析:

下面以選圖功能做爲鏈路詳細分析整個大致設計(源碼有所簡化、方便理解):

//在哪裏 [activity中]
    CoCo.with(this@MainActivity)
     //幹嗎[選圖]
    .pick()
     //結果是 [PickResult]
    .start(object : CoCoCallBack<PickResult> {
       override fun onSuccess(data: PickResult) {
            iv_image.setImageURI(data.originUri)
        }

        override fun onFailed(exception: Exception) {
        }
     })
複製代碼

第一步固然定義容器的場景:

object CoCo {

    @JvmStatic
    fun with(activity: Activity): FunctionManager {
        checkStatusFirst(activity)
        return FunctionManager.create(activity)
    }

    @JvmStatic
    fun with(fragment: androidx.fragment.app.Fragment): FunctionManager {
        checkStatusFirst(fragment.activity)
        return FunctionManager.create(fragment)
    }

    @JvmStatic
    fun with(fragment: Fragment): FunctionManager {
        checkStatusFirst(fragment.activity)
        return FunctionManager.create(fragment)
    }
}

複製代碼

定義使用場景之後、返回一個FunctionManager 對象,裏面提供咱們全部已有的和之後能夠拓展的功能:

  • 小技巧: 使用 @JvmStatic 註解能夠保持Java 調用 kotlin 代碼時 和kotlin 本身調用提供一致性的體驗
class FunctionManager(internal val container: IContainer) {


    /** 拍照 * take photo from system camera * @param fileToSave the result of take photo to save * @see TakeBuilder */
    fun take(fileToSave: File): TakeBuilder =
        TakeBuilder(this).fileToSave(fileToSave)

    /** 選圖 * select a photo from system gallery or file system * @see PickBuilder */
    fun pick(): PickBuilder =
        PickBuilder(this)

    /** 異步處理文件 * dispose an file in background thread ,and will bind the lifecycle with current container * @param disposer you can also custom disposer * @see Disposer */
    @JvmOverloads
    fun dispose(disposer: Disposer = DefaultImageDisposer.get()): DisposeBuilder =
        DisposeBuilder(this)
            .disposer(disposer)

	//........ expand other functions

}
複製代碼

當選用每個功能以後,咱們返回一個Builder、選圖對應當就是PickBuilder,而拍照對應當就是TakeBuilder,固然這都繼承自它的基類BaseFunctionBuilder:

abstract class BaseFunctionBuilder<Builder, Result>(
    internal val functionManager: FunctionManager
) {

    /** * 開始執行 */
    fun start(callback: CoCoCallBack<Result>) {
        generateWorker(getParamsBuilder()).start(null,callback)
    }

    /** * 獲取參數Builder */
    internal abstract fun getParamsBuilder(): Builder

    /** * 生成真正幹活兒的類 */
    internal abstract fun generateWorker(builder: Builder): Worker<Builder, Result>

}
複製代碼

在這個基類裏面定義了兩個泛型參數 Builder 、Result、第一個Builder 參數的其具體實現用於封裝幹活時候的全部參數、一個用於承載最終完成的結果,咱們來看看PickBuilder的實現:

class PickBuilder(fm: FunctionManager) :
    BaseFunctionBuilder<PickBuilder, PickResult>(fm) {

    internal var pickRange = Range.PICK_DICM

    /** * 在這裏拓展選圖特有的功能,好比是選擇系統相冊仍是 * @param pickRange the range you can choose * @see Range.PICK_DICM the system gallery * @see Range.PICK_CONTENT the system content file */
    fun range(@PickRange pickRange: Int = Range.PICK_DICM): PickBuilder {
        this.pickRange = pickRange
        return this
    }

    override fun getParamsBuilder(): PickBuilder {
        return this
    }

    override fun generateWorker(builder: PickBuilder): Worker<PickBuilder, PickResult> {
        return PickPhotoWorker(functionManager.container, builder)
    }
}
複製代碼

這裏生成Worker的最終實現是生成了一個 PickPhotoWorker,裏面完成了選圖的最終實現, 因此如今設計咱們的Worker類:

先設計接口抽象行爲:

interface Worker<Builder, ResultData> {

    fun start( callBack: CoCoCallBack<ResultData>)

}
複製代碼

再設計一個抽象類,這一層去持有容器引用(好比咱們須要啓動Activity去選圖、也須要感知容器生命週期) 和 咱們構建好的Builder參數:

abstract class BaseWorker<Builder, ResultData>(val iContainer: IContainer, val mParams: Builder) :
    Worker<Builder, ResultData>
複製代碼

再看看這個選圖的最終PickPhotoWorker:

class PickPhotoWorker(iContainer: IContainer, builder: PickBuilder) :
    BaseWorker<PickBuilder, PickResult>(iContainer, builder) {

    override fun start( callBack: CoCoCallBack<PickResult> ) {
        val activity = iContainer.provideActivity()
        activity ?: return
        pickPhoto(activity, callBack)
    }

    private fun pickPhoto(activity: Activity, callBack: CoCoCallBack<PickResult>) {
        val pickIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        if (null === pickIntent.resolveActivity(activity.packageManager)) {
            callBack.onFailed(BaseException("activity status error"))
            return
        }
        try {
            //start activity for result
            iContainer.startActivityResult(
                pickIntent, Constant.REQUEST_CODE_IMAGE_PICK
            ) { _: Int, resultCode: Int, data: Intent? ->
                handleResult(resultCode, data, callBack)
            }
        } catch (e: Exception) {
            callBack.onFailed(e)
        }
    }

    private fun handleResult( resultCode: Int, intentData: Intent?, callBack: CoCoCallBack<PickResult> ) {

        if (null != intentData && null != intentData.data) {
            val result = PickResult()
            result.originUri = intentData.data!!
            callBack.onSuccess(result)
        } else {
            callBack.onFailed(BaseException("null result intentData"))
        }
    }
}
複製代碼

再看看容器的接口 iContainer:

interface IContainer {

    //提供activity 判斷狀態、或者一些須要Context特有方法的場景
    fun provideActivity(): Activity?

    // startActivityResult 啓動activty、拿到回調結果
    fun startActivityResult(intent: Intent, requestCode: Int, callback: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit )

    //得到外部容器宿主
    fun getLifecycleHost(): Host

}
複製代碼

再看看它的具體實現固然也就是咱們實際用於承載一切的的Fragment:

class AcceptResultSupportFragment : Fragment(), IContainer,
    Host {

    /** * 當前生命週期狀態 */
    private var current = Host.Status.INIT

    private var requestCode: Int = 0

    private var callback: ((requestCode: Int, resultCode: Int, data: Intent?) -> Unit)? = null

    override fun startActivityResult( intent: Intent, requestCode: Int, callback: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit ) {
        this.requestCode = requestCode
        this.callback = callback
        startActivityForResult(intent, requestCode)
    }

    override fun provideActivity(): Activity? = activity

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        callback!!(requestCode, resultCode, data)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        //當外部想感知容器是否銷燬當時候,讀取這個變量便可
        current = Host.Status.LIVE
    }

    override fun onDestroy() {
        current = Host.Status.DEAD
        super.onDestroy()
        callback = null
    }

    override fun getStatus(): Int {
        return current
    }

    override fun getLifecycleHost(): Host {
        return this
    }
}
複製代碼

而這個Fragment就是在咱們第一步CoCo.with 建立FunctionManager 的時候建立的。

到這裏,咱們最開始調用的每個操做符內部的基本鏈路就分析完成了、總結一下就是:

  1. 根據當前是在Activity或者Fragment的狀態建立一個功能提供器FunctionManager:

  2. FunctionManager 選擇指定功能以後產生相應功能對於的Builder 用於拼裝各類參數

  3. Builder參數拼裝完成後調用父類公共方法 start 去生成相應功能對應的Worker,每一個Worker去作本身的實現

  4. Worker 作完成事情後回調給CallBack 拿到咱們的Result**

因此,以上述框架思想下,我拓展出了,系統拍照、系統選圖、系統裁剪、文件異步處理(壓縮、旋轉、等圖片操做)等總共四個方法,咱們只須要一行代碼便可完成,好比圖片處理:

CoCo.with(this)
     .dispose()
     .origin(imageFile.path)
     .start(object : CoCoAdapter<DisposeResult>() {

           override fun onSuccess(data: DisposeResult) {
               iv_image.setImageBitmap(Utils.getBitmapFromFile(data.savedFile!!.absolutePath))
           }
       })
複製代碼

那麼如今你們可能有疑問了:

1. 裏面本身實現了LifeCycle 的感知, 谷歌已經有了LifeCycle 組件了,爲什麼還要重複造輪子?

2. 一般咱們拍照以後每每還須要對圖片進行壓縮處理以減少大小,那按照上述的設計,先拍照再處理豈不是要在回調裏面嵌套了?

So 回答第一個問題, 之因此不使用谷歌自帶的LifeCycle控件是由於, 這套控件只提供給support包或者Androidx 的包使用,而原生的Activity、Fragment是不具有lifeCycleOwner 的能力的,咱們不能保證用戶傳入的容器是 支持lifeCycleOwner 的仍是不支持的,因此本身實現以保證更好的兼容性。

第二個問題也就是接下來要重點介紹的切換操做符的設計緣由了,經過設計咱們的切換操做符,可讓咱們一行代碼組合多種操做,以知足實際業務場景,咱們每每拍完照以後須要進行壓縮處理,經過then操做符能夠一步到位:

好比,咱們選完一張圖片要進行裁剪而後再壓縮

CoCo.with(this@MainActivity)
	  //選圖 
     .pick()
      //切換操做符
     .then()
     //裁剪 
     .crop()
      //切換操做符
   	 .then()
   	 //壓縮處理
     .dispose()
     .start(object : CoCoAdapter<DisposeResult>() {

         override fun onSuccess(data: DisposeResult) {
             iv_image.setImageBitmap(data.compressBitmap)
         }
     })
複製代碼

效果圖:

在這裏插入圖片描述

設計Then 操做符:

按照以前的設計,咱們在調用 start的方法的時候,直接生成了其對應的Worker開始執行,那如今若是咱們想串聯多個功能是否是我只須要在切換其餘功能的操做符的時候,直接生成Woker對象存下來,最後完成全部的功能以後再依次按順序調用start、將上一個Woker處理的結果丟給下一個Woker便可完成功能的串聯?

設計流程:
  1. 在FunctionManager中增長一個List用於保存全部要執行的Worker:
class FunctionManager(internal val container: IContainer) {

    internal val workerFlows = ArrayList<Any>()
}
複製代碼
  1. 而後在原有BaseWorker 的位置添加then方法,當調用then 方法的時候生成Worker並添加到list中,而後返回 FunctionManager對象,保證咱們完成一個工做以後,能夠切換到FunctionManager的其餘功能:
fun then(): FunctionManager {
        this.functionManager.workerFlows.add(generateWorker(getParamsBuilder()))
        return this.functionManager
    }
複製代碼
  1. 這個時候咱們修改Worker的 start 方法,增長一個參數用於承接上一個Worker的結果:
interface Worker<Builder, ResultData> {

    fun start(formerResult: Any?, callBack: CoCoCallBack<ResultData>)

}
複製代碼
  1. 此時咱們最終BaseFunctionBuilder的star方法的實現固然就是一個遞歸調用了:
abstract class BaseFunctionBuilder<Builder, Result>(
        internal val functionManager: FunctionManager
    ) {
    
    fun then(): FunctionManager {
        ...
    }

    /** * call start to begin the workflow */
    fun start(callback: CoCoCallBack<Result>) {
        this.functionManager.workerFlows.add(generateWorker(getParamsBuilder()))
        //循環取每個Worker
        val iterator = functionManager.workerFlows.iterator()
        if (!iterator.hasNext()) {
            return
        }
        //第一次開始,第一個Worker formerResult 爲null
        realApply(null, iterator, callback)
    }

    private fun realApply( formerResult: Any?, iterator: MutableIterator<Any>, callback: CoCoCallBack<Result> ) {
        val worker: Worker<Builder, Result> = iterator.next() as Worker<Builder, Result>
        worker.start(formerResult, object : CoCoCallBack<Result> {

            override fun onSuccess(data: Result) {
                if (iterator.hasNext()) {
                    iterator.remove()
                    //若是還有下個,遞歸繼續
                    realApply(data, iterator, callback)
                } else {
                    //沒有下個,結束流程、回調給最終結果
                    callback.onSuccess(data)
                }
            }

            override fun onFailed(exception: Exception) {
                callback.onFailed(exception)
            }
        })
    }
}
複製代碼

具體每一個子類實現就不細貼了,打擊有興趣能夠看看源碼,就是將上個處理的結果串聯起來,最終將Result一層一層傳到最終的Result中,至此咱們就完成了一行代碼完成裁剪、拍照、處理、以及它們功能的任意組合的同時、還保證了代碼的簡潔性、可讀性、易於拓展性。

總結

完整UML

image CoCo 借鑑了Glide的語法和設計思想對系統經常使用功能如拍照、選圖、裁剪 作了一些封裝、不只能夠在處理圖片等異步操做時候感知外部容器,並且經過相似於RxJava工做流的思想,這幾個操做能夠串聯流式處理,大大提升了功能的拓展性和靈活性的同時,還能保證代碼的簡潔可讀性,能夠爲咱們平常開發減小不少臃腫代碼,提升了開發效率,目前CoCo穩定版上線已經支持了多個應用穩定性運行。

GitHub地址:

github.com/soulqw/CoCo

相關文章
相關標籤/搜索