One TextView to Rule Them All: Justifying Text on Android!
The Pre-Game!
What’s the problem?
Android TextView does not support justified text!
We see articles everywhere about integrating libraries into your projects, but, as developers, we can’t become so dependent on third party libraries that we forget how to customize native tools and components to do what we need them to do.
Don’t get me wrong — I love finding the perfect library to do most of the work for me, but this may not always be the correct approach! There is even a library to help you justify text, but this is not necessarily the best way to do what we need — the developer even says this in the readme file.
In this article, we will dive into native Android customizability... because sometimes you just need to get your hands dirty and do things yourself!
What are we going to do about it?
We are going to create our own TextView!
Don’t freak out! We are not starting from scratch, but, in fact, we will extend Android’s native AppCompatTextView to build our own SmartTextView. We will be customizing this view to draw the text onto the canvas ourselves!
In this article, you will learn what methods we need to override, as well as what native view methods we will need to calculate positions and render the text.
The hidden purpose of this article...
More than just learning about Android's TextView, you will:
- Become less intimidated changing native Android views.
- See how granular we can get and how much control we have over native customizability.
- Maybe even start to believe that with patience and persistence, we can do almost anything in Android!
What you need before we begin:
You don’t need to be an Android master, but you will need:
- Some experience with Android development
- A project to create our SmartTextView inside
- A good understanding of strings, characters, and index based traversal
Game On! Let’s get started!
Development Breakdown
Here is a quick summary of the work we are about to do:
- Extend the AppCompatTextView and understand important attributes and methods
- Override the
onDraw()
method to draw the text on the canvas ourselves! - Additional Functionality (Optional Read)
⋅⋅⋅* Configuration with fonts — directly in the SmartTextView
⋅⋅⋅* Switching text displayed in the view (with a default fade animation)
4. Add support for our attributes in XML to set everything up in layout files.
5. Go the extra mile to make our SmartTextView more efficient! (Optional Read)
6. The Full Class Code
1. Extend the AppCompatTextView
First, let's extend the AppCompatTextView
, create some constructors, and an empty initialization method initialize
that we will worry about later:
public class SmartTextView extends AppCompatTextView {
public SmartTextView(Context context) {
super(context);
initialize(null);
}
public SmartTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(attrs);
}
public SmartTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(attrs);
}
private void initialize(AttributeSet attrs) { }
}
Now that we have a generic custom class to work with, let's understand some AppCompatTextView
methods we are going to need:
onDraw(Canvas canvas)
— we will need to override this to draw the textgetCurrentTextColor()
— this should be self-explanatory...getDrawableState()
— returns list of active state (clicked, checked, touched, etc..) drawables to render (shadow, backgrounds, etc...)getLineHeight()
— returns the height of a line taking into account the font, default line spacing, and extra added spacegetMeasuredWidth()
— returns the width of our view as it will be on-screengetPaddingLeft(), getPaddingRight()
— returns the view's respective paddinggetPaint()
— returns thePaint
that is used to draw the text
Don't worry about remembering these or understanding why we need them just yet, this will become clear as we move on.
onDraw(Canvas canvas)
method
2. Override the This is the bread and butter! Now we will be writing the code to actually draw the words on our SmartTextView
's canvas. Here is the full code for the onDraw(Canvas canvas)
method:
@Override
protected void onDraw(Canvas canvas) {
if (!mJustified) {
super.onDraw(canvas);
return;
}
// Manipulations to mTextPaint found in super.onDraw()...
getPaint().setColor(getCurrentTextColor());
getPaint().drawableState = getDrawableState();
// The actual String that contains all the words we will draw on screen
String fullText = mAllCaps ? getText().toString().toUpperCase() : getText().toString();
float lineSpacing = getLineHeight();
float drawableWidth = getDrawableWidth();
// Variables we need to traverse our fullText and build our lines
int lineNum = 1, lineStartIndex = 0;
int lastWordEnd, currWordEnd = 0;
if (fullText.indexOf(' ', 0) == -1) flushWord(canvas, getPaddingTop() + lineSpacing, fullText);
else {
while (currWordEnd >= 0) {
lastWordEnd = currWordEnd + 1;
currWordEnd = fullText.indexOf(' ', lastWordEnd);
if (currWordEnd != -1) {
getPaint().getTextBounds(fullText, lineStartIndex, currWordEnd, mLineBounds);
if (mLineBounds.width() >= drawableWidth) {
flushLine(canvas, lineNum, fullText.substring(lineStartIndex, lastWordEnd));
lineStartIndex = lastWordEnd;
lineNum++;
}
} else {
getPaint().getTextBounds(fullText, lineStartIndex, fullText.length(), mLineBounds);
if (mLineBounds.width() >= drawableWidth) {
flushLine(canvas, lineNum, fullText.substring(lineStartIndex, lastWordEnd));
rawFlushLine(canvas, ++lineNum, fullText.substring(lastWordEnd));
} else {
if (lineNum == 1) {
rawFlushLine(canvas, lineNum, fullText);
} else {
rawFlushLine(canvas, lineNum, fullText.substring(lineStartIndex));
}
}
}
}
}
}
So, let us go through this code step by step, and start by defining these magical flush methods. For now, just know what they do. We will dive into their implementations soon enough:
rawFlushLine(Canvas canvas, int lineNum, String line)
Prints the line at lineNum on the canvas without adjusting the spacing between words at all.
flushLine(Canvas canvas, int lineNum, String line)
Calculates the space between all words required to strech the line out to fit the full width of the view.
flushWord(Canvas canvas, float yLine, String word)
Spaces the letters of the word out to strech (or squish) to fit the full width of the view.
onDraw(Canvas canvas)
method?
Now, what are we doing in the First, it is easy to understand that if we are not justifying the text, we can use the TextView's base onDraw(Canvas canvas)
method to render the text normally:
if (!mJustified) {
super.onDraw(canvas);
return;
}
Next, we need to make the base changes to the private TextView
variable mTextPaint
that are normally done by the base method. We access mTextPaint
with getPaint()
. Don't worry too much about these — I found them by trial and error as well as by analyzing the base code:
getPaint().setColor(getCurrentTextColor());
getPaint().drawableState = getDrawableState();
The TextView
's mTextPaint
variable is extremely important because it takes into account the font, text size, text style (bold, italics, etc) and pretty much everything that has to do with our text. It is used in all calculations to determine line heights, text widths, etc...
Once we have set up the TextView
's paint, we can define the vaiables we need for our calculations:
// The actual String that contains all of the words we will draw on screen
String fullText = mAllCaps ? getText().toString().toUpperCase() : getText().toString();
float lineSpacing = getLineHeight();
float drawableWidth = getDrawableWidth();
// Variables we need to traverse our fullText and build our lines
int lineNum = 1, lineStartIndex = 0;
int lastWordEnd, currWordEnd = 0;
mAllCaps
is a variable we use to simplify tracking if all displayed text should be capitalized, and to ensure this value is accurate, we override the setAllCaps
method:
@Override
public void setAllCaps(boolean allCaps) {
mAllCaps = allCaps;
super.setAllCaps(allCaps);
}
getDrawableWidth()
is our method that calculates the width of the TextView
so that we can actually draw on by taking into account the padding:
private float getDrawableWidth() {
return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
}
Now, we can start traversing fullText
and flushing our lines as we create them:
The first edge case is if we only have one word, in which case we simply flush the word:
if (fullText.indexOf(' ', 0) == -1) flushWord(canvas, getPaddingTop() + lineSpacing, fullText);
If we do indeed have several words, we need to go through fullText
word by word.
while (currWordEnd >= 0) {
lastWordEnd = currWordEnd + 1;
currWordEnd = fullText.indexOf(' ', lastWordEnd);
...
}
As we traverse our fullText
, we need to build our lines and draw them. A line is defined by the most words that can fit in the drawable width of our view. If including the next word makes the length of our line greater than our drawable width, we can close this line and draw it on our canvas.
if (currWordEnd != -1) {
getPaint().getTextBounds(fullText, lineStartIndex, currWordEnd, mLineBounds);
if (mLineBounds.width() >= drawableWidth) {
flushLine(canvas, lineNum, fullText.substring(lineStartIndex, lastWordEnd));
lineStartIndex = lastWordEnd;
lineNum++;
}
}
if (currWordEnd != -1)
is used to ensure that we do in fact have another word. If it returns false, then we have hit the end of fullText
.
mLineBounds
is used to store the height and width of the substring between the indexes lineStartIndex
and currWordEnd
inside fullText
. Remember when I emphasized the importance of our mTextPaint? We are now using it to calculate these bounds:
getPaint().getTextBounds(fullText, lineStartIndex, currWordEnd, mLineBounds);
Finally, if we see that including this next word makes our line too big, we exclude it by flushing the substring between lineStartIndex
and lastWordEnd
(the index of the previous word's last character):
if (mLineBounds.width() >= drawableWidth) {
flushLine(canvas, lineNum, fullText.substring(lineStartIndex, lastWordEnd));
...
If we do not have another word, and we have run through the entire fullText
, we need to print our last line. This line does not need to be justified. However, there are a few edge cases to consider:
- If including the final word makes the line larger then the drawable width, we need to break it up and draw the final word on the next line. This may be a little confusing — if it is, take a closer look at what
lastWordEnd
andcurrWordEnd
are defined as for this iteration. - If this is the first line, we can ignore
lineStartIndex
and just flushfullText
.
Here is the code that handles the last line (which is determined implicitly by currWordEnd == -1
with the else
):
else {
getPaint().getTextBounds(fullText, lineStartIndex, fullText.length(), mLineBounds);
if (mLineBounds.width() >= drawableWidth) {
// flush everthing up untill the end of the final word
flushLine(canvas, lineNum, fullText.substring(lineStartIndex, lastWordEnd));
// flush the final word (here lastWordEnd can be inturpreted as the start of the final word in the line)
rawFlushLine(canvas, ++lineNum, fullText.substring(lastWordEnd));
} else {
if (lineNum == 1) {
rawFlushLine(canvas, lineNum, fullText);
}
else {
rawFlushLine(canvas, lineNum, fullText.substring(lineStartIndex));
}
}
}
That's it for onDraw(Canvas canvas)
!
Before we get into the details of our flush methods, we need to talk about mFirstLineTextHeight
. We need this variable to hold the raw pixel height of a single line not including the spacing between lines. The following method will calcuate this:
private void setFirstLineTextHeight(String firstLine) {
getPaint().getTextBounds(firstLine, 0, firstLine.length(), mLineBounds);
mFirstLineTextHeight = mLineBounds.height();
}
Now, what are we doing in our flush methods?
flushLine
: Calculates the space between all words required to strech the line out to fit the full width of the view:
private void flushLine(Canvas canvas, int lineNum, String line) {
if (lineNum == 1) setFirstLineTextHeight(line);
float yLine = getPaddingTop() + mFirstLineTextHeight + (lineNum - 1) * getLineHeight();
String[] words = line.split("\\s+");
StringBuilder lineBuilder = new StringBuilder();
for (String word : words) {
lineBuilder.append(word);
}
float xStart = getPaddingLeft();
float wordWidth = getPaint().measureText(lineBuilder.toString());
float spacingWidth = (getDrawableWidth() - wordWidth) / (words.length - 1);
for (String word : words) {
canvas.drawText(word, xStart, yLine, getPaint());
xStart += getPaint().measureText(word) + spacingWidth;
}
}
Some important calculations in this method are the following:
yLine
- The y point of the bottom left corner of the word we want to draw. This is calculated by adding the top padding (getPaddingTop()
) + the raw height of the first line (mFirstLineTextHeight
) + the height of the lines preceeding the first line, which accounts for the spacing between the lines ((lineNum - 1) * getLineHeight()
):
float yLine = getPaddingTop() + mFirstLineTextHeight + (lineNum - 1) * getLineHeight();
Remember,
getLineHeight()
takes into account the spacing between lines.
Also, this value is the same for all words in this line.
wordWidth
— this is the width of all of the words, without the spaces in between. To calculate this, we can store all of the words (just the words, no spaces) in another string and use getPaint().measureText()
to calculate the width:
String[] words = line.split("\\s+");
StringBuilder lineBuilder = new StringBuilder();
for (String word : words) {
lineBuilder.append(word);
}
float wordWidth = getPaint().measureText(lineBuilder.toString());
spacingWidth
— this is the average space between words. A simple calculation after we have the wordWidth:
float spacingWidth = (getDrawableWidth() - wordWidth) / (words.length - 1);
The last thing left to do is to actually draw each word of this line onto the canvas. Here we have the yLine
, which is the same for all words. We can simultaneously draw a word and calculate the xStart
for the next word:
for (String word : words) {
canvas.drawText(word, xStart, yLine, getPaint());
xStart += getPaint().measureText(word) + spacingWidth;
}
rawFlushLine
— prints the line at lineNum on the canvas without adjusting the spacing between words at all. Here, we can simply draw the entire line at the calculated position with yLine
and getPaddingLeft()
as the x point:
private void rawFlushLine(Canvas canvas, int lineNum, String line) {
if (lineNum == 1) setFirstLineTextHeight(line);
float yLine = getPaddingTop() + mFirstLineTextHeight + (lineNum - 1) * getLineHeight();
canvas.drawText(line, getPaddingLeft(), yLine, getPaint());
}
flushWord
— spaces the letters of the word out to strech (or squish) to fit the full width of the view. This is very similar to flushLine
. However, spacingWidth
refers to the spacing between letters. We draw the word on the canvas character by character:
private void flushWord(Canvas canvas, float yLine, String word) {
float xStart = getPaddingLeft();
float wordWidth = getPaint().measureText(word);
float spacingWidth = (getDrawableWidth() - wordWidth) / (word.length() - 1);
for (int i = 0; i < word.length(); i++) {
canvas.drawText(word, i, i + 1, xStart, yLine, getPaint());
xStart += getPaint().measureText(word, i, i + 1) + spacingWidth;
}
}
That is everything for drawing the justified text on our canvas ourselves!
Now, I understand that this may be a lot of information, so here is a shortened recap of what we did in onDraw(Canvas canvas)
:
- Break the text up into words
- Traverse word by word, appending the words to our line as we traverse
- When no more words can fit in the line, we flush the line.
Flushing the line:- We determine the y position of this line
- We determine the spacing between words
- We draw each word one by one using the constant y position, and the changing x postion of each word.
3. Additional Functionality * — Optional Read*
Our SmartTextView
is already pretty smart with this Justify functionality, but this is probably not enough for it to be the "One TextView to Rule Them All!" To take it even further, let's add support for the following:
- Configuration with fonts — directly in the
SmartTextView
- Switching text displayed in the view (with a default fade animation)
SmartTextView
1. Configuration with fonts — directly in the We will be allowing the configuration of three fonts, one for each FontType, defined as:
public enum FontType {
Thin, Reg, Thick
}
Let's store the fonts in a static HashMap for easy access. Also, let's create public static methods to register a font Typeface for each:
private static final HashMap<FontType, Typeface> mRegisteredFonts = new HashMap<>();
public static void registerFont(FontType fType, Typeface fTypeface) {
if (mRegisteredFonts.containsKey(fType)) mRegisteredFonts.remove(fType);
mRegisteredFonts.put(fType, fTypeface);
}
public static Typeface getFontTypeface(FontType fType) {
return mRegisteredFonts.get(fType);
}
Ideally, registerFont
should only be called once in the Application
's onCreate()
method for each FontType
. An example call would be:
AssetManager assets = getAssets();
SmartTextView.registerFont(SmartTextView.FontType.Thin, Typeface.createFromAsset(assets, "fonts/Roboto-Thin.ttf"));
Now that we have added the functionality to register fonts for the SmartTextView
class, let's add some methods to set the font for a particular SmartTextView
instance:
public void setFontType(FontType fType) {
mFontType = fType;
setTypeface(getFontTypeface());
}
public void setFontType(FontType fType, int style) {
mFontType = fType;
setTypeface(getFontTypeface(), style);
}
The second method is just to make it easier to set the font and the style (bold, italics, etc.) at the same time.
In setFontType
, we also keep track of which FontType
is currently set for this 'SmartTextView' with the property mFontType
.
2. Switching text displayed in the view (with a default fade animation)
We will be adding very basic functionality to switch the text being displayed in our SmartTextView
with a basic fade transition animation. This is basically just a simple method to standardize updating text:
public void switchText(int resId) {
switchText(getResources().getString(resId));
}
public void switchText(final String newText) {
animate().alpha(0).setDuration(150).withEndAction(new Runnable() {
@Override public void run() {
setText(newText);
animate().alpha(1).setDuration(150).start();
}
}).start();
}
The first method just makes it easier to display text we have saved as a resource.
4. Add support for our attributes in XML
We definitely want to be able to set all of this great functionality directly from our layout XML files when we add a SmartTextView
. We need to add attributes to justify text and set the FontType
. These attributes need to be defined in the attrs.xml
file inside the res/values folder of our app. We also need to update our implementation of SmartTextView
to take them into account.
First, let's add the attributes to the attrs.xml
file:
<declare-styleable name="SmartTextView">
<attr name="justified" format="boolean"/>
<attr name="android:textAllCaps"/>
<attr name="android:textStyle"/>
<attr name="font" format="enum">
<enum name="thin" value="0"/>
<enum name="reg" value="1"/>
<enum name="thick" value="2"/>
</attr>
</declare-styleable>
font
attribute is in regards to the Additional Functionality (optional) section.
android:textAllCaps
andandroid:textStyle
are native Android TextView attributes, but we need to add them to our declaration so that we can access them from our customSmartTextView
.
The attributes can now be set in our XML layouts as follows:
<ra.smarttextview.SmartTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:layout_marginBottom="30dp"
android:textColor="@color/text_bright"
android:text="@string/cmx_title"
android:textAllCaps="true"
android:singleLine="true"
android:textSize="40sp"
app:justified="true"
app:font="thin"/>
Note: the last two attributes are our custom attributes, accessed using the preface
app:
Finally, we need to read these attributes in our constructors and utilize them. For this, we will implement the initialize
method we are calling in all of our constructors:
private void initialize(AttributeSet attrs) {
int styleIndex = 0;
if (attrs != null) {
TypedArray attrArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.SmartTextView, 0, 0);
if (attrArray != null) {
mJustified = attrArray.getBoolean(R.styleable.SmartTextView_justified, false);
mFontType = FontType.values()[attrArray.getInt(R.styleable.SmartTextView_font, 1)];
mAllCaps = attrArray.getBoolean(R.styleable.SmartTextView_android_textAllCaps, false);
styleIndex = attrArray.getInt(R.styleable.SmartTextView_android_textStyle, 0);
}
}
setTypeface(getFontTypeface(), styleIndex);
}
getFontTypeface
is our internal method for getting the registered Typeface
for our current FontType
:
private Typeface getFontTypeface() {
return !mRegisteredFonts.containsKey(mFontType) ? getTypeface() : mRegisteredFonts.get(mFontType);
}
It uses the native
getTypeface()
method to get the system font if we do not have a registered one for thisFontType
5. Go the Extra Mile - Optional Read
At this point, we have done a lot! But for the over achievers who want to take it even further, you can attempt to make our implementation of onDraw()
and the flush methods even more efficient.
You may have noticed that I am doing a lot of String
manipulations here, and these immutable String
objects are constantly being created and destroyed through the implementation. Can you work with chars
arrays where possible to minimize overhead? Give it a try if you're up for the challenge!
6. The Full Class Code
And finally... here is the complete code for SmartTextView
:
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import java.util.HashMap;
/**
* Created by rugvedambekar on 2016-04-10.
*/
public class SmartTextView extends AppCompatTextView {
private static final String TAG = SmartTextView.class.getSimpleName();
private static final HashMap<FontType, Typeface> mRegisteredFonts = new HashMap<>();
public static void registerFont(FontType fType, Typeface fTypeface) {
if (mRegisteredFonts.containsKey(fType)) mRegisteredFonts.remove(fType);
mRegisteredFonts.put(fType, fTypeface);
}
public static Typeface getFontTypeface(FontType fType) {
return mRegisteredFonts.get(fType);
}
private boolean mJustified;
private boolean mAllCaps;
private FontType mFontType;
private int mFirstLineTextHeight = 0;
private Rect mLineBounds = new Rect();
public SmartTextView(Context context) {
super(context);
initialize(null);
}
public SmartTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(attrs);
}
public SmartTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(attrs);
}
private void initialize(AttributeSet attrs) {
int styleIndex = 0;
if (attrs != null) {
TypedArray attrArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.SmartTextView, 0, 0);
if (attrArray != null) {
mJustified = attrArray.getBoolean(R.styleable.SmartTextView_justified, false);
mFontType = FontType.values()[attrArray.getInt(R.styleable.SmartTextView_font, 1)];
mAllCaps = attrArray.getBoolean(R.styleable.SmartTextView_android_textAllCaps, false);
styleIndex = attrArray.getInt(R.styleable.SmartTextView_android_textStyle, 0);
}
}
setTypeface(getFontTypeface(), styleIndex);
}
private Typeface getFontTypeface() {
return !mRegisteredFonts.containsKey(mFontType) ? getTypeface() : mRegisteredFonts.get(mFontType);
}
public void setJustified(boolean justified) {
mJustified = justified;
}
public void setFontType(FontType fType) {
mFontType = fType;
setTypeface(getFontTypeface());
}
public void setFontType(FontType fType, int style) {
mFontType = fType;
setTypeface(getFontTypeface(), style);
}
@Override
public void setAllCaps(boolean allCaps) {
mAllCaps = allCaps;
super.setAllCaps(allCaps);
}
@Override
public void setTypeface(Typeface tf, int style) {
if (tf == null) tf = getFontTypeface();
super.setTypeface(tf, style);
}
public void switchText(int resId) {
switchText(getResources().getString(resId));
}
public void switchText(final String newText) {
animate().alpha(0).setDuration(150).withEndAction(new Runnable() {
@Override public void run() {
setText(newText);
animate().alpha(1).setDuration(150).start();
}
}).start();
}
@Override
protected void onDraw(Canvas canvas) {
if (!mJustified) {
super.onDraw(canvas);
return;
}
// Manipulations to mTextPaint found in super.onDraw()...
getPaint().setColor(getCurrentTextColor());
getPaint().drawableState = getDrawableState();
String fullText = mAllCaps ? getText().toString().toUpperCase() : getText().toString();
float lineSpacing = getLineHeight();
float drawableWidth = getDrawableWidth();
int lineNum = 1, lineStartIndex = 0;
int lastWordEnd, currWordEnd = 0;
if (fullText.indexOf(' ', 0) == -1) flushWord(canvas, getPaddingTop() + lineSpacing, fullText);
else {
while (currWordEnd >= 0) {
lastWordEnd = currWordEnd + 1;
currWordEnd = fullText.indexOf(' ', lastWordEnd);
if (currWordEnd != -1) {
getPaint().getTextBounds(fullText, lineStartIndex, currWordEnd, mLineBounds);
if (mLineBounds.width() >= drawableWidth) {
flushLine(canvas, lineNum, fullText.substring(lineStartIndex, lastWordEnd));
lineStartIndex = lastWordEnd;
lineNum++;
}
} else {
getPaint().getTextBounds(fullText, lineStartIndex, fullText.length(), mLineBounds);
if (mLineBounds.width() >= drawableWidth) {
flushLine(canvas, lineNum, fullText.substring(lineStartIndex, lastWordEnd));
rawFlushLine(canvas, ++lineNum, fullText.substring(lastWordEnd));
} else {
if (lineNum == 1) {
rawFlushLine(canvas, lineNum, fullText);
}
else {
rawFlushLine(canvas, lineNum, fullText.substring(lineStartIndex));
}
}
}
}
}
}
private float getDrawableWidth() {
return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
}
private void setFirstLineTextHeight(String firstLine) {
getPaint().getTextBounds(firstLine, 0, firstLine.length(), mLineBounds);
mFirstLineTextHeight = mLineBounds.height();
}
private void rawFlushLine(Canvas canvas, int lineNum, String line) {
if (lineNum == 1) setFirstLineTextHeight(line);
float yLine = getPaddingTop() + mFirstLineTextHeight + (lineNum - 1) * getLineHeight();
canvas.drawText(line, getPaddingLeft(), yLine, getPaint());
}
private void flushLine(Canvas canvas, int lineNum, String line) {
if (lineNum == 1) setFirstLineTextHeight(line);
float yLine = getPaddingTop() + mFirstLineTextHeight + (lineNum - 1) * getLineHeight();
String[] words = line.split("\\s+");
StringBuilder lineBuilder = new StringBuilder();
for (String word : words) {
lineBuilder.append(word);
}
float xStart = getPaddingLeft();
float wordWidth = getPaint().measureText(lineBuilder.toString());
float spacingWidth = (getDrawableWidth() - wordWidth) / (words.length - 1);
for (String word : words) {
canvas.drawText(word, xStart, yLine, getPaint());
xStart += getPaint().measureText(word) + spacingWidth;
}
}
private void flushWord(Canvas canvas, float yLine, String word) {
float xStart = getPaddingLeft();
float wordWidth = getPaint().measureText(word);
float spacingWidth = (getDrawableWidth() - wordWidth) / (word.length() - 1);
for (int i = 0; i < word.length(); i++) {
canvas.drawText(word, i, i + 1, xStart, yLine, getPaint());
xStart += getPaint().measureText(word, i, i + 1) + spacingWidth;
}
}
public enum FontType {
Thin, Reg, Thick
}
}
The Post-Game!
We got through it! I know it might have been longer than you expected, but now that you have made it to the other side, I hope a few things are clearer — like just how granular we can get when manipulating native Android views and how even if the perfect third party library does not exist, that shouldn't deter us from doing what we need!
There is nothing wrong with getting our hands dirty and tinkering with native Android components — we just have to be careful, patient, and persistent by reading the core code and taking into account all of the edge cases.
Hello,
It worked for me but for only simple texts. It didn’t work for text with hyperlink. In that case, it isn’t highlighting/ applying style for texts.
Do you have any idea about this?
Thank you so much Rigved! Didn’t go through the whole post but the portions I read were very well explained and it helped me understand what I needed quickly. Also, the code works wonders! Thanks! Been looking for something like this for a long time.