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根据文字内容、样式,计算出宽高的时候,在小米手机上可能会出现文字截断现象。

例如,前端期望如下图所示的渲染效果:

然而在小米手机上的渲染效果却是下面这样,默认标题那一段最后一行的文本被截断了:

技术干货 | Weex Android 文字渲染优化

原因

在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中没有指定widthheight,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的过程可以用下图表示:

技术干货 | Weex Android 文字渲染优化

由于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。技术干货 | Weex Android 文字渲染优化

  • ParagraphStyle,能影响文本一段或者一行显示效果的Span。技术干货 | Weex Android 文字渲染优化

  • UpdateAppearance,能动态修改文本中每一个Character显示效果的Span。技术干货 | Weex Android 文字渲染优化

很多关于文字的CSS style都可以映射到某种类型的Span上。例如,

  • font-size可以映射到android.text.style.AbsoluteSizeSpan

  • font-weightfont-style都可以映射到android.text.style.StyleSpan

  • color可以映射到android.text.style.ForegroundColorSpan

  • text-decoration可以映射到android.text.style.UnderlineSpanandroid.text.style.StrikethroughSpan

  • font-family可以映射到android.text.style.TypefaceSpan

  • text-align可以映射到android.text.style.AlignmentSpan

  • line-height可以映射到android.text.style.LineHeightSpan

Weex还支持text-overflowlines(某段文字最多显示几行)两个属性,这两个属性并没有原生的Span与之对应,需要自行实现。

text-overflowlines

lines属性指定了文字区块最多可以显示几行,text-overflow属性则说明了如果文字的行数超过了lines要如何处理,这两个属性在逻辑上关联紧密,实现的时候需要一起处理。

如果只需要支持Android API 23及以上,上述两个属性已有原生实现,使用StaticLayout.Builder即可。然而在Weex中,minSdkVersion为14,因此无法使用StaticLayout.Builder

故Weex使用如下过程来支持text-overflowlines属性:

  1. 检查当前行数是否超过lines,如果是,进入2,否则结束。

  2. 找到最后一行的第一个字符和最后一个字符,如果二者不是一个字符,进入3,否则结束。

  3. 将文字分为末行和非末行。进入4。

  4. 如果text-overflow是ellipse,用\u2026(HORIZONTAL ELLIPSIS)替换末行最后一个字符,否则什么都不做。进入5。

  5. 将非末行文字和末行文字拼接成一个新的字符串并重新layout。进入1。

在Weex中以上计算过程是一个递归的过程,当前行数小于等于lines时,则停止递归。伪代码如下:

技术干货 | Weex Android 文字渲染优化

line-height

android.text.style.LineHeightSpan并不能直接指定line-height,只能通过设置TopAscentDescentBottom几个属性,从而间接设置line-height。下图阐述了这几个参数的意义:
技术干货 | Weex Android 文字渲染优化

根据上图,line-height可以被定义为AscentDescent之间的距离。当指定的line-height大于原始的AscentDescent之间的距离时,需要扩大AscentDescent之间的距离,反之需要缩小AscentDescent之间的距离。

有如下约定:

  • basline为x轴,向下为正,向上为负。

  • leading=line-height-(descent-ascent)

  • half-leading=leading/2

  • leadinghalf-leading可能为负数。

如果leading不为0,需要根据half-leading调整AscentDescent。此外,根据StaticLayout的源代码,AscentDescent并不会作用于首行顶部和末行尾部,需要调整TopBottom以处理首行和末行。上述逻辑可以用如下伪代码表示:

技术干货 | Weex Android 文字渲染优化
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对象; 因此,在优化后,可以预期性能会得到一定幅度的提升。

下面分两种场景测试文字性能。

一段长文本

技术干货 | Weex Android 文字渲染优化

使用上图所示的一段长文本做为测试文本,针对优化前和优化后两种场景,在小米手机上得到首屏加载时间如下图所示:

次数 优化前首屏加载时间 优化后首屏加载时间
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%。

多段短文本

技术干货 | Weex Android 文字渲染优化

使用上图所示的多段短文本,针对优化前和优化后两种场景,在小米手机上得到首屏加载时间如下图所示:

次数 优化前首屏加载时间 优化后首屏加载时间
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不一致的问题,避免了为小米手机编写额外适配逻辑的成本。

此外,首屏加载性能也得到了小幅度提升。

参考文章

  1. http://instagram-engineering.tumblr.com/post/114508858967/improving-comment-rendering-on-android

  2. http://flavienlaurent.com/blog/2014/01/31/spans/

  3. 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——公共样式的主要内容,如果未能解决你的问题,请参考以下文章

CSS 公共样式分享

轮播图公共样式提取

PyQt5 创建样式公共类加载窗口样式

如何使用公共jsp页面里面的css样式

h5固定表头公共样式

vue项目引入公共样式less文件