Adds constants and runner for animations

Its not yet wired up to anything, but the basics and plumbing are
there. I'm hoping this is rich enough for animations we want to
support.

BUG=434429
TEST=none
R=erg@chromium.org, ben@chromium.org

Review URL: https://codereview.chromium.org/772893004
diff --git a/services/view_manager/BUILD.gn b/services/view_manager/BUILD.gn
index 6f022a0..847d7ad 100644
--- a/services/view_manager/BUILD.gn
+++ b/services/view_manager/BUILD.gn
@@ -18,6 +18,7 @@
     "//mojo/application",
     "//mojo/common:tracing_impl",
     "//mojo/environment:chromium",
+    "//mojo/converters/geometry",
     "//mojo/public/cpp/bindings:bindings",
     "//mojo/services/public/interfaces/window_manager",
   ]
@@ -27,6 +28,9 @@
   sources = [
     "access_policy.h",
     "access_policy_delegate.h",
+    "animation_runner.cc",
+    "animation_runner.h",
+    "animation_runner_observer.h",
     "client_connection.cc",
     "client_connection.h",
     "connection_manager.cc",
@@ -36,6 +40,8 @@
     "default_access_policy.h",
     "display_manager.cc",
     "display_manager.h",
+    "scheduled_animation_group.cc",
+    "scheduled_animation_group.h",
     "server_view.cc",
     "server_view.h",
     "server_view_delegate.h",
@@ -67,6 +73,7 @@
     "//mojo/services/public/interfaces/view_manager",
     "//mojo/services/public/interfaces/window_manager",
     "//mojo/services/public/cpp/view_manager:common",
+    "//ui/gfx",
     "//ui/gfx/geometry",
   ]
 }
@@ -91,6 +98,10 @@
 
 test("view_manager_service_unittests") {
   sources = [
+    "animation_runner_unittest.cc",
+    "scheduled_animation_group_unittest.cc",
+    "test_server_view_delegate.cc",
+    "test_server_view_delegate.h",
     "view_coordinate_conversions_unittest.cc",
     "view_manager_service_unittest.cc",
   ]
@@ -112,6 +123,7 @@
     "//mojo/services/public/interfaces/view_manager",
     "//mojo/services/public/interfaces/window_manager",
     "//testing/gtest",
+    "//ui/gfx",
     "//ui/gfx:test_support",
     "//ui/gfx/geometry",
   ]
