!!!###!!!title=单元格自定义渲染(JSX)——VisActor/VTable 教程文档!!!###!!!

自定义渲染自动布局

简介

本篇教程主要介绍如何使用 CustomLayout 实现自动布局。

- 注意 :如果您想通过定义坐标来实现完全自定义可以参考教程: CustomRender, CustomRender 写法支持用户自定义单元格内需要渲染需要的元素,使用时通过回调函数返回元素数组,指定元素的类型、样式和坐标(VTable CustomRender 布局能力设计 )。不过该使用方式比较底层,如果用户希望实现一个复杂的样式,需要手动计算各个元素的位置,手动处理对齐、换行等功能,上手比较困难,可维护性较低。-

通过 CustomLayout 是在 CustomRender API 的基础上,提供一套简单盒模型布局能力,用户通过配置容器与元素,实现对齐、折行等基础布局能力,方便实现与维护较为复杂的单元格内容。VTable使用VRender提供的图元和布局能力实现customLayout功能,目前推荐使用 JSX 写法,层级结构更加清晰,参考示例

下面是一个相对复杂的文字图标混排布局,使用 CustomLayout 实现(红色为不同容器 bounds):

CustomLayout 配置

与 customRender 类似,customLayout 也分为customLayoutheaderCustomLayout两个接口分别来配置表头和内容的自定义渲染,在 columns/rows 中配置

const option = {
  columns: [
    {
      // ......
      customLayout: (args: VTable.TYPES.CustomRenderFunctionArg) => {
        // ......
      }
    }
  ]
};

customLayout 函数返回一个对象,该对象需要有:rootContainer来指定自定义渲染内容的根节点,renderDefault指定是否需要绘制单元格原内容(与 customRender 一致)。

举一个配置示例,如下示例中使用到了 VGroup,VImage,VText,最后将其返回:

{
  customLayout: args => {
    const { table, row, col, rect } = args;
    const { height, width } = rect ?? table.getCellRect(col, row);
    const record = table.getCellOriginRecord(col, row);

    const container = (
      <VGroup
        attribute={{
          id: 'container',
          width,
          height,
          display: 'flex',
          flexWrap: 'wrap',
          justifyContent: 'flex-start',
          alignContent: 'center'
        }}
      >
        <VGroup
          attribute={{
            id: 'container-left',
            width: 60,
            height,
            fill: 'red',
            opacity: 0.1,
            display: 'flex',
            justifyContent: 'space-around',
            alignItems: 'center'
          }}
        >
          <VImage
            attribute={{
              id: 'icon0',
              width: 50,
              height: 50,
              image: record.bloggerAvatar,
              cornerRadius: 25
            }}
          ></VImage>
        </VGroup>
        <VGroup
          id="container-right"
          attribute={{
            id: 'container-right',
            width: width - 60,
            height,
            fill: 'yellow',
            opacity: 0.1,
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'space-around',
            alignItems: 'center'
          }}
        >
          <VGroup
            attribute={{
              id: 'container-right-top',
              fill: 'red',
              opacity: 0.1,
              width: width - 60,
              height: height / 2,
              display: 'flex',
              flexWrap: 'wrap',
              justifyContent: 'flex-start',
              alignItems: 'center'
            }}
          >
            <VText
              attribute={{
                id: 'bloggerName',
                text: record.bloggerName,
                fontSize: 13,
                fontFamily: 'sans-serif',
                fill: 'black',
                textAlign: 'left',
                textBaseline: 'top',
                boundsPadding: [0, 0, 0, 10]
              }}
            ></VText>
            <VImage
              attribute={{
                id: 'location-icon',
                width: 15,
                height: 15,
                image:
                  '<svg t="1684484908497" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2429" width="200" height="200"><path d="M512 512a136.533333 136.533333 0 1 1 136.533333-136.533333 136.533333 136.533333 0 0 1-136.533333 136.533333z m0-219.272533a81.92 81.92 0 1 0 81.92 81.92 81.92 81.92 0 0 0-81.92-81.92z" fill="#0073FF" p-id="2430"></path><path d="M512 831.214933a27.306667 27.306667 0 0 1-19.2512-8.055466l-214.493867-214.357334a330.5472 330.5472 0 1 1 467.490134 0l-214.357334 214.357334a27.306667 27.306667 0 0 1-19.387733 8.055466z m0-732.091733a275.933867 275.933867 0 0 0-195.106133 471.04L512 765.269333l195.106133-195.106133A275.933867 275.933867 0 0 0 512 99.1232z" fill="#0073FF" p-id="2431"></path><path d="M514.321067 979.490133c-147.456 0-306.107733-37.000533-306.107734-118.3744 0-45.602133 51.746133-81.92 145.681067-102.4a27.306667 27.306667 0 1 1 11.605333 53.384534c-78.370133 17.066667-102.673067 41.915733-102.673066 49.015466 0 18.432 88.064 63.761067 251.4944 63.761067s251.4944-45.192533 251.4944-63.761067c0-7.3728-25.258667-32.768-106.496-49.834666a27.306667 27.306667 0 1 1 11.195733-53.384534c96.6656 20.343467 150.186667 56.9344 150.186667 103.2192-0.273067 80.964267-158.9248 118.3744-306.3808 118.3744z" fill="#0073FF" p-id="2432"></path></svg>',
                boundsPadding: [0, 0, 0, 10]
              }}
            ></VImage>
            <VText
              attribute={{
                id: 'locationName',
                text: record.city,
                fontSize: 11,
                fontFamily: 'sans-serif',
                fill: '#6f7070',
                textAlign: 'left',
                textBaseline: 'top'
              }}
            ></VText>
          </VGroup>
          <VGroup
            attribute={{
              id: 'container-right-bottom',
              fill: 'green',
              opacity: 0.1,
              width: width - 60,
              height: height / 2,
              display: 'flex',
              flexWrap: 'wrap',
              justifyContent: 'flex-start',
              alignItems: 'center'
            }}
          >
            {record?.tags?.length
              ? record.tags.map((str, i) => (
                  <VTag
                    attribute={{
                      text: str,
                      textStyle: {
                        fontSize: 10,
                        fontFamily: 'sans-serif',
                        fill: 'rgb(51, 101, 238)'
                      },
                      panel: {
                        visible: true,
                        fill: '#e6fffb',
                        lineWidth: 1,
                        cornerRadius: 4
                      },
                      boundsPadding: [0, 0, 0, 10]
                    }}
                  ></VTag>
                ))
              : null}
          </VGroup>
        </VGroup>
      </VGroup>
    );

    return {
      rootContainer: container,
      renderDefault: false
    };
  };
}

