Creating a custom TextView as a section header

The picture above shows a TextView with a separator line running along it. You may have to do something like this as a custom separator; a section header in a ListView, RecyclerView; or just as a section separator in your Activity or Fragment. This may seem rather simple, but from a layout point of View, how would you accomplish this?

Defining the problem

If you background was a solid color, you could put your TextView in a FrameLayout and center it, with a solid background color, then on your FrameLayout set an XML drawable as the background. So something like:

<! — THE DRAWABLE -->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke android:color="@color/text_color_secondary" android:width="1dp" />
</shape>
<! — THE LAYOUT -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/divider_section_separator"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:layout_gravity="center"
android:background="@android:color/white"
android:text="Foo"/>
</FrameLayout>

Assuming your background color was white, this would look like it works just fine. However, there are a few issues with it.

  1. Does not work on a textured or seamless background.
  2. We are drawing 2 layers and contributing to overdraw.

We may be tempted to think that we can solve this issue with a patch9 image and, you probably can. You may even be able to make a patch9 work with a Textured background. However the overdraw issue remains. You may not need to wrap your TextView in a ViewGroup any more, but you’re still drawing a background before drawing your text.

Side: I know the overdraw problem seems rather minor, however, this is a good opportunity to learn about how drawing View’s works.

Additionally, the approaches above are not very flexible. If you want to right or left align your text instead, this requires extra assets in case of the patch9 solution or you having to add code to fiddle with padding in case of the first solution (for example, you may not want left padding when you align left).

Finding a solution Working with the RecyclerView, I had created a row separator that had text in it. So I knew this problem was possible to solve using a custom view. I decided that my custom view would extend TextView so I would get all the text rendering and positioning from a TextView for free.

The goal is rather simple, if the text is centered, draw lines to either side of it. If the text is left aligned, draw a light from it’s right end to the end of the screen. if the text is right aligned… you get the idea.

Step 1: Extending TextView

This part is pretty straight forward, create a new class that extends TextView. Your IDE should prompt you to add the default constructors. Go head and create all the constructors and then you should be able to use your new View in a layout. In my case, I called the view a SeparatorTextView and after creating the constructors, I was able to add it to my layout like so:

<com.alimuzaffar.rxjava.SeparatorTextView
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Hello"/>

We are going to start by working on centered text. We need to draw a line to either side of the text, however, to start with, lets draw a line across the centere of the TextView.

Step 2: Drawing the line

We will start by overriding the onDraw(Canvas) method. First thing we do is call super.onDraw(Canvas) since we want to retain all TextView behavior.

Next, we are going to try to draw a line across the center of the TextView. In order to draw the line, we use canvas.drawRect(…). The reason we are using a rectangle is because in the future, we may want to be able to control the thickness of the line and that is just easiest as a rectangle.

We want to draw from the left vertical center to the right vertical center. Our code will look like this:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// we use the default paint,
// our line will be the same color as the text.
Paint paint = getPaint();
int top = getHeight()/2; // start at the vertical centre
// of the textview
int left = 0; //start at the left margin
int right = getWidth(); // we draw all the way to the right
int bottom = top + 2; // we want the line to be 2 pixel thick
  int top = getHeight()/2; 
int left = 0; //start at the left margin
int right = getWidth(); //we draw all the way to the right
int bottom = top + 2; //we want the line to be 2 pixel thick
  canvas.drawRect(left, top, right, bottom, paint);
}

I’ve tried to put comments in to explain some of the variables. The result is:

Note: drawRect takes floats, however, you notice that I’m passing it integers. The reason for this is that since we are dealing in pixels, I really don’t see fractions mattering much.

Step 3: Draw 2 lines

Since we are initially working with the scenario where the text is centered, we need a line on either side of the text. In order to do this, we need to measure the width of the text and then draw the first line from start to the center of the screen minus half the width of the text. We will then draw a second line from the half way point plus half the width of the text to the end of the screen.