diff --git a/services/view_manager/animation_runner.cc b/services/view_manager/animation_runner.cc
new file mode 100644
index 0000000..df0fefd
--- /dev/null
+++ b/services/view_manager/animation_runner.cc
@@ -0,0 +1,112 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/view_manager/animation_runner.h"
+
+#include <set>
+
+#include "services/view_manager/animation_runner_observer.h"
+#include "services/view_manager/scheduled_animation_group.h"
+#include "services/view_manager/server_view.h"
+
+namespace mojo {
+namespace service {
+namespace {
+
+struct AnimationDoneState {
+  ServerView* view;
+  uint32_t animation_id;
+};
+
+}  // namespace
+
+AnimationRunner::AnimationRunner(base::TimeTicks now)
+    : next_id_(1), last_tick_time_(now) {
+}
+
+AnimationRunner::~AnimationRunner() {
+}
+
+void AnimationRunner::AddObserver(AnimationRunnerObserver* observer) {
+  observers_.AddObserver(observer);
+}
+
+void AnimationRunner::RemoveObserver(AnimationRunnerObserver* observer) {
+  observers_.RemoveObserver(observer);
+}
+
+uint32_t AnimationRunner::Schedule(ServerView* view,
+                                   const AnimationGroup& transport_group) {
+  scoped_ptr<ScheduledAnimationGroup> group(ScheduledAnimationGroup::Create(
+      view, last_tick_time_, next_id_++, transport_group));
+  if (!group.get())
+    return 0;
+
+  if (animation_map_.contains(view)) {
+    animation_map_.get(view)->SetValuesToTargetValuesForPropertiesNotIn(*group);
+    const uint32_t animation_id = animation_map_.get(view)->id();
+    animation_map_.erase(view);
+    FOR_EACH_OBSERVER(AnimationRunnerObserver, observers_,
+                      OnAnimationInterrupted(view, animation_id));
+  }
+
+  group->ObtainStartValues();
+
+  const uint32_t id = group->id();
+  animation_map_.set(view, group.Pass());
+
+  FOR_EACH_OBSERVER(AnimationRunnerObserver, observers_,
+                    OnAnimationScheduled(view, id));
+  return id;
+}
+
+ServerView* AnimationRunner::GetViewForAnimation(uint32_t id) {
+  for (ViewAnimationMap::iterator i = animation_map_.begin();
+       i != animation_map_.end(); ++i) {
+    if (i->second->id() == id)
+      return i->first;
+  }
+  return nullptr;
+}
+
+void AnimationRunner::CancelAnimationForView(ServerView* view) {
+  if (!animation_map_.contains(view))
+    return;
+
+  const uint32_t id = animation_map_.get(view)->id();
+  animation_map_.erase(view);
+  FOR_EACH_OBSERVER(AnimationRunnerObserver, observers_,
+                    OnAnimationCanceled(view, id));
+}
+
+void AnimationRunner::Tick(base::TimeTicks time) {
+  DCHECK(time >= last_tick_time_);
+  last_tick_time_ = time;
+  if (animation_map_.empty())
+    return;
+
+  std::vector<AnimationDoneState> animations_done;
+  for (ViewAnimationMap::iterator i = animation_map_.begin();
+       i != animation_map_.end(); ) {
+    // Any animations that complete are notified at the end of the loop. This
+    // way if the obsevert attempts to schedule another animation or mutate us
+    // in some other way we aren't in a bad state.
+    if (i->second->Tick(time)) {
+      AnimationDoneState done_state;
+      done_state.view = i->first;
+      done_state.animation_id = i->second->id();
+      animations_done.push_back(done_state);
+      animation_map_.erase(i++);
+    } else {
+      ++i;
+    }
+  }
+  for (const AnimationDoneState& done : animations_done) {
+    FOR_EACH_OBSERVER(AnimationRunnerObserver, observers_,
+                      OnAnimationDone(done.view, done.animation_id));
+  }
+}
+
+}  // namespace service
+}  // namespace mojo
diff --git a/services/view_manager/animation_runner.h b/services/view_manager/animation_runner.h
new file mode 100644
index 0000000..24e3bde
--- /dev/null
+++ b/services/view_manager/animation_runner.h
@@ -0,0 +1,73 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_VIEW_MANAGER_ANIMATION_RUNNER_H_
+#define SERVICES_VIEW_MANAGER_ANIMATION_RUNNER_H_
+
+#include "base/containers/scoped_ptr_hash_map.h"
+#include "base/observer_list.h"
+#include "base/time/time.h"
+
+namespace mojo {
+
+class AnimationGroup;
+
+namespace service {
+
+class AnimationRunnerObserver;
+class ScheduledAnimationGroup;
+class ServerView;
+
+// AnimationRunner is responsible for maintaing and running a set of animations.
+// The animations are represented as a set of AnimationGroups. New animations
+// are scheduled by way of Schedule(). A |view| may only have one animation
+// running at a time. Schedule()ing a new animation implicitly cancels the
+// outstanding animation. Animations progress by way of the Tick() function.
+class AnimationRunner {
+ public:
+  explicit AnimationRunner(base::TimeTicks now);
+  ~AnimationRunner();
+
+  void AddObserver(AnimationRunnerObserver* observer);
+  void RemoveObserver(AnimationRunnerObserver* observer);
+
+  // Schedules an animation for |view|. If there is an existing animation in
+  // progress for |view| it is canceled and any properties that were animating
+  // but are no longer animating are set to their target value.
+  // Returns 0 if |transport_group| is not valid.
+  uint32_t Schedule(ServerView* view, const AnimationGroup& transport_group);
+
+  // Returns the view the animation identified by |id| was scheduled for.
+  ServerView* GetViewForAnimation(uint32_t id);
+
+  // Cancels the animation scheduled for |view|. Does nothing if there is no
+  // animation scheduled for |view|. This does not change |view|. That is, any
+  // in progress animations are stopped.
+  void CancelAnimationForView(ServerView* view);
+
+  // Advance the animations updating values appropriately.
+  void Tick(base::TimeTicks time);
+
+  // Returns true if there are animations currently scheduled.
+  bool HasAnimations() const { return !animation_map_.empty(); }
+
+ private:
+  using ViewAnimationMap =
+      base::ScopedPtrHashMap<ServerView*, ScheduledAnimationGroup>;
+
+  uint32_t next_id_;
+
+  base::TimeTicks last_tick_time_;
+
+  ObserverList<AnimationRunnerObserver> observers_;
+
+  ViewAnimationMap animation_map_;
+
+  DISALLOW_COPY_AND_ASSIGN(AnimationRunner);
+};
+
+}  // namespace service
+}  // namespace mojo
+
+#endif  // SERVICES_VIEW_MANAGER_ANIMATION_RUNNER_H_
diff --git a/services/view_manager/animation_runner_observer.h b/services/view_manager/animation_runner_observer.h
new file mode 100644
index 0000000..24959f7
--- /dev/null
+++ b/services/view_manager/animation_runner_observer.h
@@ -0,0 +1,27 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_VIEW_MANAGER_ANIMATION_RUNNER_OBSERVER_H_
+#define SERVICES_VIEW_MANAGER_ANIMATION_RUNNER_OBSERVER_H_
+
+namespace mojo {
+namespace service {
+
+class ServerView;
+
+class AnimationRunnerObserver {
+ public:
+  virtual void OnAnimationScheduled(const ServerView* view, uint32_t id) = 0;
+  virtual void OnAnimationDone(const ServerView* view, uint32_t id) = 0;
+  virtual void OnAnimationInterrupted(const ServerView* view, uint32_t id) = 0;
+  virtual void OnAnimationCanceled(const ServerView* view, uint32_t id) = 0;
+
+ protected:
+  virtual ~AnimationRunnerObserver() {}
+};
+
+}  // namespace service
+}  // namespace mojo
+
+#endif  // SERVICES_VIEW_MANAGER_ANIMATION_RUNNER_OBSERVER_H_
diff --git a/services/view_manager/animation_runner_unittest.cc b/services/view_manager/animation_runner_unittest.cc
new file mode 100644
index 0000000..15d16c4
--- /dev/null
+++ b/services/view_manager/animation_runner_unittest.cc
@@ -0,0 +1,505 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/view_manager/animation_runner.h"
+
+#include "base/strings/stringprintf.h"
+#include "mojo/converters/geometry/geometry_type_converters.h"
+#include "mojo/services/public/interfaces/view_manager/view_manager_constants.mojom.h"
+#include "services/view_manager/animation_runner_observer.h"
+#include "services/view_manager/scheduled_animation_group.h"
+#include "services/view_manager/server_view.h"
+#include "services/view_manager/test_server_view_delegate.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace mojo {
+namespace service {
+namespace {
+
+class TestAnimationRunnerObserver : public AnimationRunnerObserver {
+ public:
+  TestAnimationRunnerObserver() {}
+  ~TestAnimationRunnerObserver() override {}
+
+  std::vector<std::string>* changes() { return &changes_; }
+  std::vector<uint32_t>* change_ids() { return &change_ids_; }
+
+  void clear_changes() {
+    changes_.clear();
+    change_ids_.clear();
+  }
+
+  // AnimationRunnerDelgate:
+  void OnAnimationScheduled(const ServerView* view, uint32_t id) override {
+    change_ids_.push_back(id);
+    changes_.push_back(base::StringPrintf(
+        "scheduled view=%d,%d", view->id().connection_id, view->id().view_id));
+  }
+  void OnAnimationDone(const ServerView* view, uint32_t id) override {
+    change_ids_.push_back(id);
+    changes_.push_back(base::StringPrintf(
+        "done view=%d,%d", view->id().connection_id, view->id().view_id));
+  }
+  void OnAnimationInterrupted(const ServerView* view, uint32_t id) override {
+    change_ids_.push_back(id);
+    changes_.push_back(base::StringPrintf(
+        "interruped view=%d,%d", view->id().connection_id, view->id().view_id));
+  }
+  void OnAnimationCanceled(const ServerView* view, uint32_t id) override {
+    change_ids_.push_back(id);
+    changes_.push_back(base::StringPrintf(
+        "canceled view=%d,%d", view->id().connection_id, view->id().view_id));
+  }
+
+ private:
+  std::vector<uint32_t> change_ids_;
+  std::vector<std::string> changes_;
+
+  DISALLOW_COPY_AND_ASSIGN(TestAnimationRunnerObserver);
+};
+
+}  // namespace
+
+class AnimationRunnerTest : public testing::Test {
+ public:
+  AnimationRunnerTest()
+      : initial_time_(base::TimeTicks::Now()), runner_(initial_time_) {
+    runner_.AddObserver(&runner_observer_);
+  }
+  ~AnimationRunnerTest() override { runner_.RemoveObserver(&runner_observer_); }
+
+ protected:
+  const base::TimeTicks initial_time_;
+  TestAnimationRunnerObserver runner_observer_;
+  AnimationRunner runner_;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(AnimationRunnerTest);
+};
+
+// Opacity from 1 to .5 over 1000.
+TEST_F(AnimationRunnerTest, SingleProperty) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId());
+
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 1000;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = .5;
+
+  const uint32_t animation_id = runner_.Schedule(&view, group);
+
+  ASSERT_EQ(1u, runner_observer_.changes()->size());
+  EXPECT_EQ("scheduled view=0,0", runner_observer_.changes()->at(0));
+  EXPECT_EQ(animation_id, runner_observer_.change_ids()->at(0));
+  runner_observer_.clear_changes();
+
+  EXPECT_TRUE(runner_.HasAnimations());
+
+  // Opacity should still be 1 (the initial value).
+  EXPECT_EQ(1.f, view.opacity());
+
+  // Animate half way.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(500));
+
+  EXPECT_EQ(.75f, view.opacity());
+  EXPECT_TRUE(runner_observer_.changes()->empty());
+
+  // Run well past the end. Value should progress to end and delegate should
+  // be notified.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromSeconds(10));
+  EXPECT_EQ(.5f, view.opacity());
+
+  ASSERT_EQ(1u, runner_observer_.changes()->size());
+  EXPECT_EQ("done view=0,0", runner_observer_.changes()->at(0));
+  EXPECT_EQ(animation_id, runner_observer_.change_ids()->at(0));
+
+  EXPECT_FALSE(runner_.HasAnimations());
+}
+
+// Opacity from 1 to .5, followed by transform from identity to 2x,3x.
+TEST_F(AnimationRunnerTest, TwoPropertiesInSequence) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId());
+
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 1000;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = .5;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element2 = *(sequence.elements[1]);
+  element2.property = ANIMATION_PROPERTY_TRANSFORM;
+  element2.duration = 2000;
+  element2.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element2.target_value = AnimationValue::New();
+  gfx::Transform done_transform;
+  done_transform.Scale(2, 4);
+  element2.target_value->transform = Transform::From(done_transform);
+
+  const uint32_t animation_id = runner_.Schedule(&view, group);
+  runner_observer_.clear_changes();
+
+  // Nothing in the view should have changed yet.
+  EXPECT_EQ(1.f, view.opacity());
+  EXPECT_TRUE(view.transform().IsIdentity());
+
+  // Animate half way from through opacity animation.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(500));
+
+  EXPECT_EQ(.75f, view.opacity());
+  EXPECT_TRUE(view.transform().IsIdentity());
+
+  // Finish first element (opacity).
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(1000));
+  EXPECT_EQ(.5f, view.opacity());
+  EXPECT_TRUE(view.transform().IsIdentity());
+
+  // Half way through second (transform).
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(2000));
+  EXPECT_EQ(.5f, view.opacity());
+  gfx::Transform half_way_transform;
+  half_way_transform.Scale(1.5, 2.5);
+  EXPECT_EQ(half_way_transform, view.transform());
+
+  EXPECT_TRUE(runner_observer_.changes()->empty());
+
+  // To end.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(3500));
+  EXPECT_EQ(.5f, view.opacity());
+  EXPECT_EQ(done_transform, view.transform());
+
+  ASSERT_EQ(1u, runner_observer_.changes()->size());
+  EXPECT_EQ("done view=0,0", runner_observer_.changes()->at(0));
+  EXPECT_EQ(animation_id, runner_observer_.change_ids()->at(0));
+}
+
+// Opacity from .5 to 1 over 1000, transform to 2x,3x over 500.
+TEST_F(AnimationRunnerTest, TwoPropertiesInParallel) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId(1, 1));
+
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 1000;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.start_value = AnimationValue::New();
+  element.start_value->float_value = .5;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = 1;
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence2 = *(group.sequences[1]);
+  sequence2.cycle_count = 1;
+  sequence2.elements.push_back(AnimationElement::New());
+  AnimationElement& element2 = *(sequence2.elements[0]);
+  element2.property = ANIMATION_PROPERTY_TRANSFORM;
+  element2.duration = 500;
+  element2.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element2.target_value = AnimationValue::New();
+  gfx::Transform done_transform;
+  done_transform.Scale(2, 4);
+  element2.target_value->transform = Transform::From(done_transform);
+
+  const uint32_t animation_id = runner_.Schedule(&view, group);
+
+  runner_observer_.clear_changes();
+
+  // Nothing in the view should have changed yet.
+  EXPECT_EQ(1.f, view.opacity());
+  EXPECT_TRUE(view.transform().IsIdentity());
+
+  // Animate to 250, which is 1/4 way through opacity and half way through
+  // transform.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(250));
+
+  EXPECT_EQ(.625f, view.opacity());
+  gfx::Transform half_way_transform;
+  half_way_transform.Scale(1.5, 2.5);
+  EXPECT_EQ(half_way_transform, view.transform());
+
+  // Animate to 500, which is 1/2 way through opacity and transform done.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(500));
+  EXPECT_EQ(.75f, view.opacity());
+  EXPECT_EQ(done_transform, view.transform());
+
+  // Animate to 750, which is 3/4 way through opacity and transform done.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(750));
+  EXPECT_EQ(.875f, view.opacity());
+  EXPECT_EQ(done_transform, view.transform());
+
+  EXPECT_TRUE(runner_observer_.changes()->empty());
+
+  // To end.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(3500));
+  EXPECT_EQ(1.f, view.opacity());
+  EXPECT_EQ(done_transform, view.transform());
+
+  ASSERT_EQ(1u, runner_observer_.changes()->size());
+  EXPECT_EQ("done view=1,1", runner_observer_.changes()->at(0));
+  EXPECT_EQ(animation_id, runner_observer_.change_ids()->at(0));
+}
+
+// Opacity from .5 to 1 over 1000, pause for 500, 1 to .5 over 500, with a cycle
+// count of 3.
+TEST_F(AnimationRunnerTest, Cycles) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId(1, 2));
+
+  view.SetOpacity(.5f);
+
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 3;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 1000;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element2 = *(sequence.elements[1]);
+  element2.property = ANIMATION_PROPERTY_NONE;
+  element2.duration = 500;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element3 = *(sequence.elements[2]);
+  element3.property = ANIMATION_PROPERTY_OPACITY;
+  element3.duration = 500;
+  element3.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element3.target_value = AnimationValue::New();
+  element3.target_value->float_value = .5f;
+
+  runner_.Schedule(&view, group);
+  runner_observer_.clear_changes();
+
+  // Nothing in the view should have changed yet.
+  EXPECT_EQ(.5f, view.opacity());
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(500));
+  EXPECT_EQ(.75f, view.opacity());
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(1250));
+  EXPECT_EQ(1.f, view.opacity());
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(1750));
+  EXPECT_EQ(.75f, view.opacity());
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(2500));
+  EXPECT_EQ(.75f, view.opacity());
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(3250));
+  EXPECT_EQ(1.f, view.opacity());
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(3750));
+  EXPECT_EQ(.75f, view.opacity());
+
+  // Animate to the end.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(6500));
+  EXPECT_EQ(.5f, view.opacity());
+
+  ASSERT_EQ(1u, runner_observer_.changes()->size());
+  EXPECT_EQ("done view=1,2", runner_observer_.changes()->at(0));
+}
+
+// Verifies scheduling the same view twice sends an interrupt.
+TEST_F(AnimationRunnerTest, ScheduleTwice) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId(1, 2));
+
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 1000;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = .5f;
+
+  const uint32_t animation_id = runner_.Schedule(&view, group);
+  runner_observer_.clear_changes();
+
+  // Animate half way.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(500));
+
+  EXPECT_EQ(.75f, view.opacity());
+  EXPECT_TRUE(runner_observer_.changes()->empty());
+
+  // Schedule again. We should get an interrupt, but opacity shouldn't change.
+  const uint32_t animation2_id = runner_.Schedule(&view, group);
+
+  // Id should have changed.
+  EXPECT_NE(animation_id, animation2_id);
+
+  EXPECT_EQ(nullptr, runner_.GetViewForAnimation(animation_id));
+  EXPECT_EQ(&view, runner_.GetViewForAnimation(animation2_id));
+
+  EXPECT_EQ(.75f, view.opacity());
+  EXPECT_EQ(2u, runner_observer_.changes()->size());
+  EXPECT_EQ("interruped view=1,2", runner_observer_.changes()->at(0));
+  EXPECT_EQ(animation_id, runner_observer_.change_ids()->at(0));
+  EXPECT_EQ("scheduled view=1,2", runner_observer_.changes()->at(1));
+  EXPECT_EQ(animation2_id, runner_observer_.change_ids()->at(1));
+  runner_observer_.clear_changes();
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(1000));
+  EXPECT_EQ(.625f, view.opacity());
+  EXPECT_TRUE(runner_observer_.changes()->empty());
+
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(2000));
+  EXPECT_EQ(.5f, view.opacity());
+  EXPECT_EQ(1u, runner_observer_.changes()->size());
+  EXPECT_EQ("done view=1,2", runner_observer_.changes()->at(0));
+  EXPECT_EQ(animation2_id, runner_observer_.change_ids()->at(0));
+}
+
+// Verifies Remove() works.
+TEST_F(AnimationRunnerTest, CancelAnimationForView) {
+  // Create an animation and advance it part way.
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId());
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 1000;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = .5;
+
+  const uint32_t animation_id = runner_.Schedule(&view, group);
+  runner_observer_.clear_changes();
+  EXPECT_EQ(&view, runner_.GetViewForAnimation(animation_id));
+
+  EXPECT_TRUE(runner_.HasAnimations());
+
+  // Animate half way.
+  runner_.Tick(initial_time_ + base::TimeDelta::FromMicroseconds(500));
+  EXPECT_EQ(.75f, view.opacity());
+  EXPECT_TRUE(runner_observer_.changes()->empty());
+
+  // Cancel the animation.
+  runner_.CancelAnimationForView(&view);
+
+  EXPECT_FALSE(runner_.HasAnimations());
+  EXPECT_EQ(nullptr, runner_.GetViewForAnimation(animation_id));
+
+  EXPECT_EQ(.75f, view.opacity());
+
+  EXPECT_EQ(1u, runner_observer_.changes()->size());
+  EXPECT_EQ("canceled view=0,0", runner_observer_.changes()->at(0));
+  EXPECT_EQ(animation_id, runner_observer_.change_ids()->at(0));
+}
+
+// Verifies a tick with a very large delta and a sequence that repeats forever
+// doesn't take a long time.
+TEST_F(AnimationRunnerTest, InfiniteRepeatWithHugeGap) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId(1, 2));
+
+  view.SetOpacity(.5f);
+
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 0;  // Repeats forever.
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 500;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element2 = *(sequence.elements[1]);
+  element2.property = ANIMATION_PROPERTY_OPACITY;
+  element2.duration = 500;
+  element2.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element2.target_value = AnimationValue::New();
+  element2.target_value->float_value = .5f;
+
+  runner_.Schedule(&view, group);
+  runner_observer_.clear_changes();
+
+  runner_.Tick(initial_time_ +
+               base::TimeDelta::FromMicroseconds(1000000000750));
+
+  EXPECT_EQ(.75f, view.opacity());
+
+  ASSERT_EQ(0u, runner_observer_.changes()->size());
+}
+
+// Verifies a second schedule sets any properties that are no longer animating
+// to their final value.
+TEST_F(AnimationRunnerTest, RescheduleSetsPropertiesToFinalValue) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId());
+
+  AnimationGroup group;
+  group.view_id = ViewIdToTransportId(view.id());
+  group.sequences.push_back(AnimationSequence::New());
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.cycle_count = 1;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.duration = 1000;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = .5;
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element2 = *(sequence.elements[1]);
+  element2.property = ANIMATION_PROPERTY_TRANSFORM;
+  element2.duration = 500;
+  element2.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+  element2.target_value = AnimationValue::New();
+  gfx::Transform done_transform;
+  done_transform.Scale(2, 4);
+  element2.target_value->transform = Transform::From(done_transform);
+  runner_.Schedule(&view, group);
+
+  // Schedule() again, this time without animating opacity.
+  element.property = ANIMATION_PROPERTY_NONE;
+  runner_.Schedule(&view, group);
+
+  // Opacity should go to final value.
+  EXPECT_EQ(.5f, view.opacity());
+  // Transform shouldn't have changed since newly scheduled animation also has
+  // transform in it.
+  EXPECT_TRUE(view.transform().IsIdentity());
+}
+
+}  // namespace service
+}  // namespace mojo
diff --git a/services/view_manager/scheduled_animation_group.cc b/services/view_manager/scheduled_animation_group.cc
new file mode 100644
index 0000000..d453e13
--- /dev/null
+++ b/services/view_manager/scheduled_animation_group.cc
@@ -0,0 +1,345 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/view_manager/scheduled_animation_group.h"
+
+#include <set>
+
+#include "mojo/converters/geometry/geometry_type_converters.h"
+#include "services/view_manager/server_view.h"
+
+namespace mojo {
+namespace service {
+namespace {
+
+using Sequences = std::vector<ScheduledAnimationSequence>;
+
+// Gets the value of |property| from |view| into |value|.
+void GetValueFromView(const ServerView* view,
+                      AnimationProperty property,
+                      ScheduledAnimationValue* value) {
+  switch (property) {
+    case ANIMATION_PROPERTY_NONE:
+      NOTREACHED();
+      break;
+    case ANIMATION_PROPERTY_OPACITY:
+      value->float_value = view->opacity();
+      break;
+    case ANIMATION_PROPERTY_TRANSFORM:
+      value->transform = view->transform();
+      break;
+  }
+}
+
+// Sets the value of |property| from |value| into |view|.
+void SetViewPropertyFromValue(ServerView* view,
+                              AnimationProperty property,
+                              const ScheduledAnimationValue& value) {
+  switch (property) {
+    case ANIMATION_PROPERTY_NONE:
+      break;
+    case ANIMATION_PROPERTY_OPACITY:
+      view->SetOpacity(value.float_value);
+      break;
+    case ANIMATION_PROPERTY_TRANSFORM:
+      view->SetTransform(value.transform);
+      break;
+  }
+}
+
+// Sets the value of |property| into |view| between two points.
+void SetViewPropertyFromValueBetween(ServerView* view,
+                                     AnimationProperty property,
+                                     double value,
+                                     gfx::Tween::Type tween_type,
+                                     const ScheduledAnimationValue& start,
+                                     const ScheduledAnimationValue& target) {
+  const double tween_value = gfx::Tween::CalculateValue(tween_type, value);
+  switch (property) {
+    case ANIMATION_PROPERTY_NONE:
+      break;
+    case ANIMATION_PROPERTY_OPACITY:
+      view->SetOpacity(gfx::Tween::FloatValueBetween(
+          tween_value, start.float_value, target.float_value));
+      break;
+    case ANIMATION_PROPERTY_TRANSFORM:
+      view->SetTransform(gfx::Tween::TransformValueBetween(
+          tween_value, start.transform, target.transform));
+      break;
+  }
+}
+
+gfx::Tween::Type AnimationTypeToTweenType(AnimationTweenType type) {
+  switch (type) {
+    case ANIMATION_TWEEN_TYPE_LINEAR:
+      return gfx::Tween::LINEAR;
+    case ANIMATION_TWEEN_TYPE_EASE_IN:
+      return gfx::Tween::EASE_IN;
+    case ANIMATION_TWEEN_TYPE_EASE_OUT:
+      return gfx::Tween::EASE_OUT;
+    case ANIMATION_TWEEN_TYPE_EASE_IN_OUT:
+      return gfx::Tween::EASE_IN_OUT;
+  }
+}
+
+void ConvertToScheduledValue(const AnimationValue& transport_value,
+                             ScheduledAnimationValue* value) {
+  value->float_value = transport_value.float_value;
+  value->transform = transport_value.transform.To<gfx::Transform>();
+}
+
+void ConvertToScheduledElement(const AnimationElement& transport_element,
+                               ScheduledAnimationElement* element) {
+  element->property = transport_element.property;
+  element->duration =
+      base::TimeDelta::FromMicroseconds(transport_element.duration);
+  element->tween_type = AnimationTypeToTweenType(transport_element.tween_type);
+  if (transport_element.property != ANIMATION_PROPERTY_NONE) {
+    if (transport_element.start_value.get()) {
+      element->is_start_valid = true;
+      ConvertToScheduledValue(*transport_element.start_value,
+                              &(element->start_value));
+    } else {
+      element->is_start_valid = false;
+    }
+    ConvertToScheduledValue(*transport_element.target_value,
+                            &(element->target_value));
+  }
+}
+
+bool IsAnimationValueValid(AnimationProperty property,
+                           const AnimationValue& value) {
+  switch (property) {
+    case ANIMATION_PROPERTY_NONE:
+      NOTREACHED();
+      return false;
+    case ANIMATION_PROPERTY_OPACITY:
+      return value.float_value >= 0.f && value.float_value <= 1.f;
+    case ANIMATION_PROPERTY_TRANSFORM:
+      return value.transform.get() && value.transform->matrix.size() == 16u;
+  }
+}
+
+bool IsAnimationElementValid(const AnimationElement& element) {
+  if (element.property == ANIMATION_PROPERTY_NONE)
+    return true;  // None is a pause and doesn't need any values.
+  if (element.start_value.get() &&
+      !IsAnimationValueValid(element.property, *element.start_value))
+    return false;
+  // For all other properties we require a target.
+  return element.target_value.get() &&
+         IsAnimationValueValid(element.property, *element.target_value);
+}
+
+bool IsAnimationSequenceValid(const AnimationSequence& sequence) {
+  if (sequence.elements.size() == 0u)
+    return false;
+
+  for (size_t i = 0; i < sequence.elements.size(); ++i) {
+    if (!IsAnimationElementValid(*sequence.elements[i]))
+      return false;
+  }
+  return true;
+}
+
+bool IsAnimationGroupValid(const AnimationGroup& transport_group) {
+  if (transport_group.sequences.size() == 0u)
+    return false;
+  for (size_t i = 0; i < transport_group.sequences.size(); ++i) {
+    if (!IsAnimationSequenceValid(*transport_group.sequences[i]))
+      return false;
+  }
+  return true;
+}
+
+// If the start value for |element| isn't valid, the value for the property
+// is obtained from |view| and placed into |element|.
+void GetStartValueFromViewIfNecessary(const ServerView* view,
+                                      ScheduledAnimationElement* element) {
+  if (element->property != ANIMATION_PROPERTY_NONE &&
+      !element->is_start_valid) {
+    GetValueFromView(view, element->property, &(element->start_value));
+  }
+}
+
+void GetScheduledAnimationProperties(const Sequences& sequences,
+                                     std::set<AnimationProperty>* properties) {
+  for (const ScheduledAnimationSequence& sequence : sequences) {
+    for (const ScheduledAnimationElement& element : sequence.elements)
+      properties->insert(element.property);
+  }
+}
+
+void SetPropertyToTargetProperty(ServerView* view,
+                                 AnimationProperty property,
+                                 const Sequences& sequences) {
+  // NOTE: this doesn't deal with |cycle_count| quite right, but I'm honestly
+  // not sure we really want to support the same property in multiple sequences
+  // animating at once so I'm not dealing.
+  base::TimeDelta max_end_duration;
+  scoped_ptr<ScheduledAnimationValue> value;
+  for (const ScheduledAnimationSequence& sequence : sequences) {
+    base::TimeDelta duration;
+    for (const ScheduledAnimationElement& element : sequence.elements) {
+      if (element.property != property)
+        continue;
+
+      duration += element.duration;
+      if (duration > max_end_duration) {
+        max_end_duration = duration;
+        value.reset(new ScheduledAnimationValue(element.target_value));
+      }
+    }
+  }
+  if (value.get())
+    SetViewPropertyFromValue(view, property, *value);
+}
+
+void ConvertSequenceToScheduled(const AnimationSequence& transport_sequence,
+                                base::TimeTicks now,
+                                ScheduledAnimationSequence* sequence) {
+  sequence->run_until_stopped = transport_sequence.cycle_count == 0u;
+  sequence->cycle_count = transport_sequence.cycle_count;
+  DCHECK_NE(0u, transport_sequence.elements.size());
+  sequence->elements.resize(transport_sequence.elements.size());
+
+  base::TimeTicks element_start_time = now;
+  for (size_t i = 0; i < transport_sequence.elements.size(); ++i) {
+    ConvertToScheduledElement(*(transport_sequence.elements[i].get()),
+                              &(sequence->elements[i]));
+    sequence->elements[i].start_time = element_start_time;
+    sequence->duration += sequence->elements[i].duration;
+    element_start_time += sequence->elements[i].duration;
+  }
+}
+
+bool AdvanceSequence(ServerView* view,
+                     ScheduledAnimationSequence* sequence,
+                     base::TimeTicks now) {
+  ScheduledAnimationElement* element =
+      &(sequence->elements[sequence->current_index]);
+  while (element->start_time + element->duration < now) {
+    SetViewPropertyFromValue(view, element->property, element->target_value);
+    if (++sequence->current_index == sequence->elements.size()) {
+      if (!sequence->run_until_stopped && --sequence->cycle_count == 0) {
+        SetViewPropertyFromValue(view, element->property,
+                                 element->target_value);
+        return false;
+      }
+
+      sequence->current_index = 0;
+    }
+    sequence->elements[sequence->current_index].start_time =
+        element->start_time + element->duration;
+    element = &(sequence->elements[sequence->current_index]);
+    GetStartValueFromViewIfNecessary(view, element);
+
+    // It's possible for the delta between now and |last_tick_time_| to be very
+    // big (could happen if machine sleeps and is woken up much later). Normally
+    // the repeat count is smallish, so we don't bother optimizing it. OTOH if
+    // a sequence repeats forever we optimize it lest we get stuck in this loop
+    // for a very long time.
+    if (sequence->run_until_stopped && sequence->current_index == 0) {
+      element->start_time =
+          now - base::TimeDelta::FromMicroseconds(
+                    (now - element->start_time).InMicroseconds() %
+                    sequence->duration.InMicroseconds());
+    }
+  }
+  return true;
+}
+
+}  // namespace
+
+ScheduledAnimationValue::ScheduledAnimationValue() {
+}
+ScheduledAnimationValue::~ScheduledAnimationValue() {
+}
+
+ScheduledAnimationElement::ScheduledAnimationElement()
+    : property(ANIMATION_PROPERTY_OPACITY),
+      tween_type(gfx::Tween::EASE_IN),
+      is_start_valid(false) {
+}
+ScheduledAnimationElement::~ScheduledAnimationElement() {
+}
+
+ScheduledAnimationSequence::ScheduledAnimationSequence()
+    : run_until_stopped(false), cycle_count(0), current_index(0u) {
+}
+ScheduledAnimationSequence::~ScheduledAnimationSequence() {
+}
+
+ScheduledAnimationGroup::~ScheduledAnimationGroup() {
+}
+
+// static
+scoped_ptr<ScheduledAnimationGroup> ScheduledAnimationGroup::Create(
+    ServerView* view,
+    base::TimeTicks now,
+    uint32_t id,
+    const AnimationGroup& transport_group) {
+  if (!IsAnimationGroupValid(transport_group))
+    return nullptr;
+
+  scoped_ptr<ScheduledAnimationGroup> group(
+      new ScheduledAnimationGroup(view, id, now));
+  group->sequences_.resize(transport_group.sequences.size());
+  for (size_t i = 0; i < transport_group.sequences.size(); ++i) {
+    const AnimationSequence& transport_sequence(
+        *(transport_group.sequences[i]));
+    DCHECK_NE(0u, transport_sequence.elements.size());
+    ConvertSequenceToScheduled(transport_sequence, now, &group->sequences_[i]);
+  }
+  return group.Pass();
+}
+
+void ScheduledAnimationGroup::ObtainStartValues() {
+  for (ScheduledAnimationSequence& sequence : sequences_)
+    GetStartValueFromViewIfNecessary(view_, &(sequence.elements[0]));
+}
+
+void ScheduledAnimationGroup::SetValuesToTargetValuesForPropertiesNotIn(
+    const ScheduledAnimationGroup& other) {
+  std::set<AnimationProperty> our_properties;
+  GetScheduledAnimationProperties(sequences_, &our_properties);
+
+  std::set<AnimationProperty> other_properties;
+  GetScheduledAnimationProperties(other.sequences_, &other_properties);
+
+  for (AnimationProperty property : our_properties) {
+    if (other_properties.count(property) == 0 &&
+        property != ANIMATION_PROPERTY_NONE) {
+      SetPropertyToTargetProperty(view_, property, sequences_);
+    }
+  }
+}
+
+bool ScheduledAnimationGroup::Tick(base::TimeTicks time) {
+  for (Sequences::iterator i = sequences_.begin(); i != sequences_.end();) {
+    if (!AdvanceSequence(view_, &(*i), time)) {
+      i = sequences_.erase(i);
+      continue;
+    }
+    const ScheduledAnimationElement& active_element(
+        i->elements[i->current_index]);
+    const double percent =
+        (time - active_element.start_time).InMillisecondsF() /
+        active_element.duration.InMillisecondsF();
+    SetViewPropertyFromValueBetween(
+        view_, active_element.property, percent, active_element.tween_type,
+        active_element.start_value, active_element.target_value);
+    ++i;
+  }
+  return sequences_.empty();
+}
+
+ScheduledAnimationGroup::ScheduledAnimationGroup(ServerView* view,
+                                                 uint32_t id,
+                                                 base::TimeTicks time_scheduled)
+    : view_(view), id_(id), time_scheduled_(time_scheduled) {
+}
+
+}  // namespace service
+}  // namespace mojo
diff --git a/services/view_manager/scheduled_animation_group.h b/services/view_manager/scheduled_animation_group.h
new file mode 100644
index 0000000..990808b
--- /dev/null
+++ b/services/view_manager/scheduled_animation_group.h
@@ -0,0 +1,110 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_VIEW_MANAGER_SCHEDULED_ANIMATION_GROUP_H_
+#define SERVICES_VIEW_MANAGER_SCHEDULED_ANIMATION_GROUP_H_
+
+#include <vector>
+
+#include "base/memory/scoped_ptr.h"
+#include "base/time/time.h"
+#include "mojo/services/public/interfaces/view_manager/animations.mojom.h"
+#include "ui/gfx/animation/tween.h"
+#include "ui/gfx/transform.h"
+
+namespace mojo {
+namespace service {
+
+class ServerView;
+
+struct ScheduledAnimationValue {
+  ScheduledAnimationValue();
+  ~ScheduledAnimationValue();
+
+  float float_value;
+  gfx::Transform transform;
+};
+
+struct ScheduledAnimationElement {
+  ScheduledAnimationElement();
+  ~ScheduledAnimationElement();
+
+  AnimationProperty property;
+  base::TimeDelta duration;
+  gfx::Tween::Type tween_type;
+  bool is_start_valid;
+  ScheduledAnimationValue start_value;
+  ScheduledAnimationValue target_value;
+  // Start time is based on scheduled time and relative to any other elements
+  // in the sequence.
+  base::TimeTicks start_time;
+};
+
+struct ScheduledAnimationSequence {
+  ScheduledAnimationSequence();
+  ~ScheduledAnimationSequence();
+
+  bool run_until_stopped;
+  std::vector<ScheduledAnimationElement> elements;
+
+  // Sum of the duration of all elements. This does not take into account
+  // |cycle_count|.
+  base::TimeDelta duration;
+
+  // The following values are updated as the animation progresses.
+
+  // Number of cycles remaining. This is only used if |run_until_stopped| is
+  // false.
+  uint32_t cycle_count;
+
+  // Index into |elements| of the element currently animating.
+  size_t current_index;
+};
+
+// Corresponds to a mojo::AnimationGroup and is responsible for running the
+// actual animation.
+class ScheduledAnimationGroup {
+ public:
+  ~ScheduledAnimationGroup();
+
+  // Returns a new ScheduledAnimationGroup from the supplied parameters, or
+  // null if |transport_group| isn't valid.
+  static scoped_ptr<ScheduledAnimationGroup> Create(
+      ServerView* view,
+      base::TimeTicks now,
+      uint32_t id,
+      const AnimationGroup& transport_group);
+
+  uint32_t id() const { return id_; }
+
+  // Gets the start value for any elements that don't have an explicit start.
+  // value.
+  void ObtainStartValues();
+
+  // Sets the values of any properties that are not in |other| to their final
+  // value.
+  void SetValuesToTargetValuesForPropertiesNotIn(
+      const ScheduledAnimationGroup& other);
+
+  // Advances the group. |time| is the current time. Returns true if the group
+  // is done (nothing left to animate).
+  bool Tick(base::TimeTicks time);
+
+ private:
+  ScheduledAnimationGroup(ServerView* view,
+                          uint32_t id,
+                          base::TimeTicks time_scheduled);
+
+  ServerView* view_;
+  const uint32_t id_;
+  base::TimeTicks time_scheduled_;
+  std::vector<ScheduledAnimationSequence> sequences_;
+
+  DISALLOW_COPY_AND_ASSIGN(ScheduledAnimationGroup);
+};
+
+}  // namespace service
+}  // namespace mojo
+
+#endif  // SERVICES_VIEW_MANAGER_SCHEDULED_ANIMATION_GROUP_H_
diff --git a/services/view_manager/scheduled_animation_group_unittest.cc b/services/view_manager/scheduled_animation_group_unittest.cc
new file mode 100644
index 0000000..06a2c6e
--- /dev/null
+++ b/services/view_manager/scheduled_animation_group_unittest.cc
@@ -0,0 +1,89 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/view_manager/scheduled_animation_group.h"
+
+#include "mojo/converters/geometry/geometry_type_converters.h"
+#include "mojo/services/public/interfaces/view_manager/animations.mojom.h"
+#include "services/view_manager/server_view.h"
+#include "services/view_manager/test_server_view_delegate.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace mojo {
+namespace service {
+namespace {
+
+bool IsAnimationGroupValid(const AnimationGroup& transport_group) {
+  TestServerViewDelegate view_delegate;
+  ServerView view(&view_delegate, ViewId());
+  scoped_ptr<ScheduledAnimationGroup> group(ScheduledAnimationGroup::Create(
+      &view, base::TimeTicks::Now(), 1, transport_group));
+  return group.get() != nullptr;
+}
+
+}  // namespace
+
+TEST(ScheduledAnimationGroupTest, IsAnimationGroupValid) {
+  AnimationGroup group;
+
+  // AnimationGroup with no sequences is not valid.
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+
+  group.sequences.push_back(AnimationSequence::New());
+
+  // Sequence with no elements is not valid.
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+
+  AnimationSequence& sequence = *(group.sequences[0]);
+  sequence.elements.push_back(AnimationElement::New());
+  AnimationElement& element = *(sequence.elements[0]);
+  element.property = ANIMATION_PROPERTY_OPACITY;
+  element.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+
+  // Element with no target_value is not valid.
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+
+  // Opacity must be between 0 and 1.
+  element.target_value = AnimationValue::New();
+  element.target_value->float_value = 2.5f;
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+
+  element.target_value->float_value = .5f;
+  EXPECT_TRUE(IsAnimationGroupValid(group));
+
+  // Bogus start value.
+  element.start_value = AnimationValue::New();
+  element.start_value->float_value = 2.5f;
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+
+  element.start_value->float_value = .5f;
+  EXPECT_TRUE(IsAnimationGroupValid(group));
+
+  // Bogus transform.
+  element.property = ANIMATION_PROPERTY_TRANSFORM;
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+  element.start_value->transform = Transform::From(gfx::Transform());
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+  element.target_value->transform = Transform::From(gfx::Transform());
+  EXPECT_TRUE(IsAnimationGroupValid(group));
+
+  // Add another empty sequence, should be invalid again.
+  group.sequences.push_back(AnimationSequence::New());
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+
+  AnimationSequence& sequence2 = *(group.sequences[1]);
+  sequence2.elements.push_back(AnimationElement::New());
+  AnimationElement& element2 = *(sequence2.elements[0]);
+  element2.property = ANIMATION_PROPERTY_OPACITY;
+  element2.tween_type = ANIMATION_TWEEN_TYPE_LINEAR;
+
+  // Element with no target_value is not valid.
+  EXPECT_FALSE(IsAnimationGroupValid(group));
+
+  element2.property = ANIMATION_PROPERTY_NONE;
+  EXPECT_TRUE(IsAnimationGroupValid(group));
+}
+
+}  // namespace service
+}  // namespace mojo
diff --git a/services/view_manager/server_view.cc b/services/view_manager/server_view.cc
index c9ab1af..b37897d 100644
--- a/services/view_manager/server_view.cc
+++ b/services/view_manager/server_view.cc
@@ -135,6 +135,14 @@
   delegate_->OnScheduleViewPaint(this);
 }
 
