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

fix(progressCircular): SVG transforms for indeterminate mode and performance improvements #7414

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/components/progressCircular/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h4>Indeterminate</h4>
<md-progress-circular md-mode="indeterminate"></md-progress-circular>
</div>

<h4>Theming </h4>
<h4>Theming</h4>

<p>
Your current theme colors can be used to easily colorize your progress indicator with `md-warn` or `md-accent`
Expand All @@ -34,11 +34,9 @@ <h4>Theming </h4>
<md-progress-circular md-mode="{{vm.modes[4]}}" md-diameter="96"></md-progress-circular>
</div>


<hr ng-class="{'visible' : vm.activated}">

<div id="loaders" layout="row" layout-align="start center">

<p>Progress Circular Indicators: </p>

<h5>Off</h5>
Expand Down
127 changes: 84 additions & 43 deletions src/components/progressCircular/js/progressCircularDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,16 @@ angular
'$$rAF',
'$window',
'$mdProgressCircular',
'$interval',
'$mdUtil',
'$interval',
'$log',
MdProgressCircularDirective
]);

function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $interval, $mdUtil, $log) {
function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $mdUtil, $interval, $log) {
var DEGREE_IN_RADIANS = $window.Math.PI / 180;
var MODE_DETERMINATE = 'determinate';
var MODE_INDETERMINATE = 'indeterminate';
var INDETERMINATE_CLASS = '_md-mode-indeterminate';

return {
restrict: 'E',
Expand All @@ -70,7 +69,7 @@ function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $inter
'<svg xmlns="http://www.w3.org/2000/svg">' +
'<path fill="none"/>' +
'</svg>',
compile: function(element, attrs){
compile: function(element, attrs) {
element.attr({
'aria-valuemin': 0,
'aria-valuemax': 100,
Expand All @@ -93,73 +92,66 @@ function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $inter
};

function MdProgressCircularLink(scope, element) {
var svg = angular.element(element[0].querySelector('svg'));
var path = angular.element(element[0].querySelector('path'));
var lastAnimationId = 0;
var svg = element[0].querySelector('svg');
var path = angular.element(svg.querySelector('path'));
var startIndeterminate = $mdProgressCircular.startIndeterminate;
var endIndeterminate = $mdProgressCircular.endIndeterminate;
var rotationIndeterminate = 0;
var lastAnimationId = 0;
var interval;

scope.$watchGroup(['value', 'mdMode', 'mdDiameter'], function(newValues, oldValues) {
scope.$watchGroup(['value', 'mdMode'], function(newValues, oldValues) {
var mode = newValues[1];

if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
cleanupIndeterminate();
cleanupIndeterminateAnimation();
element.addClass('ng-hide');
} else {
element.removeClass('ng-hide');

if (mode === MODE_INDETERMINATE){
if (!interval) {
interval = $interval(
animateIndeterminate,
$mdProgressCircular.durationIndeterminate + 50,
0,
false
);

element.addClass(INDETERMINATE_CLASS);
animateIndeterminate();
}

if (mode === MODE_INDETERMINATE) {
startIndeterminateAnimation();
} else {
cleanupIndeterminate();
cleanupIndeterminateAnimation();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, you said, that you wan't to stop the digests for $interval. I would rather use

$interval(fn, delay, 0, false);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll put it back in.

renderCircle(clamp(oldValues[0]), clamp(newValues[0]));
}
}
});

// This is in a separate watch in order to avoid layout, unless
// the value has actually changed.
scope.$watch('mdDiameter', function(newValue) {
var diameter = getSize(newValue);
var strokeWidth = getStroke(diameter);

// The viewBox has to be applied via setAttribute, because it is
// case-sensitive. If jQuery is included in the page, `.attr` lowercases
// all attribute names.
svg.setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
path.css('stroke-width', strokeWidth + 'px');
element.css({
width: diameter + 'px',
height: diameter + 'px'
});
});

function renderCircle(animateFrom, animateTo, easing, duration, rotation) {
var id = ++lastAnimationId;
var startTime = new $window.Date();
var startTime = $window.performance.now();
var changeInValue = animateTo - animateFrom;
var diameter = getSize(scope.mdDiameter);
var strokeWidth = $mdProgressCircular.strokeWidth / 100 * diameter;

var pathDiameter = diameter - strokeWidth;
var pathDiameter = diameter - getStroke(diameter);
var ease = easing || $mdProgressCircular.easeFn;
var animationDuration = duration || $mdProgressCircular.duration;

element.attr('aria-valuenow', animateTo);
path.attr('stroke-width', strokeWidth + 'px');

svg.css({
width: diameter + 'px',
height: diameter + 'px'
});

// The viewBox has to be applied via setAttribute, because it is
// case-sensitive. If jQuery is included in the page, `.attr` lowercases
// all attribute names.
svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);

// No need to animate it if the values are the same
if (animateTo === animateFrom) {
path.attr('d', getSvgArc(animateTo, diameter, pathDiameter, rotation));
} else {
(function animation() {
var currentTime = $window.Math.min(new $window.Date() - startTime, animationDuration);
$$rAF(function animation(now) {
var currentTime = now - startTime;

path.attr('d', getSvgArc(
ease(currentTime, animateFrom, changeInValue, animationDuration),
Expand All @@ -171,7 +163,7 @@ function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $inter
if (id === lastAnimationId && currentTime < animationDuration) {
$$rAF(animation);
}
})();
});
}
}

Expand All @@ -193,11 +185,52 @@ function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $inter
endIndeterminate = -temp;
}

function cleanupIndeterminate() {
function startIndeterminateAnimation() {
if (!interval) {
var startTime = $window.performance.now();
var animationDuration = $mdProgressCircular.rotationDurationIndeterminate;
var radius = getSize(scope.mdDiameter) / 2;

// Spares us at least a little bit of string concatenation.
radius = ' ' + radius + ', ' + radius;

// This animates the indeterminate rotation. This can be achieved much easier
// with CSS keyframes, however IE11 seems to have problems centering the rotation
// which causes a wobble in the indeterminate animation.
$$rAF(function animation(now) {
var currentTime = now - startTime;
var rotation = $mdProgressCircular.easingPresets.linearEase(currentTime, 0, 360, animationDuration);

path.attr('transform', 'rotate(' + rotation + radius + ')');

if (interval) {
$$rAF(animation);
} else {
path.removeAttr('transform');
}

// Reset the animation
if (currentTime >= animationDuration) {
startTime = now;
}
});

// This shouldn't trigger a digest which is why we don't use $interval.
interval = $interval(
animateIndeterminate,
$mdProgressCircular.durationIndeterminate + 50,
0,
false
);

animateIndeterminate();
}
}

function cleanupIndeterminateAnimation() {
if (interval) {
$interval.cancel(interval);
interval = null;
element.removeClass(INDETERMINATE_CLASS);
}
}
}
Expand Down Expand Up @@ -280,4 +313,12 @@ function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $inter

return defaultValue;
}

/**
* Determines the circle's stroke width, based on
* the provided diameter.
*/
function getStroke(diameter) {
return $mdProgressCircular.strokeWidth / 100 * diameter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @property {number} durationIndeterminate Duration of the indeterminate animation.
* @property {number} startIndeterminate Indeterminate animation start point.
* @param {number} endIndeterminate Indeterminate animation end point.
* @param {number} rotationDurationIndeterminate Duration of the indeterminate rotating animation.
* @param {function} easeFnIndeterminate Easing function to be used when animating
* between the indeterminate values.
*
Expand Down Expand Up @@ -46,9 +47,10 @@ function MdProgressCircularProvider() {
duration: 100,
easeFn: linearEase,

durationIndeterminate: 600,
startIndeterminate: 2.5,
durationIndeterminate: 500,
startIndeterminate: 3,
endIndeterminate: 80,
rotationDurationIndeterminate: 2900,
easeFnIndeterminate: materialEase,

easingPresets: {
Expand Down
23 changes: 6 additions & 17 deletions src/components/progressCircular/progress-circular.scss
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
$progress-circular-indeterminate-duration: 2.9s;

//
// Keyframe animation for the Indeterminate Progress
//
@keyframes indeterminate-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

// Used to avoid unnecessary layout
md-progress-circular {
svg {
// Ensures that the parent has the same dimensions as the circle.
display: block;
}
position: relative;

&._md-mode-indeterminate {
svg {
animation: indeterminate-rotate $progress-circular-indeterminate-duration linear infinite;
position: absolute;
overflow: visible;
top: 0;
left: 0;
}
}
}
2 changes: 1 addition & 1 deletion src/components/progressCircular/progress-circular.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('mdProgressCircular', function() {
element = $compile('<div>' + template + '</div>')($rootScope);
$rootScope.$digest();

return element.find('svg');
return element.find('md-progress-circular');
}

});