文章目录
1. 概述
在前面的文章中,我们介绍了生成式UI动态化SDK的目标是为了给大模型提供一种DSL语言描述UI,然后在端设备上展示,这里的端设备包括,车机,手机(Android、IOS,鸿蒙),电脑,在端设备提供一个容器组件,然后解析大模型生成的DSL,经过生成式UI动态化SDK将其转换为对应平台上的可交互的界面,然后展示给用户。这里可交互的界面就表示我们的DSL不仅需要描述交互事件,还要能完美描述设计师设计好的界面。接下来的文章我们会一点点的把我们的设计实现。本文会从解析DSL到最终将界面展示到端设备上的顺序详细介绍如何实现一个水平布局容器和垂直布局容器以及一个文字组件。
2. 效果展示
2.1 垂直布局容器(Column)
DSL示例:
<Otter>
<!--竖直列表容器,Layout节点用于标识布局文件-->
<Layout>
<Column alignItems="center" background="White" borderColor="#ff0000" borderWidth="2"
height="wrap_content" id="cl_container" leftBottomRadius="10" leftTopRadius="10"
paddingBottom="20px" paddingEnd="10px" paddingStart="10px" paddingTop="20px"
rightBottomRadius="10" rightTopRadius="10" width="match_parent">
<Text background="Cyan" height="200px" id="text_tip" onClick="fun{changeAttr,showToast,updateStyle}"
padding="20px" radius="20px" text="$textTip"
textAlign="center" textColor="#ffffff" textSize="8px" width="300px" />
<Spacer background="TRANSPARENT" height="20px" width="match_parent" />
<Text background="Red" height="200px" id="text_tip1" onClick="fun{changeAttr,showToast,updateStyle}"
padding="10px" radius="20px" text="Hello"
textAlign="start" textColor="#ffffff" textSize="10px" width="300px" />
<Spacer height="20px" width="match_parent" />
<Text background="Green" height="200px" id="text_tip2" onClick="fun{changeAttr,showToast,updateStyle}"
padding="10px" radius="20px" text="I Am Otter!!!"
textAlign="end" textColor="#ff0000" textSize="5px" width="300px" />
<Spacer height="20px" width="match_parent" />
</Column>
</Layout>
<Events>
<Event funId="changeAttr">
{"class":"view","params":{"fun":"changeAttributes","key":"setText","id":"text1","value":"$textTip"}}
</Event>
<Event funId="showToast">
{"class":"util","params":{"fun":"showToast","info":"$toastMsg"}}
</Event>
<Event funId="updateStyle">{ "class": "view", "params": { "fun": "changeAttributes",
"id": "text_tip", "key": "setTextStyle", "value": { "attributes": [ { "textSize":
"14px","textColor": "#CCCCCC","text": "我改变了!!!!!" } ] } } }
</Event>
</Events>
</Otter>
如上图所示,垂直布局容器(Column)就是在该容器下放置的子控件会按照垂直方向线性排列
2.2 水平布局容器(Row )
示例DSL模板
<Otter>
<!--竖直列表容器,Layout节点用于标识布局文件-->
<Layout>
<Row background="Green" borderColor="#ff0000" borderWidth="2" height="200px" id="row1"
justifyContent="center" padding="10px" radius="20px" width="match_parent" paddingEnd="10px" paddingStart="10px">
<Text background="blue" height="match_parent" id="text_tip2" radius="20px" text="Hello,Otter"
textAlign="start" textColor="#ffffff" textSize="7px" width="100px" />
<Spacer background="TRANSPARENT" height="match_parent" width="10px" />
<Text background="MAGENTA" height="match_parent" id="text_tip2" radius="20px"
text="Go,zhongxj" textAlign="center" textColor="#ffffff" textSize="5px"
width="100px" />
<Spacer height="match_parent" width="10px" />
<Text background="Yellow" height="match_parent" id="text_tip3" radius="20px"
text="Go,XCY" textAlign="end" textColor="#000000" textSize="5px"
width="100px" />
</Row>
</Layout>
<Events>
<Event funId="changeAttr">
{"class":"view","params":{"fun":"changeAttributes","key":"setText","id":"text1","value":"$textTip"}}
</Event>
<Event funId="showToast">
{"class":"util","params":{"fun":"showToast","info":"$toastMsg"}}
</Event>
<Event funId="updateStyle">{ "class": "view", "params": { "fun": "changeAttributes",
"id": "text_tip", "key": "setTextStyle", "value": { "attributes": [ { "textSize":
"14px","textColor": "#CCCCCC","text": "我改变了!!!!!" } ] } } }
</Event>
</Events>
</Otter>
水平布局容器(Row)可以让其子控件按照水平方向线性摆放,如上图所示。
2.3 本文示例动态化模板DSL
3. 生成式UI 动态化SDK的渲染流程
首先我们将模板和数据放到Android的Asset下,如下图所示:
然后通过SDK定义的API加载模板和数据并且渲染出来,加载的代码流程如下所示:
class LayoutTextTemplateActivity : AppCompatActivity() {
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_layout_text_template)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
// 1.根据模板创建一个模板Context
val templateContext = OTEngine.instance
.init(this)
.buildTemplate(
"text-demo",
"text",
"templates/text-demo/layout_text.xml"
)
// 2.将数据保存到DataBinding类中
DataBinding.instance.buildTemplateData(
AssetsUtils.parseAssetsToString(
this,
"templates/text-demo/mock-data/layout_text.json"
)
)
// 3.根据XML模板和数据创建出android原生View
val rootView = OTEngine.instance.createAndroidView(templateContext)
// 4.将根据XML模板生成的Android原生View放到端侧准备好的容器中渲染展示
findViewById<LinearLayoutCompat>(R.id.text_template_container).addView(rootView)
}
}
拿到XML模板的和模板的数据后,我们就可以将其解析成对应的节点树,然后再将节点树转换成视图树,绑定数据和事件,最终将其展现到用户的眼前。代码如下所示:
/**
参数:模板Context
返回值:Android原生View
*/
fun createAndroidView(templateContext: TemplateContext): View? {
val tContext: TemplateContext = templateContext
// 解析XML,将布局和事件分别解析出来,创建对应的视图树。
// 获取工厂对象
val factory = DocumentBuilderFactory.newInstance()
// 通过DOM工厂获取DOMBuilder对象
val builder = factory.newDocumentBuilder()
// 解析XML输入流,得到Document对象,表示一个XML文档
val document = builder.parse(
tContext.context.resources.assets.open(templateContext.sdTemplate.templatePath)
)
// 获取文档中的次节点以及节点
val documentElement = document.documentElement
// 解析UI节点,属性
val vLayoutNodeList = documentElement.getElementsByTagName(TemplateKey.LAYOUT_ROOT_NAME)
// 解析事件
val vEventNodeList = documentElement.getElementsByTagName(TemplateKey.EVENTS_ROOT_NAME)
// 创建虚拟对象节点
val vNode = VDomNode.create(vLayoutNodeList)
// 解析事件
DataBinding.instance.parseEvents(vEventNodeList)
// 计算布局信息,TODO:这里的布局信息目前没有做缓存,后面优化可以添加引擎
// 设置展示的视图窗口大小,默认为屏幕的宽高
val vSDViewPortSize = OTViewPortSize(
ScreenUtils.getScreenWidthPx(context),
ScreenUtils.getScreenHeightPx(context)
)
// 通过创建的虚拟节点树和窗口大小以及模板上下文渲染出视图树,最终给到端侧容器加载显示
val rootView = createAndroidViewByVNode(vNode, vSDViewPortSize, templateContext)
return rootView
}
渲染的流程我们就先简单的介绍一个大致的框架,然后我们接下来会通过Column和Row组件的实现来详细介绍SDK的渲染流程
4. Column和Row组件实现
Column和Row组件都可以称为线性布局组件,是界面开发中常用的组件。而且我们设计的组件必须要能够适应多端的适配问题,因为将来生成式的UI下发的端设备可能会包括Android,IOS,HarmonyOS,车机等多个平台。所以布局方案我们需要考虑多端,多屏的响应式布局述求,而浏览器场景采用了Flexbox布局方案很好的解决了多尺寸窗口的动态布局问题。所以我们的生成式UI会选择Flexbox作为布局方案,这点是参考GaiaX的布局方案选择的。而且GaiaX选择了Stretch作为Flexbox布局解析的技术方案,而且GaiaX的团队还解决了Stretch库在一些机型上的闪退问题,然后开源出来,所以我们直接使用就行了,感谢GaiaX的技术大牛们,推荐读者去看下阿里的GaiaX开源动态化SDK的源码实现,会受益很多。如果对stretch布局技术感兴趣的读者可以去看阿里的技术文章:给Stretch(Rust编写的Flexbox布局引擎)新增特性,我掉了好多头发
了解完上面的知识后,我们还需要理解两个重要的概念,分别为主轴和交叉轴:
- 主轴: 线性容器组件在布局方向的轴线,子组件默认沿着主轴排列。例如:Row组件的主轴为横向,Column组件的主轴为纵向
- 交叉轴:交叉轴是崔至于主轴方向的轴线。例如Row容器交叉轴为纵向,即垂直于控件的排列方向,而Column的交叉轴为横向。
了解了上面两个概念后,我们就可以通过属性控制Row和Column组件的子组件的对齐方式了。由于我们的布局引擎使用的是Flexbox布局,所以这里用的是Flexbox布局的属性,分别是 属性,用于设置容器的子组件在交叉轴上的对齐方式,justifyContent属性用于设置子组件在主轴方向的排列方式。
4.1 Column组件
Column组件是一个容器组件,可以让其子控件在垂直方向成线性排列。为了达到这个效果我们需要使用到布局引擎。很多读者可能会想到的方案是使用XML描述完布局后,由Android端和IOS以及其他端分别解析渲染成对应平台的布局就可以了,比如垂直的线性布局就可以渲染成Android的LinearLayout的布局,将orientation设置成垂直的。这样的确可行,但是涉及到多端布局统一的问题。而且我们的布局需要动态适配各种尺寸的窗口。所以需要一个通用的布局引擎,经过多番对比,发现业内基本使用的都是Flexbox布局引擎,不了解的读者可以去看下:Flexbox布局引擎教程 布局引擎确定后,我们就可以看下如何将其渲染成各个平台的容器组件。这里我参考的也是阿里的GaiaX,基于StretchKit实现FlexBox来作为引擎的布局方式,底层采用rust语言保证了整体方案的高性能和跨平台特性。在Android端使用绝对布局,StretchKit会通过传入的Flexbox布局属性等信息,计算出各个视图的坐标,然后通过Android的绝对布局根据坐标将对应的View放到相应的位置。Column是垂直线性排列,所以我们就解析到Column组件信息时,将Flexbox可以将子控件设置成垂直线性排列的属性都统一在SDK上设置好,最后渲染出来。具体如下所示。为了防止系统可能会将绝对布局移除,所以在SDK中,我将其自定义了下,代码如下:
@Keep
open class OTAbsoluteLayout : ViewGroup, ICornerRoundConfigCapability {
private var radius: FloatArray? = null
private var roundCornerPath: Path? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
)
... 其他部分源码见开源的代码
}
4.1.1 视图的渲染
渲染视图首先需要解析动态化模板XML文件,建立一颗虚拟节点树。如下图所示:
解析为虚拟树的代码如下所示:
class VDomNode(
var id: String = "0",
var nodeName: String,
var attr: ViewAttr?
) {
var children: MutableList<VDomNode>? = mutableListOf()
override fun toString(): String {
return "VDomNode(id='$id', nodeName='$nodeName', children=$children), attr={$attr}"
}
companion object {
fun create(nodeList: NodeList): VDomNode {
val layoutNode = nodeList.item(0) // Layout节点下只允许有一个孩子
return createVirtualNode(layoutNode)
}
private fun createVirtualNode(layoutNode: Node): VDomNode {
var attributes = layoutNode.attributes
var nodeName = layoutNode.nodeName
var node = layoutNode
var id = "0"
if (nodeName == TemplateKey.LAYOUT_ROOT_NAME) {
for (i in 0 until layoutNode.childNodes.length) {
val item = layoutNode.childNodes.item(i)
if (item.nodeType == Node.ELEMENT_NODE) {
node = item
attributes = node.attributes
nodeName = node.nodeName
}
}
}
if (attributes?.getNamedItem(TemplateKey.ATTR_ID) != null) {
id = attributes.getNamedItem(TemplateKey.ATTR_ID).nodeValue
}
val attr: ViewAttr? = when (nodeName) {
// Column 组件
TemplateKey.COLUMN -> ColumnAttr.create(attributes)
// Row 组件
TemplateKey.ROW -> RowAttr.create(attributes)
// 省略不相干代码
else -> null
}
val virtualNode = initVirtualNode(id, nodeName, attr)
initChildrenVirtual(node, virtualNode)
return virtualNode
}
private fun initChildrenVirtual(
layoutNode: Node?,
vDomNode: VDomNode
) {
if (layoutNode == null) {
return
}
val childNodes = layoutNode.childNodes
for (i in 0 until childNodes.length) {
if (childNodes.item(i).nodeType == Node.TEXT_NODE) {
continue
}
if (childNodes.item(i).nodeType == Node.COMMENT_NODE) {
continue
}
if (childNodes.item(i).nodeName == TemplateKey.LAYOUT_ITEM) {
// TODO:解析Item作为list view的item布局
val itemChildList = childNodes.item(i).childNodes
for (m in 0 until itemChildList.length) {
val itemNode = itemChildList.item(m)
if (itemNode.nodeType == Node.ELEMENT_NODE) {
if (itemNode.nodeType == Node.TEXT_NODE) {
continue
}
if (itemNode.nodeType == Node.COMMENT_NODE) {
continue
}
val vNode = createVirtualNode(itemNode)
ListContainerData.listItemVNodeMap[vDomNode.id] = vNode
}
}
continue
}
val child = createVirtualNode(childNodes.item(i))
vDomNode.children?.add(child)
}
}
private fun initVirtualNode(
id: String,
nodeName: String,
attr: ViewAttr?
): VDomNode {
return VDomNode(id, nodeName, attr)
}
}
}
解析完动态化模板后就需要对Column做Flexbox的布局属性设置
data class OTFlexBox(
internal var display: Display? = null,
internal var positionType: PositionType? = null,
internal var direction: Direction? = null,
internal var flexDirection: FlexDirection? = null,
internal var flexWrap: FlexWrap? = null,
internal var overflow: Overflow? = null,
internal var alignItems: AlignItems? = null,
internal var alignSelf: AlignSelf? = null,
internal var alignContent: AlignContent? = null,
internal var justifyContent: JustifyContent? = null,
internal var position: Rect<OTSize?>? = null,
internal var margin: Rect<OTSize?>? = null,
internal var padding: Rect<OTSize?>? = null,
internal var border: Rect<OTSize?>? = null,
internal var flexGrow: Float? = null,
internal var flexShrink: Float? = null,
internal var flexBasis: OTSize? = null,
internal var size: Size<OTSize?>? = null,
internal var minSize: Size<OTSize?>? = null,
internal var maxSize: Size<OTSize?>? = null,
internal var aspectRation: Float? = null
) {
private var finalSize: Size<Dimension>? = null
private var paddingDimension: Rect<Dimension>? = null
companion object {
fun create(viewAttr: ViewAttr?): OTFlexBox {
if (viewAttr == null) {
return OTFlexBox()
}
// 如果是Column组件,直接将其属性设置成Flexbox的column属性
val sdFlexBox = OTFlexBox()
if (viewAttr is ColumnAttr) {
sdFlexBox.flexDirection = FlexBoxConvert.flexDirection(FlexBoxKey.FLEXBOX_COLUMN)
}
// 如果是容器属性
if (viewAttr is ContainerAttr) {
sdFlexBox.justifyContent = viewAttr.justifyContent?.let {
FlexBoxConvert.justifyContent(
it
)
}
sdFlexBox.alignItems = viewAttr.alignItems?.let {
FlexBoxConvert.alignItems(it)
}
sdFlexBox.display = FlexBoxConvert.display(FlexBoxKey.FLEXBOX_FLEX)
}
// // 如果是row组件,直接将其属性设置成Flexbox的row属性
if (viewAttr is RowAttr) {
sdFlexBox.flexDirection = FlexBoxConvert.flexDirection(FlexBoxKey.FLEXBOX_ROW)
}
if (sdFlexBox.size == null) {
sdFlexBox.size = Size(
OTSize.create(viewAttr.width),
OTSize.create(viewAttr.height)
)
}
if (sdFlexBox.padding == null) {
sdFlexBox.padding = StyleConverter.instance.padding(viewAttr)
}
return sdFlexBox
}
}
val sizeForDimension: Size<Dimension>?
get() {
return if (size != null) {
if (finalSize == null) {
finalSize = Size(
size?.width?.valueDimension ?: Dimension.Auto,
size?.height?.valueDimension ?: Dimension.Auto
)
finalSize
} else {
finalSize
}
} else {
null
}
}
val paddingForDimension: Rect<Dimension>?
get() {
return if (paddingDimension == null) {
paddingDimension = Rect(
padding?.start?.valueDimension ?: Dimension.Undefined,
padding?.end?.valueDimension ?: Dimension.Undefined,
padding?.top?.valueDimension ?: Dimension.Undefined,
padding?.bottom?.valueDimension ?: Dimension.Undefined
)
paddingDimension
} else {
paddingDimension
}
}
}
设置完Flexboxe属性后,就可以使用StretchKi计算控件的布局信息了
object NodeUtils {
fun computeNodeTreeByPrepareView(
sdNode: OTNode,
size: Size<Float?>
) {
val stretchNode = sdNode.stretchNode.node
?: throw IllegalArgumentException("stretch node is null,please check!")
val layout = stretchNode.safeComputeLayout(size)
composeStretchNodeByPrepareView(sdNode, layout)
}
private fun composeStretchNodeByPrepareView(sdNode: OTNode, layout: Layout) {
val stretchNode = sdNode.stretchNode.node
?: throw IllegalArgumentException("stretch node is null, please check!")
layout.id = stretchNode.id
sdNode.stretchNode.layoutByPrepareView = layout
sdNode.children?.forEachIndexed { index, sdNode ->
composeStretchNodeByPrepareView(sdNode, layout.children[index])
}
}
}
计算完的布局信息会保存在Layout类中
data class Layout(
var x: Float,
var y: Float,
val width: Float,
val height: Float,
val children: MutableList<Layout>,
var id: String = ""
) {
companion object {
fun fromFloatArray(args: FloatArray, offset: Int): Pair<Int, Layout> {
var offset = offset
val x = args[offset++]
val y = args[offset++]
val width = args[offset++]
val height = args[offset++]
val childCount = args[offset++].toInt()
val children = mutableListOf<Layout>()
for (i in 0 until childCount) {
val child = Layout.fromFloatArray(args, offset)
offset = child.first
children.add(child.second)
}
return Pair(offset, Layout(x, y, width, height, children))
}
}
override fun toString(): String {
return "Layout(x=$x, y=$y, width=$width, height=$height, children=$children, id='$id')"
}
}
注意,这里并非笔者原创,而是和阿里开源的GaiaX完全一致,笔者只是换了DSL模板的描述语言,然后重写了渲染的部分的代码
拿到布局信息后,就可以使用我们自定义的绝对布局,将布局信息传入,得到Android原生View
fun buildAndroidView(templateContext: TemplateContext): View {
val sdNode = templateContext.rootNode
val context = templateContext.context
val eventMap = DataBinding.instance.getEventMap()
val layout = sdNode?.stretchNode?.layoutByPrepareView
val rootView = sdNode?.let {
OTViewFactory
.createView<View>(context, it.viewName)
.apply {
this.layoutParams = LayoutParamsUtils.createLayoutParams(sdNode, layout)
}.apply {
setAttr(this, sdNode, eventMap)
}
}
buildAndroidChildView(rootView as ViewGroup, sdNode, eventMap)
return rootView
}
4.1.2 事件解析绑定
我们光把视图渲染出来还不够,因为界面需要能和用户交互,不然它就和图片没啥区别了,所以我们需要在DSL中描述完事件后将其解析出来并绑定到我们生成的原生View中。在效果展示小节中,我们展示的DSL模板代码中有下面的代码:
<Events>
<Event funId="changeAttr">
{"class":"view","params":{"fun":"changeAttributes","key":"setText","id":"text1","value":"$textTip"}}
</Event>
<Event funId="showToast">
{"class":"util","params":{"fun":"showToast","info":"$toastMsg"}}
</Event>
<Event funId="updateStyle">{ "class": "view", "params": { "fun": "changeAttributes",
"id": "text_tip", "key": "setTextStyle", "value": { "attributes": [ { "textSize":
"14px","textColor": "#CCCCCC","text": "我改变了!!!!!" } ] } } }
</Event>
</Events>
这些代码就是定义事件的,在文字组件中我们可以看到其使用方式
<Text background="Red" height="200px" id="text_tip1" onClick="fun{changeAttr,showToast,updateStyle}"
padding="10px" radius="20px" text="Hello"
textAlign="start" textColor="#ffffff" textSize="10px" width="300px" />
即 onClick="fun{changeAttr,showToast,updateStyle}
在SDK中,这里需要特别提示下,SDK为了能使用自定义的名称标识对应的View,我们会使用Map记录下View的ID最后将其设置给对应的view,绑定事件的时候我们就可以解析出事件节点Event中配置的事件信息,将其绑定到指定的View中。点击后要执行的操作我们也尽可能的在SDK中实现,即用JSON描述,SDK解析执行。最后将这些都准备好后,就可以将其放到端侧提供的一个原生容器中渲染展示了。
4.2 Row组件
Row的动态化模板如下所示:
<Row background="Green" borderColor="#ff0000" borderWidth="2" height="200px" id="row1"
justifyContent="center" padding="10px" radius="20px" width="match_parent" paddingEnd="10px" paddingStart="10px">
<Text background="blue" height="match_parent" id="text_tip2" radius="20px" text="Hello,Otter"
textAlign="start" textColor="#ffffff" textSize="7px" width="100px" />
<Spacer background="TRANSPARENT" height="match_parent" width="10px" />
<Text background="MAGENTA" height="match_parent" id="text_tip2" radius="20px"
text="Go,zhongxj" textAlign="center" textColor="#ffffff" textSize="5px"
width="100px" />
<Spacer height="match_parent" width="10px" />
<Text background="Yellow" height="match_parent" id="text_tip3" radius="20px"
text="Go,XCY" textAlign="end" textColor="#000000" textSize="5px"
width="100px" />
</Row>
其事件的绑定使用和视图解析渲染和Column完全一致,不同的就是Row组件是使其子控件在水平方向上线性排列,Flexbox的相关属性配置需要改一下。然后将布局信息给到绝对布局解析渲染就可以了。
4.3 文字组件(Text)
文字组件本文主要是为了协助展示所以一起引入,使用Text标识,在Android端,文字组件会被渲染成原生的TextView展示,如下所示:
<Text background="Green" height="200px" id="text_tip2" onClick="fun{changeAttr,showToast,updateStyle}"
padding="10px" radius="20px" text="I Am Otter!!!"
textAlign="end" textColor="#ff0000" textSize="5px" width="300px" />
它的布局信息会随着容器组件一起被计算出来,然后按照坐标摆放就可以了。具体渲染后的代码如下所示:
@Keep
class Text : AppCompatTextView, IDataBindingCapability, IReleaseCapability,
IStyleBindingCapability, ICornerRoundConfigCapability {
private var radius: FloatArray? = null
private var roundCornerPath: Path? = null
private var lastStartPadding: Int? = null
private var lastEndPadding: Int? = null
private var lastTopPadding: Int? = null
private var lastBottomPadding: Int? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
context,
attrs,
defStyle
)
override fun onBindData(data: Any?) {
when (data) {
is String -> {
this.text = data.extractExpValue()
}
is JSONObject -> {
updateStyleByJsonObj(data)
}
}
}
override fun onBindStyle(attr: ViewAttr,attrData:Any?) {
if (attr is TextAttr) {
updateStyleByAttr(attr)
}
}
private fun updateStyleByAttr(attr: TextAttr) {
textSize = OTSize.create(attr.textSize).valueFloat
setTextBackground(StyleConverter.instance.backgroundLinearColor(attr.background))
val textColor = OTColor.create(attr.textColor)?.value()
// 因为背景边框的设置是依赖shader的所以设置前应该将shader置空
paint.shader = null
setTextColor(textColor ?: Color.BLACK)
includeFontPadding = attr.includeFontPadding == "true"
//设置文本的对齐方式
attr.textAlign?.let { setTextAlign(it) }
setTextPadding(StyleConverter.instance.padding(attr))
}
private fun setTextPadding(padding: Rect<OTSize?>?) {
val startPadding = padding?.start?.valueInt ?: 0
val endPadding = padding?.end?.valueInt ?: 0
val topPadding = padding?.top?.valueInt ?: 0
val bottomPadding = padding?.bottom?.valueInt ?: 0
if (lastStartPadding != startPadding
|| lastTopPadding != topPadding
|| lastEndPadding != endPadding
|| lastBottomPadding != bottomPadding
) {
this.setPadding(
startPadding,
topPadding,
endPadding,
bottomPadding
)
}
lastStartPadding = startPadding
lastEndPadding = endPadding
lastTopPadding = topPadding
lastBottomPadding = bottomPadding
}
private fun updateStyleByJsonObj(data: JSONObject) {
// TODO:添加无障碍之类的描述
val result = ExpressionWrapper.instance.getResult("\$attributes", data)
Log.d("zhongxj", "result==>$result")
if (result is JSONArray) {
result.forEach { styleObj ->
// 只处理数JSONArray中是JSONObject的情况
if (styleObj is JSONObject) {
styleObj.keys.forEach { key ->
val value = ExpressionWrapper.instance.getResult("\$$key", styleObj)
when (key) {
TemplateKey.ATTR_TEXT_SIZE -> {
this.textSize = OTSize.create(value.toString()).valueFloat
}
TemplateKey.ATTR_TEXT_COLOR -> this.setTextColor(
Color.parseColor(
value.toString()
)
)
TemplateKey.ATTR_TEXT -> text = value.toString()
}
}
}
}
}
}
@SuppressLint("SetTextI18n")
override fun release() {
this.text = ""
}
override fun configureViewRoundCorner(radius: FloatArray) {
this.radius = radius
if (radius.size == 8) {
val lt = radius[0]
val rt = radius[2]
val lb = radius[4]
val rb = radius[6]
if (lt == rt && lb == rt && lb == rb && lt > 0) {// 四个角都需要设置相同大小的圆角
roundCornerPath = null
this.clipToOutline = true
this.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
if (alpha >= 0.0f) {
outline.alpha = alpha
}
outline.setRoundRect(0, 0, view.width, view.height, lt)
}
}
} else { // 四个角的圆角大小不一样的情况下。使用Path的方法绘制
this.clipToOutline = false
this.outlineProvider = null
}
}
}
override fun configureViewRoundCornerBorder(
borderColor: Int,
borderWidth: Float,
radius: FloatArray
) {
if (borderWidth > 0) {
this.setPadding(
borderWidth.toInt(),
borderWidth.toInt(),
borderWidth.toInt(),
borderWidth.toInt()
)
}
if (background == null) {
val target = GradientDrawable()
target.shape = GradientDrawable.RECTANGLE
target.cornerRadii = radius
target.setStroke(borderWidth.toInt(), borderColor)
background = target
} else if (background is GradientDrawable) {
val target = background as GradientDrawable
target.setStroke(borderWidth.toInt(), borderColor)
target.cornerRadii = radius
}
}
}
具体的可以看开源后的源码
5. 源码地址
生成式UI的SDK我直接说可能无法完全表达清楚,所以为了方便各位读者更好了解其实现方式,我将源码开源到了github,诚挚邀请各位大牛一起共建,一起将这个事情做成,欢迎IOS,Harmony及各个平台的大牛参与。Otter生成式SDK源码地址