简介
G6 从 3.0-beta.1 发布到现在已经发布了 10 几个版本,从最初的仅支持关系图的展示,到支持 editor 底层机制,到支持了一些分析场景的业务,在这个过程中发现了一些问题,特别是交互方面,本章讨论交互状态问题,节点和边的状态量的设计当初想的过于简单,出现了下面的问题:
- 节点和边的交互状态过多,单个节点或者边有5个以上的状态。
- 多个状态之间存在互斥
问题分析
由于原先的对状态量的设计过于简单,仅考虑了状态的设置和取消 (true / false) ,而且 false 时仅取消时执行,后面节点刷新时不再调用,也带来了一些理解上的成本。用户认为的同一个节点状态量有多个值时,只能拆解成多个变量。
以节点的状态进行举例,在一个通用场景下节点有以下状态:
- hover,鼠标移动到节点上的状态
- selected,点击、框选选中
- highlight, 高亮,点击某被节点时高亮所有相邻的节点(不同于 selected)
- dark,变暗, 所有未被高亮的变暗
- 源点,点击边时显示源点
- 结束点,点击边时区分显示结束点
- 关键路径点,点击选中两个节点,显示关键路径
我们会发现这些状态应用到节点上时需要考虑一些问题:
- 样式的冲突问题,多个状态改变相同的样式
- 状态的互斥问题,多个状态不可能同时存在
状态量互斥
假设我们有四个互斥的状态:
- dark: 变暗
- light: 变亮
- light:source:作为源点变亮
- light:target :作为结束点变亮
当我们设置 dark 时,需要取消其他状态,看一下在这种情况下代码中的写法:
// 设置 dark 前需要把其他状态都取消掉
graph.setItemState(node, 'light', false);
graph.setItemState(node, 'light:source', false);
graph.setItemState(node, 'light:target', false);
graph.setItemState(node, 'dark', true);
// 改成 light
graph.setItemState(node, 'dark', false);
graph.setItemState(node, 'light:source', false);
graph.setItemState(node, 'light:target', false);
graph.setItemState(node, 'light', true);
// 取消所有
graph.setItemState(node, 'dark', false);
graph.setItemState(node, 'light:source', false);
graph.setItemState(node, 'light:target', false);
graph.setItemState(node, 'light', false);
- 互斥的状态量必须在设置前,将其他状态量设置成 false,否则会出问题
有两种方案可以解决这个问题:
- 增加一个清理状态量的接口
- 支持单个状态量支持多个值
清理状态量
增加一个 clearItemStates(Item, states) 接口来处理这个问题,上面的写法可以写成:
const states = ['light', 'light:source', 'light:target', 'dark'];
// 设置 dark 前需要把其他状态都取消掉
graph.clearItemStates(node, states);
graph.setItemState(node, 'dark', true);
// 改成 light
graph.clearItemStates(node, states);
graph.setItemState(node, 'light', true);
// 取消所有
graph.clearItemStates(node, states);
- 逻辑同上面的实现完全一样,但是代码更加简单好组织,同时也易于理解
- 需要缓存所有的状态量,算是一个最佳实践,G6 已经支持这种方式
状态量支持多值
可以将多个状态量合并成一个 nodeActive 包含 ‘light’, ‘light:source’, ‘light:target’, ‘dark’ 4 个值,可以实现成:
// 设置 dark
graph.setItemState(node, 'nodeActive','dark');
// 设置 light
graph.setItemState(node, 'nodeActive','light');
// 清理
graph.setItemState(node, 'nodeActive',null);
- 这种写法更好理解,代码更简单
- 但是存在概念一致的问题, null 作为取消状态量有些不好理解,特别是单值状态量 selected 和 active 时 false 作为取消状态量的值同这里也不一致。
注意
: 这种方式还没有支持,原因在于状态对应的配置项和自定义 shape 中有大量需要做的功能
状态量的实现
自定义节点修改状态量
定义节点的状态量后,需要思考每个状态量对应的样式,针对用户的这个诉求 G2 在自定义 Shape 的时候提供了 setState 方法供用户进行复写:
G2.registerNode('custom', {
setState(name, value, item) {
const keyShape = item.getKeyShape();
if (name == 'selected') {
if(value) {
// do something
} else {
// do something
}
} else if(name == 'active') {
// do something
}
}
}, 'rect');
通过复写自定义 shape 上的接口,这是一种最灵活的方式,
优点:
- 可以针对每种状态的设置、取消进行样式的调整。
- 也可以通过添加元素、增加动画来实现某些状态
缺点:
const graph = new Graph({
nodeStateStyles: {
'selected': {
fill: 'red'
},
'active': {
lineWidth: 2
}
}
});
graph.setItemState(item, 'selected', true);
graph.setItemState(item, 'selected', false);
- 设置状态和回滚状态,样式会进行自动的设置和回滚
- 没有任何内置的状态名,selected, active 都是用户自己定义的状态名