Fire animation events at the correct times. (Bug 435442, patch 14) r=bzbarsky

This commit is contained in:
L. David Baron 2011-04-11 23:18:44 -07:00
Родитель b250994f5e
Коммит 719d41cdd2
4 изменённых файлов: 256 добавлений и 15 удалений

Просмотреть файл

@ -4790,6 +4790,12 @@ PresShell::FlushPendingNotifications(mozFlushType aType)
mFrameConstructor->ProcessPendingRestyles();
}
// Dispatch any 'animationstart' events those (or earlier) restyles
// queued up.
if (!mIsDestroying) {
mPresContext->AnimationManager()->DispatchEvents();
}
// Process whatever XBL constructors those restyles queued up. This
// ensures that onload doesn't fire too early and that we won't do extra
// reflows after those constructors run.
@ -4797,12 +4803,13 @@ PresShell::FlushPendingNotifications(mozFlushType aType)
mDocument->BindingManager()->ProcessAttachedQueue();
}
// Now those constructors might have posted restyle events. At the same
// time, we still need up-to-date style data. In particular, reflow
// depends on style being completely up to date. If it's not, then style
// context reparenting, which can happen during reflow, might suddenly pick
// up the new rules and we'll end up with frames whose style doesn't match
// the frame type.
// Now those constructors or events might have posted restyle
// events. At the same time, we still need up-to-date style data.
// In particular, reflow depends on style being completely up to
// date. If it's not, then style context reparenting, which can
// happen during reflow, might suddenly pick up the new rules and
// we'll end up with frames whose style doesn't match the frame
// type.
if (!mIsDestroying) {
nsAutoScriptBlocker scriptBlocker;
mFrameConstructor->CreateNeededFrames();

Просмотреть файл

@ -41,9 +41,9 @@
#include "nsRuleProcessorData.h"
#include "nsStyleSet.h"
#include "nsCSSRules.h"
#include "mozilla/TimeStamp.h"
#include "nsStyleAnimation.h"
#include "nsSMILKeySpline.h"
#include "nsEventDispatcher.h"
using namespace mozilla;
@ -66,6 +66,11 @@ struct AnimationSegment
*/
struct ElementAnimation
{
ElementAnimation()
: mLastNotification(LAST_NOTIFICATION_NONE)
{
}
nsString mName; // empty string for 'none'
float mIterationCount; // NS_IEEEPositiveInfinity() means infinite
PRUint8 mDirection;
@ -89,9 +94,20 @@ struct ElementAnimation
TimeStamp mPauseStart;
TimeDuration mIterationDuration;
enum {
LAST_NOTIFICATION_NONE = PRUint32(-1),
LAST_NOTIFICATION_END = PRUint32(-2)
};
// One of the above constants, or an integer for the iteration
// whose start we last notified on.
PRUint32 mLastNotification;
InfallibleTArray<AnimationSegment> mSegments;
};
typedef nsAnimationManager::EventArray EventArray;
typedef nsAnimationManager::AnimationEventInfo AnimationEventInfo;
/**
* Data about all of the animations running on an element.
*/
@ -105,12 +121,15 @@ struct ElementAnimations : public mozilla::css::CommonElementAnimationData
{
}
void EnsureStyleRuleFor(TimeStamp aRefreshTime);
void EnsureStyleRuleFor(TimeStamp aRefreshTime,
EventArray &aEventsToDispatch);
bool IsForElement() const { // rather than for a pseudo-element
return mElementProperty == nsGkAtoms::animationsProperty;
}
void PostRestyleForAnimation(nsPresContext *aPresContext) {
nsRestyleHint hint =
(mElementProperty == nsGkAtoms::animationsProperty)
? eRestyle_Self : eRestyle_Subtree;
nsRestyleHint hint = IsForElement() ? eRestyle_Self : eRestyle_Subtree;
aPresContext->PresShell()->RestyleForAnimation(mElement, hint);
}
@ -144,7 +163,8 @@ ElementAnimationsPropertyDtor(void *aObject,
}
void
ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime)
ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime,
EventArray& aEventsToDispatch)
{
if (!mNeedsRefreshes) {
// All of our animations are paused or completed.
@ -167,7 +187,7 @@ ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime)
nsCSSPropertySet properties;
for (PRUint32 i = mAnimations.Length(); i-- != 0; ) {
const ElementAnimation &anim = mAnimations[i];
ElementAnimation &anim = mAnimations[i];
if (anim.mSegments.Length() == 0 ||
anim.mIterationDuration.ToMilliseconds() <= 0.0) {
@ -187,7 +207,18 @@ ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime)
// iterations we've completed up to the current position.
double currentIterationCount =
currentTimeDuration / anim.mIterationDuration;
bool dispatchStartOrIteration = false;
if (currentIterationCount >= double(anim.mIterationCount)) {
// Dispatch 'animationend' when needed.
if (IsForElement() &&
anim.mLastNotification !=
ElementAnimation::LAST_NOTIFICATION_END) {
anim.mLastNotification = ElementAnimation::LAST_NOTIFICATION_END;
AnimationEventInfo ei(mElement, anim.mName, NS_ANIMATION_END,
currentTimeDuration);
aEventsToDispatch.AppendElement(ei);
}
if (!anim.FillsForwards()) {
// No animation data.
continue;
@ -203,6 +234,8 @@ ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime)
continue;
}
currentIterationCount = 0.0;
} else {
dispatchStartOrIteration = !anim.IsPaused();
}
}
@ -223,6 +256,24 @@ ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime)
positionInIteration = 1.0 - positionInIteration;
}
// Dispatch 'animationstart' or 'animationiteration' when needed.
if (IsForElement() && dispatchStartOrIteration &&
whichIteration != anim.mLastNotification) {
// Notify 'animationstart' even if a negative delay puts us
// past the first iteration.
// Note that when somebody changes the animation-duration
// dynamically, this will fire an extra iteration event
// immediately in many cases. It's not clear to me if that's the
// right thing to do.
PRUint32 message =
anim.mLastNotification == ElementAnimation::LAST_NOTIFICATION_NONE
? NS_ANIMATION_START : NS_ANIMATION_ITERATION;
anim.mLastNotification = whichIteration;
AnimationEventInfo ei(mElement, anim.mName, message,
currentTimeDuration);
aEventsToDispatch.AppendElement(ei);
}
NS_ABORT_IF_FALSE(0.0 <= positionInIteration &&
positionInIteration <= 1.0,
"position should be in [0-1]");
@ -450,6 +501,7 @@ nsAnimationManager::CheckAnimationRule(nsStyleContext* aStyleContext,
}
newAnim->mStartTime = oldAnim->mStartTime;
newAnim->mLastNotification = oldAnim->mLastNotification;
if (oldAnim->IsPaused()) {
if (newAnim->IsPaused()) {
@ -470,7 +522,11 @@ nsAnimationManager::CheckAnimationRule(nsStyleContext* aStyleContext,
ea->mAnimations.SwapElements(newAnimations);
ea->mNeedsRefreshes = true;
ea->EnsureStyleRuleFor(refreshTime);
ea->EnsureStyleRuleFor(refreshTime, mPendingEvents);
// We don't actually dispatch the mPendingEvents now. We'll either
// dispatch them the next time we get a refresh driver notification
// or the next time somebody calls
// nsPresShell::FlushPendingNotifications.
}
return GetAnimationRule(aElement, aStyleContext->GetPseudoType());
@ -781,11 +837,29 @@ nsAnimationManager::WillRefresh(mozilla::TimeStamp aTime)
l = PR_NEXT_LINK(l)) {
ElementAnimations *ea = static_cast<ElementAnimations*>(l);
nsRefPtr<css::AnimValuesStyleRule> oldStyleRule = ea->mStyleRule;
ea->EnsureStyleRuleFor(mPresContext->RefreshDriver()->MostRecentRefresh());
ea->EnsureStyleRuleFor(mPresContext->RefreshDriver()->MostRecentRefresh(),
mPendingEvents);
if (oldStyleRule != ea->mStyleRule) {
ea->PostRestyleForAnimation(mPresContext);
}
}
DispatchEvents(); // may destroy us
}
void
nsAnimationManager::DispatchEvents()
{
EventArray events;
mPendingEvents.SwapElements(events);
for (PRUint32 i = 0, i_end = events.Length(); i < i_end; ++i) {
AnimationEventInfo &info = events[i];
nsEventDispatcher::Dispatch(info.mElement, mPresContext, &info.mEvent);
if (!mPresContext) {
break;
}
}
}
nsCSSKeyframesRule*

