注: 本文中全部的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!重要的事情说三遍,请各位大佬帮帮忙,点一下玩一年,开源不花一分钱!