布局能力

以此表头为例

分为A B CD C D五个container

表头部分横向布局,分为三部分(A B CD):

  • 左右两侧(A B),宽度像素指定(由 icon size 决定),高度为单元格高度
  • 中间部分(CD)高度单元格高度,指定宽度单元格宽度 - AB 总宽度

中间部分纵向布局,分为两部分(C D):

  • 上部(C)指定高度(由“全部”文字样式确定),宽度为父级 container 宽度
  • 下部(D)不指定高度,宽度为父级 container 宽度,实际高度由布局结果确定,超过容器部分被截断

中间下部(D)横向布局,有三个 element:分组文字、省份按钮、城市按钮

其中省份按钮和城市按钮是多个 element 组合而成,整个容器的高度由布局折行结果决定,最小高度为不换行显示为一行;最大高度为三个 element 都折行显示,显示为三行

JSX 图元

详细说明请参考 VRender 提供的教程:VRender图元配置

容器图元

容器图元VGroup是盒模型布局容器,支持元素在其中自动布局;VGroup的子元素可以是VGroup,也可以是基础图元;布局支持配置以下属性

  • display: 布局模式(flex开启 flex 布局模式)
  • flexDirection: 主轴的方向
  • flexWrap: 单行显示还是多行显示
  • justifyContent: 行向轴分配内容元素之间和周围的空间规则
  • alignItems: 交叉轴上的对齐规则
  • alignContent: 主轴上的对齐规则

基础图元

基础的自定义图元,目前支持VRect VCircle VText VImage

图元类型基础属性
rectwidth, height, stroke, fill, lineWidth, cornerRadius...
circleradius, startAngle, endAngle, stroke, fill, lineWidth...
texttext, fontSize, fontFamily, fill...
imageimage, width, height

基础自定义组件,目前支持VTag VRadio VCheckbox

图元类型基础属性
tagtext, textStyle, shape, padding...
radiochecked, disabled, text, icon...
checkboxchecked, disabled, text, icon...

图元可以配置boundsPadding属性,实现 margin 效果 boundsPadding: [marginTop, marginRight, marginBottom, marginLeft] 图元的 margin 会计算在图元所占的空间

图元状态更新及交互事件

在基础属性外,可以使用状态更新来实现 hover 等交互效果:

<VImage
  attribute={{
    id: 'row-down',
    image: collapseDown,
    width: 20,
    height: 20,
    cursor: 'pointer'
  }}
  stateProxy={(stateName: string) => {
    if (stateName === 'hover') {
      return {
        background: {
          fill: '#ccc',
          cornerRadius: 5,
          expandX: 1,
          expandY: 1
        }
      };
    }
  }}
  onMouseEnter={event => {
    event.currentTarget.addState('hover', true, false);
    event.currentTarget.stage.renderNextFrame();
  }}
  onMouseLeave={event => {
    event.currentTarget.removeState('hover', false);
    event.currentTarget.stage.renderNextFrame();
  }}
></VImage>