Note: You can get the width of the text by calling getTextSize() however, in my experience, the result from this method can be incorrect. So, to measure this, we use the paint object.

int horizontalCenter = right / 2;
int textWidth = (int) paint.measureText(getText().toString());
int halfTextWidth = textWidth/2;
canvas.drawRect(left, 
top,
horizontalCenter — halfTextWidth,
bottom,
paint);
canvas.drawRect(horizontalCenter + halfTextWidth,
top,
right,
bottom,
paint);

If you run this code, you will see that something doesn’t look quite right.

Step 4: Adjust for the padding

The reason for this is that our code is not making adjustments for padding (we have 16dp padding on our TextView) so we are drawing longer lines than we need to. In order to adjust for padding, we add the padding to the left and we subtract it from the right. From top, we need to add top padding and subtract bottom padding. We don’t need to change bottom at the moment since it’s relative to top.

This will give us code like below:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// we use the default paint,
// our line will be the same color as the text.
Paint paint = getPaint();
//start at the vertical center of the textview
int top = (getHeight() + getPaddingTop() —
getPaddingBottom())/2;
//start at the left margin
int left = getPaddingLeft();
//we draw all the way to the right
int right = getWidth() — getPaddingRight();
//we want the line to be 2 pixel thick
int bottom = top + 2;
  int horizontalCenter = (getWidth() + getPaddingLeft() —
getPaddingRight()) / 2;
  int textWidth = (int) paint.measureText(getText().toString());
int halfTextWidth = textWidth/2;
  canvas.drawRect(left, 
top,
horizontalCenter — halfTextWidth,
bottom,
paint);
canvas.drawRect(horizontalCenter + halfTextWidth,
top,
right,
bottom,
paint);
}

When we run this code, we see that now, our lines are within the TextView’s padding and the lines run up to the text and then continues again after it.

As an additional aesthetic measure, we can add some padding for the text so the lines don’t run all the way up to the text. In order to do this, we need to subtract padding from the end position of the first line and add it to the starting position of the second line.

int padding = 16;
canvas.drawRect(left,
top,
horizontalCenter — halfTextWidth — padding, //right
bottom,
paint);
canvas.drawRect(horizontalCenter + halfTextWidth + padding,
top,
right,
bottom,
paint);

Of course in your final solution, you probably don’t want to hard code padding to use around the text in onDraw and should probably convert between dip and pixels, however, I’ve hard code it in onDraw to show the concept.

Looking nice!

Step 5: The gravity of the situation

So far we have only handled the case where the text is centered. We will now add in checks for the gravity and make sure to only draw one line when the gravity is left or right.

int gravity = getGravity();
if ((gravity & Gravity.LEFT) == Gravity.LEFT) {
canvas.drawRect(left + textWidth + padding, //left
top,
right,
bottom,
paint);
} else if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) {
canvas.drawRect(left,
top,
right — textWidth — padding, //right
bottom,
paint);
} else {
canvas.drawRect(left,
top,
horizontalCenter — halfTextWidth — padding,
bottom,
paint);
canvas.drawRect(horizontalCenter + halfTextWidth + padding,
top,
right,
bottom,
paint);
}

That should do it.

Step 6: Make it reusable

You can how do more advance stuff like allow the separator and the text to have different colors by having a different paint object for the dividers.

You can also allow the use of an xml drawable or a patch9 image for the divider. One thing to note if you try this, if you are using a patch9 (the same kind that android uses for dividers) then you have to draw it much like the rectangles using the starting and ending points of the image. However, an xml drawable has to be drawn so that it takes up the full space. I.e. the top has to be 0 and the bottom has to be the height of the view, adjusted for padding.

I have the demo code and full finished code up in a gist.

Have your say

Let me know what you think of this approach in the comments and as always, for more Android development article or follow me on Medium, LinkedIn, Twitter or Google+ and please recommend this article.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.