Animated Custom View — Driven by tests! — Part 2

Maciej Najbar
FAcademy
Published in
7 min readFeb 14, 2017

Welcome again! So in Part 1 we finished on drawing our custom view. Not much this far, probably the most boring part of all three. Let’s have a look on our TO-DO list. Bolded text is what yet to come:

  • 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

No time to hesitate any longer. Let’s get to work!

Animate and show FPS rate

To animate our view we need to refresh our view every frame. In Android, if something changes in our view, we need to call a special method called invalidate, so let’s do that.

@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(3), dotPaint);
invalidate();
}

Now, believe me or not, but our view is redrawn as fast as possible. Right after our dot is drawn, our view gets invalid and has to be redrawn — so it does happen.

To see how frequent the view is redrawn we’ll add frame counter in top-left corner.

@Override
protected void onDraw(Canvas canvas) {
canvas.drawText(“Frames per second: “, 0.f, 0.f, fpsPaint);
canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(3), dotPaint);
invalidate();
}

ALT+ENTER on fpsPaint and click Create field ‚fpsPaint’.

Again we go to activity_main.xml to see results. We build…

Oups — I forgot to instantiate an object. Sorry, I always forget. Let me get back to LoadingView and add a missing snippet in constructor.


fpsPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}

Now I can move back to activity_main.xml file, click build. Hmm.. Can you see any text? No? Me neither. Maybe it’s because I told canvas to draw the text in 0,0 position. I will get back to LoadingView and change position to be at 0,textSize.

@Override
protected void onDraw(Canvas canvas) {
canvas.drawText(“Frames per second: “, 0.f,fpsPaint.getTextSize(), fpsPaint);
canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(3), dotPaint);
invalidate();
}

Now I check it again in activity_main.xml, after building my project. I can see it now! But I think it takes too much. I’ll change text „Frame per second:” to just „FPS:”. Yup.. That’s fine by me.

But now I realized that it just shows text FPS, and I wanted to see some value. I’ll take that text out and make it formattable string.

