Accurate Measurements With getTextBounds()06 Aug 2020 | tags: android
We have a few custom spans in our app and over the last few days I have been poring over one of them. I was trying to see if the implementation could be improved but before that could happen I needed to understand what it was trying to do first.
This particular span, among other things, deals with drawing some text on a
Canvas. We provide some styling information – text size, the text colour, and font – and in the process of drawing there’s a lot of measurements and maths.
We use this span when we want to display a bunch of text where different parts may have differing font sizes. Something similar to this:
Under normal circumstances, we could use
RelativeSizeSpan to set the font size. Doing that however, both words would be drawn referencing the same baseline (more on that later):
We want the word with the smaller font size to be centred vertically in relation to the rest of the text, so we need to figure out where to draw it. That, then, is the main purpose of our custom span. Unfortunately, our piece of code that’s currently doing this is not very well documented and the
TextPaint documentation is very… sparse.
Hold your horses
Before we get in too deep about the implementation, here’s a quick primer on some of the terms in typography:
In other words, we want to figure out how we should adjust the baseline of the text in our span so we can tell Android where we want it to be drawn. To figure this out, the code calls
open fun getTextBounds(text: String!, start: Int, end: Int, bounds: Rect!): Unit
In this post we will talk about what this method does but more importantly about what it means.
And let them go
Official Android documentation says the following:
Retrieve the text boundary box and store to bounds. Return in bounds (allocated by the caller) the smallest rectangle that encloses all of the characters, with an implied origin at (0,0).
… and it did not mean anything to me.
Now that we know some of the key terms in typography, let’s look at what this method tells us when we give it a piece of text. Let’s take “
NEW” from the string above, for example.
The method requires several values:
textis the text we want to measure,
startis the position of the character where we should start measuring,
endis position+1 of the character where we should stop measuring,
boundsis where the results of the measurement will be stored
The Javadoc says that the caller (me, Zarah) should allocate the
Rect and then give it to
val textBounds = Rect() // allocate the Rect val text = "NEW" // text we want to measure val start = 0 // position of the character to start with (N) val end = 3 // position+1 of character to end with (W) textPaint.getTextBounds(text, start, end, textBounds)
And now our
textBounds will contain a bunch of values. Remember that the font and the font size affects how much space we need to draw our text, and this is what I got for a custom font and size of
Rect( bottom = 0 left = 2 right = 66 top = -20 )
Turned into a photo:
"implied origin at (0,0)" the Javadoc refers to is the bottom-leftmost corner of the baseline and the values in our
textBounds are relative to this point (not to be confused with the
(0, 0) is the top-leftmost corner). Since our text is all caps, the
bottom coordinate of our bounding box aligns with the baseline (how convenient). Values in
textBounds increase as you go down and to the right of the origin, and decrease as you go the opposite way.
It is important to note here that the text may not be drawn exactly over the origin; there may be a leading space between the first letter and the leftmost edge of our bounding box, just like in our example here. (I don’t know enough about typography but from my experiments it looks like this depends on how the font renders each letter).
Convenience, sort of
NEW is all fine and good, but what if we give it “
Something”? We now have a mix of lower- and uppercase letters, as well as letters with ascenders (things that go up) and descenders (things that go down – but in my head I call them tails).
With the same custom font and size, our
textBounds now look like this:
Rect( bottom = 6 left = 1 right = 133 top = -20 )
Turned into a photo:
Now that we have enough information about how far down from the baseline our descent line is (0 to
bottom) and how far up from the baseline our ascent line is (0 to
top) (look at us using typography terms! Go us!), we can do the necessary maths to tell Android where exactly we want our text to be drawn with respect to the
In our specific case, given our custom font and the font size, we came up with this formula:
val textY = ((bottomOfBg - textBounds.height()) / 2F) + // half of remaining space in the bg unoccupied by text (0F - textBounds.top) + // distance from top of text to baseline of text (textBounds.bottom / 2F) // half of "tails"
Purple (0,0) is the Canvas origin
Green (0,0) is the text origin
Grey outline is textBounds for each word
bottomOfBghere is the bottom of the blue box in the image above
- we decided to add
(textBounds.bottom / 2F)to nudge the text even more to make it more visually pleasing (this is what works for us and the custom font we use, so your mileage might vary)
textBoundscan also give the full height of the bounding box, but if you want to know precise vertical distances relative to the baseline, that information is contained within
If you want to know more about Android typography, here is an excellent article about it.
//TODO Live Templates06 Mar 2020 | tags: android studio
Throughout my career, I have worked in projects of all sizes. I have taken part in greenfield projects and some that are a few years old. One of the lessons I have learned over the years is that no one ever goes back to fix the
In our current project, we are trying to mitigate the unchecked growth of this list (we have some from 2015 ). One of the solutions we are trying to explore is to create labeled
// TODO-Zarah (06 Mar 2020): Some comments go here
This means that when leaving a
TODO, devs would have to leave their name and the date in the comment. We then do periodic checks (usually before a release) to make sure that we are actively going back and actually fixing the issues.
Now typing the same thing over and over is indeed very annoying,
To help alleviate the pain, we decided to employ parameterised live templates. There are a whole bunch of these templates (
Live Templates) available in Android Studio, like the one for making a
Never forget .show() again
To create our template, open
Live Templates then click on the
+ sign on the right of the panel. I chose to create a
Template Group to contain our custom templates, but it’s also fine to directly add a new item to any of the existing groups.
Template creation menu
Live Template Anatomy
Android Studio will ask for some information when creating templates:
Live template anatomy
Abbreviation: What the user should type to use a template
Description: Short text to appear in the context menu
Template text: The actual template, including variables
Context: Gives Android Studio hints on when it should suggest the template (choosing Java and Kotlin is usually sufficient)
Variables are either pre-defined values or input fields. In our case, we have several of these:
- person the
TODOis assigned to
- the date the
- the actual comment for the
TODO(optional, can be left out of the template)
To auto-populate the variable values, click on the
Edit Variables button and define the expressions to use. There are a lot of pre-defined functions we can leverage.
On my work computer, the
user() function gives back my work-imposed ID and it’s not pretty. To use a better (i.e., my actual name) value without having to type it over and over, we can override Android Studio’s custom properties (
Edit custom properties) and add the value:
And here’s our brand new template in action:
Custom template in action
At the start of this post I mentioned that we do periodic checks on our
TODOs. When I look at our
TODO panel (
Tool Windows >
TODO), there are hundreds of them across so many files. Before a release, we review all the templated ones so we can follow up with the devs who left the comments.
To make this task easier, we created a
Unfortunately the results of this search is not exportable, so if we want to generate a report we need to run code analysis (
Run inspection by name) and choose
TODO. This can be exported to either HTML or XML which makes it easier to share with the team.
Your Privilege is Showing21 Jan 2020 | tags: musings
When I left the Philippines five years ago, I had a high-paying job at the heart of the country’s financial district. I was living a very comfortable life: I can afford an annual membership to a yoga studio, I bought an off-the-plan apartment with views of Manila Bay, I get to treat my parents to a holiday once in a while, I get to travel with my friends – we were even able to go abroad a couple of times!
When I left the Philippines, I was getting paid an annual salary of PHP1.7M. That may sound a lot, but in Australian Dollars that is only 48k – slightly above the minimum wage set by the Fair Work Commission, and way less than what a junior developer would earn.
But the thing is, that IS a lot of money. Chances are a lot of people outside tech might work all their life and never get to that salary range at all. I am indeed extremely luckier and more privileged than other people in my home country.
With all that money, it was still a challenge to do one thing I would have loved to do more of when I was younger – to travel. I’ve always read books and seen shows where teenagers go to exotic places, or heartbroken lovers go to the airport and buy tickets to “anywhere”, or people doing the whole Eat Pray Love thing.
You see, I had the unfortunate case of being born in the Philippines to parents who were also born in the Philippines. Outside of traveling outside Southeast Asia, having a Philippine passport is like traveling in super hard mode:
Them: YoU haVEn'T BeEn tO LoNDon???— Zarah Dominguez 🦉 (@zarahjutz) July 17, 2019
Me: No, it's really difficult to get a visa.
Them: *puzzled looks*
Me:: *pulls list out* pic.twitter.com/i7tnokjUBb
And that’s just going to ONE country. At some point in the past I swore that I am going on a Europe tour on my 40th birthday. That would mean applying for a Schengen visa; which means I need a full itinerary planned out; which means I have to prepay ALL hotels and transportation to all the countries I want to visit; which does not guarantee me being granted a visa at all. It takes a lot of physical and emotional energy getting all these requirements together, not to mention the monetary investment when applying for a visa (for Schengen, non-refundable fee of ~PHP4,000 – equivalent to a month’s rent). I checked the requirements again when I got my Australian passport, and guess what, I can apply online and then I basically just show up? Whut.
Look, the requirements I showed in that tweet is NOT unique to the UK. Almost all countries have the same requirements for Filipinos. Even when I got my work permit in Australia, I have to provide almost the same exact requirements if I want to go to New Zealand (and now that I am an Australian citizen I can just show up there? Like… It’s still me??).
Don’t even get me started on how I get treated when I land in the country I’m visiting. Some questions I have been asked by immigration:
- you have a Philippine passport, why are you flying from Australia?
- how did you find your job in Australia?
- who invited you to go to this conference?
- why did they invite you to this conference?
I’ve traveled twice so far on my Australian passport and I have to say I have received the warmest welcome I have experienced ever.
Anyway, what I am trying to say is that the past couple of days suck because of something I keep on seeing on Twitter. People have been posting all the countries they have been to, and all the others on their bucket list.
I guess sometimes it’s easy to lose sight of how things we take for granted are just faraway dreams for some. When I started traveling internationally, I couldn’t have imagined the amount of suffering and humiliation that I had to go through just to get visas. I have lost count of how many times I have been looked down upon because of the country I was born in, or the passport I am holding. I know that there are SO MANY people out there who have the same aspirations and the same bucket lists as those we see in the meme but have resigned themselves to the fact that it will just be that – a bucket list.
I recognise that as it is, I am much much luckier and more privileged than most. I was given the chance to set myself up for success, I have a supportive family behind me, and I have been given so many opportunities to pull myself up. I am not discounting the fact that those people have worked hard to be where they are, and I am sure they deserve all the fun and enjoyment that they are having.
All I hope for is that we all do not forget how extremely valuable freedom of movement is, and how liberating it must be to have the means to enjoy that freedom.