27

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

(我不是在谈论特定的语言和库,如 jQuery 或 UIKit,除非它们迫使你以特定的方式思考管理相互依赖的动画,我对此很感兴趣。)

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

iOS 主屏幕

然而,隐藏的复杂性是惊人的
关于界面,我注意到了一些事情:

  • 当您几乎不触摸图标时,它的不透明度会发生变化,但大小变化会延迟。
  • 如果您在其他两个应用程序之间拖动一个应用程序,则在所有应用程序重新排列以移动可用空间之前会有明显的延迟。因此,如果您只是继续在屏幕上移动应用程序,那么在您安顿下来之前什么都不会发生。
  • 重新排列是逐行发生的,首先是你悬停在的那一行,它会触发链中的下一行,到之前空闲空间所在的那一行。
  • 如果您删除了一个应用程序,它将在现在可用的空间中删除,而不仅仅是您删除它的位置。
  • 如果您将一个应用程序悬停在另一个应用程序上,则会出现径向灯,闪烁两次,然后才会创建一个组。
  • 如果组在空闲空间的右侧创建,然后被丢弃,它将在丢弃时向左动画以占用空闲空间。

我敢肯定这里还有更多我没有注意到的复杂性

连续动画与离散动作

粗略概括,对于同一接口上下文中的每一对,(animation, user_action)您需要确定如果user_actionanimation已经运行时发生什么。

在大多数情况下,您可以

  • 取消动画;
  • 随时随地更改动画;
  • 忽略动作;
  • 将动作排队到动画完成时。

但是在动画过程中可能会有几个动作,你必须决定要丢弃哪些动作,要排队,以及是在动画结束时执行所有排队的动作,还是只执行最后一个动作。

如果动画结束时有东西排队,并且动画发生了变化,你需要决定排队的动作是否仍然有意义,或者需要被删除。

如果这听起来太理论化,请考虑一个真实世界的示例:您如何处理用户向下拖动应用程序,等待重新排列开始,然后立即将应用程序向上拖动并释放它?您如何确保动画在所有可能的情况下都流畅且可信?

适合工作的工具

我发现自己甚至无法将一半的可能场景记在脑海中。随着 UI 表现力的增加,可能的状态数量开始严重违反 7±2 规则

因此,我的问题如下:

你如何驯服动画设计和实现的复杂性?

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

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

代码必须有多复杂才能准确表示 UI 的所有可能状态?当某个布尔变量为真到将其设置为假的函数时,它是否会是一个事件处理程序添加另一个事件处理程序,除非另一个事件处理程序在它之前运行?

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

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

(考虑到我不必自己编写动画——也就是说,我确实可以访问像 jQuery 或 Core Animation 这样的框架,这些框架可以满足animate(styles, callback)我的需求,而且我可以cancel做到。)

如果数据结构、设计模式、DSL 有助于解决问题,它们都是好的。

4

1 回答 1

34

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

考虑状态和动作的一种方法是有限状态机

阅读 这篇解释如何应用 FSM 在 JavaScript 中实现自定义工具提示的文章:

在此处输入图像描述

这个例子可能看起来有点令人费解,但确实说明了这一点:有限状态机迫使您思考哪些状态是可能的,它们之间的哪些转换是有效的,何时应该触发它们以及一旦它们应该执行什么代码

因为 IBM 的文章禁止使用其代码示例,所以我建议您也阅读Ben Nadel 的一篇文章,该文章使用 FSM 实现下拉菜单小部件。

本写道:

我已经看到有限状态机可以处理大型复杂任务并将其分解为更小、更易于管理的状态的方式。我什至尝试将这种心态应用于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 Animations另一个介绍)感兴趣。它们现在是 iOS 8 中的默认设置,并且已经被Kevin Doughty提倡了好几年了。

这是一种不同的方法,在保持系统状态的同时,您允许多个(甚至相反的)动画同时处于活动状态。这会给你带来疯狂的结果,但这是一个有趣的概念。

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

附加动画
(来源:ronnqvi.st

对于附加动画的准系统框架无关示例,请查看alexkuz 的附加动画模块(演示)。


如果你读到这里,你一定对动画很感兴趣!目前,我对react-state-stream方法很感兴趣。它建议将动画表达为状态的惰性序列。这开辟了许多可能性,例如表达无限动画,从动画中逐渐添加和删除变换等等。

如果你想看一篇关于动画的文章,我建议是程楼的动画思考。

于 2012-06-01T19:26:54.383 回答