Weex——公共样式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Weex——公共样式相关的知识,希望对你有一定的参考价值。
参考技术A 所有 weex 标签都有以下基本样式规则。weex 盒模型基于 CSS 盒模型,每个 weex 元素都可视作一个盒子。我们一般在讨论设计或布局时,会提到「盒模型」这个概念。
元素实际的内容(content)、内边距(paddings)、边框(borders)、外边距(margins),形成一层层的盒子包裹起来,这就是盒模型大体上的含义。
注意:目前在 <image> 和 <text> 组件上尚无法只定义一个或几个角的 border-radius。比如你无法在这两个组件上使用 border-top-left-radius。
weex 盒模型的 box-sizing 默认为 border-box,即盒子的宽高包含内容、内边距和边框的宽度,不包含外边距的宽度。
示例:
weex 布局模型基于 CSS 的 Flexbox。以便所有页面元素的排版能够一致可预测,同时页面布局能适应各种设备或者屏幕尺寸。
Flexbox 包含 flex 容器和 flex 成员项。如果一个 weex 元素可以容纳其他元素,那么它就成为 flex 容器。需要注意的是,flexbox 的老版规范相较新版有些出入,比如是否能支持 wrapping。这些都描述在 W3C 的工作草案中了,你需要注意下新老版本之间的不同。另外,老版本只在安卓 4.4 版以下得到支持。
在 weex 中,Flexbox 是默认且唯一的样式模型,所以你不需要手动为元素添加 display: flex; 属性。
flex: <number>
flex 属性定义了 flex 成员项在容器中占据的尺寸。如果所有成员项都设置为 flex: 1,那么它们就有相等的宽度(水平排列)或者相等的高度(垂直排列)。如果一共有两个成员项,其中一个 flex: 1,另一个 flex: 2,那么第一个将占据 1/3 的空间,另一个占据 2/3。如果所有 flex 成员项都不设置 flex 属性,它们将根据容器的 justify-content 属性来决定如何排列。
一组平分了容器的图片。
一张固定宽度的图片加上一段流动布局的文本。
复杂的混合布局。
一段文本左对齐,其他内容右对齐。
我们可以使用以下属性来定位一个 weex 元素。
你可以按照以下步骤来规划 weex 页面的样式。
技术干货 | Weex Android 文字渲染优化
背景
在做Weex Android适配工作的时候,发现当Text
没有设置高度,需要Weex根据文字内容、样式,计算出宽高的时候,在小米手机上可能会出现文字截断现象。
例如,前端期望如下图所示的渲染效果:
然而在小米手机上的渲染效果却是下面这样,默认标题那一段最后一行的文本被截断了:
原因
在Android系统中,View的渲染可以分为Measure,Layout,Draw三步,对于Measure这一步,Weex和原生Android略有不同:
在Android系统中,默认渲染文字的方式是使用
TextView
及其子类,TextView
的宽度或高度可以使用wrap_content
,match_parent
或指定的值。在Weex中,Weex View的宽度和高度是由CSS属性指定或者css-layout根据flex属性计算出来的,Layout的时候使用
FrameLayout.LayoutParams
进行布局,因此并不存在wrap_content
,match_parent
这两个概念。对于文字View,如果在CSS中没有指定width
和height
,css-layout会要求文字节点(TextDom)根据文字内容和文字样式(如font-size
,font-family
,line-height
等),使用android.text.Layout
,计算出该节点的宽度和高度,然后使用Weex view的布局方式。
在Draw这一步的时候,无论是Weex还是原生Android,都是使用TextView.onDraw()
进行绘制的。在绘制文字的时候,TextView
会根据文字内容和文字样式生成一个android.text.Layout
对象,并根据此对象把文字画出来。
Weex渲染Text的过程可以用下图表示:
由于DOM和View针对同一个TextDom
,生成了两个android.text.Layout
对象。而android.text.Layout
是一个接口,在DOM层和View层上可能使用了不同的实现,即DOM和View生成的android.text.Layout
可能不一样。换句话说,DOM层负责Measure,View层负责Draw,Measure与Draw的结果可能存在差异,这样就可能导致了文字截断现象。
例如,对于一段中英文混排的文字,DOM层可能把文字计算成4行,而由于换行规则(android.text.StaticLayout.nComputeLineBreaks()
)不同,View层可能把文字计算成3行,这样就出现了Measure和Draw的结果不一致,发生了文字截断现象。
解决方案
DOM层和View层使用同一个对象分别进行Measure和Draw,确保Measure和Draw的结果一致,即可解决此问题。这个方案需要解决两个问题:
使用一种统一的方案表示多种文字样式(
font-size
,font-family
等)。找到一种方法,可以根据文字样式和文字内容计算文字区块的高度和宽度。
对于第一个问题使用Span
解决,第二个问题则使用Layout
机制解决。
Span
Android中的Span
类似于html的<span>
标签,可以用来描述一段inline文本的样式。
Span可以大致分为如下三种类型:
CharacterStyle
,能影响文本中的每一个Character显示效果的Span。ParagraphStyle
,能影响文本一段或者一行显示效果的Span。UpdateAppearance
,能动态修改文本中每一个Character显示效果的Span。
很多关于文字的CSS style都可以映射到某种类型的Span
上。例如,
font-size
可以映射到android.text.style.AbsoluteSizeSpan
。font-weight
和font-style
都可以映射到android.text.style.StyleSpan
。color
可以映射到android.text.style.ForegroundColorSpan
。text-decoration
可以映射到android.text.style.UnderlineSpan
或android.text.style.StrikethroughSpan
。font-family
可以映射到android.text.style.TypefaceSpan
。text-align
可以映射到android.text.style.AlignmentSpan
。line-height
可以映射到android.text.style.LineHeightSpan
。
Weex还支持text-overflow
和lines
(某段文字最多显示几行)两个属性,这两个属性并没有原生的Span与之对应,需要自行实现。
text-overflow
和lines
lines
属性指定了文字区块最多可以显示几行,text-overflow
属性则说明了如果文字的行数超过了lines
要如何处理,这两个属性在逻辑上关联紧密,实现的时候需要一起处理。
如果只需要支持Android API 23及以上,上述两个属性已有原生实现,使用StaticLayout.Builder
即可。然而在Weex中,minSdkVersion
为14,因此无法使用StaticLayout.Builder
。
故Weex使用如下过程来支持text-overflow
和lines
属性:
检查当前行数是否超过lines,如果是,进入2,否则结束。
找到最后一行的第一个字符和最后一个字符,如果二者不是一个字符,进入3,否则结束。
将文字分为末行和非末行。进入4。
如果text-overflow是ellipse,用
\u2026
(HORIZONTAL ELLIPSIS)替换末行最后一个字符,否则什么都不做。进入5。将非末行文字和末行文字拼接成一个新的字符串并重新layout。进入1。
在Weex中以上计算过程是一个递归的过程,当前行数小于等于lines
时,则停止递归。伪代码如下:
line-height
android.text.style.LineHeightSpan
并不能直接指定line-height
,只能通过设置Top
,Ascent
,Descent
,Bottom
几个属性,从而间接设置line-height
。下图阐述了这几个参数的意义:
根据上图,line-height
可以被定义为Ascent
和Descent
之间的距离。当指定的line-height
大于原始的Ascent
和Descent
之间的距离时,需要扩大Ascent
和Descent
之间的距离,反之需要缩小Ascent
和Descent
之间的距离。
有如下约定:
basline为x轴,向下为正,向上为负。
leading
=line-height
-(descent
-ascent
)half-leading
=leading
/2leading
和half-leading
可能为负数。
如果leading
不为0,需要根据half-leading
调整Ascent
和Descent
。此外,根据StaticLayout
的源代码,Ascent
和Descent
并不会作用于首行顶部和末行尾部,需要调整Top
和Bottom
以处理首行和末行。上述逻辑可以用如下伪代码表示:
Build a span
在Android中,构建String
可以使用StringBuilder
,构建Span
的时候,则可以使用Spannable
接口的三个子类:
SpannedString
适用于文字的内容和文字的span都不变化的场景。SpannableString
适用于文字的内容不变,但是文字的span可能变化的场景。SpannableStringBuilder
适用于文字和文字的span都可能变化的场景。
在Weex中,使用的是SpannableString
,每次更新文字内容,会创建一个新的Span
。
Layout
使用Spannable
接口后,得到的仅仅是一个文本流,并不包含文字区域的高度、宽度、首行、末行这些与Measure或Layout相关的内容,因此还需要使用android.text.Layout
对文字进行Measure和Layout。使用android.text.Layout
时,把Spannable
与文字区块的宽度做为Layout
的构造函数的参数,即可完成文字的Layout过程。android.text.Layout
有以下三种实现方式:
BoringLayout
单行文字,文字方向为LTR。StaticLayout
根据Spannable
和指定宽度计算文字行数,文字方向由文字内容决定DynamicLayout
除了含有StaticLayout
的功能外,还包含动态更新功能。当Spannable
更新的时候,Layout.getLines()
也会随之变化。在内部实现上,DynamicLayout
有一个Watcher,这个Watcher观察着Spannable
的变化。DynamicLayout
一般与SpannableStringBuilder
配合使用。
此外,可以使用Layout.draw(Canvas c)
来把Layout
对象画在指定的Canvas
上。在DOM中生成Layout
对象,计算出文字的宽度和高度后,把Layout
对象传递给View,View调用Layout.draw(Canvas c)
即可把文字画出来,这样就保证了Layout与Draw的一致性。
线程同步
在Weex中,DOM相关的操作运行在DOM线程中,View相关的操作运行在UI线程中,二者可能同时操作同一个Layout对象,这样就存在着线程同步问题。考虑到加锁对性能的影响,Weex没有使用锁,而是AtomicReference
解决这个问题。
DOM线程内有两个android.text.Layout
对象,一个是TextDom的私有成员变量,一个是AtomicReference
中保存的引用。之后使用如下机制保证UI线程和DOM线程不会操作同一个android.text.Layout
对象,避免了加锁带来的额外开销。
UI线程通过
AtomicReference
来读取Layout
对象。DOM线程在计算开始的时候,生成一个新的
android.text.Layout
对象。在计算过程中把计算的中间结果也保存到这个对象中。DOM线程计算结束后,把计算结果更新到AtomicReference
中,同时清空私有成员变量android.text.Layout
。
即UI线程负责Read,DOM线程负责Write;Read与Write操作的不是同一个对象,在DOM线程完成工作后,会更新Read输出的对象。
性能
优化前的方案使用Layout和Text对象进行渲染,一次渲染需要生成两个Layout对象;
优化后的方案使用Layout和对象进行文字渲染,一次渲染只需要生成一个Layout对象; 因此,在优化后,可以预期性能会得到一定幅度的提升。
下面分两种场景测试文字性能。
一段长文本
使用上图所示的一段长文本做为测试文本,针对优化前和优化后两种场景,在小米手机上得到首屏加载时间如下图所示:
次数 | 优化前首屏加载时间 | 优化后首屏加载时间 |
---|---|---|
1 | 825 | 813 |
2 | 832 | 721 |
3 | 816 | 761 |
4 | 852 | 756 |
5 | 850 | 750 |
6 | 838 | 766 |
7 | 863 | 781 |
8 | 846 | 780 |
9 | 793 | 753 |
10 | 905 | 727 |
平均 | 842 | 760.8 |
根据以上数据,可以看到优化后,一段长文本的渲染性能得到一定的提升,上述数据的性能提升幅度为(842-760.8)/842=9.6%。
多段短文本
使用上图所示的多段短文本,针对优化前和优化后两种场景,在小米手机上得到首屏加载时间如下图所示:
次数 | 优化前首屏加载时间 | 优化后首屏加载时间 |
---|---|---|
1 | 987 | 987 |
2 | 1056 | 869 |
3 | 948 | 880 |
4 | 997 | 822 |
5 | 969 | 947 |
6 | 1036 | 967 |
7 | 939 | 900 |
8 | 869 | 878 |
9 | 826 | 949 |
10 | 931 | 832 |
平均 | 955.8 | 903.1 |
根据以上数据,可以看到优化后,多段短文本的渲染性能也得到了一定的提升,上述数据的性能提升幅度为(955.8-903.1)/955.8=5.5%。
结论
使用以上的优化策略,在小米手机上得到了如下的文字渲染效果,可以看到,文字截断的现象消失了。
上述优化策略的流程如下图所述:
这次改进使用Layout和Span机制,解决了Measure和Draw不一致的问题,避免了为小米手机编写额外适配逻辑的成本。
此外,首屏加载性能也得到了小幅度提升。
参考文章
http://instagram-engineering.tumblr.com/post/114508858967/improving-comment-rendering-on-android
http://flavienlaurent.com/blog/2014/01/31/spans/
http://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font
玉冈,Weex团队的Android开发工程师 Github帐号https://github.com/YorkShen
以上是关于Weex——公共样式的主要内容,如果未能解决你的问题,请参考以下文章