什么是复杂动画的良好抽象?

时间:2012-06-01 15:31:33

标签: design-patterns animation user-interface language-agnostic abstraction

您如何设计和实现复杂的UI交互动画?

(我不是在讨论jQuery或UIKit这样的特定语言和库,除非它们强迫您以特定的方式思考管理相互依赖的动画,我感兴趣。)

考虑一个看似“简单”的任务,比如设计和编程iOS主屏幕。

iOS home screen

但是,隐藏复杂性的程度令人震惊 我注意到关于界面的一些事情:

  • 当您几乎没有触摸图标时,其不透明度会发生变化,但尺寸更改会延迟。
  • 如果您在其他两个应用之间拖动应用,则在重新排列所有应用以移动可用空间之前会有明显的延迟。因此,如果您只是继续在屏幕上移动应用程序,那么在您安顿下来之前不会发生任何事情。
  • 重新排列逐行发生,首先进入你盘旋的线,然后触发链中的下一行,到达之前可用空间的行。
  • 如果您放弃一个应用程序,它将会在现在可用的空间中掉落,而不仅仅是放下它的位置。
  • 如果您将应用悬停在其他应用上,则会出现径向光,闪烁两次,然后才会创建一个组。
  • 如果该组是在自由空间中创建的,然后被丢弃,则会在放弃时动画留下以占据可用空间。

我确信更加复杂,我没注意到

连续动画与离散动作

在粗略概括中,对于同一界面上下文中的每对(animation, user_action),您需要确定 user_action发生了什么animation 已经正在运行

在大多数情况下,你可以

  • 取消动画;
  • 随时更改动画;
  • 忽略行动;
  • 将动作排队到动画结束时。

但是动画期间可能会有几个动作,你必须决定丢弃哪些动作,排队哪些动作,以及动画结束时是否执行所有排队动作,或者只是最后一个动作。

如果在动画完成时某些事项排队,并且动画发生了变化,则需要确定排队的操作是否仍然有意义,或者是否需要删除。

如果这听起来过于理论化,会考虑一个真实世界的例子:您如何处理用户向下拖动应用,等待重新排列开始,然后立即向上拖动应用并释放它?在各种情况下,您如何确保动画流畅和可信?

工作的正确工具

我发现自己甚至无法保留一半的可能情景。随着UI的表现力增加,可能状态的数量开始猛烈地违反7±2规则

因此,我的问题如下:

  

您如何控制设计和实施动画的复杂性?

我对找到解决问题的有效方法以及解决问题的方法感兴趣。

作为一个例子,事件和观察者被证明是大多数UI的非常有效的抽象
但是,您能否依靠事件作为主要抽象来设计和实现类似iOS的拖放式屏幕?

代码必须如何纠结才能准确地表示UI的所有可能状态?当一个布尔变量对于将其设置为false的函数为真时,它是否是一个事件处理程序添加另一个事件处理程序,除非在它之前运行另一个事件处理程序?

“你有没有听说过课程?”你可能想知道。为什么,我有,但这些课程要分享的状态太多了。

总而言之,我正在寻找与语言无关的(尽管可能是受语言或框架启发的)技术,用于管理复杂的相互依赖,可取消,动画的发生按顺序或一次,描述他们对用户操作的反应

(所有这一切都考虑到我必须自己编写动画 - 也就是说,我确实可以访问jQuery或Core Animation这样的框架,animate(styles, callback)对我而言,我可以cancel。)

如果数据结构,设计模式,DSL都有助于解决这个问题,那么这些都是很好的。

1 个答案:

答案 0 :(得分:34)

在所描述的问题中,存在系统状态的隐含概念。动画是有状态的,因此它们的任何组合都是有状态的,可以说更是如此。

考虑状态和行动的一种方法是Finite-state machines

阅读this article,了解如何应用FSM在JavaScript中实现自定义工具提示:

enter image description here

这个例子可能看起来有点复杂,但确实说明了这一点:有限状态机迫使你认为哪些状态是可能的,它们之间的转换是有效的,何时应该被触发以及应该执行哪些代码他们

