зеркало из https://github.com/mozilla/gecko-dev.git
Fire animation events at the correct times. (Bug 435442, patch 14) r=bzbarsky
This commit is contained in:
Родитель
b250994f5e
Коммит
719d41cdd2
|
@ -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>
|
||||
|
|
Загрузка…
Ссылка в новой задаче