Related
I'm trying to format Hashtags inside a TextView/EditText (Say like Chips mentioned in the Material Design Specs). I'm able to format the background using ReplacementSpan. But the problem is that I'm not able to increase the line spacing in the TextView/EditText. See the image below
The question is how do I add top and bottom margin for the hashtags?
Here is the code where I add the background to the text:
/**
* First draw a rectangle
* Then draw text on top
*/
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
RectF rect = new RectF(x, top, x + measureText(paint, text, start, end), bottom);
paint.setColor(backgroundColor);
canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);
paint.setColor(textColor);
canvas.drawText(text, start, end, x, y, paint);
}
I had a similar problem a while ago and this is the solution I've come up with:
The hosting TextView in xml:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="18dp"
android:paddingBottom="18dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:gravity="fill"
android:textSize="12sp"
android:lineSpacingExtra="10sp"
android:textStyle="bold"
android:text="#{viewModel.renderedTagBadges}">
A custom version of ReplacementSpan
public class TagBadgeSpannable extends ReplacementSpan implements LineHeightSpan {
private static int CORNER_RADIUS = 30;
private final int textColor;
private final int backgroundColor;
private final int lineHeight;
public TagBadgeSpannable(int lineHeight, int textColor, int backgroundColor) {
super();
this.textColor = textColor;
this.backgroundColor = backgroundColor;
this.lineHeight = lineHeight;
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
final float textSize = paint.getTextSize();
final float textLength = x + measureText(paint, text, start, end);
final float badgeHeight = textSize * 2.25f;
final float textOffsetVertical = textSize * 1.45f;
RectF badge = new RectF(x, y, textLength, y + badgeHeight);
paint.setColor(backgroundColor);
canvas.drawRoundRect(badge, CORNER_RADIUS, CORNER_RADIUS, paint);
paint.setColor(textColor);
canvas.drawText(text, start, end, x, y + textOffsetVertical, paint);
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return Math.round(paint.measureText(text, start, end));
}
private float measureText(Paint paint, CharSequence text, int start, int end) {
return paint.measureText(text, start, end);
}
#Override
public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) {
fontMetricsInt.bottom += lineHeight;
fontMetricsInt.descent += lineHeight;
}
}
And finally a builder that creates the Spannable
public class AndroidTagBadgeBuilder implements TagBadgeBuilder {
private final SpannableStringBuilder stringBuilder;
private final String textColor;
private final int lineHeight;
public AndroidTagBadgeBuilder(SpannableStringBuilder stringBuilder, int lineHeight, String textColor) {
this.stringBuilder = stringBuilder;
this.lineHeight = lineHeight;
this.textColor = textColor;
}
#Override
public void appendTag(String tagName, String badgeColor) {
final String nbspSpacing = "\u202F\u202F"; // none-breaking spaces
String badgeText = nbspSpacing + tagName + nbspSpacing;
stringBuilder.append(badgeText);
stringBuilder.setSpan(
new TagBadgeSpannable(lineHeight, Color.parseColor(textColor), Color.parseColor(badgeColor)),
stringBuilder.length() - badgeText.length(),
stringBuilder.length()- badgeText.length() + badgeText.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
stringBuilder.append(" ");
}
#Override
public CharSequence getTags() {
return stringBuilder;
}
#Override
public void clear() {
stringBuilder.clear();
stringBuilder.clearSpans();
}
}
The outcome will look something like this:
Tweak the measures in TagBadgeSpannable to your liking.
I've uploaded a very minimal sample project using this code to github so feel free to check it out.
NOTE: The sample uses Android Databinding and is written MVVM style
Text markup in Android is so poorly documented, writing this code is like feeling your way through the dark.
I've done a little bit of it, so I will share what I know.
You can handle line spacing by wrapping your chip spans inside a LineHeightSpan. LineHeightSpan is an interface that extends the ParagraphStyle marker interface, so this tells you it affects appearance at a paragraph level. Maybe a good way to explain it is to compare your ReplacementSpan subclass to an HTML <span>, whereas a ParagraphStyle span like LineHeightSpan is like an HTML <div>.
The LineHeightSpan interface consists of one method:
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv, int v,
Paint.FontMetricsInt fm);
This method is called for each line in your paragraph
text is your Spanned string.
start is the index of the character at the start of the current line
end is the index of the character at the end of the current line
spanstartv is (IIRC) the vertical offset of the entire span itself
v is (IIRC) the vertical offset of the current line
fm is the FontMetrics object, which is actually a returned (in/out) parameter. Your code will make changes to fm and TextView will use those when drawing.
So what the TextView will do is call this method once for every line it processes. Based on the parameters, along with your Spanned string, you set up the FontMetrics to render the line with the values of your choosing.
Here's an example I did for a bullet item in a list (think <ol><li>) where I wanted some separation between each list item:
#Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) {
int incr = Math.round(.36F * fm.ascent); // note: ascent is negative
// first line: add space to the top
if (((Spanned) text).getSpanStart(this) == start) {
fm.ascent += incr;
fm.top = fm.ascent + 1;
}
// last line: add space to the bottom
if (((Spanned) text).getSpanEnd(this) == end) {
fm.bottom -= incr;
}
}
Your version will probably be even simpler, just changing the FontMetrics the same way for each line that it's called.
When it comes to deciphering the FontMetrics, the logger and debugger are your friends. You'll just have to keep tweaking values until you get something you like.
Doesnt BackgroundColorSpan work?
For your specific case, you can also set the lineSpacing for the TextView.
One last option (didn't test this), would be to calculate the height of the span to be larger than the one that you are drawing. You can check getSize implementation in DynamicDrawableSpan to see how to set the height of the span using the given FontMetrics instance as a parameter.
I want to add a span at the end of each line in multiline strings. I tried to draw just a char, but nothing appeared at end of lines. But when ImageSpan is set, it can increase line height, but still no image at the line.
private void addSpansAtEnd() {
String s = "First line\nSecond line\Third line\n";
SpannableStringBuilder e = new SpannableStringBuilder(s);
int stop = start + count;
for (int start = -1;
(start = s.indexOf("\n", start)) > -1 && start < stop;
++start) {
e.setSpan(
/*new ImageSpan(getContext(), R.drawable.ic_line),*/
new NewLineSpan(),
start,
start + 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/* Trying to replace \\n with other character. */
private class NewLineSpan extends ReplacementSpan {
#Override
public int getSize(
Paint paint,
CharSequence text,
int start,
int end,
Paint.FontMetricsInt fm) {
return 0;
}
#Override
public void draw(
Canvas canvas,
CharSequence text,
int start,
int end,
float x,
int top,
int y,
int bottom,
Paint paint) {
paint.setColor(0xFF666666);
canvas.drawText("o", x, y, paint);
}
}
How do I draw at end of lines?
Putting a span on a newline seems sort of weird. A newline goes across two lines but doesn't have any width. I think you might have more luck if you add a space before the newline then put the span around the space:
for (int start = -1;
(start = s.indexOf("\n", start)) > -1 && start < stop;
start += 2) { // increment by 2 to skip the displaced newline
e.insert(start, " "); // insert a space for the span
e.setSpan(new ImageSpan(getContext(), R.drawable.ic_line),
start,
start + 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
stop++; // because a space was added
}
As the question indicates, I am working on a TextView which will show formatted text using SpannableStringBuilder. It has multiple paragraphs and I would like to know what would be the easiest (or at least the least complicated) way to set spacing between paragraphs using some inbuilt span. Is this possible? Or will I be required to build a custom span class for this?
Implement the LineHeightSpan and override chooseHeight method as follows
#Override
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv, int v, FontMetricsInt fm) {
Spanned spanned = (Spanned) text;
int st = spanned.getSpanStart(this);
int en = spanned.getSpanEnd(this);
if (start == st) {
fm.ascent -= TOP_SPACING;
fm.top -= TOP_SPACING;
}
if (end == en) {
fm.descent += BOTTOM_SPACING;
fm.bottom += BOTTOM_SPACING;
}
}
Don't forget to add \n at the end of your each paragraph text.
I'm trying to align to the right a portion of a RadioButton text using the AlignmentSpan class. However it is not working because the text is not aligned as expected.
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append(option.getLabel());
int start = builder.length() + 1;
builder.append(" ");
builder.append(price);
builder.append("€");
int end = builder.length();
builder.setSpan(new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
final StyleSpan bss = new StyleSpan(android.graphics.Typeface.BOLD);
builder.setSpan(bss, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
As you can see from the code I also apply a StyleSpan which works properly.
N.B RadioButton has android:layout_width="match_parent"
Random advice, might work or not, but... Did you try injecting Unicode control characters in the string?
(improperly) Using characters such as U+200E, U+200F, U+202A...U+202E you can convince the text renderer that they are parts of RTL mixed with LTR. Not sure if that helps or not, you might need to have stuff in separated paragraphs anyway, but that's the only thing I could think of right now.
Try this once android:layout_width="_wrap_content".
and try this and tell me result....
final StyleSpan bss = new StyleSpan(android.graphics.Typeface.BOLD);
builder.setSpan(bss, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
int end = builder.length();
builder.setSpan(new AlignmentSpan.Standard(Alignment.ALIGN_OPPOSITE), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
If i m not wrong You are overriding the span initially set by you. So try to set the span together.
I had a similar problem: How to combine in the same row left and right aligned text in TextView (RadioButton is TextView too). I have found a way in using the ReplacementSpan.
The idea is add one additional symbol (which won't be actual drawn) to the end of text and attach to this symbol your ReplacementSpan where you will be able to do whatever you want - as example drawing additional piece of text in proper place (aligned to right). ReplacementSpan lets define the width of the additional symbol, and this space will be unbreakable.
So, my variant of implementation this idea is can be used like this:
RightAlignLastLetterSpan.attach(
textView,
"right_aligned_piece_of_text",
R.style.TextAppereance_of_your_right_aligned_text);
It adds a text given as second argument to the textView styled with style given as third argument. Added text will be right aligned.
Here is full sources of RightAlignLastLetterSpan:
class RightAlignLastLetterSpan extends ReplacementSpan {
#SuppressWarnings("FieldCanBeLocal") private static boolean DEBUG = false;
private float textWidth = -1;
#Nullable private TextAppearanceSpan spanStyle;
#Nullable private String text;
#NonNull private TextView tv;
protected RightAlignLastLetterSpan(#NonNull TextView tv) {this.tv = tv;}
public static boolean attach(#Nullable TextView tv, #Nullable String text, #StyleRes int resourceTextAppearance) {
if (tv == null || isEmpty(text)) {
logWrongArg();
return false;
}
RightAlignLastLetterSpan span = new RightAlignLastLetterSpan(tv);
span.setSpanStyle(new TextAppearanceSpan(tv.getContext(), resourceTextAppearance));
span.setText(text);
SpannableString ss = new SpannableString(new StringBuffer(tv.getText()).append(" _"));
ss.setSpan(span, ss.length() - 1, ss.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(ss);
return true;
}
public void setSpanStyle(#Nullable TextAppearanceSpan spanStyle) {
textWidth = -1;
this.spanStyle = spanStyle;
}
public void setText(#Nullable String text) {
textWidth = -1;
this.text = text;
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
if (textWidth < 0) {
applyStyle(paint);
textWidth = isEmpty(this.text) ? 0 : paint.measureText(this.text);
}
return Math.round(textWidth);
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
if (textWidth == 0 || this.text == null) {
return;
}
int lineCount = tv.getLineCount();
if (lineCount < 1) {return;}
Rect lineBounds = new Rect();
int baseline = tv.getLineBounds(lineCount - 1, lineBounds);
lineBounds.offset(-tv.getPaddingLeft(), -tv.getPaddingTop());
baseline -= tv.getPaddingTop();
if (DEBUG) {
paint.setColor(Color.argb(100, 100, 255, 100));
canvas.drawRect(lineBounds, paint);
paint.setColor(Color.argb(100, 255, 100, 100));
canvas.drawRect(x, top, x + textWidth, bottom, paint);
}
applyStyle(paint);
canvas.drawText(this.text, lineBounds.right - textWidth, baseline, paint);
}
public void applyStyle(Paint paint) {
if (paint instanceof TextPaint && spanStyle != null) {
TextPaint tp = (TextPaint) paint;
spanStyle.updateDrawState(tp);
}
}
}
I have been trying to figure this one out for the last couple of days now, and have had no success...
I'm learning android right now, and am currently creating a calculator with history as my learning project. I have a TextView that is responsible for displaying all history... I'm using a digital font that looks like a calculator font, but this only looks good for digits and decimals and comma's. I want all operators to be highlighted and in a different font (Arial Narrow at the moment). I have been able to get this to work beautifully using a spannable string where I'm specifying a font color as well as a font using a CustomTypeFaceSpan class to apply my custom fonts.
The problem... When I mix the Typefaces, there seems to be an issue with the line height, so I found this post which demonstrates using another custom defined class to apply a line height to each added line of spannable text:
public class CustomLineHeightSpan implements LineHeightSpan{
private final int height;
public CustomLineHeightSpan(int height){
this.height = height;
}
#Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, FontMetricsInt fm) {
fm.bottom += height;
fm.descent += height;
}
}
This does not seem to work, and I can not figure out why. If I don't apply the different typefaces, then it displays as expected with no space above the first line, and about 5px spacing between lines. When I apply the alternate typefaces, there is a space of about 10 to 15px above the first line of text and the line spacing is about the same 10 to 15px.
There is no difference in the font size, only the typeface. What am I missing. I implemented the CustomLineHeightSpan which implements LineHeightSpan and overrides the chooseHeight method. I call it like so:
WordtoSpan.setSpan(new CustomLineHeightSpan(10), operatorPositions.get(ii), operatorPositions.get(ii) + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
It does not seem to matter what I put in the call to CustomLineHeightSpan. Nothing changes...
Anybody have any idea what I'm missing... I'm sure it's an "I can't believe I missed that" answer, but can't seem to figure it out at the moment.
Thanks for the help guys :-)
I finally found a more in depth example of the use of LineHeightSpan... Actually LineHeightSpan.WithDensity to be more precise... The following is the excerpt that helped me to resolve my issue:
private static class Height implements LineHeightSpan.WithDensity {
private int mSize;
private static float sProportion = 0;
public Height(int size) {
mSize = size;
}
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv, int v,
Paint.FontMetricsInt fm) {
// Should not get called, at least not by StaticLayout.
chooseHeight(text, start, end, spanstartv, v, fm, null);
}
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv, int v,
Paint.FontMetricsInt fm, TextPaint paint) {
int size = mSize;
if (paint != null) {
size *= paint.density;
}
if (fm.bottom - fm.top < size) {
fm.top = fm.bottom - size;
fm.ascent = fm.ascent - size;
} else {
if (sProportion == 0) {
/*
* Calculate what fraction of the nominal ascent
* the height of a capital letter actually is,
* so that we won't reduce the ascent to less than
* that unless we absolutely have to.
*/
Paint p = new Paint();
p.setTextSize(100);
Rect r = new Rect();
p.getTextBounds("ABCDEFG", 0, 7, r);
sProportion = (r.top) / p.ascent();
}
int need = (int) Math.ceil(-fm.top * sProportion);
if (size - fm.descent >= need) {
/*
* It is safe to shrink the ascent this much.
*/
fm.top = fm.bottom - size;
fm.ascent = fm.descent - size;
} else if (size >= need) {
/*
* We can't show all the descent, but we can at least
* show all the ascent.
*/
fm.top = fm.ascent = -need;
fm.bottom = fm.descent = fm.top + size;
} else {
/*
* Show as much of the ascent as we can, and no descent.
*/
fm.top = fm.ascent = -size;
fm.bottom = fm.descent = 0;
}
}
}
}
This was taken from this example.
What it does is as quoted below:
Forces the text line to be the specified height, shrinking/stretching
the ascent if possible, or the descent if shrinking the ascent further
will make the text unreadable.
I hope this helps the next person :-)