states是一个对象,它的每个key表示一个状态ID,对应的值是一组属性:

有了states以后,我们就可以切换元素的状态:

  1. const layer = scene.layer();
  2. const states = {
  3. stateA: {
  4. bgcolor: 'red',
  5. scale: 0.5,
  6. rotate: 45,
  7. },
  8. stateB: {
  9. scale: 1.0,
  10. bgcolor: 'green',
  11. rotate: 0,
  12. },
  13. stateC: {
  14. scale: 0.8,
  15. bgcolor: 'blue',
  16. rotate: 60,
  17. },
  18. };
  19. const stateNames = Object.keys(states);
  20. const s = new Label(stateNames[0]);
  21. s.attr({
  22. anchor: 0.5,
  23. size: [200, 200],
  24. pos: [770, 300],
  25. font: '64px Arial',
  26. lineHeight: 200,
  27. textAlign: 'center',
  28. fillColor: 'white',
  29. states,
  30. state: stateNames[0],
  31. });
  32. layer.append(s);
  33. let i = 0;
  34. setInterval(() => {
  35. s.attr({state: stateNames[++i % 3]});
  36. }, 1000);

我们可以在状态切换的时候给状态切换设置行为,方法是设置actions属性。这个属性是一个数组,每个元素是一个action描述对象,包含以下内容:

from,toboth,设置状态切换选择器,action,设置动作timing:

  1. const actions = [
  2. {
  3. from: 'stateA',
  4. to: 'stateB',
  5. action: {
  6. duration: 500,
  7. easing: 'ease-in-out',
  8. },
  9. },{
  10. both: ['stateB', 'stateC'],
  11. action: {
  12. duration: 800,
  13. easing: 'cubic-bezier(0.26, 0.09, 0.37, 0.18)',
  14. },
  15. },{
  16. from: 'stateC',
  17. action: {
  18. duration: 1000,
  19. },
  20. },{
  21. to: 'stateC',
  22. action: {
  23. duration: 500,
  24. }
  25. },
  26. ];
  27. sprite.attr('actions', actions);

Action的匹配规则如下:

当一个状态stateA切换到stateB的时候,优先匹配{from:'stateA', to:'stateB'},如果不存在这个Action选择器,那么匹配{to:'stateB'},如果也不存在,那么匹配{from:'stateA'}{both: ['stateA', 'stateB']}是简写,会生成{from:'stateA', to:'stateB'}{from:'stateB', to:'stateA'}两个选择器。对应的,{both:['stateA']}也会生成{from:'stateA'}{to:'stateA'}两个选择器。

状态切换的时候,我们可以监听state变化的事件。

一个元素的状态从a变化为b,会触发两个事件,一个是state-from-a,一个是state-to-b。事件参数包括四个属性,分别是:

  • from: 元素的源状态名,即a
  • to: 元素的目的状态名,即b
  • action: 元素切换状态的action对象,该对象是一个动画timing对象,由前面的Action选择器规则匹配出来。
  • animation: 元素切换状态的Animation对象。

当状态从stateA切换到stateB的时候,如果匹配到{from:'stateA', to:'stateB'}的Action并执行动画,此时状态再切换回stateA,如果上一个Action动画还没执行完成,此时默认不会执行{to:'stateB', from:'stateA'}选择器下的Action(或者其他更低优先级的选择器选择的Action),而是反向执行前一个未完成的Action,这样我们做状态双向切换的动画就比较自然。如果我们要强制执行新的Action,可以给{from:'stateA', to:'stateB'}的Action设置一个reversable:false属性,以强制忽略反向Action,执行新的Action。

