自定义元素

实际上我们在前面一些例子里已经看到过,我们能够继承Sprite一系列类来扩展新的精灵类型。很多例子里我们创建了一些简单的精灵类型。现在我们尝试创建一类更复杂的UI元素。

创建新的精灵类型 - 图1

我们可以很容易制作一组进度条UI组件,在这里我简单写了一个可以有三种展现类型的ProgressBar类(当然也可以将它拆分成3个不同的子类),可以看到通过spritejs实现UI组件是一件很容易的事情。

  1. const scene = new Scene('#progressbar', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. class ProgressBar extends Sprite {
  4. get contentSize() {
  5. let [width, height] = this.attr('size');
  6. const {slotLength, slotWidth, type} = this.attr();
  7. if(type === 'bar') {
  8. if(width === '') {
  9. width = slotLength;
  10. }
  11. if(height === '') {
  12. height = slotWidth;
  13. }
  14. } else if(type === 'circle') {
  15. if(width === '') {
  16. width = Math.round(slotLength / Math.PI + slotWidth);
  17. }
  18. if(height === '') {
  19. height = Math.round(slotLength / Math.PI + slotWidth);
  20. }
  21. } else if(type === 'hourglass') {
  22. if(width === '') {
  23. width = slotWidth;
  24. }
  25. if(height === '') {
  26. height = slotLength;
  27. }
  28. }
  29. return [width, height];
  30. }
  31. render(t, context) {
  32. super.render(t, context);
  33. const progress = this.attr('progress'),
  34. slotColor = this.attr('slotColor'),
  35. progressColor = this.attr('progressColor'),
  36. type = this.attr('type');
  37. const p = progress / 100;
  38. const [width, height] = this.contentSize;
  39. if(type === 'bar') {
  40. context.beginPath();
  41. context.rect(0, 0, width, height);
  42. context.fillStyle = slotColor;
  43. context.fill();
  44. const progressRect = [0, 0, width * p, height];
  45. context.beginPath();
  46. context.rect(...progressRect);
  47. context.fillStyle = progressColor;
  48. context.fill();
  49. } else if(type === 'circle') {
  50. const slotWidth = this.attr('slotWidth');
  51. const r = width / 2;
  52. context.beginPath();
  53. context.arc(r, r, r - slotWidth / 2, 0, 2 * Math.PI);
  54. context.lineWidth = slotWidth;
  55. context.strokeStyle = slotColor;
  56. context.stroke();
  57. context.beginPath();
  58. context.arc(r, r, r - slotWidth / 2, -0.5 * Math.PI, (2 * p - 0.5) * Math.PI);
  59. context.lineWidth = slotWidth;
  60. context.strokeStyle = progressColor;
  61. context.stroke();
  62. } else if(type === 'hourglass') {
  63. context.beginPath();
  64. context.moveTo(width / 2, height / 2);
  65. context.lineTo(0, 0);
  66. context.lineTo(width, 0);
  67. context.closePath();
  68. context.fillStyle = slotColor;
  69. context.fill();
  70. context.beginPath();
  71. context.moveTo(width / 2, height / 2);
  72. context.lineTo(0, height);
  73. context.lineTo(width, height);
  74. context.closePath();
  75. context.fillStyle = progressColor;
  76. context.fill();
  77. const dx = (1 - p ** 2) * width / 2,
  78. dy = (1 - p ** 2) * height / 2;
  79. context.beginPath();
  80. context.moveTo(width / 2, height / 2);
  81. context.lineTo(width / 2 - dx, height / 2 - dy);
  82. context.lineTo(width / 2 + dx, height / 2 - dy);
  83. context.closePath();
  84. context.fillStyle = progressColor;
  85. context.fill();
  86. context.beginPath();
  87. context.moveTo(width / 2, height / 2);
  88. context.lineTo(width / 2 - dx, height / 2 + dy);
  89. context.lineTo(width / 2 + dx, height / 2 + dy);
  90. context.closePath();
  91. context.fillStyle = slotColor;
  92. context.fill();
  93. }
  94. }
  95. }
  96. ProgressBar.defineAttributes({
  97. init(attr) {
  98. attr.setDefault({
  99. progress: 0,
  100. slotColor: 'grey',
  101. progressColor: 'green',
  102. type: 'bar', // bar, circle, hourglass
  103. slotLength: 200,
  104. slotWidth: 25,
  105. });
  106. },
  107. progress(attr, val) {
  108. attr.clearCache();
  109. attr.set('progress', val);
  110. },
  111. slotColor(attr, val) {
  112. attr.clearCache();
  113. attr.set('slotColor', parseColorString(val));
  114. },
  115. progressColor(attr, val) {
  116. attr.clearCache();
  117. attr.set('progressColor', parseColorString(val));
  118. },
  119. type(attr, val) {
  120. attr.clearCache();
  121. attr.set('type', val);
  122. },
  123. slotLength(attr, val) {
  124. attr.clearCache();
  125. attr.set('slotLength', val);
  126. },
  127. slotWidth(attr, val) {
  128. attr.clearCache();
  129. attr.set('slotWidth', val);
  130. },
  131. });
  132. const p1 = new ProgressBar();
  133. p1.attr({
  134. anchor: [0.5, 0.5],
  135. progress: 45,
  136. pos: [250, 300],
  137. slotLength: 500,
  138. type: 'circle',
  139. });
  140. layer.append(p1);
  141. p1.animate([
  142. {progress: 0},
  143. {progress: 100},
  144. ], {
  145. duration: 10000,
  146. iterations: Infinity,
  147. easing: 'ease-in-out',
  148. });
  149. const label = new Label('0%');
  150. label.attr({
  151. anchor: [0.5, 0.5],
  152. pos: [250, 300],
  153. font: '36px Arial',
  154. });
  155. layer.append(label);
  156. p1.on('update', (evt) => {
  157. const progress = evt.target.attr('progress');
  158. label.text = `${progress.toFixed(0)}%`;
  159. });
  160. const p2 = new ProgressBar();
  161. p2.attr({
  162. anchor: [0.5, 0.5],
  163. progress: 0,
  164. pos: [770, 300],
  165. slotLength: 300,
  166. slotWidth: 45,
  167. progressColor: 'rgb(192,0,0)',
  168. borderRadius: 20,
  169. });
  170. layer.append(p2);
  171. Effects.progressColor = Effects.color;
  172. Effects.slotColor = Effects.color;
  173. p2.animate([
  174. {progress: 0, progressColor: 'rgb(192,0,0)'},
  175. {progress: 50, progressColor: 'rgb(192, 192, 0)'},
  176. {progress: 100, progressColor: 'rgb(0, 192, 0)'},
  177. ], {
  178. duration: 5000,
  179. iterations: Infinity,
  180. });
  181. const p3 = new ProgressBar();
  182. p3.attr({
  183. anchor: [0.5, 0.5],
  184. progress: 0.5,
  185. pos: [1150, 300],
  186. slotLength: 100,
  187. slotWidth: 50,
  188. progressColor: '#cc6',
  189. type: 'hourglass',
  190. });
  191. layer.append(p3);
  192. p3.attr({
  193. progress: 0.5,
  194. });
  195. p3.animate([
  196. {progress: 0},
  197. {progress: 100},
  198. ], {
  199. duration: 3000,
  200. iterations: Infinity,
  201. });

我们可以看到,要扩展Sprite类,只需要继承Sprite、Label、Path或Group这四个类,通过静态方法defineAttributes定义一些新的属性。

  1. Sprite.defineAttributes({
  2. init(attr) {
  3. // 这是构造函数,在这里可以通过 setDefault 给属性设置初始值
  4. },
  5. foo(attr, val) {
  6. // 添加一个叫做 foo 的属性
  7. // 如果不需要其他处理,只需要将它保存在 attribute 对象中即可
  8. attr.set('foo', val)
  9. attr.clearCache() // 如果这个属性需要清缓存,则调用 clearCache,什么属性需要清缓存,具体见“缓存策略”一节
  10. }
  11. })

扩展了属性,我们要实现继承类的一些方法。一般来说,我们只要重写get contentSizerender方法,前者负责在不给元素设置size的情况下计算元素的大小,后者负责元素具体的渲染过程。

  1. class MyElement extends Sprite {
  2. get contentSize() {
  3. let [width, height] = this.attr('size')
  4. if(width === '') {
  5. // 当宽度为默认值时,处理
  6. }
  7. if(height === '') {
  8. // 当宽度为默认值时,处理
  9. }
  10. return [width, height]
  11. }
  12. render(t, context) {
  13. super.render(t, context)
  14. // 处理具体绘制过程
  15. }
  16. }

这样我们就可以很方便地扩展我们需要的新元素了。

除了initAttributes方法外,如果我们通过babel和webpack编译,我们还可以使用decorate来定义属性,我们可以继承Sprite.Attr类。

定义属性特殊操作

我们定义元素属性的时候,需要理解和使用一些概念:

  • reflow: 当定义了一个新属性,该属性改变会引起元素的contentSizeclientSizeoffsetSize等box大小变化的时候,由于spritejs默认缓存了这些属性计算值,因此需要显式调用attr.clearFlow()来通知引擎清除这些元素缓存的值。

  • cache: 当定义了一个新属性,该属性改变会引起元素的外观呈现发生变化(包括改变size、color、border、shape等)时,由于spritejs缓存策略可能会将元素缓存,因此需要显式调用attr.clearCache()来通知引擎清除缓存。在大部分情况下,元素的属性都引起外观改变(除了pos、transform、layout相关的这类只做位置变换的属性),因此通常情况下自定义的属性都要进行clearCache操作。

  • quietSet: 如果定义一个属性,既不影响元素外观,又不影响box大小(如Path的bounding属性只影响事件的hit判断),那么可以使用attr.quietSet()来代替attr.set()设置元素属性,这样元素的这个属性变化的时候,不会通知layer做update操作,能够减少消耗,提升性能。

  • relayout: 当继承一个Group,定义的新属性如果是layout相关的属性,那么需要显式调用attr.subject.relayout()来通知元素清除layout,这样在绘制的时候才能重新计算layout。

插件封装

我们可以自定义spritejs的插件库,spritejs提供了use操作,能够载入并初始化插件。

use(component, options, merge = true)方法支持三个参数,返回插件对象。

  • component 要使用的组件,该组件暴露一个function或者{install:function}的接口。
  • options 传给接口的配置。
  • merge 默认值true,将该接口返回的内容merge到spritejs对象上。
  1. import * as MyElement from 'my-element';
  2. spritejs.use(MyElement);
  3. const el = new spritejs.MyElement();
  4. ...

MyElement插件的定义

  1. export function install({Sprite, registerNodeType}, options) {
  2. class MyAttr extends Sprite.Attr {
  3. constructor() {
  4. this.setDefault({
  5. // 设置默认属性
  6. })
  7. }
  8. @attr
  9. foo(val) {
  10. this.set('foo', val)
  11. this.clearCache()
  12. }
  13. }
  14. class MyElement extends Sprite {
  15. ...
  16. }
  17. registerNodeType('myelement', MyElement); // 定义新的nodeType名为myelement
  18. return {MyElement}
  19. }

注意由于spritejs使用babel-transform-decorators-runtimebabel-decorators-runtime两个扩展来支持ES6的decorators特性,所以需要安装这两个依赖:

  1. npm i -D babel-transform-decorators-runtime
  2. npm i -S babel-decorators-runtime

对应的.babelrc推荐配置为:

  1. {
  2. "presets": ["env"],
  3. "plugins": ["transform-runtime",
  4. "transform-decorators-runtime",
  5. "transform-class-properties"]
  6. }

一个例子,通过继承Group类实现矢量图形的绘制。