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

Commit 640b55d

Browse files
crisbetoThomasBurleson
authored andcommitted
feat(progressCircular): implement progressCircular to use SVG
Closes #7322.
1 parent c60d16b commit 640b55d

File tree

7 files changed

+402
-497
lines changed

7 files changed

+402
-497
lines changed

src/components/progressCircular/demoBasicUsage/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ angular
3232
if ( (j < 5) && !self.modes[j] && self.activated ) {
3333
self.modes[j] = 'indeterminate';
3434
}
35-
if ( counter++ % 4 == 0 ) j++;
35+
if ( counter++ % 4 === 0 ) j++;
3636

3737
}, 100, 0, true);
3838
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
/**
2+
* @ngdoc directive
3+
* @name mdProgressCircular
4+
* @module material.components.progressCircular
5+
* @restrict E
6+
*
7+
* @description
8+
* The circular progress directive is used to make loading content in your app as delightful and
9+
* painless as possible by minimizing the amount of visual change a user sees before they can view
10+
* and interact with content.
11+
*
12+
* For operations where the percentage of the operation completed can be determined, use a
13+
* determinate indicator. They give users a quick sense of how long an operation will take.
14+
*
15+
* For operations where the user is asked to wait a moment while something finishes up, and it’s
16+
* not necessary to expose what's happening behind the scenes and how long it will take, use an
17+
* indeterminate indicator.
18+
*
19+
* @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**.
20+
*
21+
* Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then `.ng-hide`
22+
* will be auto-applied as a style to the component.
23+
*
24+
* Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute.
25+
* If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead.
26+
* @param {number=} value In determinate mode, this number represents the percentage of the
27+
* circular progress. Default: 0
28+
* @param {number=} md-diameter This specifies the diameter of the circular progress. The value
29+
* should be a pixel-size value (eg '100'). If this attribute is
30+
* not present then a default value of '50px' is assumed.
31+
*
32+
* @usage
33+
* <hljs lang="html">
34+
* <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
35+
*
36+
* <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
37+
*
38+
* <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
39+
*
40+
* <md-progress-circular md-mode="indeterminate"></md-progress-circular>
41+
* </hljs>
42+
*/
43+
44+
angular
45+
.module('material.components.progressCircular')
46+
.directive('mdProgressCircular', [
47+
'$$rAF',
48+
'$window',
49+
'$mdProgressCircular',
50+
'$interval',
51+
'$mdUtil',
52+
'$log',
53+
MdProgressCircularDirective
54+
]);
55+
56+
function MdProgressCircularDirective($$rAF, $window, $mdProgressCircular, $interval, $mdUtil, $log) {
57+
var DEGREE_IN_RADIANS = $window.Math.PI / 180;
58+
var MODE_DETERMINATE = 'determinate';
59+
var MODE_INDETERMINATE = 'indeterminate';
60+
var INDETERMINATE_CLASS = '_md-mode-indeterminate';
61+
62+
return {
63+
restrict: 'E',
64+
scope: {
65+
value: '@',
66+
mdDiameter: '@',
67+
mdMode: '@'
68+
},
69+
template:
70+
'<svg xmlns="http://www.w3.org/2000/svg">' +
71+
'<path fill="none"/>' +
72+
'</svg>',
73+
compile: function(element, attrs){
74+
element.attr({
75+
'aria-valuemin': 0,
76+
'aria-valuemax': 100,
77+
'role': 'progressbar'
78+
});
79+
80+
if (angular.isUndefined(attrs.mdMode)) {
81+
var hasValue = angular.isDefined(attrs.value);
82+
var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
83+
var info = "Auto-adding the missing md-mode='{0}' to the ProgressCircular element";
84+
85+
$log.debug( $mdUtil.supplant(info, [mode]) );
86+
attrs.$set('mdMode', mode);
87+
} else {
88+
attrs.$set('mdMode', attrs.mdMode.trim());
89+
}
90+
91+
return MdProgressCircularLink;
92+
}
93+
};
94+
95+
function MdProgressCircularLink(scope, element) {
96+
var svg = angular.element(element[0].querySelector('svg'));
97+
var path = angular.element(element[0].querySelector('path'));
98+
var lastAnimationId = 0;
99+
var startIndeterminate = $mdProgressCircular.startIndeterminate;
100+
var endIndeterminate = $mdProgressCircular.endIndeterminate;
101+
var rotationIndeterminate = 0;
102+
var interval;
103+
104+
scope.$watchGroup(['value', 'mdMode', 'mdDiameter'], function(newValues, oldValues) {
105+
var mode = newValues[1];
106+
107+
if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
108+
cleanupIndeterminate();
109+
element.addClass('ng-hide');
110+
} else {
111+
element.removeClass('ng-hide');
112+
113+
if (mode === MODE_INDETERMINATE){
114+
if (!interval) {
115+
interval = $interval(
116+
animateIndeterminate,
117+
$mdProgressCircular.durationIndeterminate + 50,
118+
0,
119+
false
120+
);
121+
122+
element.addClass(INDETERMINATE_CLASS);
123+
animateIndeterminate();
124+
}
125+
126+
} else {
127+
cleanupIndeterminate();
128+
renderCircle(clamp(oldValues[0]), clamp(newValues[0]));
129+
}
130+
}
131+
});
132+
133+
function renderCircle(animateFrom, animateTo, easing, duration, rotation) {
134+
var id = ++lastAnimationId;
135+
var startTime = new $window.Date();
136+
var changeInValue = animateTo - animateFrom;
137+
var diameter = getSize(scope.mdDiameter);
138+
var strokeWidth = $mdProgressCircular.strokeWidth / 100 * diameter;
139+
140+
var pathDiameter = diameter - strokeWidth;
141+
var ease = easing || $mdProgressCircular.easeFn;
142+
var animationDuration = duration || $mdProgressCircular.duration;
143+
144+
element.attr('aria-valuenow', animateTo);
145+
path.attr('stroke-width', strokeWidth + 'px');
146+
147+
svg.css({
148+
width: diameter + 'px',
149+
height: diameter + 'px'
150+
});
151+
152+
// The viewBox has to be applied via setAttribute, because it is
153+
// case-sensitive. If jQuery is included in the page, `.attr` lowercases
154+
// all attribute names.
155+
svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
156+
157+
// No need to animate it if the values are the same
158+
if (animateTo === animateFrom) {
159+
path.attr('d', getSvgArc(animateTo, diameter, pathDiameter, rotation));
160+
} else {
161+
(function animation() {
162+
var currentTime = $window.Math.min(new $window.Date() - startTime, animationDuration);
163+
164+
path.attr('d', getSvgArc(
165+
ease(currentTime, animateFrom, changeInValue, animationDuration),
166+
diameter,
167+
pathDiameter,
168+
rotation
169+
));
170+
171+
if (id === lastAnimationId && currentTime < animationDuration) {
172+
$$rAF(animation);
173+
}
174+
})();
175+
}
176+
}
177+
178+
function animateIndeterminate() {
179+
renderCircle(
180+
startIndeterminate,
181+
endIndeterminate,
182+
$mdProgressCircular.easeFnIndeterminate,
183+
$mdProgressCircular.durationIndeterminate,
184+
rotationIndeterminate
185+
);
186+
187+
// The % 100 technically isn't necessary, but it keeps the rotation
188+
// under 100, instead of becoming a crazy large number.
189+
rotationIndeterminate = (rotationIndeterminate + endIndeterminate) % 100;
190+
191+
var temp = startIndeterminate;
192+
startIndeterminate = -endIndeterminate;
193+
endIndeterminate = -temp;
194+
}
195+
196+
function cleanupIndeterminate() {
197+
if (interval) {
198+
$interval.cancel(interval);
199+
interval = null;
200+
element.removeClass(INDETERMINATE_CLASS);
201+
}
202+
}
203+
}
204+
205+
/**
206+
* Generates an arc following the SVG arc syntax.
207+
* Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
208+
*
209+
* @param {number} current Current value between 0 and 100.
210+
* @param {number} diameter Diameter of the container.
211+
* @param {number} pathDiameter Diameter of the path element.
212+
* @param {number=0} rotation The point at which the semicircle should start rendering.
213+
* Used for doing the indeterminate animation.
214+
*
215+
* @returns {string} String representation of an SVG arc.
216+
*/
217+
function getSvgArc(current, diameter, pathDiameter, rotation) {
218+
// The angle can't be exactly 360, because the arc becomes hidden.
219+
var maximumAngle = 359.99 / 100;
220+
var startPoint = rotation || 0;
221+
var radius = diameter / 2;
222+
var pathRadius = pathDiameter / 2;
223+
224+
var startAngle = startPoint * maximumAngle;
225+
var endAngle = current * maximumAngle;
226+
var start = polarToCartesian(radius, pathRadius, startAngle);
227+
var end = polarToCartesian(radius, pathRadius, endAngle + startAngle);
228+
var arcSweep = endAngle < 0 ? 0 : 1;
229+
var largeArcFlag;
230+
231+
if (endAngle < 0) {
232+
largeArcFlag = endAngle >= -180 ? 0 : 1;
233+
} else {
234+
largeArcFlag = endAngle <= 180 ? 0 : 1;
235+
}
236+
237+
return 'M' + start + 'A' + pathRadius + ',' + pathRadius +
238+
' 0 ' + largeArcFlag + ',' + arcSweep + ' ' + end;
239+
}
240+
241+
/**
242+
* Converts Polar coordinates to Cartesian.
243+
*
244+
* @param {number} radius Radius of the container.
245+
* @param {number} pathRadius Radius of the path element
246+
* @param {number} angleInDegress Angle at which to place the point.
247+
*
248+
* @returns {string} Cartesian coordinates in the format of `x,y`.
249+
*/
250+
function polarToCartesian(radius, pathRadius, angleInDegrees) {
251+
var angleInRadians = (angleInDegrees - 90) * DEGREE_IN_RADIANS;
252+
253+
return (radius + (pathRadius * $window.Math.cos(angleInRadians))) +
254+
',' + (radius + (pathRadius * $window.Math.sin(angleInRadians)));
255+
}
256+
257+
/**
258+
* Limits a value between 0 and 100.
259+
*/
260+
function clamp(value) {
261+
return $window.Math.max(0, $window.Math.min(value || 0, 100));
262+
}
263+
264+
/**
265+
* Determines the size of a progress circle, based on the provided
266+
* value in the following formats: `X`, `Ypx`, `Z%`.
267+
*/
268+
function getSize(value) {
269+
var defaultValue = $mdProgressCircular.progressSize;
270+
271+
if (value) {
272+
var parsed = parseFloat(value);
273+
274+
if (value.lastIndexOf('%') === value.length - 1) {
275+
parsed = (parsed / 100) * defaultValue;
276+
}
277+
278+
return parsed;
279+
}
280+
281+
return defaultValue;
282+
}
283+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @ngdoc service
3+
* @name $mdProgressCircular
4+
* @module material.components.progressCircular
5+
*
6+
* @description
7+
* Allows the user to specify the default options for the `progressCircular` directive.
8+
*
9+
* @property {number} progressSize Diameter of the progress circle in pixels.
10+
* @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
11+
* @property {number} duration Length of the circle animation in milliseconds.
12+
* @property {function} easeFn Default easing animation function.
13+
* @property {object} easingPresets Collection of pre-defined easing functions.
14+
*
15+
* @property {number} durationIndeterminate Duration of the indeterminate animation.
16+
* @property {number} startIndeterminate Indeterminate animation start point.
17+
* @param {number} endIndeterminate Indeterminate animation end point.
18+
* @param {function} easeFnIndeterminate Easing function to be used when animating
19+
* between the indeterminate values.
20+
*
21+
* @property {(function(object): object)} configure Used to modify the default options.
22+
*
23+
* @usage
24+
* <hljs lang="js">
25+
* myAppModule.config(function($mdProgressCircular) {
26+
*
27+
* // Example of changing the default progress options.
28+
* $mdProgressCircular.configure({
29+
* progressSize: 100,
30+
* strokeWidth: 20,
31+
* duration: 800
32+
* });
33+
* });
34+
* </hljs>
35+
*
36+
*/
37+
38+
angular
39+
.module('material.components.progressCircular')
40+
.provider("$mdProgressCircular", MdProgressCircularProvider);
41+
42+
function MdProgressCircularProvider() {
43+
var progressConfig = {
44+
progressSize: 50,
45+
strokeWidth: 10,
46+
duration: 100,
47+
easeFn: linearEase,
48+
49+
durationIndeterminate: 600,
50+
startIndeterminate: 2.5,
51+
endIndeterminate: 80,
52+
easeFnIndeterminate: materialEase,
53+
54+
easingPresets: {
55+
linearEase: linearEase,
56+
materialEase: materialEase
57+
}
58+
};
59+
60+
return {
61+
configure: function(options) {
62+
progressConfig = angular.extend(progressConfig, options || {});
63+
return progressConfig;
64+
},
65+
$get: function() { return progressConfig; }
66+
};
67+
68+
function linearEase(t, b, c, d) {
69+
return c * t / d + b;
70+
}
71+
72+
function materialEase(t, b, c, d) {
73+
// via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
74+
// with settings of [0, 0, 1, 1]
75+
var ts = (t /= d) * t;
76+
var tc = ts * t;
77+
return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
78+
}
79+
}

0 commit comments

Comments
 (0)