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

Commit

Permalink
fix($animate): only execute a timeout when transitions or keyframe an…
Browse files Browse the repository at this point in the history
…imations 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
  • Loading branch information
matsko authored and mhevery committed Aug 23, 2013
1 parent fb3a7db commit ee2f3d2
Show file tree
Hide file tree
Showing 3 changed files with 364 additions and 331 deletions.
11 changes: 2 additions & 9 deletions docs/component-spec/annotationsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,12 @@ describe('Docs Annotations', function() {
expect(foldout.html()).toContain('loading');
}));

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

element.triggerHandler('click');
$httpBackend.flush();

$timeout.flushNext(0);
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(1000);

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

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

//enter
element.triggerHandler('click');
$httpBackend.flush();
$timeout.flushNext(0);
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(1000);

//hide
element.triggerHandler('click');
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(200);
$timeout.flushNext(0);

//show
element.triggerHandler('click');
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(500);
$timeout.flushNext(0);
Expand Down
149 changes: 77 additions & 72 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ angular.module('ngAnimate', ['ng'])
*/
enter : function(element, parent, after, done) {
$delegate.enter(element, parent, after);
performAnimation('enter', 'ng-enter', element, parent, after, done);
performAnimation('enter', 'ng-enter', element, parent, after, function() {
$timeout(done || noop, 0, false);
});
},

/**
Expand Down Expand Up @@ -350,7 +352,9 @@ angular.module('ngAnimate', ['ng'])
*/
move : function(element, parent, after, done) {
$delegate.move(element, parent, after);
performAnimation('move', 'ng-move', element, null, null, done);
performAnimation('move', 'ng-move', element, null, null, function() {
$timeout(done || noop, 0, false);
});
},

/**
Expand All @@ -361,7 +365,8 @@ angular.module('ngAnimate', ['ng'])
* @description
* Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class.
* Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide
* the animate service the setup and active CSS classes in order to trigger the animation.
* the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions
* or keyframes are defined on the -add CSS class).
*
* Below is a breakdown of each step that occurs during addClass animation:
*
Expand Down Expand Up @@ -395,7 +400,8 @@ angular.module('ngAnimate', ['ng'])
* @description
* Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value
* from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in
* order to provide the animate service the setup and active CSS classes in order to trigger the animation.
* order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if
* no CSS transitions or keyframes are defined on the -remove CSS class).
*
* Below is a breakdown of each step that occurs during removeClass animation:
*
Expand Down Expand Up @@ -546,90 +552,89 @@ angular.module('ngAnimate', ['ng'])
function animate(element, className, done) {
if (!($sniffer.transitions || $sniffer.animations)) {
done();
} else {
var activeClassName = '';
$timeout(startAnimation, 1, false);

//this acts as the cancellation function in case
//a new animation is triggered while another animation
//is still going on (otherwise the active className
//would still hang around until the timer is complete).
return onEnd;
}

function parseMaxTime(str) {
var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : [];
forEach(values, function(value) {
total = Math.max(parseFloat(value) || 0, total);
});
return total;
return;
}

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

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

//one day all browsers will have these properties
var w3cAnimationProp = 'animation';
var w3cTransitionProp = 'transition';
var durationKey = 'Duration',
delayKey = 'Delay',
animationIterationCountKey = 'IterationCount';

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

var durationKey = 'Duration',
delayKey = 'Delay',
animationIterationCountKey = 'IterationCount';
var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]),
parseMaxTime(elementStyles[vendorTransitionProp + delayKey]));

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

var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]),
parseMaxTime(elementStyles[vendorTransitionProp + delayKey]));
var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]),
parseMaxTime(elementStyles[vendorTransitionProp + durationKey]));

var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]),
parseMaxTime(elementStyles[vendorAnimationProp + delayKey]));
var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]),
parseMaxTime(elementStyles[vendorAnimationProp + durationKey]));

var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]),
parseMaxTime(elementStyles[vendorTransitionProp + durationKey]));

var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]),
parseMaxTime(elementStyles[vendorAnimationProp + durationKey]));
if(animationDuration > 0) {
animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0,
parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0,
1);
}

if(animationDuration > 0) {
animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0,
parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0,
1);
}
duration = Math.max(animationDelay + animationDuration,
transitionDelay + transitionDuration,
duration);
}
});

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

$timeout(done, duration * 1000, false);
$timeout(function() {
element.addClass(activeClassName);
$timeout(done, duration * 1000, false);
},0,false);

//this will automatically be called by $animate so
//there is no need to attach this internally to the
//timeout done method
return function onEnd(cancelled) {
element.removeClass(activeClassName);

//only when the animation is cancelled is the done()
//function not called for this animation therefore
//this must be also called
if(cancelled) {
done();
}
}
}
else {
done();
}

//this will automatically be called by $animate so
//there is no need to attach this internally to the
//timeout done method
function onEnd(cancelled) {
element.removeClass(activeClassName);

//only when the animation is cancelled is the done()
//function not called for this animation therefore
//this must be also called
if(cancelled) {
done();
}
function parseMaxTime(str) {
var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : [];
forEach(values, function(value) {
total = Math.max(parseFloat(value) || 0, total);
});
return total;
}
}

Expand Down
Loading

0 comments on commit ee2f3d2

Please sign in to comment.