Progress Bar: Plain to Animated
In this series of written and video tutorials, I'll progressively improve the progress bar (see what I did there?) by adding JavaScript…
Take control of your career. Build JavaScript mobile apps.
Catch Dave Coffin, Nathan Walker, and Alex Ziskind at ngAtlanta in February 2020 for an advanced NativeScript with Angular workshop called Breathe life into mobile UX with solid architecture lessons
. You can register now and take your NativeScript skills up a notch. Register here.
In this series of written and video tutorials, I'll progressively improve the progress bar (see what I did there?) by adding JavaScript animation to its movement, and then using RxJS to animate it.
TJ VanToll wrote an excellent post on building a simple progress bar with just JavaScript and CSS in a NativeScript Angular application. After the post came out, Peter Staev tweeted that the template could be simplified a bit too.
Then it hit me; THIS MUST BE ANIMATED!
There are two animation approaches that I've been using for a while that I wanted to show with the progress bar:
- Plain (brute force) JavaScript animation
- Animation using RxJS
So I've written down these approaches in this post and created video recordings of all the approaches to go along with the post. You can jump between parts here:
- TJ's Approach Modified
- Peter's Simplified Template
- Alex's JavaScript Animation Added
- Alex's RxJS Animation Added
- Alex's Animation in a Pipe (UPDATE!)
*Note The videos for each approach is included in each section and all the code can be found on GitHub.
TJ's Approach Modified
The progress bar in TJ's approach works well. However, the updates all arrive at a steady rate and the updates are tiny. This looks nice, but doesn't represent the real stresses that a progress bar might experience in the world. A more realistic progress bar would jump around to random points and update sporadically.
I've simulated that using a separate function that returns a random progress update between the current progress and 100 percent. Otherwise, the rest of the TypeScript code is pretty much the same as in TJ's post.
util.ts
export function getNewPercentValue(startingFrom: number): number {
const newVal = Math.floor(Math.random() * 10) + startingFrom + 10;
if (newVal > 100) {
return 100;
} else {
return newVal;
}
}
Oh and instead of background
in the CSS, I've also used background-color
, to make the CSS property more compatible with older versions of NativeScript. You can watch me build this progress bar from scratch in this video:
Peter's Simplified Template
Peter Staev suggested a more streamlined approach to the template.
Instead of using a binding to the GridLayout
's columns
attribute, where we have to do a crazy string concatenation to calculate new column widths, we can just directly bind internal StackLayout
's style.width
attribute to the percent and use an expression in the binding like this:
<GridLayout class="progressbar">
<StackLayout col="0" class="progressbar-value" [style.width]="percent + '%'"></StackLayout>
</GridLayout>
Here is a short video showing this new, simpler approach:
Alex's JavaScript Animation Added
Here is where the animation comes in. First, I wanted to show a plain, brute-force JavaScript animation technique that I've been using. This utilizes a reusable animate
function that takes in an easing
function, a step
function (more on that later), and a duration
for the animation.
The reusable animate
function can be used for any JavaScript animation, not just the progress bar, and it can be part of a set of utility functions. But here, I've just put it inside my component file.
function animate(
easing: (p: number) => number,
step: (delta: number) => void,
duration: number
) {
var start = new Date();
var id = setInterval(function() {
var timePassed = new Date().valueOf() - start.valueOf();
var progress = timePassed / duration;
if (progress > 1) progress = 1;
var delta = easing(progress);
step(delta);
if (progress == 1) {
clearInterval(id);
}
}, 10);
}
This function is just an animation loop that calculates where in the animation we are currently. Then it runs our animation progress through the easing function. And finally it executes the step function that we passed in. The step function performs the visual update to our UI for every slice of the animation. Since the animate function doesn't care about what we're updating, it's the responsibility of the component to pass in the step function that will do the UI update. In our case, the step function just updates the percent.
Here is the updated component ngOnInit
function that has our percent update loop:
public ngOnInit(): void {
let percentCurrent = 0;
let intervalId = setInterval(() => {
const oldPercent = percentCurrent;
percentCurrent = getNewPercentValue(percentCurrent);
animate(
d3.easeQuadOut,
(delta) => {
const newWidth = amountFromTo({ from: oldPercent, to: percentCurrent })(delta);
this.percent = newWidth;
},
300);
if (percentCurrent >= 100) {
clearInterval(intervalId);
}
}, 1000);
}
Notice that we are updating the percent inside the step function that we pass to the animate
function as a parameter.
The full code listing is here, and the video explaining all this is here:
Alex's RxJS Animation Added
Most Angular applications these days take advantage of RxJS. We use RxJS for internal application stores as I show in the Application State chapter of the NativeScript with Angular Pro course. We also use RxJS for HTTP calls, and so on.
Since RxJS is so prevalent in our apps, we can leverage it for animations too! In the video below I walk you through adding reusable animation functions that are built on RxJS and use them to animate our progress bar.
Here is a set of reusable RxJS higher order functions that can be used to animate ANYTHING! They were inspired by this amazing talk by Ben Lesh.
const distance = (d: number) => (t: number) => t * d;
const msElapsed = (scheduler = asyncScheduler) =>
defer(() => {
const start = scheduler.now();
return interval(0, scheduler).pipe(map(t => scheduler.now() - start));
});
const duration = (ms, scheduler = asyncScheduler) =>
msElapsed(scheduler).pipe(
map(elapsedMs => elapsedMs / ms),
takeWhile(t => t <= 1)
);
const prevAndCurrent = (initialValue: number) => (
source$: Observable<number>
) =>
source$.pipe(
startWith(initialValue),
bufferCount(2, 1)
);
const tween = (ms: number, easing: (t: number) => number) => (
source$: Observable<number>
) =>
source$.pipe(
prevAndCurrent(0),
takeWhile(([p, n]) => n <= 100),
switchMap(([p, n]) =>
duration(ms).pipe(
map(easing),
map(distance(n - p)),
map(v => n + v)
)
)
);
Now our initial progress update loop just has to update a BehaviorSubject
called percent$
let percentCurrent = 0;
let intervalId = setInterval(() => {
percentCurrent = getNewPercentValue(percentCurrent);
this.percent$.next(percentCurrent);
if (percentCurrent >= 100) {
clearInterval(intervalId);
}
}, 1000);
And our StackLayout
's [style.width]
attribute gets bound to a new Observable
called percentAnimated$
using the async pipe for automatic subscription.
<GridLayout rows="50,50" class="progressbar">
<StackLayout #pbVal col="0" class="progressbar-value" [style.width]="(percentAnimated$ | async) + '%'"></StackLayout>
<Button text="go" row="1" (tap)="onTap()"></Button>
</GridLayout>
When we initialize our component, we can just derive the new percentAnimated$
observable from the percent$
BehaviorSubject, and pipe it through the tween
animation function.
public ngOnInit(): void {
this.percentAnimated$ = this.percent$.pipe(
tween(400, d3.easeQuadOut)
);
}
*Note I've made a few other updates in styling, and I added a button to trigger the progress. So please see the full code listing here.
You can see me adding the RxJS animation implementation in this video:
Abstracting To Angular Pipe
Now that we have all the code in place, wouldn't it be nice to create a reusable "thing" that we can plug into any RxJS Observable that we want to "tween"? As it happens, Angular comes with a built-in concept called a Pipe
, which is just a class that implements the PipeTransform
interface and is declared along-side other Components in a module. If you don't know about Pipes, I suggest reading up on them, and then coming back here to watch the video below.
In this bonus video, I show you how to abstract away the code we've written so far in this post and in the previous videos of the progress bar series. The code is abstracted away into an Angular Pipe, making it more easily consumable.
If you like this style of video tutorials, you might like our courses. The latest Hands-on UI course is a pretty popular starting point for those starting out learning NativeScript and how to work with UI.
Let me know if you enjoyed this short tutorial on Twitter: @digitalix, and what else you'd like to see in these tutorials.