Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit ee2f3d2

Browse files
matskomhevery
authored andcommitted
fix($animate): only execute a timeout when transitions or keyframe animations are used
ngAnimate causes a 1ms flicker on the screen when no CSS animations are present on the element. The solution is to change $animate to only use $timeouts when a duration is found on the element before the transition/keyframe animation takes over. Closes #3613
1 parent fb3a7db commit ee2f3d2

File tree

3 files changed

+364
-331
lines changed

3 files changed

+364
-331
lines changed

docs/component-spec/annotationsSpec.js

+2-9
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,12 @@ describe('Docs Annotations', function() {
118118
expect(foldout.html()).toContain('loading');
119119
}));
120120

121-
it('should download a foldout HTML page and animate the contents', inject(function($httpBackend, $timeout) {
121+
it('should download a foldout HTML page and animate the contents', inject(function($httpBackend, $timeout, $sniffer) {
122122
$httpBackend.expect('GET', url).respond('hello');
123123

124124
element.triggerHandler('click');
125125
$httpBackend.flush();
126126

127-
$timeout.flushNext(0);
128-
$timeout.flushNext(1);
129127
$timeout.flushNext(0);
130128
$timeout.flushNext(1000);
131129

@@ -134,27 +132,22 @@ describe('Docs Annotations', function() {
134132
expect(foldout.text()).toContain('hello');
135133
}));
136134

137-
it('should hide then show when clicked again', inject(function($httpBackend, $timeout) {
135+
it('should hide then show when clicked again', inject(function($httpBackend, $timeout, $sniffer) {
138136
$httpBackend.expect('GET', url).respond('hello');
139137

140138
//enter
141139
element.triggerHandler('click');
142140
$httpBackend.flush();
143141
$timeout.flushNext(0);
144-
$timeout.flushNext(1);
145-
$timeout.flushNext(0);
146142
$timeout.flushNext(1000);
147143

148144
//hide
149145
element.triggerHandler('click');
150-
$timeout.flushNext(1);
151146
$timeout.flushNext(0);
152147
$timeout.flushNext(200);
153-
$timeout.flushNext(0);
154148

155149
//show
156150
element.triggerHandler('click');
157-
$timeout.flushNext(1);
158151
$timeout.flushNext(0);
159152
$timeout.flushNext(500);
160153
$timeout.flushNext(0);

src/ngAnimate/animate.js

+77-72
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,9 @@ angular.module('ngAnimate', ['ng'])
282282
*/
283283
enter : function(element, parent, after, done) {
284284
$delegate.enter(element, parent, after);
285-
performAnimation('enter', 'ng-enter', element, parent, after, done);
285+
performAnimation('enter', 'ng-enter', element, parent, after, function() {
286+
$timeout(done || noop, 0, false);
287+
});
286288
},
287289

288290
/**
@@ -350,7 +352,9 @@ angular.module('ngAnimate', ['ng'])
350352
*/
351353
move : function(element, parent, after, done) {
352354
$delegate.move(element, parent, after);
353-
performAnimation('move', 'ng-move', element, null, null, done);
355+
performAnimation('move', 'ng-move', element, null, null, function() {
356+
$timeout(done || noop, 0, false);
357+
});
354358
},
355359

356360
/**
@@ -361,7 +365,8 @@ angular.module('ngAnimate', ['ng'])
361365
* @description
362366
* Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class.
363367
* Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide
364-
* the animate service the setup and active CSS classes in order to trigger the animation.
368+
* the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions
369+
* or keyframes are defined on the -add CSS class).
365370
*
366371
* Below is a breakdown of each step that occurs during addClass animation:
367372
*
@@ -395,7 +400,8 @@ angular.module('ngAnimate', ['ng'])
395400
* @description
396401
* Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value
397402
* from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in
398-
* order to provide the animate service the setup and active CSS classes in order to trigger the animation.
403+
* order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if
404+
* no CSS transitions or keyframes are defined on the -remove CSS class).
399405
*
400406
* Below is a breakdown of each step that occurs during removeClass animation:
401407
*
@@ -546,90 +552,89 @@ angular.module('ngAnimate', ['ng'])
546552
function animate(element, className, done) {
547553
if (!($sniffer.transitions || $sniffer.animations)) {
548554
done();
549-
} else {
550-
var activeClassName = '';
551-
$timeout(startAnimation, 1, false);
552-
553-
//this acts as the cancellation function in case
554-
//a new animation is triggered while another animation
555-
//is still going on (otherwise the active className
556-
//would still hang around until the timer is complete).
557-
return onEnd;
558-
}
559-
560-
function parseMaxTime(str) {
561-
var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : [];
562-
forEach(values, function(value) {
563-
total = Math.max(parseFloat(value) || 0, total);
564-
});
565-
return total;
555+
return;
566556
}
567557

568-
function startAnimation() {
569-
var duration = 0;
570-
forEach(className.split(' '), function(klass, i) {
571-
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
572-
});
558+
//one day all browsers will have these properties
559+
var w3cAnimationProp = 'animation';
560+
var w3cTransitionProp = 'transition';
573561

574-
element.addClass(activeClassName);
562+
//but some still use vendor-prefixed styles
563+
var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation';
564+
var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition';
575565

576-
//one day all browsers will have these properties
577-
var w3cAnimationProp = 'animation';
578-
var w3cTransitionProp = 'transition';
566+
var durationKey = 'Duration',
567+
delayKey = 'Delay',
568+
animationIterationCountKey = 'IterationCount';
579569

580-
//but some still use vendor-prefixed styles
581-
var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation';
582-
var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition';
570+
//we want all the styles defined before and after
571+
var duration = 0, ELEMENT_NODE = 1;
572+
forEach(element, function(element) {
573+
if (element.nodeType == ELEMENT_NODE) {
574+
var elementStyles = $window.getComputedStyle(element) || {};
583575

584-
var durationKey = 'Duration',
585-
delayKey = 'Delay',
586-
animationIterationCountKey = 'IterationCount';
576+
var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]),
577+
parseMaxTime(elementStyles[vendorTransitionProp + delayKey]));
587578

588-
//we want all the styles defined before and after
589-
var ELEMENT_NODE = 1;
590-
forEach(element, function(element) {
591-
if (element.nodeType == ELEMENT_NODE) {
592-
var elementStyles = $window.getComputedStyle(element) || {};
579+
var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]),
580+
parseMaxTime(elementStyles[vendorAnimationProp + delayKey]));
593581

594-
var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]),
595-
parseMaxTime(elementStyles[vendorTransitionProp + delayKey]));
582+
var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]),
583+
parseMaxTime(elementStyles[vendorTransitionProp + durationKey]));
596584

597-
var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]),
598-
parseMaxTime(elementStyles[vendorAnimationProp + delayKey]));
585+
var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]),
586+
parseMaxTime(elementStyles[vendorAnimationProp + durationKey]));
599587

600-
var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]),
601-
parseMaxTime(elementStyles[vendorTransitionProp + durationKey]));
602-
603-
var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]),
604-
parseMaxTime(elementStyles[vendorAnimationProp + durationKey]));
588+
if(animationDuration > 0) {
589+
animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0,
590+
parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0,
591+
1);
592+
}
605593

606-
if(animationDuration > 0) {
607-
animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0,
608-
parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0,
609-
1);
610-
}
594+
duration = Math.max(animationDelay + animationDuration,
595+
transitionDelay + transitionDuration,
596+
duration);
597+
}
598+
});
611599

612-
duration = Math.max(animationDelay + animationDuration,
613-
transitionDelay + transitionDuration,
614-
duration);
615-
}
600+
/* there is no point in performing a reflow if the animation
601+
timeout is empty (this would cause a flicker bug normally
602+
in the page */
603+
if(duration > 0) {
604+
var activeClassName = '';
605+
forEach(className.split(' '), function(klass, i) {
606+
activeClassName += (i > 0 ? ' ' : '') + klass + '-active';
616607
});
617608

618-
$timeout(done, duration * 1000, false);
609+
$timeout(function() {
610+
element.addClass(activeClassName);
611+
$timeout(done, duration * 1000, false);
612+
},0,false);
613+
614+
//this will automatically be called by $animate so
615+
//there is no need to attach this internally to the
616+
//timeout done method
617+
return function onEnd(cancelled) {
618+
element.removeClass(activeClassName);
619+
620+
//only when the animation is cancelled is the done()
621+
//function not called for this animation therefore
622+
//this must be also called
623+
if(cancelled) {
624+
done();
625+
}
626+
}
627+
}
628+
else {
629+
done();
619630
}
620631

621-
//this will automatically be called by $animate so
622-
//there is no need to attach this internally to the
623-
//timeout done method
624-
function onEnd(cancelled) {
625-
element.removeClass(activeClassName);
626-
627-
//only when the animation is cancelled is the done()
628-
//function not called for this animation therefore
629-
//this must be also called
630-
if(cancelled) {
631-
done();
632-
}
632+
function parseMaxTime(str) {
633+
var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : [];
634+
forEach(values, function(value) {
635+
total = Math.max(parseFloat(value) || 0, total);
636+
});
637+
return total;
633638
}
634639
}
635640

0 commit comments

Comments
 (0)