public class LoadingView extends View {
private static final String FPS = “FPS: %d”;

@Override
protected void onDraw(Canvas canvas) {
canvas.drawText(FPS, 0.f, fpsPaint.getTextSize(), fpsPaint);

Ok, now in activity_main.xml it shows text “FPS: %d”. Not really what I meant.

We’ll need to create a tool that will help us calculate frames (of course TDD). We leave our LoadingView for now and let’s create a new class FramesManager.

ALT+ENTER on FramesManager class name and create a test, bearing in mind that it has to go to /test/ package.

Insert out first method:

@Before public void setUp() {
framesManager = new FramesManager();
}

ALT+ENTER on framesManager and click Create field ‚framesManager’.

Now we need to make sure that our frames manager can calculate frames.

@Test public void framesManagerMonitorsFramesProgress() {
framesManager.frame();
Assert.assertEquals(1, framesManager.getFramesCount());
}

ALT+ENTER on frame method and click create method.

ALT+ENTER on getFramesCount and click Create method ‚getFramesCount’.

You should now see the class FramesManager just like mine:

public class FramesManager {
public void frame() {
}
public int getFramesCount() {
return 0;
}
}

Run test class FramesManagerTest and watch the result. Fails — as expected.

Let’s follow the TDD process and make the smallest step we can. Let’s change result of getFramesCount to return 1:


public int getFramesCount() {
return 1;
}

Now we need to make sure, that our code does exactly what we mean. Create new test:

@Test public void makeSureFramesManagerIsAwareOfFramesPassed() {
framesManager.frame();
framesManager.frame();
framesManager.frame();
Assert.assertEquals(3, framesManager.getFramesCount());
}

Not really. Expected value is 3 but the actual is 1. Let’s fix that with the smallest step we can (I know you can throw up with that sentence and I know you can see the perfect solution instantly, but believe me, this is really good practice to do as little as possible).

Go back to FramesManager class and change getFramesCount method:

public int getFramesCount() {
return frames;
}

ALT+ENTER on frames and click Create field ‚frames’.

Run the tests now.

Crap! One of our test was already green, now both of them are red…

No worries, we invoke in our tests method frame that doesn’t do anything at all for now.

Ok, but in our tests we wanted to increment frame every time we call frame method. Let’s increment frames value in that method then.

public void frame() {
frames++;
}

Run tests.

Hell yeah! Both green. Now we are sure that invoking method frame makes our frame manager monitoring the progress of frames. Working code we can finally use in our LoadingView.

@Override
protected void onDraw(Canvas canvas) {
canvas.drawText(FPS, 0.f, fpsPaint.getTextSize(), fpsPaint);
canvas.drawCircle(
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(50),
loadingComputations.dpToPx(3), dotPaint);
framesManager.frame();
invalidate();
}

ALT+ENTER on framesManager and click Create field ‚framesManager’. Make sure framesManager field is type of FramesManager itself.

Let’s make use of our formattable string FPS and put our frames count into it:


canvas.drawText(String.format(Locale.getDefault(), FPS, framesManager.getFramesCount()), 0.f, fpsPaint.getTextSize(), fpsPaint);

Let’s go to activity_main.xml file and see what is shows in Preview now.

Ehh.. NullPointerException.. We need to initialize framesUpdater. Do me a favor and add in constructor this line:

framesManager = new FramesManager();

Now we can get back to our activity_main.xml file, build and see the result.

YES! One step at the time. We can see the FPS: and some digit. Let’s see it live. Run application on physical device.

Ooook. It does shows us frames, but not exactly Frames Per Second, but more like Frames Drawn So Far.

Now we need to make sure that our frames manager knows that every second it needs to reset the counter.

We go back to FramesManagerTest class and we create a new failing test:

@Test public void resetsFramesCountAfterOneSecond() throws Exception {
framesManager.frame();
framesManager.frame();
framesManager.frame();
Thread.sleep(1001);
framesManager.frame();
Assert.assertEquals(1, framesManager.getFramesCount());
}

After we run our test we can see that our frames manager is not aware of resetting. We need to check the difference between last frame and the current one if time is grater than 1 second. If it is, we reset frames count.

public void frame() {
long currentFrame = System.currentTimeMillis();
if (currentFrame — lastFrame > TimeUnit.SECONDS.toMillis(1)) {
frames = 0;
}
frames++;
lastFrame = currentFrame;
}

ALT+ENTER on lastFrame and click Create field ‚lastFrame’.

The time of our current frame, after all calculations, becomes our previous frame, so we can measure time between each two frames.

Let’s run our tests.

Wooohooo! Green! Thunderstruck!

But we need to be sure that it works as expected. One test is never convincing. One test means Happy Path. Let’s go to FramesManagerTest again and create another test — just to make sure:

@Test public void resetsFramesCountsAfterOneSecondInTotal() throws Exception {
framesManager.frame();
Thread.sleep(300); // 300ms
framesManager.frame();
Thread.sleep(300); // 600ms
framesManager.frame();
Thread.sleep(300); // 900ms
framesManager.frame();
Thread.sleep(300); // 1200ms
framesManager.frame();
assertEquals(1, framesManager.getFramesCount());
}

As I thought so.. Our previous test held process for 1s and that caused that our test worked. It didn’t work with more complicated scenario.

Let’s go back to FramesManager and change the code. Add another field that will be a summary of each time frame and then compare if after several frames, time is grater than 1s, if so, reset frames and clean timeSpan, so it can measure the time again.


timeSpan += currentFrame — lastFrame;
if (timeSpan > TimeUnit.SECONDS.toMillis(1)) {
timeSpan = 0;
frames = 0;
}

ALT+ENTER on timeSpan and click Create field ‚timeSpan’.

Run all our tests.

GREEN!!!! Uff.. I want to be sure though, I’ll make some refactor in our last test method, and make another round of frames. I changed method name to describe more precisely of whats the goal:

@Test public void resetsFramesCountsAfterOneSecondRepeatedly() throws Exception {
framesManager.frame();
Thread.sleep(300); // 300ms
framesManager.frame();
Thread.sleep(300); // 600ms
framesManager.frame();
Thread.sleep(300); // 900ms
framesManager.frame();
Thread.sleep(300); // 1200ms
framesManager.frame();
Thread.sleep(300); // 300ms
framesManager.frame();
Thread.sleep(300); // 600ms
framesManager.frame();
Thread.sleep(300); // 900ms
framesManager.frame();
Thread.sleep(300); // 1200ms
framesManager.frame();
Assert.assertEquals(1, framesManager.getFramesCount());
}

Run tests.

Tick, tock, tick, tock…..

Greeeeeeeeen!

Now that we know it works we can run our application on physical device and see the result.

Hmm.. It does resets the FPS after 1s, but FPS should be some static number, not something that draws frames numbers from 1 to X in a loop.

We need to create another variable that will keep FPS and refresh it every second.

Ok, so open class FramesManagerTest and insert test method like the one below:

@Test public void showsFramesPerSecond() throws Exception {
framesManager.frame();
framesManager.frame();
framesManager.frame();
Thread.sleep(1001);
framesManager.frame();
assertEquals(3, framesManager.fps());
}

ALT+ENTER on fps and click Create method ‚fps’.

Run tests.

Failing.. I know it can annoy you so far, but make this newly created method to return 3.

Green. But we know — Happy Path, so let’s create a new test method:

@Test public void showsCorrectFramesPerSecond() throws Exception {
framesManager.frame();
framesManager.frame();
framesManager.frame();
Thread.sleep(1001);
framesManager.frame();
framesManager.frame();
Thread.sleep(1001);
framesManager.frame();
assertEquals(2, framesManager.fps());
}

Run tests. You were right. It’s failing.

Now we can change our code:

public int fps() {
return fps;
}

ALT+ENTER on fps and click Create field ‚fps’.

Run tests. No way! Two tests fail.

From the tests we can read that we want to save frames count right before reset.

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;
}

Run tests.

Hurray! Green again. Our code is so stable..

Let’s now use that new method in our LoadingView:


canvas.drawText(String.format(Locale.getDefault(), FPS, framesManager.fps()), 0.f, fpsPaint.getTextSize(), fpsPaint);

And let’s see the result on physical device. Run the app.

Wow.. it works. And it works really nice. I get result on my device, something between 60–62 FPS. Pretty good result.

Closure

Congratulations! You finished second part of this journey. Animating this view was a big step forward. I know it doesn’t do much, but it’s a small piece of something much bigger. Read Part 3 and get the job done.

--

--