angular-phonecat教程中文版

步骤12——应用动画

在这个最终步骤中,我们通过在以前代码基础上附加CSS和JavaScript实现动画效果来增强手机展示web程序。

  • ngAnimater使得在程序中可以应用动画
  • 公用的ng指令自动利用钩子(hook)来触发动画
  • 当标准的DOM操作被赋予了动画时,对应的元素将会应用指定的动画效果,这些操作包括了利用ngRepeat插入或者移除DOM节点,以及利用ngClass对元素指定或者去除类型设定。

工作区切换到步骤12

直接用浏览器访问步骤12在线演示

大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。

依赖

动画功能由Angular的ngAnimate模块提供,它不属于Angular框架核心。此外我们使用JQuery来提供额外的JavaScript动画支持。 我们使用Bower来安装客户端依赖。这一步骤中我们会更新bower.json配置文件来包含新的依赖关系:

{
  "name": "angular-seed",
  "description": "A starter project for AngularJS",
  "version": "0.0.0",
  "homepage": "https://github.com/angular/angular-seed",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "angular": "1.2.x",
    "angular-mocks": "~1.2.x",
    "bootstrap": "~3.1.1",
    "angular-route": "~1.2.x",
    "angular-resource": "~1.2.x",
    "jquery": "1.10.2",
    "angular-animate": "~1.2.x"
  }
}
  • "angular-animate": "~1.2.x"告诉bower需要安装1.2.x版本的angular-animate兼容组件。

  • "jquery": "1.10.2"告诉bower需要1.10.2版本的JQuery,注意,JQuery不属于Angular库,它是一个标准的JQuery库。我们可以用bower安装第三方的库。

我们必须执行bower来下载安装,这里运行npm install

如果你是在全局环境中使用bower安装,你可能会用直接到bower install指令,但是在这个项目中我们已经预配使用npm来启动bower install,所以你只需要: npm install

如何利用ngAnimate实现动画

如何利用AngularJS实现动画,请首先阅读AngularJS Animation Guide(AngularJS动画指南)。

模板

需要在HTML模板代码中进行一些改动以链接诸如angular-animate.js等定义动画所需的资源文件。动画模块被称为ngAnimate,其在angular-animate中定义,并包含让程序应用动画的必要代码。

这里,我们在index文件中进行必要改动。 app/index.html

...
  <!-- for CSS Transitions and/or Keyframe Animations -->
  <link rel="stylesheet" href="css/animations.css">

  ...

  <!-- jQuery is used for JavaScript animations (include this before angular.js) -->
  <script src="bower_components/jquery/jquery.js"></script>

  ...

  <!-- required module to enable animation support in AngularJS -->
  <script src="bower_components/angular-animate/angular-animate.js"></script>

  <!-- for JavaScript Animations -->
  <script src="js/animations.js"></script>

...

重要提醒:在Angular 1.3中必须要使用jQuery 2.1以上版本,而jQuery1.x版本是不被正式支持的。一定要在所有AngularJS脚本之前加载jQuery,否则AngularJS可能不能正确检测到jQuery而造成动画不按预想工作。

可以创建动画中的CSS代码(animations.css)创建和JavaScript代码(animations.js)。不过在此之前我们创建一个新依赖ngAnimate的模块,就像前面的ngResource服务一样。

模块与动画

app/js/animations.js:

angular.module('phonecatAnimations', ['ngAnimate']);
  // ...
  // this module will later be used to define animations
  // .

然后把这个模块加入到我们的程序中... app/js/app.js:

 // ...
angular.module('phonecatApp', [
  'ngRoute',

  'phonecatAnimations',
  'phonecatControllers',
  'phonecatFilters',
  'phonecatServices',
]);
// ...

现在这个手机展示已经准备好动画环境了,让我们创作一些动画。

在动态ngRepeat中使用CSS的过渡动画

让我们在phone-list.html中添加CSS过渡动画到ngRepeat指令处理过程中。首先,我们给重复的元素添加一个额外CSS类,这样我们就可以以此钩住(hook)CSS过渡动画。 app/partials/phone-list.html

<!--
  Let's change the repeater HTML to include a new CSS class
  which we will later use for animations:
-->
<ul class="phones">
  <li ng-repeat="phone in phones | filter:query | orderBy:orderProp"
      class="thumbnail phone-listing">
    <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
    <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
    <p>{{phone.snippet}}</p>
  </li>
</ul>

注意我们是怎么添加phone-listingCSS类的?这些就是我们获得动画效果所有必须的HTML代码。

现在为CSS过渡动画实现代码

.phone-listing.ng-enter,
.phone-listing.ng-leave,
.phone-listing.ng-move {
  -webkit-transition: 0.5s linear all;
  -moz-transition: 0.5s linear all;
  -o-transition: 0.5s linear all;
  transition: 0.5s linear all;
}