通过绑定事件,更新图元状态,实现交互更新图元样式效果。

API

VRect

矩形图元

keytypedescription
widthnumber矩形宽度
heightnumber矩形高度
lineWidthnumber描边宽度
cornerRadiusnumber角弧度
fillstring填充颜色
strokestring描边颜色

VCircle

圆形图元

keytypedescription
radiusnumber半径
startAnglenumber起始弧度
endAnglenumber结束弧度
lineWidthnumber描边宽度
fillstring填充颜色
strokestring描边颜色

VText

文字图元

keytypedescription
textstring文字内容
fontSizestring字号
fontFamilystring字体
fillstring文字颜色

VImage

图片图元

keytypedescription
widthnumber图片宽度
heightnumber图片高度
imagestringHTMLImageElement | HTMLCanvasElement | 图片内容

VLine

线图元

keytypedescription
points{x: number, y: number}[]组成 line 的点坐标
lineWidthnumber描边宽度
strokestring描边颜色

VGroup

容器

keytypedescription
widthnumber容器宽度
heightnumber容器高度
display'relative' | 'flex'布局模式(flex开启 flex 布局模式)
flexDirection'row' | 'row-reverse' | 'column' | 'column-reverse'主轴的方向
flexWrap'nowrap' | 'wrap'单行显示还是多行显示
justifyContent'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around'行向轴分配内容元素之间和周围的空间规则
alignItems'flex-start' | 'flex-end' | 'center'交叉轴上的对齐规则
alignContent'flex-start' | 'center' | 'space-between' | 'space-around'主轴上的对齐规则

VTag

标签组件

keytypedescription
textStyleITextGraphicAttribute文字样式,同 text 图元属性
shapeTagShapeAttributes标签中图表的样式配置
spacenumber图标与文字之间的距离
paddingnumber[]内容与边框之间的距离
panelBackgroundAttributes外部边框及背景的样式,同 rect 图元属性
minWidthnumber最小宽度
maxWidthnumber最大宽度

VRadio

标签组件

keytypedescription
interactiveboolean是否可交互
disabledboolean是否禁用
checkedboolean是否选中
spaceBetweenTextAndIconnumber图标与文字间聚
textITextGraphicAttribute文字样式,同 text 图元属性
circle{disableFill?: IColor;checkedFill?: IColor;checkedStroke?: IColor;disableCheckedFill?: IColor;disableCheckedStroke?: IColor;}图标样式

VCheckbox

标签组件

keytypedescription
interactiveboolean是否可交互
disabledboolean是否禁用
checkedboolean是否选中
indeterminateboolean是否处于不确定状态
spaceBetweenTextAndIconnumber图标与文字间聚
textITextGraphicAttribute文字样式,同 text 图元属性
icon{checkIconImage?: stringHTMLImageElement
box{disableFill?: IColor;checkedFill?: IColor;checkedStroke?: IColor;disableCheckedFill?: IColor;disableCheckedStroke?: IColor;}图表背景样式

CustomLayout 创建图元对象用法

- customLayout 支持对象创建的写法

CustomLayout 创建图元对象的写法,需要通过createXXX创建图元,具体创建时配置属性可以参考VRender图元配置

例如:

import { createText, createGroup } from '@visactor/vtable/es/vrender';

const text1 = new createText({
  text: 'text',
  fontSize: 28,
  fontFamily: 'sans-serif',
  fill: 'black'
});

const container = new createGroup({
  height,
  width
});
containerRight.add(text1);

return {
  rootContainer: container,
  renderDefault: false
};

动画

VTable支持在自定义布局中,使用VRender提供的动画能力,具体使用方法请参考VRender动画。需要注意的是动画需要配置为VTable实例上的timeline,以保证动画的一致性。

如果以JSX方式创建图元,需要在图元标签上添加animation属性和timelineanimation属性为一个数组,内是VRender动画中的操作,会在实例化对象后进行链式调用,例如:

<VImage
  attribute={{
    id: 'icon',
    width: 50,
    height: 50,
    src: record.bloggerAvatar,
    shape: 'circle',
    anchor: [25, 25]
  }}
  animation={[
    ['to', { angle: 2 * Math.PI }, 1000, 'linear'],
    ['loop', Infinity]
  ]}
  timeline={table.animationManager.timeline}
></VImage>

如果以实例化的方式创建图元,需要注意需要调用一次animation.setTimeline(table.animationManager.timeline);,例如:

import {createImage} from '@visactor/vtable/es/vrender';

const icon = createImage({
  id: 'icon',
  width: 50,
  height: 50,
  src: record.bloggerAvatar,
  shape: 'circle',
  anchor: [25, 25]
});
iconGroup.add(icon);

const animation = icon.animate();
animation.setTimeline(table.animationManager.timeline);
animation.to({ angle: 2 * Math.PI }, 1000, 'linear').loop(Infinity);