Просмотреть файл

@ -42,6 +42,9 @@
#include "nsCSSPseudoElements.h"
#include "nsStyleContext.h"
#include "nsTHashtable.h"
#include "nsGUIEvent.h"
#include "mozilla/TimeStamp.h"
#include "nsThreadUtils.h"
class nsCSSKeyframesRule;
struct AnimationSegment;
@ -64,6 +67,28 @@ public:
mKeyframesRules.Init(16); // FIXME: make infallible!
}
struct AnimationEventInfo {
nsRefPtr<mozilla::dom::Element> mElement;
nsAnimationEvent mEvent;
AnimationEventInfo(mozilla::dom::Element *aElement,
const nsString& aAnimationName,
PRUint32 aMessage, mozilla::TimeDuration aElapsedTime)
: mElement(aElement),
mEvent(PR_TRUE, aMessage, aAnimationName, aElapsedTime.ToSeconds())
{
}
// nsAnimationEvent doesn't support copy-construction, so we need
// to ourselves in order to work with nsTArray
AnimationEventInfo(const AnimationEventInfo &aOther)
: mElement(aOther.mElement),
mEvent(PR_TRUE, aOther.mEvent.message,
aOther.mEvent.animationName, aOther.mEvent.elapsedTime)
{
}
};
// nsIStyleRuleProcessor (parts)
virtual void RulesMatching(ElementRuleProcessorData* aData);
virtual void RulesMatching(PseudoElementRuleProcessorData* aData);
@ -93,6 +118,17 @@ public:
mKeyframesListIsDirty = PR_TRUE;
}
typedef InfallibleTArray<AnimationEventInfo> EventArray;
/**
* Dispatch any pending events. We accumulate animationend and
* animationiteration events only during refresh driver notifications
* (and dispatch them at the end of such notifications), but we
* accumulate animationstart events at other points when style
* contexts are created.
*/
void DispatchEvents();
private:
ElementAnimations* GetElementAnimations(mozilla::dom::Element *aElement,
nsCSSPseudoElements::Type aPseudoType,
@ -112,6 +148,8 @@ private:
bool mKeyframesListIsDirty;
nsDataHashtable<nsStringHashKey, nsCSSKeyframesRule*> mKeyframesRules;
EventArray mPendingEvents;
};
#endif /* !defined(nsAnimationManager_h_) */

