自定义交互

简介

G2 4.0 最大的一个变化是所有的交互不再内置,全部通过交互语法搭配而成,前面章节介绍了交互的使用 本章介绍如何自定义交互。

自定义交互

所有内置的交互都是通过自定义交互的代码实现的,G2 住的交互的接口定义为:

  1. G2.registerInteraction(name, InteractionClass | InteractionSteps);

G2 提供了两种注册自定交互的方式

  • 基于 Interaction 基类继承自己的 Interaction 类,所以上面接口的第二个方式是一个继承 Interaction 的类
  • 通过交互语法组合交互

使用类继承的方式

为了兼容 G2 3.x 的交互定义方式,在 G2 4.0 中我们依然保留了类继承的方式来实现交互,首先我们来看 Interaction 类的定义:

  1. /*
  2. * 交互的基类
  3. */
  4. class Interaction {
  5. /** view 或者 chart */
  6. protected view: View;
  7. /** 配置项 */
  8. protected cfg: LooseObject;
  9. constructor(view: View, cfg: LooseObject) {
  10. this.view = view;
  11. this.cfg = cfg;
  12. }
  13. public init() {
  14. this.initEvents();
  15. }
  16. protected initEvents() {}
  17. protected clearEvents() {}
  18. public destroy() {
  19. this.clearEvents();
  20. }
  21. }

继承 Interaction 时一般仅需要绑定事件、移除事件

  1. import { Interaction } from '@antv/g2';
  2. class MyInteraction extends Interaction {
  3. protected initEvents() {
  4. const view = this.view;
  5. view.on('mousedown', ev => this.onMouseDown)
  6. }
  7. onMouseDown = ev => {
  8. // do any thing
  9. }
  10. protected clearEvents() {
  11. const view = this.view;
  12. view.off('mousedown', ev => this.onMouseDown)
  13. }
  14. }
  15. G2.registerInteraction('my-interaction', MyInteraction);

使用交互语法组合

在交互语法中一个交互可以由多个交互环节组成,每个交互环节可以有多个触发和反馈,所以在配置交互时可以配置每个环节,每个环节都是数组,都有 trigger 和 action

  • showEnable:标识交互可以发生
  • start: 交互开始
  • processing: 交互持续
  • end: 交互结束
  • rollback: 回滚

每个交互环节有多个触发和反馈构成,每个触发和反馈是一个对象:

  1. {
  2. trigger: 'eventName',
  3. action: 'actionName:method' | [] | function(context) {},
  4. isEnable(context) {}, // 可选
  5. callback(context) {}, // 可选
  6. once: false // 可选,默认 false
  7. }

其中:

  • trigger 触发一个交互环节的事件名,是所有 Chart 支持的事件
  • action 触发的反馈,可以是字符串也可以是数组,是所有内置和用户自定义的 Action,参考 交互反馈 Action 列表

    • 字符串由 ’actionName:method‘ 组成
    • 列表时可以使用相同的 action ,也可以使用不同的 action ,例如: ['element-active:clear', 'element-active:active', 'mask:clear']

除了 trigger 和 action 之外还有其他几个属性:

  • isEnable(context): 是否可以触发
  • callback(context): 触发后执行完所有 action 的方法后会调用回调函数
  • once: boolean, 是否在一个环节内仅能执行一次

由于交互都是有多个交互环节构成,所以在这种方式下,你进需要组合你需要的交互环节,在每个环节中指定 trigger 和 action 即可,我们以两个 Interaction 来说明

  • 允许对 tooltip 进行锁定的交互
  • 鼠标移动到坐标轴文本上,对应的图表元素高亮

允许锁定的 tooltip

我们可以参考默认 tooltip 的实现, 有两个环节 start 和 end

  1. // 注册 tooltip 的 interaction
  2. registerInteraction('tooltip', {
  3. start: [{ trigger: 'plot:mousemove', action: 'tooltip:show' }],
  4. end: [{ trigger: 'plot:mouseleave', action: 'tooltip:hide' }],
  5. });

