生成式UI 动态化SDK的研发(二)--实现垂直布局、水平布局以及文字组件

news2024/11/25 23:38:29

文章目录

  • 1. 概述
  • 2. 效果展示
    • 2.1 垂直布局容器(Column)
    • 2.2 水平布局容器(Row )
    • 2.3 本文示例动态化模板DSL
  • 3. 生成式UI 动态化SDK的渲染流程
  • 4. Column和Row组件实现
    • 4.1 Column组件
    • 4.1.1 视图的渲染
      • 4.1.2 事件解析绑定
    • 4.2 Row组件
    • 4.3 文字组件(Text)
  • 5. 源码地址

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源码地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2247539.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript中的this指向绑定规则(超全)

JavaScript中的this指向绑定规则&#xff08;超全&#xff09; 1.1 为什么需要this? 为什么需要this? 在常见的编程语言中&#xff0c;几乎都有this这个关键字&#xff08;Objective-C中使用的是self),但是在JavaScript中的this和常见的面向对象语言中的this不太一样 常见面…

Vision Transformer(VIT模型)

【11.1 Vision Transformer(vit)网络详解-哔哩哔哩】 https://b23.tv/BgsYImJ 工作流程&#xff1a; ①将输入的图像进行patch的划分 ②Linear Projection of Flatted patches&#xff0c;将patch拉平并进行线性映射生成token ③生成CLS token&#xff08;用向量有效地表示整…

2024年11月最新 Alfred 5 Powerpack (MACOS)下载

在现代数字化办公中&#xff0c;我们常常被繁杂的任务所包围&#xff0c;而时间的高效利用成为一项核心需求。Alfred 5 Powerpack 是一款专为 macOS 用户打造的高效工作流工具&#xff0c;以其强大的定制化功能和流畅的用户体验&#xff0c;成为众多效率爱好者的首选。 点击链…

C#里怎么样检测文件的属性?

C#里怎么样检测文件的属性? 对于文件来说,在C#里有一种快速的方法来检查文件的属性。 比如文件是否已经压缩, 文件是否加密, 文件是否是目录等等。 属性有下面这么多: 例子演示如下: /** C# Program to View the Information of the File*/ using System; using Syste…

网络安全,文明上网(4)掌握网络安全技术

前言 在数字化时代&#xff0c;个人信息和企业数据的安全变得尤为重要。为了有效保护这些宝贵资产&#xff0c;掌握一系列网络安全技术是关键。 核心技术及实施方式 1. 网络监控与过滤系统&#xff1a; 这些系统构成了网络防御体系的基石&#xff0c;它们负责监控网络通信&…

Vue 项目中如何使用FullCalendar 时间段选择插件(类似会议室预定、课程表)

本文中是基于VUEelementui项目中实现的前后端分离的前端功能部分&#xff1a; 插件的官方文档&#xff1a;FullCalendar 1.安装对应依赖&#xff08;统一安装版本为6.15&#xff09; npm install --save fullcalendar/core6.15 npm install --save fullcalendar/daygrid6.…

Oracle SQL优化③——表的连接方式

前言 表&#xff08;结果集&#xff09;与表&#xff08;结果集&#xff09;之间的连接方式非常重要&#xff0c;如果CBO选择了错误的连接方式&#xff0c;本来几秒就能出结果的SQL可能执行一天都执行不完。如果想要快速定位超大型SQL性能问题&#xff0c;就必须深入理解表连接…

小程序25- iconfont 字体图标的使用

项目中使用到图标&#xff0c;一般由公司设计进行设计&#xff0c;设计好后上传到阿里巴巴矢量图标库 日常开发过程中&#xff0c;也可以通过 iconfont 图标库下载使用自带的图标 补充&#xff1a;使用 iconfont 图标库报错&#xff1a;Failed to load font 操作步骤&#xff…

Java基于SpringBoot+Vue的藏区特产销售平台

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

selinux及防火墙

selinux说明 SELinux 是 Security-Enhanced Linux 的缩写&#xff0c;意思是安全强化的 linux 。 SELinux 主要由美国国家安全局&#xff08; NSA &#xff09;开发&#xff0c;当初开发的目的是为了避免资源的误用。 httpd进程标签&#xff08;/usr/share/nginx/html &#…

