Animated Custom View — Driven by tests! — Part 3
Part 3 seems to be a long one. But it’s worth it — I promise. In this part we’ll dive more into virtual presentation. If you want to finish this part faster, read the note “optional” under next header.
So, let’s remind ourselves what we’ve accomplished so far:
- Extend View
- Set fixed size
- Set background to show view’s area
- Draw a dot
- Animate and show FPS rate
- Cut FPS to at most 60 FPS (optional)
- Move a dot on sinusoidal path
- Make sure dot jumps only up
- Add other 2 dots so it’s 3 shown
- Add time offset so they bounce independently
Cut FPS to at most 60 FPS (optional)
Note:
If you’d like to skip this part — because you’re not interested or you want to return to it later — just when you encounter method “framesManager.canGo()” make sure its implementation always returns true.Go directly to headline “Move a dot on sinusoidal path” — from here it should be 5mins reading.
Lots of wise (gaming) articles claim that 60 FPS gives us the smoothest animation and it became a standard for animations. So why won’t we stick to this rule and draw at most 60 FPS.
We should understand now what exactly means Frames Per Second. It’s a number of drawn images within elapsed time and more precisely 1 second.
60 FPS = 60frames / 1s = 60frames / 1000ms.
So if we want to have 60 FPS, we need to calculate the time that is allowed for us to draw 1 frame.
60 frames / 1s = 1frame / time
60 / 1s = 1 / time
60*time = 1s
time = 1s / 60 = 1000ms / 60 = 16.(6)ms
16.(6)ms! This is the time that we have to draw 1 frame!
So.. now we’re smarter with this information.
But what if we don’t need 60 Frames Per Second, maybe we just need 30.
Let’s do that, just for fun. 60 is perhaps what your device gets by default. So let’s do 30 FPS to see if that actually works as expected.
Create a test:
@Test public void frameShouldBeSkipped() {
framesManager.frame(); // Initialization frame (we need previous frame’s time)
framesManager.frame();
assertEquals(false, framesManager.canGo());
}
ALT+ENTER on canGo and Create method ‚canGo’.
You receive method implementation like this:
public boolean canGo() {
return false;
}
When you run your test, it will pass. This means it’s a wrong test. Test at first should fail. So in that situation we have to return here true, like this:
public boolean canGo() {
return true;
}
Run test and watch it burn.
We can start implementation. Let’s change that to false:
public boolean canGo() {
return false;
}
Unfortuntely I’m not kidding. This will make our test pass and isn’t it something that we really want?
Having our tests pass, we can create another test — that is not a Happy Path:
@Test public void frameCanBeDrawn() throws Exception {
framesManager.frame(); // Initialization frame (we need previous frames’s time)
Thread.sleep(100);
framesManager.frame();
Assert.assertEquals(true, framesManager.canGo());
}
Run the tests, and it turns out that this one fails — classic.
So we go back to the code in FramesManager class and we change canGo implementation:
public boolean canGo() {
return timeFrame > TIME_PER_FRAME;
}
ALT+ENTER on timeFrame and click Create field ‚timeFrame’ — make sure its type is long.
ALT+ENTER on TIME_PER_FRAME and click Create content field ‚TIME_PER_FRAME’ — this one should be of type double. Like below:
private static final double TIME_PER_FRAME = 1000 / 30;
When we run our test we can see that nothing changed. It’s the fact that timeFrame is 0 and it’s never grater than 1000 / 30. We need to get the time between each two frames. I remember we already implemented this one.
Let’s look into our frame method:
public void frame() {
long currentFrame = System.currentTimeMillis();timeSpan += currentFrame — lastFrame;
if (timeSpan > TimeUnit.SECONDS.toMillis(1)) {
timeSpan = 0;
fps = getFramesCount();
frames = 0;
}frames++;
lastFrame = currentFrame;
}
There it is. Bolded. I knew we had it, but we used it for calculating the timeSpan (which is the period of 1s). We need to take it out for our timeFrame and we’ll add timeFrame to timeSpan. Sounds good!
…
timeFrame = currentFrame — lastFrame;
timeSpan += timeFrame;
…
Done.
Run tests.
Yaay! Tests claim it works correct. So do I then.
Now we can go to LoadingView and we can make use of our canGo method. So what we want to do? We want to draw only if canGo returns true. That’s why we refactor our drawing function and we have something like:
@Override
protected void onDraw(Canvas canvas) {
if (framesManager.canGo()) {
canvas.drawText(String.format(Locale.getDefault(), FPS, framesManager.fps()), 0.f, fpsPaint.getTextSize(), fpsPaint);canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(3), dotPaint);
}framesManager.frame();
invalidate();
}
We need to leave invalidate method outside of if block, because otherwise onDraw would finish its run without forcing our view to refresh.
We also leave frame method outside, since it calculates our time between two frames, which is needed for canGo method to determine if it can draw or not.
Let’s run our App on physical device.
Ok… Now it draws nothing. Nevertheless tests pass, we don’t get what we wanted. This proves that TDD approach isn’t god like, but definitely reduces the chances of failure and if we break something we’ll know it.
We need to create another test then. Something that will go the buggy path. Let’s think a little…
So what we know so far.
1. We know from our tests, that if time between last and current frame is less than 1000 / 30, then it doesn’t allow to draw.
2. We also know that if time between last and current frame is more than 1000 / 30, then it does allow to draw.
Where is the problem then?
View draws frames much faster than 1000 / 30[ms], let’s add a test that will cover this scenario:
@Test public void doubleCheckIfFrameCanBeDrawn() throws Exception {
framesManager.frame(); // Initialization frame (we need previous frames’s time)
Thread.sleep(20);
framesManager.frame();
Thread.sleep(20);
framesManager.frame();
Assert.assertEquals(true, framesManager.canGo());
}
Uff, it failed. That’s a good sign — because we just found a bug that we can fix forever.
It resets our timeFrame each turn. The common sense tells us that yes, first frame shouldn’t be drawn, but the second frame is far later than 33ms (because over 40ms) so it definitely should.
Let’s go to our code and change the logic of frame method to fix our test.
public void frame() {
long currentFrame = System.currentTimeMillis();long timeStep = currentFrame — lastFrame;
timeFrame += timeStep;
timeSpan += timeStep;
if (timeSpan > TimeUnit.SECONDS.toMillis(1)) {
timeSpan = 0;
fps = getFramesCount();
frames = 0;
}if (canGo()) {
frames++;
timeFrame = 0;
}lastFrame = currentFrame;
}
Look at the bolded parts. I sum the time between frames, but I also check if I should increment frames and when I do it, I reset timeFrame variable.
Let’s run our tests.
OMG.. We broke tests a lot. Due to the fact we are depending on time now, we need to change our tests that fail, since they are not time aware now.
We will now use some TDD practice that helps us play with time. Ladies and Gentlemen let me introduce you the TimestampProvider. It looks like following:
public interface TimestampProvider {
long timestamp();
}
Create this interface.
We will also need its implementation, so let’s create a class that implements our layer of abstraction, and call it RealTimestampProvider. This will return our current time:
public class RealTimestampProvider implements TimestampProvider {
@Override
public long timestamp() {
return 0;
}
}
ALT+ENTER on RealTimestampProvider class and click Create Test. Again, make sure it’s /test/ folder in the path.
So as always we start with:
@Before public void setUp() {
realTimestampProvider = new RealTimestampProvider();
}
ALT+ENTER on realTimestampProvider and click Create field ‚realTimestampProvider’.
We can create our test:
@Test public void providerGivesUsCurrentTime() {
long now = System.currentTimeMillis();
Assert.assertEquals(now, realTimestampProvider.timestamp());
}
We run it, and it fails. Which means that we can go to the code and change it to what we meant.
@Override
public long timestamp() {
return System.currentTimeMillis();
}
This time it’s about time, so we can’t really guess the long number because it changes too quickly, so we won’t need to double check it works. We just go and put „currentTimeMillis”.
Let’s run our test.
Does it pass? Does it fail? I don’t know, sometimes it passes and sometimes it fails. This is called „Flaky test” because test should always pass or should always fail. When it’s half way there it’s not a reliable test.
It depends on how quickly time elapsed between one timestamp call and the next one. Let’s be more precise and let’s put nanoTime into test:
@Test public void providerGivesUsCurrentTime() {
long now = System.nanoTime();
Assert.assertEquals(now, realTimestampProvider.timestamp());
}
And into timestamp method:
@Override
public long timestamp() {
return System.nanoTime();
}
Now we can see that our test fails every time. So we need to check if the time is correct for some delta time.
Let’s make sure that our delta time is not grater than 2ms. If in the application we have 2ms of error than I can live with that.
So let’s fix our test first:
@Test public void providerGivesUsCurrentTime() {
long now = System.nanoTime();
Assert.assertTrue(“Timestamp fits 2ms window”, realTimestampProvider.timestamp() — now < TimeUnit.MILLISECONDS.toMillis(2));
}
We run our test now and boom! It passes every time. Ok, so if something works for nano seconds it will definitely work for mili seconds. Let’s change our implementation to millis again. First in tests:
@Test public void providerGivesUsCurrentTime() {
long now = System.currentTimeMillis();
Assert.assertTrue(“Timestamp fits 2ms window”, realTimestampProvider.timestamp() — now < TimeUnit.MILLISECONDS.toMillis(2));
}
And in RealTimestampProvider:
@Override
public long timestamp() {
return System.currentTimeMillis();
}
Let’s run our tests at least 10 times and see results.
Works for me! We can start using our reliable timestamp provider.
But when it comes to tests we would like to have a possibility to cheat a little with time. We would like to have control, to add time if we need it and how much we need without waiting for it.
For these occasions we have to create a new timestamp provider class — fake. Let’s create a new class FakeTimestampProvider that implements TimestampProvider interface.
public class FakeTimestampProvider implements TimestampProvider {
@Override
public long timestamp() {
return 0;
}
}
ALT+ENTER on FakeTimestampProvider class name and click Create test.
@Before public void setUp() {
fakeTimestampProvider = new FakeTimestampProvider();
}
ALT+ENTER on fakeTimestampProvider and click Create field ‚fakeTimestampProvider’.
Now test:
@Test public void timestampReturnsOneSecond() {
long second = TimeUnit.SECONDS.toMillis(1);
fakeTimestampProvider.add(second);
Assert.assertEquals(second, fakeTimestampProvider.timestamp());
}
ALT+ENTER on add and click Create method ‚add’ — in FakeTimestampProvider, because it will only be this class’s feature to manipulate the time.
Let’s run our test… Failed. Good — I’m really surprised that I’m glad that my tests fail.
Let’s go back to the implementation of our Fake class.
public class FakeTimestampProvider implements TimestampProvider {
@Override
public long timestamp() {
return 1000;
}public void add(long millis) {
}
}
Passed.
But let’s be sure. New test:
@Test public void timestampReturnsElapsedTime() {
fakeTimestampProvider.add(1000);
fakeTimestampProvider.add(500);
Assert.assertEquals(1500, fakeTimestampProvider.timestamp());
}
Ok. When we run it we see that it failed. It returned one second when we specifically provided 1s and 500ms elapsed. We gotta fix it if we want to use it after all.
Let’s go back to the code and change it to this final version:
public class FakeTimestampProvider implements TimestampProvider {
private long timeInMillis;@Override
public long timestamp() {
return timeInMillis;
}public void add(long millis) {
timeInMillis += millis;
}
}
Run the tests. They both passed, which means that it supposed to work.
Ok, now we can use our timestamp provider in our test cases. Let’s move back to FramesManagerTest class and the first thing we need to do is to pass dependency to FramesManager. It will depend on TimestampProvider interface in tests and in real application. So let’s do this. In setUp method change the code to following:
@Before public void setUp() {
framesManager = new FramesManager(fakeTimestampProvider);
}
ALT+ENTER on fakeTimestampProvider and click Create field ‚fakeTimestampProvider’ — with type of our FakeTimestampProvider.
Again ALT+ENTER on fakeTimestampProvider in FramesManager constructor and click Create constructor.
The constructor should use TimestampProvider interface not our class implementation (SOLID rules):
public FramesManager(TimestampProvider timestampProvider) {
}
ALT+ENTER on timestampProvider and click Create field for parameter ‚timestampProvider’. The result should be like this:
private TimestampProvider timestampProvider;
Let’s run our tests of FramesUpdaterTest class and see if it helped.
So.. While compiling tests, my LoadingView cried about FramesManager parameter. Fortunately we have implementation of RealTimestampProvider, so let’s pass it there. This provider is meant to be used by real time view.
…
framesManager = new FramesManager(new RealTimestampProvider());
…
And run tests for FramesUpdaterTest class again. Ok, so it didn’t help at all… Right, we didn’t use it!
Ok, so wherever we used System.currentTimeMillis, we want to use our provider now. Which is here:
public void frame() {
long currentFrame = timestampProvider.timestamp();
…
Let’s run again. All failed… After looking into logs we can see it’s NullPointerException. Uff.. We just need to create an instance of our FakeTimestampProvider in tests:
@Before public void setUp() {
fakeTimestampProvider = new FakeTimestampProvider();
framesManager = new FramesManager(fakeTimestampProvider);
}
Run tests… It’s even worse than it used to be before we used that timestamp provider. But it’s a step forward. Believe me. I also had doubts when I was told by David that I’ll understand it one day. It’s like a Rubic’s cube. When you start breaking it you want to move back to the state you thought was closer to the end. Unfortunately without breaking it, we will not go further. So let’s start fixing what we broke. We need to control the time of frames.
Let’s go test by test. The first one is „framesManagerMonitorsFramesProgress” timestamp provider needs to tell it what time it is:
@Test public void framesManagerMonitorsFramesProgress() {
framesManager.frame();
fakeTimestampProvider.add(1000 / 30 + 1);
Assert.assertEquals(1, framesManager.getFramesCount());
}
I added one TIME_PER_FRAME value + 1 ms to make sure it goes to the next frame.
But it fails. Of course it fails, because it should have initialization frame to make sure we have last frame’s timestamp so the subtraction of current frame and last frame returns positive result. So let’s correct our test:
@Test public void framesManagerMonitorsFramesProgress() {
framesManager.frame(); // Initialization frame
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
Assert.assertEquals(1, framesManager.getFramesCount());
}
Run tests………… Passed! Great. So let’s do next one:
@Test public void makeSureFramesManagerIsAwareOfFramesPassed() {
framesManager.frame(); // Initialization frame
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
Assert.assertEquals(3, framesManager.getFramesCount());
}
Passed!
It’s time to present the next rule of TDD.
If something repeats at least three times, it should be simplified to some common code.
I can see few things so far (based on those two tests above).
1. Initialization frame — we can move it to setUp method. This method runs before each test anyway, so why we have to remember of initialization frame in our test cases if this one call can do that.
2. 1000 / 30 + 1 — this can become a static final field
3. Repetition of add and frame. Let’s close it in a loop.
1. Initialization frame:
@Before public void setUp() {
fakeTimestampProvider = new FakeTimestampProvider();
framesManager = new FramesManager(fakeTimestampProvider);
framesManager.frame(); // Initialization frame
}
…
@Test public void framesManagerMonitorsFramesProgress() {
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
Assert.assertEquals(1, framesManager.getFramesCount());
}
…
@Test public void makeSureFramesManagerIsAwareOfFramesPassed() {
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
fakeTimestampProvider.add(1000 / 30 + 1);
framesManager.frame();
Assert.assertEquals(3, framesManager.getFramesCount());
}
2. 1000 / 30 + 1
private static final long TIME_PER_FRAME = 1000 / 30 + 1;
…
fakeTimestampProvider.add(TIME_PER_FRAME);
3. Loop
@Test public void makeSureFramesManagerIsAwareOfFramesPassed() {
for (int i = 0; i < 3; ++i) {
fakeTimestampProvider.add(TIME_PER_FRAME);
framesManager.frame();
}
Assert.assertEquals(3, framesManager.getFramesCount());
}
Much better.. Now my eyes stopped bleeding.
Let’s continue with our great improvements:
@Test public void resetsFramesCountAfterOneSecond() {
for (int i = 0; i < 3; ++i) {
fakeTimestampProvider.add(TIME_PER_FRAME);
framesManager.frame();
}fakeTimestampProvider.add(1001);
framesManager.frame();
Assert.assertEquals(1, framesManager.getFramesCount());
}
Notice here that I removed Thread.sleep method, because I don’t rely on real time now, but on my fake timestamp provider instead. This also speeds up our tests, because we don’t need to wait 1s for our test to continue. We just tell our application that it’s 1s later when it elapsed in real 1ms. Isn’t that awesome?!
Next test:
@Test public void resetsFramesCountsAfterOneSecondRepeatedly() {
for (int i = 0; i < 8; ++i) {
fakeTimestampProvider.add(300);
framesManager.frame();
}
Assert.assertEquals(1, framesManager.getFramesCount());
}
Passed again! Lightning mode!
Next test:
@Test public void showsFramesPerSecond() throws Exception {
for (int i = 0; i < 3; ++i) {
fakeTimestampProvider.add(TIME_PER_FRAME);
framesManager.frame();
}
fakeTimestampProvider.add(1001);
framesManager.frame();
Assert.assertEquals(3, framesManager.fps());
}
Passing again!
Next test:
@Test public void showsCorrectFramesPerSecond() throws Exception {
for (int i = 0; i < 3; ++i) {
fakeTimestampProvider.add(TIME_PER_FRAME);
framesManager.frame();
}fakeTimestampProvider.add(1001);
for (int i = 0; i < 2; ++i) {
fakeTimestampProvider.add(TIME_PER_FRAME);
framesManager.frame();
}fakeTimestampProvider.add(1001);
framesManager.frame();
Assert.assertEquals(2, framesManager.fps());
}
Passed!
Next test — this one passed, but we should change it anyway, to be using our current flow:
@Test public void frameShouldBeSkipped() {
fakeTimestampProvider.add(1);
framesManager.frame();
Assert.assertEquals(false, framesManager.canGo());
}
This test actually with our current implementation of the code is useless. Because if frame cannot be drawn it passes, but also if the frame can be drawn it passes as well, because frame is drawn and value of timeFrame is reset to 0, which gives us again result that it can’t be drawn. But let’s leave it. Test are more like story teller of what we want to achieve but running tests it gives us information if we succeeded in delivery.
Next test:
@Test public void frameCanBeDrawn() throws Exception {
fakeTimestampProvider.add(100);
framesManager.frame();
Assert.assertEquals(true, framesManager.canGo());
}
Crap.. it fails no matter what. This means that our current solution is not correct. We can’t let tests fail. We need to fix that. Let’s go back to the code then.
public void frame() {
if (canGo()) {
timeFrame = 0;
}long currentFrame = timestampProvider.timestamp();long timeStep = currentFrame — lastFrame;
timeFrame += timeStep;
timeSpan += timeStep;
if (timeSpan > TimeUnit.SECONDS.toMillis(1)) {
timeSpan = 0;
fps = getFramesCount();
frames = 0;
}if (canGo()) {
frames++;
}lastFrame = currentFrame;
}
I divided our condition into two. At the beginning I’m checking if I should reset the timeFrame. In the second I check if I should increment frames count.
Let’s check if we didn’t break previous test by that change by running all of them.
Fortunately all of them pass. Phew!
Last test!
@Test public void doubleCheckIfFrameCanBeDrawn() throws Exception {
for (int i = 0; i < 2; ++i) {
fakeTimestampProvider.add(20);
framesManager.frame();
}
Assert.assertEquals(true, framesManager.canGo());
}
Passed!
All tests pass again!
One giant leap for the programmer, one small step for mankind. But that’s us — programmers. Nobody knows what effort we put into making their lives easier.
Let’s run our Application on physical device one more time and let’s see if we made any progress.
Nooooo. Something doesn’t work as expected. It cuts values by half. When I set TIME_PER_FRAME to 1000 / 30 I get about ~22 FPS, when I set 1000 / 60, I get ~44 FPS
But the worst is that it scintillates..
I realized that we’re losing too much time in our calculations. If one time step between frame and a frame is 28ms, we get only every two frames.
- 28[ms]
- 56[ms] — reset
- 28[ms]
- 56[ms] — reset
But there is our precious time that we loose. Let’s create a test to prove it:
@Test public void doesNotLooseTimeForTimeFrame() {
for (int i = 0; i < 3; ++i) {
fakeTimestampProvider.add(TIME_PER_FRAME / 2 + TIME_PER_FRAME / 3);
framesManager.frame();
}
Assert.assertEquals(2, framesManager.getFramesCount());
}
Let’s make a change in the code:
public void frame() {
if (canGo()) {
timeFrame %= TIME_PER_FRAME;
}long currentFrame = timestampProvider.timestamp();long timeStep = currentFrame — lastFrame;
timeFrame += timeStep;
timeSpan += timeStep;
if (timeSpan > TimeUnit.SECONDS.toMillis(1)) {
timeSpan = 0;
fps = getFramesCount();
frames = 0;
}if (canGo()) {
frames++;
}lastFrame = currentFrame;
}
Replace your method, run tests. All pass! Good — another good did!
But 30FPS scintillates too much. Let’s go back to 60 FPS. Change value of TIME_PER_FRAME to 1000 / 60 in FramesManager and FramesManagerTest.
Move a dot on sinusoidal path
Now we can move our dot.
First math.
Sinusoid has it’s period from 0–2π. Which means:
sin(0) = sin(2π)
Ok, so we need one more thing. Due to the fact we don’t monitor our time with π fractions we need to make sure we do so. We need to determine how long our animation must last. Let’s say 1s — I love 1s.
Ok. So if:
1[s] = 2*π
Then for our ’t’ that we know, because we will pass that (in ms):
t[ms] = X, where X is a sinus parameter
This gives us X * 1[s] = 2*π*t[ms]…
X = 2*π*t[ms]/1000[ms] = 2*π*t / 1000
Tests! Tests! Tests!
We need to make sure that our code will give us the correct result of the equation above. We can go back to our forgotten class LoadingComputationTest class and create a new test:
@Test public void calculatesSinusoidResultBasedOnTime() {
loadingComputations = new LoadingComputations(0.f);
Assert.assertEquals(0., loadingComputations.verticalPosition(0), 0.002);
}
ALT+ENTER on verticalPosition and click Create method ‚verticalPosition’.
public double verticalPosition(long time) {
return 1;
}
Let’s return 1 to meet the expected failure of our test.
After our test watching failed we can change it back to 0 and be happy we made one test more to pass!
But it’s not over, we know we want to use our fantastic equation. So we need to create a new method that will check other parts of sinusoid and compare the results. We know value for:
1. sin(π/6) = 1/2
2. sin(π/4) = √2 / 2
3. sin(π/2) = 1
This would mean that time value would equal respectively:
1. ~83ms
2. 125ms
3. 250ms
Let’s put those numbers into our test by extending existing one. Unit test must check one thing, but it is one thing but couple of different parameters.
@Test public void calculatesSinusoidResultBasedOnTime() {
loadingComputations = new LoadingComputations(0.f);
Assert.assertEquals(0., loadingComputations.verticalPosition(0), 0.002);Assert.assertEquals(1./2, loadingComputations.verticalPosition(83), 0.002);Assert.assertEquals(Math.sqrt(2)/2, loadingComputations.verticalPosition(125), 0.002);Assert.assertEquals(1, loadingComputations.verticalPosition(250), 0.002);
}
Ok, so after running we can see that it fails on our second assertion. Let’s fix that in the code.
We know that we want our equation to start playing the main role already. Change the code to:
public double verticalPosition(long time) {
double X = 2 * Math.PI * time / ANIMATION_LENGTH;
return Math.sin(X);
}
ALT+ENTER on ANIMATION_LENGTH and click Create constant field ANIMATION_LENGTH — with type of long and value 1000.
We can run tests!
Seems to work! Let’s put it into our LoadingView and see what happens.
@Override
protected void onDraw(Canvas canvas) {
if (framesManager.canGo()) {
canvas.drawText(String.format(Locale.getDefault(), FPS, framesManager.fps()), 0.f, fpsPaint.getTextSize(), fpsPaint);canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50) * (float)loadingComputations.verticalPosition(System.currentTimeMillis()),
loadingComputations.dpToPx(3), dotPaint);
}framesManager.frame();
invalidate();
}
Run our application on physical device and see for yourself.
Make sure dot jumps only up
It animates! Not exactly what we expected! But isn’t that exciting?! Ok, so what we want, is to have it inside our view frame. We know that it jumps right now with value from -1 to 1, so when it’s 1 it is in the center of view, when it’s 0 it’s on top and when it’s -1 it’s totally out of scope.
We want our equation to return only values from 0 to 1. To do that, we need to modify our test:
@Test public void calculatesSinusoidResultBasedOnTime() {
loadingComputations = new LoadingComputations(0.f);Assert.assertEquals((0. + 1)/2, loadingComputations.verticalPosition(0), 0.002);Assert.assertEquals((1./2 + 1)/2, loadingComputations.verticalPosition(83), 0.002);Assert.assertEquals((Math.sqrt(2)/2 + 1)/2, loadingComputations.verticalPosition(125), 0.002);Assert.assertEquals((1 + 1)/2, loadingComputations.verticalPosition(250), 0.002);
}
Because + 1 will give us values from 0 to 2 we need to also devide the whole result by 2.
Run the test. Test fails. Good.
Now back to the code. And we do the same:
public double verticalPosition(long time) {
double X = 2 * Math.PI * time / ANIMATION_LENGTH;
return (Math.sin(X) + 1) / 2.;
}
Run the App now. Ohh yeah! It’s animating within the frame!
But I don’t think that this should go this much up and down. Let’s cut its way to let’s say 20dp.
Go to LoadingView and and change onDraw method to:
@Override
protected void onDraw(Canvas canvas) {
if (framesManager.canGo()) {
canvas.drawText(String.format(Locale.getDefault(), FPS, framesManager.fps()), 0.f, fpsPaint.getTextSize(), fpsPaint);canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(20) * (float)loadingComputations.verticalPosition(System.currentTimeMillis()),
loadingComputations.dpToPx(3), dotPaint);
}framesManager.frame();
invalidate();
}
Subtracting pixels from base position moves dot up.
Looks better, doesn’t it?
Add other 2 dots so it’s 3 shown
We can add now two other dots. Let’s say they will be in a distance of 12dp from one another, this will give us a 6dp space between them (radius of one is 3dp and the other 3dp as well which gives us 6dp in total just for dots).
Expand our onDraw method by additional commands for drawing circles:
@Override
protected void onDraw(Canvas canvas) {
if (framesManager.canGo()) {
canvas.drawText(String.format(Locale.getDefault(), FPS, framesManager.fps()), 0.f, fpsPaint.getTextSize(), fpsPaint);canvas.drawCircle(
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(12),
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(20) * (float)loadingComputations.verticalPosition(System.currentTimeMillis()),
loadingComputations.dpToPx(3), dotPaint);canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(20) * (float)loadingComputations.verticalPosition(System.currentTimeMillis()),
loadingComputations.dpToPx(3), dotPaint);canvas.drawCircle(
loadingComputations.dpToPx(50) + loadingComputations.dpToPx(12),
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(20) * (float)loadingComputations.verticalPosition(System.currentTimeMillis()),
loadingComputations.dpToPx(3), dotPaint);
}framesManager.frame();
invalidate();
}
One on the left, one on the right and one in center.
Isn’t that pure joy seeing our code working that easily? Everything thanks to the testing!
Add time offset so they bounce independently
We would like to see them going independently though. We should definitely add offsets so their time is different for each one.
Ready! Tests! Go!
LoadingComputationsTest class and add test that will calculate sinusoid values but for time that offsets let’s say 125ms (for simplicity in tests):
@Test public void calculatesSinusoidResultBasedOnTimeWithOffset125ms() {
loadingComputations = new LoadingComputations(0.f);Assert.assertEquals((0. + 1)/2, loadingComputations.verticalPosition(-125, 125L), 0.002);Assert.assertEquals((Math.sqrt(2)/2 + 1)/2, loadingComputations.verticalPosition(0, 125L), 0.002);Assert.assertEquals((1 + 1)/2, loadingComputations.verticalPosition(125, 125L), 0.002);
}
ALT+ENTER on verticalPositions’s parameters and click Add ‚long’ as 2nd parameter..
Now you need to find all the places that use this method and insert value 0, so we don’t break previous calculations.
Run tests. As expected only our last test failed. Let’s fix it right away.
We need to add to our previous time value offset and include it in calculations. In that case we get:
public double verticalPosition(long time, long offset) {
double X = 2 * Math.PI * (time + offset) / ANIMATION_LENGTH;
return (Math.sin(X) + 1) / 2.;
}
Run tests again — Passed!
We can now add this behavior to our LoadingView. Let’s get back to our onDraw method and let’s add offset 125 (it doesn’t have to be 125ms — it was perfect number for tests because we knew exact results of sinus for 0, 125 and 250. Now in the real example you can use any offset you want).
In onDraw:
@Override
protected void onDraw(Canvas canvas) {
if (framesManager.canGo()) {
canvas.drawText(String.format(Locale.getDefault(), FPS, framesManager.fps()), 0.f, fpsPaint.getTextSize(), fpsPaint);canvas.drawCircle(
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(12),
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(20) * (float)loadingComputations.verticalPosition(System.currentTimeMillis(), 0),
loadingComputations.dpToPx(3), dotPaint);canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(20) * (float)loadingComputations.verticalPosition(System.currentTimeMillis(), 125),
loadingComputations.dpToPx(3), dotPaint);canvas.drawCircle(
loadingComputations.dpToPx(50) + loadingComputations.dpToPx(12),
loadingComputations.dpToPx(50) — loadingComputations.dpToPx(20) * (float)loadingComputations.verticalPosition(System.currentTimeMillis(), 250),
loadingComputations.dpToPx(3), dotPaint);
}framesManager.frame();
invalidate();
}
Run the App and see the results.
Closure
I’m not going to extend that view further in this article. In fact, I wanted to create a much bigger view for showing a wallet value. I haven’t expected that just for loading I will write this much. It’s my first article to public, which means I’m not aware of how long the articles should be, but I wanted to show every reader what are the key points of programming and building a big effect from very little steps.
This view is not perfect, there is still much to be done e.g.:
- Add separate thread that watches the time for each frame and refreshes it when it’s time — that would eliminate scintillation when we have lower FPS than maximum our machine can do. Now, you can just change TIME_PER_FRAME to 1000 / 100 and I don’t think any device on the market can draw over 100 FPS, so it will draw each frame.
- I would add also a rest time for our dots, because now they run indefinitely and after 30 seconds it gets boring.
- I’d also add some style able parameters to personalize our view.
- I also thought of possibility to change our view’s size
- Probably many others.
But I think after this article you will gain a powerful tool which is knowledge of how to start with TDD.
If someone got that far (to the end of this article), I’m really amazed and flattered at the same time.
Thanks!