但是允许锁定 tooltip 则需要增加两个环节

  • 锁定 tooltip
  • 解除 tooltip 的锁定
  1. // 注册 tooltip 的 interaction
  2. registerInteraction('locked-tooltip', {
  3. start: [{ trigger: 'plot:mousemove', action: 'tooltip:show' }],
  4. processing: [
  5. {
  6. trigger: 'plot:click',
  7. isEnable(context) {
  8. return !context.view.isTooltipLocked();
  9. },
  10. action: context => {
  11. context.view.lockTooltip();
  12. },
  13. },
  14. {
  15. trigger: 'plot:click',
  16. isEnable(context) {
  17. return context.view.isTooltipLocked();
  18. },
  19. action: context => {
  20. context.view.unlockTooltip();
  21. },
  22. },
  23. ],
  24. end: [{ trigger: 'plot:mouseleave', action: 'tooltip:hide' }],
  25. });

active 坐标轴文本

这个交互我们可以分拆为两个环节:

  • 鼠标移动进入坐标轴文本,对应的坐标轴文本 active,对应的 element active
  • 鼠标移动出坐标轴文本, 对应的坐标轴文本取消 active,对应的 element 取消 active

通过上面的分析,我们可以发现

  • 触发对象是 axis-label,事件是 'mouseenter' 和 'mouseleave'
  • 相关的 Action 是坐标轴文本 active 和图表元素 Element active 的 Action查找 [支持的 Action] 我们可以发现,这两个 Action 分别为:
  • list-highlight: 列表组件 axis 和 component 的列表项 a ctive
  • element-highlight:图表元素 active

最终的交互语法是:

  1. registerInteraction('axis-label-highlight', {
  2. start: [
  3. {
  4. trigger: 'axis-label:mouseenter',
  5. action: ['list-active:active', 'element-active:active'],
  6. },
  7. ],
  8. end: [
  9. {
  10. trigger: 'axis-label:mouseleave',
  11. action: ['list-active:reset', 'element-active:reset'],
  12. },
  13. ],
  14. });

Context 上下文

context 中有大量函数,可以在组合交互语法和自定 Action 时使用,contex 的定义:

  1. /** 交互上下文的接口定义 */
  2. export interface IInteractionContext extends LooseObject {
  3. /**
  4. * 当前触发的事件对象
  5. */
  6. event: LooseObject;
  7. /**
  8. * 当前的 view
  9. */
  10. view: View;
  11. /** 交互相关的 Actions */
  12. actions: IAction[];
  13. /**
  14. * 缓存属性,用于上下文传递信息
  15. * @param key 键名
  16. * @param value 值
  17. */
  18. cache(key: string, value?: any);
  19. /**
  20. * 获取 action
  21. * @param name - action 的名称
  22. * @returns 指定 name 的 Action
  23. */
  24. getAction(name): IAction;
  25. /**
  26. * 获取当前的点
  27. * @returns 返回当前的点
  28. */
  29. getCurrentPoint(): Point;
  30. /**
  31. * 获取当前的图形
  32. */
  33. getCurrentShape(): IShape;
  34. /**
  35. * 添加 action
  36. * @param action 指定交互 action
  37. */
  38. addAction(action: IAction);
  39. /**
  40. * 移除 action
  41. * @param action 移除的 action
  42. */
  43. removeAction(action: IAction);
  44. /**
  45. * 事件触发时是否在 view 内部
  46. */
  47. isInPlot();
  48. /**
  49. * 是否在组件的包围盒内
  50. * @param name 组件名,可省略
  51. */
  52. isInComponent(name?: string);
  53. /**
  54. * 是否在指定的图形内
  55. * @param name shape 的名称
  56. */
  57. isInShape(name);
  58. /**
  59. * 销毁
  60. */
  61. destroy();
  62. }
  • 可以通过 context.isInPlot() 判定事件触发时,事件发生的位置是否在绘图区域内
  • 可以通过 context.isInComponent('legend') 判定是否发生在 legend 的包围盒内
  • 可以通过 context.event.target 或者 context.getCurrentShape() 获取触发的图形