+void ServerView::SetTransform(const gfx::Transform& transform) {
+  if (transform_ == transform)
+    return;
+
+  transform_ = transform;
+  delegate_->OnScheduleViewPaint(this);
+}
+
 void ServerView::SetProperty(const std::string& name,
                              const std::vector<uint8_t>* value) {
   auto it = properties_.find(name);
diff --git a/services/view_manager/server_view.h b/services/view_manager/server_view.h
index afce3d3..24a6496 100644
--- a/services/view_manager/server_view.h
+++ b/services/view_manager/server_view.h
@@ -12,6 +12,7 @@
 #include "mojo/services/public/interfaces/view_manager/view_manager.mojom.h"
 #include "services/view_manager/ids.h"
 #include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/transform.h"
 
 namespace mojo {
 namespace service {
@@ -63,6 +64,9 @@
   float opacity() const { return opacity_; }
   void SetOpacity(float value);
 
+  const gfx::Transform& transform() const { return transform_; }
+  void SetTransform(const gfx::Transform& transform);
+
   const std::map<std::string, std::vector<uint8_t>>& properties() const {
     return properties_;
   }
@@ -73,7 +77,7 @@
   bool IsDrawn(const ServerView* root) const;
 
   void SetSurfaceId(cc::SurfaceId surface_id);
-  const cc::SurfaceId surface_id() const { return surface_id_; }
+  const cc::SurfaceId& surface_id() const { return surface_id_; }
 
 #if !defined(NDEBUG)
   std::string GetDebugWindowHierarchy() const;
@@ -94,6 +98,7 @@
   gfx::Rect bounds_;
   cc::SurfaceId surface_id_;
   float opacity_;
+  gfx::Transform transform_;
 
   std::map<std::string, std::vector<uint8_t>> properties_;
 
diff --git a/services/view_manager/server_view_delegate.h b/services/view_manager/server_view_delegate.h
index 66be4a9..cf19051 100644
--- a/services/view_manager/server_view_delegate.h
+++ b/services/view_manager/server_view_delegate.h
@@ -5,6 +5,8 @@
 #ifndef SERVICES_VIEW_MANAGER_SERVER_VIEW_DELEGATE_H_
 #define SERVICES_VIEW_MANAGER_SERVER_VIEW_DELEGATE_H_
 
+#include "mojo/services/public/interfaces/view_manager/view_manager_constants.mojom.h"
+
 namespace gfx {
 class Rect;
 }
diff --git a/services/view_manager/test_server_view_delegate.cc b/services/view_manager/test_server_view_delegate.cc
new file mode 100644
index 0000000..6177bf8
--- /dev/null
+++ b/services/view_manager/test_server_view_delegate.cc
@@ -0,0 +1,59 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/view_manager/test_server_view_delegate.h"
+
+namespace mojo {
+namespace service {
+
+TestServerViewDelegate::TestServerViewDelegate() {
+}
+
+TestServerViewDelegate::~TestServerViewDelegate() {
+}
+
+void TestServerViewDelegate::OnWillDestroyView(ServerView* view) {
+}
+
+void TestServerViewDelegate::OnViewDestroyed(const ServerView* view) {
+}
+
+void TestServerViewDelegate::OnWillChangeViewHierarchy(ServerView* view,
+                                                       ServerView* new_parent,
+                                                       ServerView* old_parent) {
+}
+
+void TestServerViewDelegate::OnViewHierarchyChanged(
+    const ServerView* view,
+    const ServerView* new_parent,
+    const ServerView* old_parent) {
+}
+
+void TestServerViewDelegate::OnViewBoundsChanged(const ServerView* view,
+                                                 const gfx::Rect& old_bounds,
+                                                 const gfx::Rect& new_bounds) {
+}
+
+void TestServerViewDelegate::OnViewSurfaceIdChanged(const ServerView* view) {
+}
+
+void TestServerViewDelegate::OnViewReordered(const ServerView* view,
+                                             const ServerView* relative,
+                                             OrderDirection direction) {
+}
+
+void TestServerViewDelegate::OnWillChangeViewVisibility(ServerView* view) {
+}
+
+void TestServerViewDelegate::OnViewSharedPropertyChanged(
+    const ServerView* view,
+    const std::string& name,
+    const std::vector<uint8_t>* new_data) {
+}
+
+void TestServerViewDelegate::OnScheduleViewPaint(const ServerView* view) {
+}
+
+}  // namespace service
+}  // namespace mojo
diff --git a/services/view_manager/test_server_view_delegate.h b/services/view_manager/test_server_view_delegate.h
new file mode 100644
index 0000000..a1ef839
--- /dev/null
+++ b/services/view_manager/test_server_view_delegate.h
@@ -0,0 +1,49 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_VIEW_MANAGER_TEST_SERVER_VIEW_DELEGATE_H_
+#define SERVICES_VIEW_MANAGER_TEST_SERVER_VIEW_DELEGATE_H_
+
+#include "base/basictypes.h"
+#include "services/view_manager/server_view_delegate.h"
+
+namespace mojo {
+namespace service {
+
+class TestServerViewDelegate : public ServerViewDelegate {
+ public:
+  TestServerViewDelegate();
+  ~TestServerViewDelegate() override;
+
+ private:
+  // ServerViewDelegate:
+  void OnWillDestroyView(ServerView* view) override;
+  void OnViewDestroyed(const ServerView* view) override;
+  void OnWillChangeViewHierarchy(ServerView* view,
+                                 ServerView* new_parent,
+                                 ServerView* old_parent) override;
+  void OnViewHierarchyChanged(const ServerView* view,
+                              const ServerView* new_parent,
+                              const ServerView* old_parent) override;
+  void OnViewBoundsChanged(const ServerView* view,
+                           const gfx::Rect& old_bounds,
+                           const gfx::Rect& new_bounds) override;
+  void OnViewSurfaceIdChanged(const ServerView* view) override;
+  void OnViewReordered(const ServerView* view,
+                       const ServerView* relative,
+                       OrderDirection direction) override;
+  void OnWillChangeViewVisibility(ServerView* view) override;
+  void OnViewSharedPropertyChanged(
+      const ServerView* view,
+      const std::string& name,
+      const std::vector<uint8_t>* new_data) override;
+  void OnScheduleViewPaint(const ServerView* view) override;
+
+  DISALLOW_COPY_AND_ASSIGN(TestServerViewDelegate);
+};
+
+}  // namespace service
+}  // namespace mojo
+
+#endif  // SERVICES_VIEW_MANAGER_TEST_SERVER_VIEW_DELEGATE_H_
diff --git a/services/view_manager/view_coordinate_conversions_unittest.cc b/services/view_manager/view_coordinate_conversions_unittest.cc
index f7eba91..7867175 100644
--- a/services/view_manager/view_coordinate_conversions_unittest.cc
+++ b/services/view_manager/view_coordinate_conversions_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "services/view_manager/server_view.h"
 #include "services/view_manager/server_view_delegate.h"
+#include "services/view_manager/test_server_view_delegate.h"
 #include "services/view_manager/view_coordinate_conversions.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/gfx/geometry/rect.h"
@@ -11,46 +12,11 @@
 
 namespace mojo {
 namespace service {
-namespace {
-
-class MockServerViewDelegate : public ServerViewDelegate {
- public:
-  MockServerViewDelegate() {}
-  ~MockServerViewDelegate() override {}
-
- private:
-  // ServerViewDelegate:
-  void OnWillDestroyView(ServerView* view) override {}
-  void OnViewDestroyed(const ServerView* view) override {}
-  void OnWillChangeViewHierarchy(ServerView* view,
-                                 ServerView* new_parent,
-                                 ServerView* old_parent) override {}
-  void OnViewHierarchyChanged(const ServerView* view,
-                              const ServerView* new_parent,
-                              const ServerView* old_parent) override {}
-  void OnViewBoundsChanged(const ServerView* view,
-                           const gfx::Rect& old_bounds,
-                           const gfx::Rect& new_bounds) override {}
-  void OnViewSurfaceIdChanged(const ServerView* view) override {}
-  void OnViewReordered(const ServerView* view,
-                       const ServerView* relative,
-                       OrderDirection direction) override {}
-  void OnWillChangeViewVisibility(ServerView* view) override {}
-  void OnViewSharedPropertyChanged(
-      const ServerView* view,
-      const std::string& name,
-      const std::vector<uint8_t>* new_data) override {}
-  void OnScheduleViewPaint(const ServerView* view) override {}
-
-  DISALLOW_COPY_AND_ASSIGN(MockServerViewDelegate);
-};
-
-}  // namespace
 
 using ViewCoordinateConversionsTest = testing::Test;
 
 TEST_F(ViewCoordinateConversionsTest, ConvertRectBetweenViews) {
-  MockServerViewDelegate d1, d2, d3;
+  TestServerViewDelegate d1, d2, d3;
   ServerView v1(&d1, ViewId()), v2(&d2, ViewId()), v3(&d3, ViewId());
   v1.SetBounds(gfx::Rect(1, 2, 100, 100));
   v2.SetBounds(gfx::Rect(3, 4, 100, 100));