详细探索xinput1_3.dll:功能、问题与xinput1_3.dll丢失的解决方案

本文旨在深入探讨xinput1_3.dll这一动态链接库文件。首先介绍其在计算机系统中的功能和作用&#xff0c;特别是在游戏和输入设备交互方面的重要性。然后分析在使用过程中可能出现的诸如文件丢失、版本不兼容等问题&#xff0c;并提出相应的解决方案&#xff0c;包括重新安装相关…

生成对抗网络模拟缺失数据,辅助PAMAP2数据集仿真实验

PAMAP2数据集是一个包含丰富身体活动信息的数据集&#xff0c;它为我们提供了一个理想的平台来开发和测试HAR模型。本文将从数据集的基本介绍开始&#xff0c;逐步引导大家通过数据分割、预处理、模型训练&#xff0c;到最终的性能评估&#xff0c;在接下来的章节中&#xff0c…

IEC61850读服务器目录命令——GetServerDirectory介绍

IEC61850标准中的GetServerDirectory命令是变电站自动化系统中非常重要的一个功能&#xff0c;它主要用于读取服务器的目录信息&#xff0c;特别是服务器的逻辑设备节点&#xff08;LDevice&#xff09;信息。以下是对GetServerDirectory命令的详细介绍。 目录 一、命令功能 …

基于CNN+RNNs(LSTM, GRU)的红点位置检测(pytorch)

1 项目背景 需要在图片精确识别三跟红线所在的位置&#xff0c;并输出这三个像素的位置。 其中&#xff0c;每跟红线占据不止一个像素&#xff0c;并且像素颜色也并不是饱和度和亮度极高的红黑配色&#xff0c;每个红线放大后可能是这样的。 而我们的目标是精确输出每个红点的…

前端:JavaScript (学习笔记)【2】

目录 一&#xff0c;数组的使用 1&#xff0c;数组的创建 [ ] 2&#xff0c;数组的元素和长度 3&#xff0c;数组的遍历方式 4&#xff0c;数组的常用方法 二&#xff0c;JavaScript中的对象 1&#xff0c;常用对象 &#xff08;1&#xff09;String和java中的Stri…

全面解析多种mfc140u.dll丢失的解决方法,五种方法详细解决

当你满心期待地打开某个常用软件&#xff0c;却突然弹出一个错误框&#xff0c;提示“mfc140u.dll丢失”&#xff0c;那一刻&#xff0c;你的好心情可能瞬间消失。这种情况在很多电脑用户的使用过程中都可能出现。无论是游戏玩家还是办公族&#xff0c;面对这个问题都可能不知所…

STM32总体架构简单介绍

目录 一、引言 二、STM32的总体架构 1、三个被动单元 &#xff08;1&#xff09;内部SRAM &#xff08;2&#xff09;内部闪存存储器 &#xff08;3&#xff09;AHB到APB的桥&#xff08;AHB to APBx&#xff09; 2、四个主动&#xff08;驱动&#xff09;单元 &#x…

【PHP】 环境以及插件的配置,自学笔记(一)

文章目录 环境的准备安装 XAMPPWindowMacOS 配置开发环境Vscode 关于 PHP 的插件推荐Vscode 配置 php 环境Apache 启动Hello php配置热更新 参考 环境的准备 下载 XAMPP , 可以从 官网下载 https://www.apachefriends.org/download.html 安装 XAMPP XAMPP 是一个跨平台的集成开…

跟着问题学5——深度学习中的数据集详解(1)

深度学习数据集的创建与读取 数据 &#xff08;计算机术语&#xff09; 数据(data)是事实或观察的结果&#xff0c;是对客观事物的逻辑归纳&#xff0c;是用于表示客观事物的未经加工的的原始素材。 数据可以是连续的值&#xff0c;比如声音、图像&#xff0c;称为模拟数据。…

实验-Linux文件系统和磁盘管理

操作1 远程连接Linux系统 下述连接方式2选一即可。 使用xshell工具连接Linux系统。打开xshell&#xff0c;新建连接&#xff0c;将主机ip修改为实际Linux系统的ip(ifconfig命令查看)&#xff0c;可以新建多个xshell会话&#xff0c;使用不同的用户名登录&#xff0c;方便后续…