由于IBM的文章禁止使用其code sample,因此我建议您阅读使用FSM实现下拉菜单小部件的an article by Ben Nadel

本写道:

  

我已经看到有限状态机可以执行大型复杂任务并将其分解为更小,更易管理的状态。我甚至试图将这种心态运用到binding and unbinding event handlers in JavaScript。但是,现在我对状态机,特别是状态转换更加熟悉,我想尝试将这种思维模式应用于一个有凝聚力的用户界面(UI)小部件。

这是他的代码的略微删减版本:

var inDefault = {
    description: "I am the state in which only the menu header appears.",
    setup: function() {
       dom.menu.mouseenter(inHover.gotoState);
    },    
    teardown: function() {
         dom.menu.unbind("mouseenter");
    }
};

var inHover = {
    description: "I am the state in which the user has moused-over the header of the menu, but the menu has now shown yet.",
    setup: function() {
        dom.menu.addClass("menuInHover");
        dom.menu.mouseleave(inDefault.gotoState);
        dom.header.click(
            function(event) {
                event.preventDefault();
                gotoState(inActive); 
            }
       );    
    },
    teardown: function() {
        dom.menu.removeClass("menuInHover");
        dom.menu.unbind("mouseleave");
        dom.header.unbind("click"); 
    }    
};

var inActive = {
     description: "I am the state in which the user has clicked on the menu and the menu items have been shown. At this point, menu items can be clicked for fun and profit.",

    setup: function() {
        dom.menu.addClass("menuInActive");
        dom.stage.mousedown(
            function(event) {
                var target = $(event.target);
                if (!target.closest("div.menu").length) {
                    gotoState(inDefault); 
                } 
            }
       );
       dom.header.click(
            function(event) {
                event.preventDefault();
                 gotoState(inHover);

            }
       );
       dom.items.delegate(
            "li.item",
            "click",
            function(event) {
                console.log(
                    "Clicked:",
                    $.trim($(this).text())
               );

            }
       );
    },    
    teardown: function() {
        dom.menu.removeClass("menuInActive"); 
        dom.stage.unbind("mousedown", inDefault.gotoState);
        dom.header.unbind("click"); 
        dom.items.undelegate("li.item", "click");
    }
};

请注意,事件处理程序在进入状态时会被绑定,在离开此状态时会被解除绑定。

FSM在解决此问题时给予的最大优势是使状态明确

虽然每个动画可能对包含系统的状态有所贡献,但您的系统永远不会同时处于两种状态或根本没有状态,并且调试几乎变得微不足道,因为您总是可以看到系统(或每个子系统)的状态因为你的州设计很有意义。

此外,通过强制您明确设计状态,使用FSM可以排除您不考虑特定状态/操作组合的可能性。没有“未定义的行为”,因为每次转换都是明确的,并且是您的FSM设计的一部分。


如果你已经读过这篇文章,你可能会对Additive Animationsanother intro)感兴趣。它们现在是iOS 8中的默认设置,并且Kevin Doughty已经提倡了好几年了。

这是一种不同的方法,在保持系统状态的同时,允许多个(甚至相反的)动画同时处于活动状态。这可以给你crazy results,但这是一个有趣的概念。

主要思想是避免将动画定义为从绝对值A到绝对值B的东西,而是将动画定义为相对于其最终值(每个动画从-Delta变为0)。这允许您通过在每个时间点对它们的相对值求和来无缝地组合多个动画,并避免因反转或取消引起的尖峰:

additive animations http://ronnqvi.st/images/additive-interaction.gif

对于添加动画的准系统框架不可知示例,请查看alexkuz's additive-animation模块(demo)。


如果您已经读过这篇文章,那么您一定对动画非常感兴趣!目前,我对react-state-stream方法很感兴趣。它建议将动画表达为懒惰的状态序列。这开辟了许多可能性,例如表达无限动画,逐渐添加和删除动画中的变换等等。

如果您想阅读一篇关于动画的文章,我建议由Cheng Lou Thoughts on Animation