Animated Custom View — Driven by tests! — Part 1

Maciej Najbar
FAcademy
Published in
7 min readFeb 12, 2017

--

Edit: I’ve received a feedback about this article that it’s too long for a single article and it should be divided into parts. Also I would like to thank a lot Kamil Burczyk who studied this story thoroughly and pointed out few very interesting improvements. This story may be extended with future steps for a final view, so stay tuned.

Creating Custom Views in Android is probably the most common phrase searched in Google. Recently it’s louder and louder about Test Driven Development approach in Android development. Why won’t we join these two topics together and go step-by-step while creating a custom view driven by tests.

Not long time ago I needed to create some „cool” way of presenting balance in a wallet. I figured out that it will probably be the best chance to introduce it as custom view.

I work for Grand Parade (William Hill) as Android Developer and my direct leader is David Marin (who used to work for Shazam). He doesn’t let feature branches be merged to master if tests don’t cover at least 80% of the code.

This guy was mentoring me to dive into TDD. I found completely no practical tutorials on the Internet which would show more complicated examples than e.g. email verifier. Which is good example, but definitely too easy to see that process in action.

I decided to present of what I’ve learned so far using Test Driven Development on real example. It’s not going to show the full view, because while writing this article it turned out that that I had written 30 pages just to show TDD in action for a loading view.

You can find the code here: https://github.com/macieknajbar/AnimatedCustomViewTDD

Are you ready?

First of all, we need to have a plan of what we want to achieve. So we need to have and idea of how something should look like and a TO-DO list of steps.

Bear in mind I’m using Android Studio on Mac OS. Shortcuts may differ on different software.

Idea

Let’s have a look at the view I will present in this tutorial:

TO-DO:

  • 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

Extend View

Start new Android project with an empty Activity (for simplicity of this tutorial leave Activity’s name MainActivity and xml’s name activity_main.xml).

Create a new Java class — LoadingView — as I did. Extend View class from Android SDK.

public class LoadingView extends View {  public LoadingView(Context context, AttributeSet attrs) {    super(context, attrs);  }}

Set fixed size

I always prefer to use dp units instead of pixels. To do that we need a conversion method.

Let’s start Test Driven Development.

Before I start I want to say, that we’re not going to test LoadingView itself, but we will use tested code inside.

Let’s create a new class then — LoadingComputations.

public class LoadingComputations {}

ALT+ENTER on class name and click Create test.

Confirm and make sure it goes to /test/ folder. Initialize test class:

import org.junit.Before;public class LoadingComputationsTest {  private LoadingComputations loadingComputations;  @Before public void setUp() {    loadingComputations = new LoadingComputations();  }}

Our first test class. We’re good to go!

Start with defining first test:

@Test public void checksComputerAbilityToMultiply() {
}

If we run this test now, it will pass — it’s empty it checks nothing, if something is not false, then it’s true. But Test Driven Development tells us that:

Defined test must fail first and we must make it green by the power of code.

Let’s add a body:

@Test public void checksComputerAbilityToMultiply() {  Assert.assertEquals(2.5f, loadingComputations.dpToPx(1), 0.001);}

ALT+ENTER on highlighted missing method and click Create method ‚dpToPx’. You may notice 0.001 value which is an accuracy against float value.

This should result of code in LoadingComputation’s class, similar to this:

float dpToPx(int size) {
return 0;
}

Run the test and witness its failure!

YES!

java.lang.AssertionError:Expected :2.5Actual :0.0

Go back to code and change method ‚dpToPx’ to:

float dpToPx(int size) {
return 2.5f;
}

I know you started to swear right now — I did that too. I was turning into red, because I thought David was messing around with me. I wanted to shut down the computer or else he treats me with respect.

He just said „it’s TDD man”. I thank him for that.

Ok.. Be patient and we’ll go to the end. So let’s run the test and check if it’s green.

Yes — it is… surprising.

Create another test and make sure this works as expected:

@Test public void doubleCheckConversionIsCorrect() {  Assert.assertEquals(7.5f, loadingComputations.dpToPx(3), 0.001);}

Uff.. It’s not magic after all. Our code is wack! We need to fix it.

Now we can change definition of our method:

float dpToPx(int size) {  return size * 2.5f;}