.phone-listing.ng-enter,
.phone-listing.ng-move {
  opacity: 0;
  height: 0;
  overflow: hidden;
}

.phone-listing.ng-move.ng-move-active,
.phone-listing.ng-enter.ng-enter-active {
  opacity: 1;
  height: 120px;
}

.phone-listing.ng-leave {
  opacity: 1;
  overflow: hidden;
}

.phone-listing.ng-leave.ng-leave-active {
  opacity: 0;
  height: 0;
  padding-top: 0;
  padding-bottom: 0;
}

正如所见,phone-listingCSS类通过向对象添加或者去除(CSS类)而使得动画钩子触发。

  • ng-enter类被赋予一个新加入并渲染展示在页面中的手机元素。
  • ng-move类赋予在列表中移动的手机元素
  • ng-leave类赋予从列表中去除的元素

手机列表根据ng-repeat属性添加或者删除元素。例如如果转换器输出数据改变,导致在列表中动态的添加或者放置元素。

这点尤为需要注意,在动画工作时,有两套CSS类可被加入元素:

  1. 一套是"starting"类,这代表了动画的开始形式
  2. 一套"active"类,代表了动画结束的形式

这个命名为starting的类会触发一些有ng-前缀的行为(例如enter,move或者leve),像enter就会触发ng-enter

active的类像starting类一样也会触发一些行为,只是这些行为是有-active后缀。这两套CSS类的名称可以在开发中指定动画的实现,是开始还是结束。

在我们的例子中,在列表中添加或者移动时,元素会从高度0扩展到高度120像素,在去除时逐渐消隐掉(缩小),同时还有一个淡入淡出效果。所有这些操作都是通过CSS过渡声明在前面的例子代码中。

尽管许多现代浏览器都能很好的支持CSS过渡CSS动画,但IE9及以前版本是不支持的。如果你想对老的浏览器提供兼容的动画效果,可以考虑使用基于JavaScript的动画,这将在后面描述。

通过CSS提供ngView关键帧动画

接着,让我们为ngView中的内容切换提供动画。

首先,还是要添加一个新的CSS类到我们的HTML中,就像上面示例一样。这次,不是插入到ng-repeat所在的元素中,而是加入到应用ng-view命令所在的元素。为此,我们需要对HTML进行一些小改动,所以我们可以在视图变化时有更多的控制动画。 app/index.html

<div class="view-container">
  <div ng-view class="view-frame"></div>
</div>

通过这点改变,嵌入在view-containerCSS类元素下的ng-view指令所在元素有了view-frameCSS类属性。而view-containerCSS类元素下的各个子元素有了position: relative样式设定,使得ng-view是相对于父级元素进行定位实现动画转换。

在这里,我们添加一些CSS到animations.css中实现转换动画: app/css/animations.css

.view-container {
  position: relative;
}

.view-frame.ng-enter, .view-frame.ng-leave {
  background: white;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.view-frame.ng-enter {
  -webkit-animation: 0.5s fade-in;
  -moz-animation: 0.5s fade-in;
  -o-animation: 0.5s fade-in;
  animation: 0.5s fade-in;
  z-index: 100;
}

.view-frame.ng-leave {
  -webkit-animation: 0.5s fade-out;
  -moz-animation: 0.5s fade-out;
  -o-animation: 0.5s fade-out;
  animation: 0.5s fade-out;
  z-index:99;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@-moz-keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@-webkit-keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}
@-moz-keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}
@-webkit-keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* don't forget about the vendor-prefixes! */

不是太疯狂!只是一个简单的划入划出效果就出现在页面中。这里的唯一不寻常的是,我们使用绝对定位来前一个页面(其有ng-leave类型设定),使其在下一个页面位置(通过ng-enter确认)的顶部。前一个页面将被移除,所以会淡出,新的页面将淡入并移动到原来的顶部。

在页面渲染时,一旦离开动画完成(动画对应元素在显示中被移除),或者一旦进入动画完成(被赋予了ng-enterng-enter-active CSS 类的元素),都会移除附加的预订CSS类(即动画相关的CSS类,例如ng-enterng-enter-activeng-leve等)。这显得十分自然流畅,页面处理流程没有任何跳来跳去。

这些CSS类利用ng-repeat赋予所有需要的元素(开始和结束类)。每次页面加载ng-view都会创建一个它的副本,下载模板并且添加内容。这将确保所有的内容在一个单页HTML的元素中来允许简单的动画控制。

要了解更多CSS动画,请看WEB平台文档

利用JavaScript实现ngClass动画