自定义 Action

使用交互语法搭配交互时,需要使用到 Action,你可以从 G2 已经内置的 Action 列表 中选取,也可以选择自定义 Action。自定义 Action 有两个步骤:

  • 实现 Action 的继承类
  • 注册 Action

继承 Action

Action 的接口定义非常简单:

  1. /** 交互反馈的定义 */
  2. export interface IAction {
  3. /**
  4. * 交互 action (反馈)的名称
  5. */
  6. name: string;
  7. /**
  8. * 上下文
  9. */
  10. context: IInteractionContext;
  11. /**
  12. * 销毁函数
  13. */
  14. destroy();
  15. }
  • name 名称即在交互组合中使用的名称
  • destroy 方法销毁在 Action 实现中的资源(图形、数组等)

自定义 Action 时可以基于 Action 的基类 继承,也可以基于现有的任何 Action 继承,Action 中的每个方法代表一个反馈行为,实现为无参数的函数,我们来看一下 cursor Action 的实现:

  1. /**
  2. * 鼠标形状的 Action
  3. */
  4. class CursorAction extends Action {
  5. private setCursor(cursor) {
  6. const view = this.context.view;
  7. view.getCanvas().setCursor(cursor);
  8. }
  9. /**
  10. * 默认光标(通常是一个箭头)
  11. */
  12. public default() {
  13. this.setCursor('default');
  14. }
  15. /** 光标呈现为指示链接的指针(一只手) */
  16. public pointer() {
  17. this.setCursor('pointer');
  18. }
  19. /** 此光标指示某对象可被移动。 */
  20. public move() {
  21. this.setCursor('move');
  22. }
  23. /** 光标呈现为十字线。 */
  24. public crosshair() {
  25. this.setCursor('crosshair');
  26. }
  27. /** 此光标指示程序正忙(通常是一只表或沙漏)。 */
  28. public wait() {
  29. this.setCursor('wait');
  30. }
  31. /** 此光标指示可用的帮助(通常是一个问号或一个气球)。 */
  32. public help() {
  33. this.setCursor('help');
  34. }
  35. /** 此光标指示文本。 */
  36. public text() {
  37. this.setCursor('text');
  38. }
  39. /**
  40. * 此光标指示矩形框的边缘可被向右(东)移动。
  41. */
  42. public eResize() {
  43. this.setCursor('e-resize');
  44. }
  45. /**
  46. * 此光标指示矩形框的边缘可被向左(西)移动。
  47. */
  48. public wResize() {
  49. this.setCursor('w-resize');
  50. }
  51. /**
  52. * 此光标指示矩形框的边缘可被向上(北)移动。
  53. */
  54. public nResize() {
  55. this.setCursor('n-resize');
  56. }
  57. /**
  58. * 此光标指示矩形框的边缘可被向下(南)移动。
  59. */
  60. public sResize() {
  61. this.setCursor('s-resize');
  62. }
  63. /**
  64. * 光标指示可移动的方向 右上方(东北)
  65. */
  66. public neResize() {
  67. this.setCursor('ne-resize');
  68. }
  69. /**
  70. * 光标指示可移动的方向 左上方(西北)
  71. */
  72. public nwResize() {
  73. this.setCursor('nw-resize');
  74. }
  75. /**
  76. * 光标指示可移动的方向右下方(东南)
  77. */
  78. public seResize() {
  79. this.setCursor('se-resize');
  80. }
  81. /**
  82. * 光标指示可移动的方向左下方(西南)
  83. */
  84. public swResize() {
  85. this.setCursor('sw-resize');
  86. }
  87. /**
  88. * 光标指示可以在上下方向移动
  89. */
  90. public nsResize() {
  91. this.setCursor('ns-resize');
  92. }
  93. /**
  94. * 光标指示可以在左右方向移动
  95. */
  96. public ewResize() {
  97. this.setCursor('ew-resize');
  98. }
  99. }

注册 Action

G2 提供了 registerAction 的接口用于注册 Action

  1. G2.registerAction('cursor', CursorAction);