The Joy of Custom Views: How to make a centered title Toolbar in Android

Custom views in Android can be quite a pain in the ass. I work at a startup in Denmark that specialises in developing mobile apps for clients, and every one of them needs something custom for it to stand out in a crowd.

Custom views are the best and worst part of Android development for me. The joy of creating a one of a kind is often offset with the terror and headache of either hacking pieces of the framework together and hoping they don’t crash, or drawing everything yourself then finding one tiny hiccup that means the whole thing needs to be restructured.

In any case, in time you build up a long list of do’s and don’ts and little shortcuts here and gotcha’s there, and I thought I’d share them with you good folks, because trying every combination yourself sucks big time.

This article was also shared on Medium.

A Toolbar with a centered title, easy right?

Centering a title in a toolbar sounds like a stupidly simple thing to do, but from my experience it’s one of those “stupidly simple, provided you already have the answer” kind of things. When I make custom Views, I want them with as few side effects as possible, the best case having it as easy to use as the framework equivalent or easier. So, some constraints:

  • A direct Toolbar replacement which can be used solely in XML, with customizations possible but not necessary.
  • Transparent to any user of the standard Toolbar/Actionbar API.
  • Transparent to the framework (Allows menu items to be inflated as standard, without affecting the centeredness of the title).

So let’s get on with it.

Disclaimer: this view has been created over time with refinements added progressively reflecting my needs. I wish I could promise it will work in every situation, but I can’t. In any case, it’s a damn good place to start if you need something like this with more custom features.

On to the code

Obviously we want to extend the framework Toolbar, as we want as much out of the box as possible:

public class CenteredTitleToolbar extends Toolbar {}

Every time I build a custom View I end up needing to initialize something when it’s first instantiated. As all Views have at least 3 constructors I create an init() method called in every one of the constructors:

public CenteredTitleToolbar(Context context) {
    super(context);
    init();
}

public CenteredTitleToolbar(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    init();
}

public CenteredTitleToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
       init();
}

The init() method itself has been boiled down to very little with all the refinements over time:

private void init() {
    _screenWidth = getScreenSize().x;

    _titleTextView = new TextView(getContext()); _titleTextView.setTextAppearance(getContext(), R.style.ToolbarTitleText);
    _titleTextView.setText("Title");
    addView(_titleTextView);
}

We get the width of the device’s screen (see below), then create a TextView, and add it to the Toolbar just like any other layout.

Gotcha 1: When creating this Toolbar I couldn’t for the life of me get the TextView centered in all circumstances. Adding or removing the back button would offset it from the left, and the same would happen on the right with option menu items. Dealing with anything other than the absolute screen size and position just wouldn’t work.

The exact code to get the device’s screen width can be found pretty easy on StackOverflow, but here it is for ease:

public Point getScreenSize() {
    WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    Display display = windowManager.getDefaultDisplay();
    Point screenSize = new Point();
    display.getSize(screenSize);

    return screenSize;
}

As for the style that’s referenced, we always use AppCompat, but you can just replace the parent with the framework equivalent. Here’s where we set the title color:

<style name="ToolbarTitleText" parent="Base.TextAppearance.Widget.AppCompat.Toolbar.Title">
    <item name="android:textColor">@color/white_pure</item>
</style>

This is where things get interesting, at the moment we have a TextView inside out Toolbar, but leaving it at that would give us two titles if we used the Actionbar API’s, and neither one would be centered.

We have our screen width, so now we want to center the title in the Toolbar. The width and height values of any view will be 0 until the first layout pass occurs, so that’s where we’ll center the title.

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);

    if (_centerTitle) {
        int[] location = new int[2];
        _titleTextView.getLocationOnScreen(location);
        _titleTextView.setTranslationX(_titleTextView.getTranslationX() + (-location[0] + _screenWidth / 2 - _titleTextView.getWidth() / 2));
    }
}

_centerTitle is a boolean flag we set to enable or disable the centered title. If false we never offset the title, and so it stays on the left, just as the standard framework version.

We use getLocationOnScreen() here to get the absolute X value of the TextView. By subtracting that offset, we can get the exact distance from the title’s current X position and the offset we need to center it, whether we have an Up/Home button or not.

Gotcha 2: Between getX()getLeft()getLocationOnScreen()getLocationInWindow(), and getGlobalVisibleRect(), there’s a lot of View “X” values available. I won’t go into the details of each here, but suffice to say getLocationOnScreen() returns the X and Y values compared to the top left corner of the screen, which is what we want here.

Gotcha 3: You’ve probably also noticed we add the value of _titleTextView.getTranslationX() to the offset value we calculate. This is because as we’re working with a mix of relative offsets (location and getTranslationX()) and absolute offsets (_screenWidth) we have to account for any current relative offset. In other words; if the title is already centered, but is being laid out again (When we change title text for example) -location[0] will equal _screenWidth / 2 — _titleTextView.getWidth() / 2, so without the getTranslationX() call, the offset will be set to 0, moving the text back to the left.

@Override
public void setTitle(CharSequence title) {
    _titleTextView.setText(title);
    requestLayout();
}

@Override
public void setTitle(int titleRes) {
    _titleTextView.setText(titleRes);
    requestLayout();
}

These two methods simply override the default Toolbar implementation and update our title instead of the stock one. We call requestLayout() to ensure that the text is re-centered in case the new title is longer or shorter than the original.

Gotcha 3: One of the many issues I had making this was trying to understand why the Actionbar API’s weren’t working with my custom Toolbar. It finally boiled down to a stupidly small difference; my setTitle() method took a String as a parameter, rather than a CharSequence, so didn’t override the default Toolbar implementation. Also don’t forget to include the int resource version as well, as you may never use it in your code, but the framework might.

Last but not least:

public void setTitleCentered(boolean centered) {
    _centerTitle = centered;
    requestLayout();
}

I personally hard code the value to true when the field is declared, but that’s up to you.

And that’s pretty much it. Stupidly simple eh? I hope this proves useful to any budding Android devs out there, and saves you the hassle I went through getting this working right. Feel free to recommend or share this, it can only help out everyone involved. Until next time, happy coding!

Checkout all the code from this article here.