Просмотреть файл

@ -60,6 +60,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=435442
75% { padding-bottom: 120px; -moz-animation-timing-function: linear }
100% { padding-bottom: 20px; -moz-animation-timing-function: ease-out }
}
#withbefore::before, #withafter::after {
content: "";
-moz-animation: anim2 1s linear alternate infinite;
}
</style>
</head>
<body>
@ -78,6 +83,7 @@ function advance_clock(milliseconds) {
var display = document.getElementById("display");
var div = null;
var cs = null;
var events_received = [];
function new_div(style) {
if (div != null || cs != null) {
ok(false, "test author forgot to call done_div");
@ -90,10 +96,49 @@ function new_div(style) {
display.appendChild(div);
cs = getComputedStyle(div, "");
}
function listen() {
events_received = [];
function listener(event) {
events_received.push(event);
}
div.addEventListener("animationstart", listener, false);
div.addEventListener("animationiteration", listener, false);
div.addEventListener("animationend", listener, false);
}
function check_events(events_expected, desc) {
// This function checks that the list of events_expected matches
// the received events -- but it only checks the properties that
// are present on events_expected.
is(events_received.length, events_expected.length,
"number of events received for " + desc);
for (var i = 0,
i_end = Math.min(events_expected.length, events_received.length);
i != i_end; ++i) {
var exp = events_expected[i];
var rec = events_received[i];
for (var prop in exp) {
if (prop == "elapsedTime") {
// Allow floating point error.
ok(Math.abs(rec.elapsedTime - exp.elapsedTime) < 0.000002,
"events[" + i + "]." + prop + " for " + desc +
" received=" + rec.elapsedTime + " expected=" + exp.elapsedTime);
} else {
is(rec[prop], exp[prop], "events[" + i + "]." + prop + " for " + desc);
}
}
}
for (var i = events_expected.length; i < events_received.length; ++i) {
ok(false, "unexpected " + events_received[i].type + " event for " + desc);
}
events_received = [];
}
function done_div() {
display.removeChild(div);
div = null;
cs = null;
if (events_received.length) {
ok(false, "caller should have called check_events");
}
}
// take over the refresh driver right from the start.
@ -119,6 +164,7 @@ function test_fill_mode(fill_mode, fills_backwards, fills_forwards)
desc = "default fill mode: ";
}
new_div(style);
listen();
if (fills_backwards)
is(cs.marginLeft, "0px", desc + "does affect value during delay (0s)");
else
@ -128,7 +174,12 @@ function test_fill_mode(fill_mode, fills_backwards, fills_forwards)
is(cs.marginLeft, "0px", desc + "does affect value during delay (2s)");
else
is(cs.marginLeft, "30px", desc + "doesn't affect value during delay (2s)");
check_events([], "before start in test_fill_mode");
advance_clock(1000);
check_events([{ type: 'animationstart', target: div,
bubbles: true, cancelable: true,
animationName: 'anim1', elapsedTime: 0.0 }],
"right after start in test_fill_mode");
if (fills_backwards)
is(cs.marginLeft, "0px", desc + "affects value at start of animation");
advance_clock(125);
@ -141,7 +192,12 @@ function test_fill_mode(fill_mode, fills_backwards, fills_forwards)
is(cs.marginLeft, "90px", desc + "affects value during animation");
advance_clock(2375);
is(cs.marginLeft, "99.5px", desc + "affects value during animation");
check_events([], "before end in test_fill_mode");
advance_clock(125);
check_events([{ type: 'animationend', target: div,
bubbles: true, cancelable: true,
animationName: 'anim1', elapsedTime: 10.0 }],
"right after end in test_fill_mode");
if (fills_forwards)
is(cs.marginLeft, "100px", desc + "affects value at end of animation");
advance_clock(10);
@ -976,6 +1032,35 @@ is_approx(px_to_num(cs.marginTop), 100 - 50 * gTF.ease_in(0.2), 0.01,
"delay and implicit starting values test");
done_div();
// test large negative delay that causes the animation to start
// in the fourth iteration
new_div("-moz-animation: anim2 1s -3.6s ease-in 5 alternate forwards");
listen(); // rely on no flush having happened yet
is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.4), 0.01,
"large negative delay test at 0ms");
check_events([{ type: 'animationstart', target: div,
animationName: 'anim2', elapsedTime: 3.6 }],
"right after start in large negative delay test");
advance_clock(380);
is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.02), 0.01,
"large negative delay test at 380ms");
check_events([]);
advance_clock(20);
is(cs.marginRight, "0px", "large negative delay test at 400ms");
check_events([{ type: 'animationiteration', target: div,
animationName: 'anim2', elapsedTime: 4.0 }],
"right after start in large negative delay test");
advance_clock(800);
is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.8), 0.01,
"large negative delay test at 1200ms");
check_events([]);
advance_clock(200);
is(cs.marginRight, "100px", "large negative delay test at 1400ms");
check_events([{ type: 'animationend', target: div,
animationName: 'anim2', elapsedTime: 5.0 }],
"right after start in large negative delay test");
done_div();
/*
* css3-animations: 3.9. The 'animation-fill-mode' Property
* http://dev.w3.org/csswg/css3-animations/#the-animation-fill-mode-property-
@ -991,6 +1076,43 @@ done_div();
// shorthand vs. longhand is adequately tested by the
// property_database.js-based tests.
/**
* Basic tests of animations on pseudo-elements
*/
new_div("");
listen();
div.id = "withbefore";
var cs_before = getComputedStyle(div, ":before");
is(cs_before.marginRight, "0px", ":before test at 0ms");
advance_clock(400);
is(cs_before.marginRight, "40px", ":before test at 400ms");
advance_clock(800);
is(cs_before.marginRight, "80px", ":before test at 1200ms");
is(cs.marginRight, "0px", ":before animation should not affect element");
advance_clock(800);
is(cs_before.marginRight, "0px", ":before test at 2000ms");
advance_clock(300);
is(cs_before.marginRight, "30px", ":before test at 2300ms");
check_events([], "no events should be fired for animations on :before");
done_div();
new_div("");
listen();
div.id = "withafter";
var cs_after = getComputedStyle(div, ":after");
is(cs_after.marginRight, "0px", ":after test at 0ms");
advance_clock(400);
is(cs_after.marginRight, "40px", ":after test at 400ms");
advance_clock(800);
is(cs_after.marginRight, "80px", ":after test at 1200ms");
is(cs.marginRight, "0px", ":after animation should not affect element");
advance_clock(800);
is(cs_after.marginRight, "0px", ":after test at 2000ms");
advance_clock(300);
is(cs_after.marginRight, "30px", ":after test at 2300ms");
check_events([], "no events should be fired for animations on :after");
done_div();
SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
</script>