How to add ImageSpan at end of lines in multiline text - android

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
}

Related

Android - Add Margin for SpannableStringBuilder using ReplacementSpan

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.

EditText setSpan for special characters

How to set span for special characters, like \n \t \r , etc ? Right now if i do this:
getText().setSpan(DynamicListView.mBackgroundColor, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Thanks
Not 100% sure I understand the question, as the special characters '\n' nd '\r' are line breaks, so how could they be styled as they will be invisible?
That said, here is a method for styling certain chars in an EditText. You can add as many chars as you like to the end of the method and any instances of those characters will be styled.
//...
editText.setText(getSpannedText(editText.getText(), `u`, `r`));
//...
private static SpannableString getSpannedText(String text, char... triggers) {
SpannableString spanString = new SpannableString(text);
for (int i = 0; i < spanString.length(); i++) {
for (char trigger : triggers) {
if (spanString.charAt(i) == trigger) {
spanString.setSpan(new ForegroundColorSpan(Color.CYAN), i, i+1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return spanString;
}
Fixed on my side by Draw functions, here is code of EditText:
if(getText().toString().substring(start,end).indexOf("\t")>=0
|| getText().toString().substring(start,end).indexOf("\n")>=0
|| getText().toString().substring(start,end).indexOf("\r")>=0) {
TextPaint paint = new TextPaint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(mBackgroundColor);
paint.bgColor = mBackgroundColor;
Layout layout = getLayout();
int line = layout.getLineForOffset(start);
int baseline = layout.getLineBaseline(line);
int ascent = layout.getLineAscent(line);
float x = layout.getPrimaryHorizontal(start);
float y = baseline + ascent;
Rect rect = new Rect();
//rect.set(start, 0, Math.round(layout.getSecondaryHorizontal(end)), getLineHeight()+5);
rect.set(start, 0, Math.round(layout.getSecondaryHorizontal(end)), getLineHeight()+5);
rect.offset(Math.round(x), Math.round(y));
Log.d("debug","tabs " + rect.toString());
canvas.drawRect(rect, paint);
}

RecyclerView with wrap content items

I need to implement next UI element:
Unknown size list of strings (That came from a server call)
Any item should be wrap content.
If an item is not fits to row, he will be in the next row.
All list/grid is centered
I thought of using RecyclerView with StaggeredGridLayoutManager
But I don't know if it the right way, any ideas?
I don't sure that method would be helpful for you, but instead of
using RecyclerView with StaggeredGridLayoutManager
you can use third-party FlowLayout:
First implementation (Android flow layout)
Second implementation (Flow layout)
Check this gist for full example:
https://github.com/davidbeloo/Hashtags
The fact that you have a varying number of cells in each row means you would have to work rather hard to get any value from a recycling approach. Because in order to know which data goes in row 17 you have to (pre) measure all the data in rows 0 - 16.
Depending on your use case. If the list is bounded at some reasonable number of items. Using a single TextView, with some clever use of spans may be a better solution. Just collect all your hashtags into a single string, and use RoundedBackgroundSpan (see link) to add the colored backgrounds. then wrap the whole thing in a ScrollView.
EDIT 1: Added possible solution code.
public class RoundedBackgroundSpan extends ReplacementSpan {
int mBackgroundColor;
int mTextColor;
float mRoundedCornerRadius;
float mSidePadding = 10; // play around with these as needed
float mVerticalPadding = 30; // play around with these as needed
public RoundedBackgroundSpan(final int backgroundColor, final int textColor, final float roundedCornerRadius)
{
mBackgroundColor = backgroundColor;
mTextColor = textColor;
mRoundedCornerRadius = roundedCornerRadius;
}
#Override
public int getSize(final Paint paint, final CharSequence text, final int start, final int end, final Paint.FontMetricsInt fm)
{
return Math.round(MeasureText(paint, text, start, end) + (2 * mSidePadding));
}
#Override
public void draw(final Canvas canvas, final CharSequence text, final int start, final int end, final float x, final int top, final int y, final int bottom, final Paint paint)
{
// draw the rounded rectangle background
RectF rect = new RectF(x, -mVerticalPadding + ((bottom + top) / 2) + paint.getFontMetrics().top, x + MeasureText(paint, text, start, end) + (2 * mSidePadding), mVerticalPadding + ((bottom + top) / 2) + paint.getFontMetrics().bottom);
paint.setColor(mBackgroundColor);
canvas.drawRoundRect(rect, mRoundedCornerRadius, mRoundedCornerRadius, paint);
// draw the actual text
paint.setColor(mTextColor);
canvas.drawText(text, start, end, x + mSidePadding, ((bottom + top) / 2), paint);
}
private float MeasureText(Paint paint, CharSequence text, int start, int end)
{
return paint.measureText(text, start, end);
}
}
And somewhere else (activity / fragment most likely)
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
for (String hashTag : hashTags)
{
stringBuilder.append(hashTag);
stringBuilder.setSpan(new RoundedBackgroundSpan(getRandomColor(), getResources().getColor(android.R.color.darker_gray), 10), stringBuilder.length() - hashTag.length(), stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
stringBuilder.append(" ");
}
textView.setText(stringBuilder);
And somewhere in your xml (note android:lineSpacingMultiplier="3" and android:gravity="center")
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:lineSpacingMultiplier="3"
android:gravity="center"
android:padding="10dp"
android:id="#+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>

incorrect words with Spannable

I would like to have a custom Spannable like below picture :
I would like to create a Zigzag line under a incorrect words.
How can I do?
You most definitely want to do some checking on this implementation I hacked together. But still. It's in hopes it still manages to give some basis for implementing such a feature properly.
Actual span class, which, surprisingly ,doesn't replace anything. Only hopes that it really is possible to draw the original span text as-is given two lines of code. Plus additionally draws the 'underline'.
private class ErrorSpan extends ReplacementSpan {
private Paint errorPaint;
public ErrorSpan() {
errorPaint = new Paint();
errorPaint.setColor(Color.RED);
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end,
FontMetricsInt fm) {
return (int)paint.measureText(text, start, end);
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {
// Render the red zigzag lines below text
float width = paint.measureText(text, start, end);
canvas.save();
canvas.clipRect(x, bottom - 5, x + width, bottom);
for (float lineX = x; lineX < x + width; lineX += 10) {
canvas.drawLine(lineX, bottom - 5, lineX + 5, bottom, errorPaint);
canvas.drawLine(lineX + 5, bottom, lineX + 10, bottom - 5, errorPaint);
}
canvas.restore();
// Render the span text as-is
canvas.drawText(text, start, end, x, y, paint);
}
};
Pardon me for using magic numbers in line drawing loop (which quite likely could be way more effective too) - but hopefully it manages to give good enough basis for creating production quality implementation at the end.
And usage would be somewhere around:
TextView tv = (TextView)findViewById(R.id.textview);
Spannable spannable = Spannable.Factory.getInstance()
.newSpannable("testtest\ntesttest");
spannable.setSpan(new ErrorSpan(), 4, 8, 0);
spannable.setSpan(new ErrorSpan(), 9, 13, 0);
tv.setText(spannable);

AlignmentSpan not working for RadioButton 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);
}
}
}

Categories

Resources