状态切换 - 图3

  1. const scene = new Scene('#state-reversable', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const button1 = new Label('reversable');
  4. button1.attr({
  5. anchor: 0.5,
  6. font: '32px Arial',
  7. border: [3, 'blue'],
  8. padding: 10,
  9. pos: [500, 300],
  10. state: 'normal',
  11. states: {
  12. hover: {
  13. scale: 1.2,
  14. },
  15. normal: {
  16. scale: 1,
  17. },
  18. },
  19. actions: [
  20. {
  21. duration: 500,
  22. },
  23. ],
  24. });
  25. layer.append(button1);
  26. button1.on('mouseenter', function () {
  27. this.attr('state', 'hover');
  28. });
  29. button1.on('mouseleave', function () {
  30. this.attr('state', 'normal');
  31. });
  32. const button2 = button1.cloneNode();
  33. button2.attr({
  34. text: 'not reversable',
  35. x: x => x + 500,
  36. actions: [
  37. {
  38. both: ['hover', 'normal'],
  39. duration: 500,
  40. reversable: false,
  41. },
  42. ],
  43. });
  44. layer.append(button2);
  45. button2.on('mouseenter', function () {
  46. this.attr('state', 'hover');
  47. });
  48. button2.on('mouseleave', function () {
  49. this.attr('state', 'normal');
  50. });

我们可以通过resolveStates(states, before, after)方法批量设置一组state,然后让元素从开始state变更到结束state。

每个元素的resolveStates(states, before, after)方法是互斥的,也就是说如果同一时间调用两组resolveState(),spritejs会立即结束前面的动作,执行后一组动作。

  1. const scene = new Scene('#state-resolveStates', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const button1 = new Label('动作 1');
  4. button1.attr({
  5. anchor: 0.5,
  6. font: '32px Arial',
  7. border: [3, 'blue'],
  8. padding: 10,
  9. pos: [500, 300],
  10. });
  11. layer.append(button1);
  12. const button2 = button1.cloneNode();
  13. button2.attr({
  14. text: '动作 2',
  15. y: y => y + 100,
  16. });
  17. layer.append(button2);
  18. button1.on('mouseenter', () => {
  19. layer.style.cursor = 'pointer';
  20. });
  21. button1.on('mouseleave', () => {
  22. layer.style.cursor = '';
  23. });
  24. button2.on('mouseenter', () => {
  25. layer.style.cursor = 'pointer';
  26. });
  27. button2.on('mouseleave', () => {
  28. layer.style.cursor = '';
  29. });
  30. const block = new Sprite({
  31. pos: [800, 300],
  32. size: [100, 100],
  33. bgcolor: 'red',
  34. state: 'a',
  35. states: {
  36. a: {
  37. scale: 1.0,
  38. rotate: 0,
  39. },
  40. b: {
  41. scale: 1.5,
  42. rotate: 0,
  43. },
  44. c: {
  45. scale: 1.2,
  46. rotate: 180,
  47. },
  48. d: {
  49. scale: 1.0,
  50. rotate: -45,
  51. },
  52. },
  53. actions: [
  54. {
  55. both: ['a', 'b'],
  56. duration: 1000,
  57. },
  58. {
  59. both: ['b', 'c'],
  60. duration: 1000,
  61. },
  62. {
  63. both: ['c', 'd'],
  64. duration: 1000,
  65. },
  66. {
  67. both: ['d', 'a'],
  68. duration: 1000,
  69. },
  70. ],
  71. });
  72. layer.append(block);
  73. button1.on('click', () => {
  74. });
  75. button2.on('click', () => {
  76. });
  • beforeEnter 当元素或它的父级元素被append到layer上之前的临时状态。
  • afterEnter 当元素或它的父级元素被append到layer上之后的临时状态。
  • beforeExit 当元素或它的父级元素被从layer上remove之前的临时状态。
  • afterExit 当元素或它的父级元素被从layer上remove之后的临时状态。
  • show 当元素被调用hide()方法之前的临时状态,或被调用show()方法之后的临时状态。
  • hide 当元素被调用hide()方法之后的状态。
  • default 元素默认的初始状态。

状态切换 - 图5

在上面的例子里,我们给元素设置了beforeEnter和afterExit的状态,在append和remove的时候,spritejs会自动触发动作。enterexit行为有默认的action,值为:

  1. [
  2. {
  3. from: 'beforeEnter',
  4. duration: 300,
  5. ease: 'ease-in',
  6. },
  7. {
  8. from: 'beforeExit',
  9. duration: 300,
  10. ease: 'ease-out',
  11. }
  12. ]

我们可以设置元素的actions属性来改写它们。

  1. // 延长动画时间
  2. sprite.attr('actions', [
  3. {
  4. from: 'beforeEnter',
  5. duration: 600,
  6. ease: 'ease-in',
  7. },
  8. {
  9. from: 'beforeExit',
  10. duration: 600,
  11. ease: 'ease-out',
  12. }
  13. ])

除了控制enter和exit之外,我们还可以通过给元素增加hide状态来控制它的显示隐藏,通过它我们可以很方便地实现fade-in和fade-out效果:

在使用group的时候,我们可以将子元素一一添加到group上,然后再将group添加到parent上,此时group下的子元素的enter行为会被触发。我们可以通过设置enterMode和exitMode来改变enter/exit行为的触发方式,可选的方式如下:

  • normal 默认值,enter时同时触发自身和子元素的enter,exit时先同时触发自身和子元素的exit
  • onebyone enter时先触发自身的enter,然后根据zOrder顺序依次触发子元素的enter,exit时先根据zOrder顺序依次触发子元素的exit,然后触发自身的exit
  • onebyone-reverse enter时先触发自身的enter,然后根据zOrder的倒序依次触发子元素的enter,exit时先根据zOrder倒序依次触发子元素的exit,然后触发自身的exit

状态切换 - 图7

  1. const scene = new Scene('#state-mode', {viewport: ['auto', 'auto'], resolution: [1540, 600]});
  2. const layer = scene.layer();
  3. const group = new Group({
  4. display: 'flex',
  5. size: [700, 100],
  6. pos: [770, 300],
  7. anchor: 0.5,
  8. bgcolor: 'grey',
  9. enterMode: 'onebyone',
  10. exitMode: 'onebyone-reverse',
  11. });
  12. for(let i = 0; i < 5; i++) {
  13. const block = new Sprite({
  14. size: [100, 100],
  15. bgcolor: 'red',
  16. margin: [0, 10, 0, 10],
  17. states: {
  18. beforeEnter: {
  19. translate: [500, 0],
  20. },
  21. afterExit: {
  22. translate: [500, 0],
  23. },
  24. },
  25. });
  26. group.append(block);
  27. }
  28. const button1 = new Label('批量添加');
  29. button1.attr({
  30. anchor: 0.5,
  31. font: '32px Arial',
  32. border: [3, 'blue'],
  33. padding: 10,
  34. pos: [500, 200],
  35. states: {
  36. beforeEnter: {
  37. translate: [-1000, 0],
  38. },
  39. },
  40. });
  41. layer.append(button1);
  42. const button2 = button1.cloneNode();
  43. button2.attr({
  44. text: '批量移除',
  45. x: x => x + 300,
  46. });
  47. layer.append(button2);
  48. button1.on('mouseenter', () => {
  49. layer.style.cursor = 'pointer';
  50. });
  51. button1.on('mouseleave', () => {
  52. layer.style.cursor = '';
  53. });
  54. button2.on('mouseenter', () => {
  55. layer.style.cursor = 'pointer';
  56. });
  57. button2.on('mouseleave', () => {
  58. layer.style.cursor = '';
  59. });
  60. button1.on('click', () => {
  61. layer.append(group);
  62. });
  63. button2.on('click', () => {
  64. group.remove();