注: 本文中全部的xml均可以直接貼去mock測試,文章中還附有運行預覽的截圖。閱讀本文可能須要您億點點的時間。css
在線上,對於某些適用於要求強展現、輕交互、高可配場景,RN和WebView顯得不夠靈活,性能表現也不夠好。html
使用RN時要佔據整個Activity,並且Native和Js的通訊損耗不可避,WebView的狀況則更加糟糕,還要lock主線程來加載webkit。這在二級、三級頁面還好,在首頁是絕對不能用這種掉性能的方案的。java
而且對於首頁feed流卡片、一級頁面的活動區塊來講,這些頁面的邏輯自己就不強,並且每每也只是須要局部動態化,因此綜合來看RN和WebView都不是最優選,咱們須要第三條路。android
對於這些應用場景,美團APP團隊有本身的一套閉源的動態化容器MTFlexbox來應對,可讓佈局快速開發上線,而且不受發版限制。以前在美團實習期間有幸學習過MTFlexbox,因此這裏我要感謝一下美團APP終端業務研發組的同窗們,以及我本來的Leader。git
這套方案不只承載了美團首頁的動態化,還解決了團首頁在連續滑動過程當中出現FPS波動的問題,詳情能夠參考他們的文章👉Litho在動態化方案MTFlexbox中的實踐。程序員
實習結束返校以後,本身一直想作一個在美團實習的總結,但又不知道以什麼爲主題比較合適。思索以後,以爲本身對於MTFlexbox理解得以及其適用的業務特色理解的還算深入,因此最終我選擇了嘗試本身去設計實現一個與MTFlexbox功能相似的開源框架Gbox,讓Gbox成爲MTFlexbox的開源替代品。github
注:Gbox使用kotlin開發,在Apache開源協議下發布,我雖說Gbox是MTFlexbox的開源替代品,可是Gbox ≠ MTFlexbox🙅,它不包含MTFlexbox的任何源代碼,也不是MTFlexbox的兼容版本,它是一個徹底基於開源軟件實現的全新開源軟件。web
Gbox的開發,從需求分析->設計->技術選型->編碼->bug修復,花了大約我三週的時間。在技術選型時主要評估可移植性(或跨平臺性)和性能指標,最終確認了Litho+Tomcat EL+kotlin的技術選型。json
其實框架的渲染層本來是打算使用Flutter實現的,可是代碼寫到一半才發現這玩意過重了(包大小too大),而後又從新回到Litho這條線上進行開發,這期間本身踩了好多Litho、Drawable和Canvas的坑,本身對Android的整個渲染體系也有了更深入的理解。後端
目前爲止,Gbox已經基本基本穩定,但還會有小特性持續補充進來,你能夠在github上👉找到它。
Gbox是對業務以及性能友好的:
${}
包圍對比美團首頁線上方案MTFlexbox,左爲MTFlexbox(美團APP),右爲Gbox(Gbox的實時預覽APP)。
因爲佈局文件字太多,因此我就不直接往文章裏仍了,你能夠在Gbox的github倉庫上拿到👉這個佈局文件。
使用git clone 源碼:
git clone https://github.com/sanyuankexie/Gbox
因爲項目中使用了APT技術,因此將源碼clone完畢後,須要先rebuild一次。
首先咱們須要安裝overview APP,打開找到overview模塊,將overview APP安裝到你的測試機上。
接下來是最重要的一步,確保你的手機和你的電腦處於同一網絡環境中,推薦是使用熱點。
找到mock模塊中的MockTestCase文件。
運行JUnit的@Test,便可在控制檯中生成地址二維碼,你還能夠更改layout和data的路徑使用其餘樣式或者mock數據。注意Android Studio的主題色需爲白色,不然生成的二維碼沒法被手機識別。
(調整爲白色主題👇)
能夠看到二維碼已經在控制檯生成。
此時,使用overview
APP掃描控制檯生成的二維碼,便可預覽電腦上的佈局文件。
打開LiveReload開關,能夠實現實時預覽的效果,在電腦上修改佈局以後,使用Ctrl+S保存,便可刷新到overview APP上。控制檯用於打印埋點和點擊信息(若是有的狀況下)。
那麼Gbox是如何實現實時預覽的呢?
原理其實很簡單,也許你都已經猜到了。mock模塊打開了一個http服務器,overview掃碼拿到的是電腦的ip地址和端口號,而後overview每隔一秒去請求服務器下發佈局和數據,這樣就可完成佈局的實時預覽。附上源碼👉MockSession.kt。
在開始編寫佈局以前咱們須要瞭解Gbox的綁定表達式。
Gbox的綁定表達式是基於嵌入式Tomcat(對!就是用在Spring Boot上那個)所使用的EL表達式類庫開發的,因此它支持EL表達式的全部特性,包括Java Bean訪問、方法調用、三元表達式、數學運算等的。
假如你有一個像下面同樣的json:
{ "number":1000, "control":{ "display":true }, "text":"這段文字不會被顯示" }
編寫下面的綁定表達式
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="row"> <Text textSize="30" text="${control.display?'其餘文本':text}" height="100"> </Text> </Flex>
這個Text將不會顯示文本'這段文字不會被顯示',而會顯示'其餘文本'。值得注意的是在綁定表達式中字符串常量使用單引號包裹。
數學運算:
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="row"> <Text textSize="30" text="${number+1000}" height="100"> </Text> </Flex>
不止是text屬性,你還能夠在任意屬性中使用綁定表達式,以知足你數據展現的須要。
因爲Gbox是基於Litho的UI框架,而Litho又是使用yoga這個基於flexbox佈局模型的佈局引擎的,因此首先要支持的就是Flex,顧名思義,就是彈性容器。
Flex的實現很是簡單,你能夠理解爲加強版的LinenerLayout,它支持如下屬性:
首先是flexDirection,它用來指定主軸方向,支持row、column、rowReverse、columnReverse四種排布方式,下面是row和column的截圖,沒有填寫flexDirection時則默認爲row。
<?xml version="1.0" encoding="utf-8"?> <Flex flexDirection="row"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex>
<?xml version="1.0" encoding="utf-8"?> <Flex flexDirection="column"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex>
接下來是justifyContent屬性,它標識了其全部子Layout在主軸上的對齊方式,包含flexStart、flexEnd、center、spaceBetween、spaceAround五種,下面我經過編寫一個xml,展現了該效果。
flexStart、flexEnd、center無需多言,而spaceBetween、spaceAround須要解釋一下。
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="column"> <Flex justifyContent="flexStart" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex justifyContent="flexEnd" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex justifyContent="center" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex justifyContent="spaceBetween" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex justifyContent="spaceAround" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> </Flex>
屏幕過小,一張截圖截不完。
而後是alignItems,它描述的的子Layout在副軸上的對齊方式,支持flexStart、flexEnd、center、baseline、stretch五種。
其中baseline表示與項目的第一行文字的基線對齊,stretch指定時,若是子Layout未指定高度,則會佔滿父Layout。
編寫下面的xml:
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" height="360" background="yellow" flexDirection="row"> <Flex height="360" alignItems="flexStart" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex height="360" alignItems="flexEnd" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex height="360" alignItems="center" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex height="360" alignItems="baseline" margin="5"> <Flex background="red" width="100" height="100"> </Flex> <Flex background="blue" width="100" height="100"> </Flex> </Flex> <Flex height="360" alignItems="stretch" margin="5"> <Flex background="red" width="100"> </Flex> <Flex background="blue" width="100"> </Flex> </Flex> </Flex>
在上面的佈局中,你會發現360這個值會常常出現,沒錯,在Gbox中它是一個特殊值。爲了作屏幕適配,Gbox的大小單位是設備獨立的,它以屏幕寬度做爲基準,將屏幕寬度分爲360份,一個單位的像素值=屏幕寬度像素值/360,這也恰好是2x設計圖紙的大小,相信大家的UI設計師會喜歡Gbox的。
Gbox的全部佈局屬性和層級,最終將被應用到facebook的yoga佈局引擎中去,不管佈局有多複雜您都不須要擔憂,由於全部的計算都是在可指定的Layout線程中進行的,根本不會影響主線程,而且最終生成在屏幕上的View是沒有這些冗餘佈局層級的。有關更多Litho的信息,建議您查閱Litho的相關文檔。
Frame實現了相似Android上FrameLayout的佈局效果,用於實現Flex難以實現的多層疊加效果。
在Frame上,Gbox採用了比Flex更激進的佈局測量策略。咱們都知道,在Android的FrameLayout中onMeasure會去測量全部的子Layout,最終才能肯定寬高,Gbox中利用Litho的Component的不可變性(線程安全),將這一操做進行了並行化。
Frame擁有獨立的線程池,能夠併發地測量全部地子Layout,最終的結果在一個線程聚集,下圖演示了該過程。
PS: 說人話就是調用了java.util.concurrent.Executors#newCachedThreadPool
,而後等待在一個線程等待其餘java.util.concurrent.Future
完成,源碼連接👉FrameFactory.kt
編寫下面的佈局實現疊加效果:
<?xml version="1.0" encoding="utf-8"?> <Frame width="360" height="360" background="yellow" flexDirection="row"> <Flex background="red" width="100" height="100"> </Flex> <Flex marginTop="50" marginLeft="50" background="blue" width="100" height="100"> </Flex> </Frame>
Image不只僅只是一張簡單的ImageView,它封裝了Glide圖片加載引擎,支持異步加載、圓角裁剪和高斯模糊。
使用url來加載網絡圖片:
Gbox沒有使用Litho的State來實現異步圖片加載,因此不會觸發Litho的佈局更新,而是直接替換底層的Drawble,而後調用invalidateDrawable,刷新髒矩形。
PS: 說人話就是使用了DrawableWrapper.kt 。
mock所使用的json數據:
{ "image2": "http://5b0988e595225.cdn.sohucs.com/images/20180606/0a49d21848324503a1e04c4b942a1631.png" }
編寫xml:
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="row"> <Image width="360" height="360" url="${image2}"> </Image> </Flex>
borderRadius存在時,內部的圖片會被裁剪:
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="row"> <Image scaleType="fitXY" borderRadius="100" width="360" height="360" url="${image2}"> </Image> </Flex>
使用blurRadius和blurSampling控制高斯模糊:
blurRadius爲弧度,值在1-25之間,blurSampling爲採樣率,值要比1大。
Gbox使用renderscript技術將高斯模糊的效率最大化,可以減小使用高斯模糊時圖片出現的延遲時間。
PS: 說人話就是在Glide加載圖片的時候加了個Transformation
,源碼連接👉BlurTransformation.kt
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="row"> <Image blurRadius="25" blurSampling="2" scaleType="fitXY" borderRadius="100" width="360" height="360" url="${image2}"> </Image> </Flex>
與ImageView同樣支持scaleType:
細心的朋友會發現,其實上面已經在使用fitXY了,笑~
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="row"> <Image scaleType="fitEnd" width="360" height="360" url="${image2}"> </Image> </Flex>
Text用於顯示文本,目前支持一下屬性修飾:
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="column"> <Text textColor="#59a9ff" textSize="30" text="文本1111"> </Text> <Text textColor="red" textSize="30" text="文本1111"> </Text> <Text textColor="blue" textSize="30" text="文本1111"> </Text> </Flex>
Gbox對傳統業務組件也是友好的。
若是將Gbox接入之後以前寫的自定義View都得從頭編寫的話,那Gbox就失去了快速開發的意義了。因此Gbox也支持原生View的接入。
PS: 說人話就是封裝了com.facebook.litho.ViewCompatComponent
,源碼👉NativeFactory.kt。
使用type屬性,編寫下面的代碼,就能實現下圖中所展現的樣式。
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="row"> <Flex flexDirection="row"> <Text marginLeft="10" textSize="28" text="開關"> </Text> <Native marginLeft="10" type="ch.ielse.view.SwitchView"> </Native> </Flex> </Flex>
值得注意的是Native已是佈局樹的葉子節點,這意味着不支持再使用Native包裹其餘節點。
Scroller就是ScrollView,與ScrollView同樣,它只能有一個子View,它由兩個屬性控制樣式:
<?xml version="1.0" encoding="utf-8"?> <Scroller> <Flex> <Image width="360" height="600" scaleType="fitXY" url="${image2}"> </Image> </Flex> </Scroller>
【這...這就不展現了吧...我實在是以爲這玩意截圖沒啥意義...】
當model數據中有列表數據須要展開時,就須要用到for標籤。
for標籤有三個屬性,在使用時都是必須指定的,分別是var,from,to,index用於指定循環中迭代器的名字,from和to則指定了var的的迭代範圍。
好比你有下面的數據:
{ "height":1000, "itemTexts":["Gbox","Facebook","Litho","Google"] }
編寫下面的佈局:
<?xml version="1.0" encoding="utf-8"?> <Flex width="360" background="yellow" flexDirection="column"> <for var="index" from="1" to="3"> <Text textSize="30" text="${itemTexts[index]}"> </Text> </for> </Flex>
能夠被等價展開成:
<Flex width="360" background="yellow" flexDirection="column"> <Text textSize="30" text="${itemTexts[1]}"> </Text> <Text textSize="30" text="${itemTexts[2]}"> </Text> <Text textSize="30" text="${itemTexts[3]}"> </Text> </Flex>
for標籤var所指定的迭代器只會在for循環所包括的佈局標籤中生效
PS: for標籤是調用了Tomcat EL的ELContext#enterLambdaScope
和ELContext#exitLambdaScope
實現的,代碼我就不在這裏貼了,你能夠👉直接跳轉到github看源碼
目前Gbox還支持一些內置函數,內置函數必須在綁定表達式中才能調用:
utils:check(o:Any)
能夠檢測一個變量是否有效,爲空或者大小爲0的集合或者爲空的字符串都會返回false值
check方法是由kotlin實現的:
fun check(o: Any?): Boolean { return when (o) { is String -> o.isNotEmpty() is Collection<*> -> !o.isEmpty() is Number -> o.toInt() != 0 else -> o != null } }
在下面的佈局邏輯中,屏幕上不會展現任何東西,由於json中沒有'no_found'這個變量。
<?xml version="1.0" encoding="utf-8"?> <Flex> <Image width="360" height="360" url="${utils:check(no_found)?image2:''}"> </Image> </Flex>
draw:gradient(o:Orientation,vararg colors: String)
用於實現漸變色,第一個參數爲漸變色的方向,有t2b(上到下),tr2bl(上右到下左),l2r(右到左),br2tl(下右到上左),b2t(下到上),r2l(右到左),tl2br(上左到下右)八種方向可選,第二個參數是可變參,可傳入若干個顏色的字符串
kotlin的源碼實現:
fun gradient(orientation: GradientDrawable.Orientation, vararg colors: String): GradientDrawable { return GradientDrawable(orientation, colors.map { parseColor(it) }.toIntArray()) }
編寫xml實現漸變色:
<?xml version="1.0" encoding="utf-8"?> <Flex> <Flex width="360" height="360" background="${draw:gradient(tl2br,'red','blue','yellow')}"> </Flex> </Flex>
除了上述屬性以外還有不少屬性是通用的,受限於篇幅,這裏我對一些比較重要的屬性進行簡單介紹。
background屬性是對全部Widget都通用的,用於爲圖片指定背景,它支持如下三種顯示來源:
以圖片url爲背景
<?xml version="1.0" encoding="utf-8"?> <Flex> <Flex width="360" height="360" background="${image2}"> </Flex> </Flex>
borderRadius 用於實現裁剪背景邊界的圓角。
<?xml version="1.0" encoding="utf-8"?> <Flex> <Flex borderRadius="100" width="360" height="360" background="${image2}"> </Flex> </Flex>
你還可使用borderColor和borderWidth爲邊界指定寬度和顏色。
<?xml version="1.0" encoding="utf-8"?> <Flex> <Flex borderColor="red" borderWidth="10" borderRadius="100" width="360" height="360" background="${image2}"> </Flex> </Flex>
使用clickUrl並打開overview APP的控制檯,點擊圖片,就能在EventListener中收到點擊事件傳遞下來的信息,它能夠是一個url,供外部跳轉使用。
<?xml version="1.0" encoding="utf-8"?> <Flex> <Flex clickUrl="拉菲世界第一可愛" borderColor="red" borderWidth="10" borderRadius="100" width="360" height="360" background="${image2}"> </Flex> </Flex>
於此相似的屬性還有reportView(曝光時上報),reportClick(點擊時上報)均可以在控制檯查看。
還有一些通用屬性,我也在這裏也簡單列了一下:
寬高
可見性
外邊距
內邊距
Flexbox所支持的其餘屬性
對於Flexbox這部分,我真誠地推薦你去看阮一峯老師的文章Flex佈局教程。
Gbox中的對應屬性基本上就是將這些屬性使用java的命名風格,取消中位線,第二個單詞開始使用大寫。如在css中的align-content在Gbox中就是alignContent。
在後端,可能集成者須要創建一個統一的佈局管理系統,包括:
固然這些也在將來的開發計劃中,歡迎您的PR。
值得注意的是,雖然編寫佈局使用的是xml,可是您能夠發現最終mock服務器下發到客戶端的只有json,這是由於在下發佈局時mock服務器已經將xml轉換爲了json,這樣作的目的是爲了佈局集成到數據接口中下發,使用統一工具進行解析,因此Gbox沒有耦合xml的解析模塊,而是最大化的利用了現有基礎設施。因此在後端集成時須要把xml轉換爲json來保存。
Gbox使用jitpack進行構建,在你的根項目的build.gradle中添加
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
而後在模塊中依賴
dependencies { implementation 'com.github.LukeXeon.flexbox:core:latest.release' }
Sync項目,便可將SDK集成到項目中,Gbox的包大小不算太大,約爲1M左右,這其中主要是Facebook的Litho的大小。
Gbox基於Litho,因此你還得先找個地方初始化Litho,能夠是Application,或者第一個Activity裏,反正用LithoView以前初始化就好了。
SoLoader.init(this, false);
LithoView是Gbox的容器,在xml中使用:
<com.facebook.litho.LithoView android:id="@+id/host" android:layout_width="match_parent" android:layout_height="match_parent"> </com.facebook.litho.LithoView>
而後在Java層中拿到實例,調用setComponentAsync方法,傳入一個新構造的DynamicBox。
mLithoView.setComponentAsync( DynamicBox.create(c) .bind(data) .layout(layout) .eventListener(this) .build() );
其中layout是一個com.guet.flexbox.NodeInfo類的實例,它用來描述一顆佈局樹,是整棵佈局樹的樹根。
而data則是綁定到佈局中的數據,它支持多種數據格式自動匹配,分爲如下兩種狀況:
在overview APP中,我使用了Retrofit來簡化了這一過程。
添加Retrofit的依賴:
implementation 'com.squareup.retrofit2:retrofit:2.6.2' implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
用Retrofit實例化接口:
package com.guet.flexbox.overview; import com.guet.flexbox.NodeInfo; import java.util.Map; import retrofit2.Call; import retrofit2.http.GET; public interface MockService { @GET("/data") Call<Map<String, Object>> data(); @GET("/layout") Call<NodeInfo> layout(); } mMockService = new Retrofit.Builder() .baseUrl(url) .client(new OkHttpClient()) .addConverterFactory(GsonConverterFactory.create()) .build() .create(MockService.class);
EventListener用來處理事件回調,目前有三種事件點擊、上報點擊和上報曝光。
package com.guet.flexbox enum class EventType { CLICK, REPORT_CLICK, REPORT_VIEW } package com.guet.flexbox interface EventListener { fun onEvent(type: EventType, action: String?) }
所傳入的action,就是你在xml中編寫的字符串所解析獲得的結果。
這裏吐槽一下,寫文檔是真的難受,但沒有高質量的文檔是不行的啊,別人會看不懂你在幹啥,因此還真是驗證了那句老話:「程序員想要的是內容詳實的文檔,但本身歷來不寫也最討厭不寫文檔的同事」。
對於以前有同窗拉了個人代碼出現沒跑起來的狀況,我在這裏說一聲抱歉。如今我在github上的倉庫如今也分了master和develop分支,develop分支供我本身開發測試新特性使用,master分支爲穩定的主分支用於jitpack打包,develop分支的代碼穩定後,我纔會和入master,這樣應該就不會出現以前那樣的問題了。
新框架多多少少會有些小問題,Gbox代碼中的註釋我也會在往後補充,還請各位海函,發現了問題你能夠直接給我提issue,我會在github上跟進,或者直接給我發郵件imlkluo@qq.com,感謝您的支持。
如下是未來會進行嘗試的的方向:
最後的最後,求star!求star!求star!重要的事情說三遍,請各位大佬幫幫忙,點一下玩一年,開源不花一分錢!