我们将在我们的程序中添加其他动画,转换到phone-detail.html页面,我们已经有了一个不错的缩略图切换效果:点击缩略图,将显示对应的手机不同角度图片,但我们能否为它添加动画呢?

让我们先考虑一下这一过程:当你点击缩略图,会改变显示不同角度图像(状态)来反映新点选的缩略图。在HTML中要改变描述状态最好是使用类。所以像前面一样,我们可以通过指定CSS类来描述一个动画,这次每当CSS类本身发生变化时会显示动画。

每当一个新手机缩略图被选中,.activeCSS类就会添加到匹配的图片上,并播放动画。

首先,我们开始调整phone-detail.html页面中的HTML代码:

app/partials/phone-detail.html

<!-- We're only changing the top of the file -->
<div class="phone-images">
  <img ng-src="{{img}}"
       class="phone"
       ng-repeat="img in phone.images"
       ng-class="{active:mainImageUrl==img}">
</div>

<h1>{{phone.name}}</h1>

<p>{{phone.description}}</p>

<ul class="phone-thumbs">
  <li ng-repeat="img in phone.images">
    <img ng-src="{{img}}" ng-mouseenter="setImage(img)">
  </li>
</ul>

就像缩略图,我们使用一个转换器来显示所有的概要文件列表图片,但是我们没有动画,也没有重复关联动画。反而,我们把视线投向了ng-class指令,因为无论何时,只有active类被激活,它就被加入到元素,并作为可视元素渲染显示出来。另外,其他不同角度的图片是隐藏的。在这个例子中,同一时间总有一个元素(图片)是active而被显示在屏幕上的。

active类被加入到元素,在active-addactive-add-active类被添加前AngularJS可以触发一个动画。当元素移除时,active-removeactive-remove-active类被赋予到元素,这也可以触发另一个动画。

为了确保手机图片中页面第一次加载时正确显示,我们也要处理一下页面CSS样式: app/css/app.css:

.phone-images {
  background-color: white;
  width: 450px;
  height: 450px;
  overflow: hidden;
  position: relative;
  float: left;
}

...

img.phone {
  float: left;
  margin-right: 3em;
  margin-bottom: 2em;
  background-color: white;
  padding: 2em;
  height: 400px;
  width: 400px;
  display: none;
}

img.phone:first-child {
  display: block;
  }

你可能想我们是不是要创建另外的CSS动画?事实上,我们不这样做,这里我们将借此机会学习如何利用animation()方法创建基于JavaScript的动画: app/js/animations.js

var phonecatAnimations = angular.module('phonecatAnimations', ['ngAnimate']);

phonecatAnimations.animation('.phone', function() {

  var animateUp = function(element, className, done) {
    if(className != 'active') {
      return;
    }
    element.css({
      position: 'absolute',
      top: 500,
      left: 0,
      display: 'block'
    });

    jQuery(element).animate({
      top: 0
    }, done);

    return function(cancel) {
      if(cancel) {
        element.stop();
      }
    };
  }

  var animateDown = function(element, className, done) {
    if(className != 'active') {
      return;
    }
    element.css({
      position: 'absolute',
      left: 0,
      top: 0
    });

    jQuery(element).animate({
      top: -500
    }, done);

    return function(cancel) {
      if(cancel) {
        element.stop();
      }
    };
  }

  return {
    addClass: animateUp,
    removeClass: animateDown
  };
});

注意,这里我们使用了jQuery来实现动画。在AngularJ中实现动画时jQuery并不是必须的,但是直接用JavaScript实现动画其实已经超出了本教程的范围。为了更多了解jQuery.animat请查看jQuery文档

我们注册了addClassremoveClass回调函数,其会在.phone内部的任何元素添加或是移除类属性时调用。当.active类型被添加到元素(通过ng-class指令),则JavaScript的addClass回调被调用,而且所对应的element作为参数传入。最后一个参数done是一个回调函数。事实上done是为了通知Angular知道JavaScript动画已经完成,从而调用以完成后续工作。

removeClass回调也基于同样的机制工作,不过是插入到一个类从元素中移除时。

通过JavaScript的回调,我们创建了对DOM的手动操作,在代码内,这些就是element.css()element.animate()被执行。回调定位了下一个元素在500像素下,并移动前后两张图片,并动态同步移动以实现动画。这就是一个传送带动画,在animate功能完成后,调用done

注意:addClassremoveClass每次返回一个函数,这是一个可选项,以实现函数调用链(可以让一个动画结束后进入下一个动画——或者功能),一个布尔值被传递,让开发者知道动画是否被调用。这通常被用于在动画结束后执行一些清理工作。

小结

你已经完成了它!我们在一个相对短的时间内就创建了一个web程序。最后(下一页),我们将指出如何进一步学习。