Run entire test class and watch the whole test case turning into green. Pretty amazing seeing your code passing tests — I hope you feel the same way.

But wait a minute.. Something’s not right. What’s that magic number 2.5f? This should be device’s density. What if density is 2.0f or 3.0f? Will our code still work as expected?

Let’s find out:

@Test public void convertForDensityEqual3() {  Assert.assertEquals(9.f, loadingComputations.dpToPx(3), 0.001);}

Run tests.

Crap! — Failed.

What to do then? We need to make sure that our code works fine on any density.

We need to create a dependency. Which means that our class LoadingComputation will depend on some external value.

Let’s do it. In test class write float value as a param in LoadingComputation constructor.

@Before public void setUp() {  loadingComputations = new LoadingComputations(2.5f);}

ALT+ENTER and click Create constructor.

This will result with the code similar to mine:

public LoadingComputations(float density) {}

ALT+ENTER on density and click Create field for parameter ‚density’.

private float density;public LoadingComputations(float density) {  this.density = density;}

Pretty amazing how Android Studio writes the code for us just by using ALT+ENTER shortcut.

Let’s change our method now:

float dpToPx(int size) {  return size * density;}

And run the test again.

Ok — at least we didn’t break those previously working, but still the newest one is failing.

We need to create a new instance of LoadingComputations with different density, which would mean refactor on test side. Let’s remove @Before setUp method body and paste initialization in separate test cases, since they will differ.

private LoadingComputations loadingComputations;@Test public void checksComputerAbilityToMultiply() {  loadingComputations = new LoadingComputations(2.5f);  Assert.assertEquals(2.5f, loadingComputations.dpToPx(1), 0.001);}@Test public void doubleCheckConversionIsCorrect() {  loadingComputations = new LoadingComputations(2.5f);  Assert.assertEquals(7.5f, loadingComputations.dpToPx(3), 0.001);}@Test public void convertForDensityEqual3() {  loadingComputations = new LoadingComputations(3.f);  Assert.assertEquals(9.f, loadingComputations.dpToPx(3), 0.001);}

Run the test class and watch.

FINALLY! All tests pass.

Now we’re sure that our method returns correct result. Which means that we can finally set a fixed size of our view with using our fantastic converter.

We go to LoadingView class and create an object of our LoadingComputation inside.

public class LoadingView extends View {  private LoadingComputations loadingComputations;  public LoadingView(Context context, AttributeSet attrs) {    super(context, attrs);    loadingComputations =    new LoadingComputations(
getResources().getDisplayMetrics().density);
}}

Override view’s measure method, and set fixed size of 100x100 dp.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
(int) loadingComputations.dpToPx(100),
(int) loadingComputations.dpToPx(100));
}

We’ll need our method dpToPx to return float (because drawing figures on canvas require floats), so let’s cast results to int.

Set background to show view’s area

Let’s put our view into MainActivity’s layout and set background color so we can see our view’s size. In activity_main.xml file insert view:

<com.example.customview.LoadingView
android:background=”#FFEFEFEF”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />

layout_width and layout_height attributes are ignored for now, because we set fixed size 100x100dp for simplicity.

Draw a dot

Now we can start drawing. So first thing we have to do is to override onDraw method within our LoadingView.

@Override
protected void onDraw(Canvas canvas) {
}

Awesome. Our view now has its own drawing logic — which basically doesn’t do anything at all. Let’s change that and draw a dot in center of our view.

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

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

Go to activity_main.xml and click „build” on Preview section.

NullPointerException — that’s not always a bad sign. This gives us information that we’re moving forward. Ok.. we forgot to instantiate a dotPaint object. Go back to LoadingView and in constructor put that line:

public LoadingView(Context context, AttributeSet attrs) {

dotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}

Now we can go back to activity_main.xml and build our project again.

Success! We can see dot drawn. We also notice our dot default color is black. In my opinion we can change that too. Let’s add another line to constructor then:


dotPaint.setColor(0xFFB0B0B0);
}

We go to activity_main.xml, we build and… Ok, much better (or just better).

TDD is really a carve practice you make sure every time you make a step, that the result is what you expected to achieve.

Great! We finished another milestone in our process.

Closure

Let’s finish Part 1 here. Hope you enjoy it this far, but believe me, Part 2 and Part 3 bring you much more.

--

--