ReplacementSpan cuts line in TextView - android

I need to use custom underline for text in my TextView.
I use ReplacementSpan to do this. But it cuts text at the end of the first line.
Here is my CustomUnderlineSpan class:
public class CustomUnderlineSpan extends ReplacementSpan {
private int underlineColor;
private int textColor;
public CustomUnderlineSpan(int underlineColor, int textColor) {
super();
this.underlineColor = underlineColor;
this.textColor = textColor;
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
paint.setStrokeWidth(3F);
paint.setColor(textColor);
canvas.drawText(text, start, end, x, y, paint);
paint.setColor(underlineColor);
int length = (int) paint.measureText(text.subSequence(start, end).toString());
canvas.drawLine(x, bottom, length + x, bottom, paint);
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return Math.round(paint.measureText(text, start, end));
}
}
This is method to implement CustomUnderlineSpan for all text length:
public static Spannable getCustomUnderlineSpan(String string, int underlineColor, int textColor) {
Spannable spannable = new SpannableString(string);
CustomUnderlineSpan customUnderlineSpan = new CustomUnderlineSpan(underlineColor, textColor);
spannable.setSpan(customUnderlineSpan, 0, spannable.length(), 0);
return spannable;
}
And here is setting text to TextView:
String text = "Just text to underline Just text to underline Just text" +
"to underline Just text to underline Just text to underline Just text" +
"to underline Just text to underline Just text to underline";
textView.setText(getCustomUnderlineSpan(text,
Color.parseColor("#0080ff"), Color.parseColor("#000000")), TextView.BufferType.SPANNABLE);
Result:
Do you have any suggestions why text cuts at the end of line?
Thanks!

Solved
Using DynamicDrawableSpan instead ReplacementSpan solved the problem.
It seems ReplacementSpan just cant perform line breaks.
Here is my code:
public class CustomUnderlineSpan extends DynamicDrawableSpan {
private int underlineColor;
private int textColor;
private final float STROKE_WIDTH = 3F;
public CustomUnderlineSpan(int underlineColor, int textColor) {
super(DynamicDrawableSpan.ALIGN_BASELINE);
this.underlineColor = underlineColor;
this.textColor = textColor;
}
#Override
public Drawable getDrawable() {
return null;
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end,
Paint.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) {
int length = (int) paint.measureText(text.subSequence(start, end).toString());
paint.setColor(underlineColor);
paint.setStrokeWidth(STROKE_WIDTH);
canvas.drawLine(x, bottom - STROKE_WIDTH / 2, length + x, bottom - STROKE_WIDTH / 2, paint);
paint.setColor(textColor);
canvas.drawText(text.subSequence(start, end).toString(), x, y, paint);
}
}

Related

Android BackgroundColorSpan breaking with lineSpacing less than 1

I have got a textView to which I do this:
textView.setLineSpacing(1f, .70f);
and then I would like to set a background color only to specific words in that textView so I've tried this:
spannableStringBuilder.setSpan(new BackgroundColorSpan(bckgndColor), spanStart, spanEnd, 0);
The problem is that because of the lineSpacing it shows up like this:
as opposed to if I were to comment out the lineSpacing() line I would have this, which is perfect:
Any ideas on how to solve this problem? I tried playing around with the BackgroundColorSpan object, but after digging in the code of the class I see that it only does this:
/**
* Updates the background color of the TextPaint.
*/
#Override
public void updateDrawState(#NonNull TextPaint textPaint) {
textPaint.bgColor = mColor;
}
and even if I override the class, I don't have access to any "Rect" value to adjust or ... dunno..
Any ideas are appreciated. Thank you !
With help from MikeM. this is what I was looking for:
public class FauxBackgroundColorSpan extends ReplacementSpan {
private final Rect tmpBounds = new Rect();
private final int backgroundColor;
public FauxBackgroundColorSpan(#ColorInt int color) {
backgroundColor = color;
}
#Override
public int getSize(Paint paint, CharSequence text, int start,
int end, Paint.FontMetricsInt fm) {
// Necessary for full length spans to be drawn.
if (fm != null) {
final Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
fm.top = pfm.top;
fm.ascent = pfm.ascent;
fm.descent = pfm.descent;
fm.bottom = pfm.bottom;
}
// This would normally be the width of whatever we're replacing the text with, but
// we just return the text's own measure, since we're not really replacing anything.
return getTextMeasure(paint, 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) {
// Draw the background.
final int previousColor = paint.getColor();
final Paint.Style previousStyle = paint.getStyle();
paint.setColor(backgroundColor);
paint.setStyle(Paint.Style.FILL);
final Rect bounds = tmpBounds;
paint.getTextBounds(text.toString(), start, end, bounds);
canvas.drawRect(x, y + bounds.top,
x + getTextMeasure(paint, text, start, end), y + bounds.bottom, paint);
paint.setStyle(previousStyle);
paint.setColor(previousColor);
// Draw the text we "replaced".
canvas.drawText(text, start, end, x, y, paint);
}
private int getTextMeasure(Paint paint, CharSequence text, int start, int end) {
return (int) (paint.measureText(text, start, end) + .5f);
}
}

Alternative to ReplacementSpan in Android

I have an app that pages large text and sets multiple spans to each word or sentence. I am using ReplacementSpan to draw the background for each word. I cannot use BackgroundSpan because it is too simple and doesn't give me control over the canvas. Because of ReplacementSpan extending MetricAffectingSpan which affects the layout of the text, breaking my paging completely. I am using StaticLayout to calculate the text for each page, and StaticLayout doesn't allow spanning so it can calculate a priori the spanning size influences.
Is there a replacement to ReplacementSpan? How can I draw the background I want without affecting the size and layout of the text itself?
This is the code for my replacementspan:
public class BackgroundColorWithoutLineHeightSpan extends ReplacementSpan {
private static final float DP_ACTIVE = ViewsUtils.dpToPx(4);
private static final int DP_OUTSIDE_PADDING = (int) ViewsUtils.dpToPx(6);
private static final float DP_PHRASE = ViewsUtils.dpToPx(4);
private static final float DP_ROUNDED = ViewsUtils.dpToPx(3);
private final int mColor;
private final int mTextHeight;
private int mBorderColor;
private boolean mIsSelected;
private boolean mIsPhrase;
public BackgroundColorWithoutLineHeightSpan(int color, int textHeight, boolean isPhrase) {
mColor = color;
mTextHeight = textHeight;
mIsPhrase = isPhrase;
}
public BackgroundColorWithoutLineHeightSpan(int color, int textHeight, boolean isSelected, int borderColor, boolean isPhrase) {
mColor = color;
mTextHeight = textHeight;
mIsSelected = isSelected;
mBorderColor = borderColor;
mIsPhrase = isPhrase;
}
#Override
public int getSize(#NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return Math.round(measureText(paint, text, start, end));
}
#Override
public void draw(#NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
canvas.save();
Rect newRect = canvas.getClipBounds();
newRect.inset(-DP_OUTSIDE_PADDING, -DP_OUTSIDE_PADDING);
canvas.clipRect(newRect, Region.Op.REPLACE);
float measuredText = measureText(paint, text, start, end);
int paintColor = paint.getColor();
if (!mIsSelected) {
RectF rect;
rect = new RectF(x, top, x + measuredText, top + mTextHeight);
paint.setStrokeWidth(0.0f);
paint.setColor(mColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawRoundRect(rect, DP_ROUNDED, DP_ROUNDED, paint);
} else {
RectF rect;
if (mIsPhrase) {
rect = new RectF(x - DP_PHRASE, top - DP_PHRASE, x + measuredText + DP_PHRASE, top + mTextHeight + DP_PHRASE);
} else {
rect = new RectF(x - DP_ACTIVE, top - DP_ACTIVE, x + measuredText + DP_ACTIVE, top + mTextHeight + DP_ACTIVE);
}
paint.setStrokeWidth(0.0f);
paint.setColor(mColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawRoundRect(rect, DP_ROUNDED, DP_ROUNDED, paint);
RectF border;
if (mIsPhrase) {
border = new RectF(x - DP_PHRASE, top - DP_PHRASE, x + measuredText + DP_PHRASE, top + mTextHeight + DP_PHRASE);
} else {
border = new RectF(x - DP_ACTIVE, top - DP_ACTIVE, x + measuredText + DP_ACTIVE, top + mTextHeight + DP_ACTIVE);
}
paint.setColor(mBorderColor);
paint.setStrokeWidth(4.0f);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRoundRect(border, DP_ROUNDED, DP_ROUNDED, paint);
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(paintColor);
canvas.drawText(text, start, end, x, y, paint);
canvas.restore();
}
private float measureText(Paint paint, CharSequence text, int start, int end) {
return paint.measureText(text, start, end);
}
}
try this simple span, it draws solid red background over all the span (even if it is multi-line span) but you can draw whatever you like:
class LBS implements LineBackgroundSpan {
private final TextView tv;
private int start;
private int end;
public LBS(TextView tv, int start, int end) {
this.tv = tv;
this.start = start;
this.end = end;
}
#Override
public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lnum) {
Layout layout = tv.getLayout();
int startLine = layout.getLineForOffset(this.start);
int endLine = layout.getLineForOffset(this.end);
if (startLine <= lnum && lnum <= endLine) {
if (startLine == lnum) {
left = (int) layout.getPrimaryHorizontal(this.start);
}
if (endLine == lnum) {
right = (int) layout.getPrimaryHorizontal(this.end);
}
int origColor = p.getColor();
p.setColor(Color.RED);
c.drawRect(left, top, right, bottom, p);
p.setColor(origColor);
}
}
}
testing code (setting 0 and ssb.length() as a start and end is not very efficient so you can optimize it):
TextView tv = new TextView(this);
setContentView(tv);
tv.setTextSize(32);
SpannableStringBuilder ssb = new SpannableStringBuilder("Chop a handfull spinach, pork shoulder, and dill in a large cooker over medium heat, cook for six minutes and varnish with some bok choy.");
LBS span = new LBS(tv, 30, 100);
ssb.setSpan(span, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(ssb);
Log.d(TAG, "onCreate text [" + ssb.subSequence(30, 100) + "]");
EDIT
if you have multiple words to mark / highlight you could use such a modified version of it:
class LBS implements LineBackgroundSpan {
TextView tv;
List<Pair<Integer, Integer>> ranges;
public LBS(TextView tv) {
this.tv = tv;
ranges = new ArrayList<>();
}
public void add(int start, int end) {
ranges.add(new Pair<>(start, end));
}
#Override
public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lnum) {
Layout layout = tv.getLayout();
for (Pair<Integer, Integer> range : ranges) {
int startLine = layout.getLineForOffset(range.first);
int endLine = layout.getLineForOffset(range.second);
if (startLine <= lnum && lnum <= endLine) {
if (startLine == lnum) {
left = (int) layout.getPrimaryHorizontal(range.first);
}
if (endLine == lnum) {
right = (int) layout.getPrimaryHorizontal(range.second);
}
int origColor = p.getColor();
p.setColor(Color.RED);
c.drawRect(left, top, right, bottom, p);
p.setColor(origColor);
}
}
}
}
test code:
TextView tv = new TextView(this);
setContentView(tv);
tv.setTextSize(32);
String text = "Chop a handfull spinach, pork shoulder, and dill in a large cooker over medium heat, cook for six minutes and varnish with some bok choy.";
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
LBS span = new LBS(tv);
String[] words = {
"spinach, pork shoulder", "cooker", "with some bok choy",
};
for (String word : words) {
int idx = text.indexOf(word);
span.add(idx, idx + word.length());
}
ssb.setSpan(span, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(ssb);

Adding a padding/margin to a Spannable

I'm using BackgroundColorSpan to customize parts of a TextView.
Here's the code I have:
String s = "9.5 Excellent!";
s.setSpan(new BackgroundColorSpan(darkBlue, 0, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
s.setSpan(new BackgroundColorSpan(darkBlue, 3, 14, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Which gives me the following result:
And this is what I'm trying to achieve:
As you can see, I'm trying to add a padding to the "9.5" as well as the "excellent!" Strings, but I've been unable to find a solution so far.
Is there a way to add this padding/margin to these Spannables?
You can use ReplacementSpan.
In your Activity:
TextView tagsTextView = (TextView) mView.findViewById(R.id.tagsTextView);
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
SpannableString tag1 = new SpannableString("9.5");
stringBuilder.append(tag1);
stringBuilder.setSpan(new TagSpan(getResources().getColor(R.color.blue), getResources().getColor(R.color.white)), stringBuilder.length() - tag1.length(), stringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString tag2 = new SpannableString("excellent!");
stringBuilder.append(tag2);
stringBuilder.setSpan(new TagSpan(getResources().getColor(R.color.blueLight), getResources().getColor(R.color.blue)), stringBuilder.length() - tag2.length(), stringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tagsTextView.setText(stringBuilder, TextView.BufferType.SPANNABLE);
TagSpan.java
public class TagSpan extends ReplacementSpan {
private static final float PADDING = 50.0f;
private RectF mRect;
private int mBackgroundColor;
private int mForegroundColor;
public TagSpan(int backgroundColor, int foregroundColor) {
this.mRect = new RectF();
this.mBackgroundColor = backgroundColor;
this.mForegroundColor = foregroundColor;
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
// Background
mRect.set(x, top, x + paint.measureText(text, start, end) + PADDING, bottom);
paint.setColor(mBackgroundColor);
canvas.drawRect(mRect, paint);
// Text
paint.setColor(mForegroundColor);
int xPos = Math.round(x + (PADDING / 2));
int yPos = (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2)) ;
canvas.drawText(text, start, end, xPos, yPos, paint);
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
return Math.round(paint.measureText(text, start, end) + PADDING);
}
}
You can use RoundedBackgroundSpan.
It supports both padding and margin
It avoids the situation where the text is not centered in the TextView.
It can also set BackgroundColor for the text.
public class RoundedBackgroundSpan extends ReplacementSpan {
private final int mBackgroundColor;
private final int mTextColor;
private final int mPaddingLeft;
private final int mPaddingRight;
private final int mMarginLeft;
private final int mMarginRight;
/**
* Add rounded background for text in TextView.
* #param backgroundColor background color
* #param textColor text color
* #param paddingLeft padding left(including background)
* #param paddingRight padding right(including background)
* #param marginLeft margin left(not including background)
* #param marginRight margin right(not including background)
*/
public RoundedBackgroundSpan(int backgroundColor, int textColor,
int paddingLeft,
int paddingRight,
int marginLeft,
int marginRight) {
mBackgroundColor = backgroundColor;
mTextColor = textColor;
mPaddingLeft = paddingLeft;
mPaddingRight = paddingRight;
mMarginLeft = marginLeft;
mMarginRight = marginRight;
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fm) {
return (int) (mMarginLeft + mPaddingLeft +
paint.measureText(text.subSequence(start, end).toString()) +
mPaddingRight + mMarginRight);
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
int bottom, Paint paint) {
float width = paint.measureText(text.subSequence(start, end).toString());
RectF rect = new RectF(x + mMarginLeft, top
- paint.getFontMetricsInt().top + paint.getFontMetricsInt().ascent
, x + width + mMarginLeft + mPaddingLeft + mPaddingRight, bottom);
paint.setColor(mBackgroundColor);
canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, paint);
paint.setColor(mTextColor);
canvas.drawText(text, start, end, x + mMarginLeft + mPaddingLeft,
y - paint.getFontMetricsInt().descent / 2, paint);
}
}

Android SpannableString set background behind part of text

I would like to create something similar as seen on this image:
I managed to create evertyhing with SpannableStringBuilder, except the orange rounded rectangle. I can set the background to that color with BackgroundColorSpan, but I can't find a way to make it rounded. Any ideas how can I achieve this?
Thanks in advance!
EDIT:
I'm using Xamarin.Android, but here is my code:
stringBuilder.SetSpan(new BackgroundColorSpan(Application.Context.Resources.GetColor(Resource.Color.orangeColor)), stringBuilder.Length() - length, stringBuilder.Length(), SpanTypes.ExclusiveExclusive);
If anyone's having difficulty with Roosevelt's code sample (I sure was, maybe because it's Xamarin.Android?), here's a translation into a more basic Android java version:
public class RoundedBackgroundSpan extends ReplacementSpan {
private static int CORNER_RADIUS = 8;
private int backgroundColor = 0;
private int textColor = 0;
public RoundedBackgroundSpan(Context context) {
super();
backgroundColor = context.getResources().getColor(R.color.gray);
textColor = context.getResources().getColor(R.color.white);
}
#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);
}
#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);
}
}
And for usage, the following code segment is taken from an Activity and basically puts a nice rounded-corner background around each tag string, with a spacial buffer in between each tag. Note that the commented out line just puts in a background color, which doesn't produce as nice a look...
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
String between = "";
for (String tag : eventListing.getTags()) {
stringBuilder.append(between);
if (between.length() == 0) between = " ";
String thisTag = " "+tag+" ";
stringBuilder.append(thisTag);
stringBuilder.setSpan(new RoundedBackgroundSpan(this), stringBuilder.length() - thisTag.length(), stringBuilder.length() - thisTag.length() + thisTag.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//stringBuilder.setSpan(new BackgroundColorSpan(getResources().getColor(R.color.gray)), stringBuilder.length() - thisTag.length(), stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
TextView tv = new TextView(this);
tv.setText(stringBuilder);
I managed to solve my problem, based on pskink's suggestion.
Here is my class:
public class RoundedBackgroundSpan : ReplacementSpan
{
public override void Draw(Canvas canvas, ICharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)
{
var rect = new RectF(x, top, x + MeasureText(paint, text, start, end), bottom);
paint.Color = Application.Context.Resources.GetColor(Resource.Color.nextTimeBackgroundColor);
canvas.DrawRoundRect(rect, Application.Context.Resources.GetDimensionPixelSize(Resource.Dimension.localRouteDetailsRoundRectValue), Application.Context.Resources.GetDimensionPixelSize(Resource.Dimension.localRouteDetailsRoundRectValue), paint);
paint.Color = Application.Context.Resources.GetColor(Resource.Color.nextTimeTextColor);
canvas.DrawText(text, start, end, x, y, paint);
}
public override int GetSize(Paint paint, ICharSequence text, int start, int end, Paint.FontMetricsInt fm)
{
return Math.Round(MeasureText(paint, text, start, end));
}
private float MeasureText(Paint paint, ICharSequence text, int start, int end)
{
return paint.MeasureText(text, start, end);
}
}
Example usage:
var stringBuilder = new SpannableStringBuilder();
var stringToAppend = "hello world";
stringBuilder.Append(stringToAppend);
stringBuilder.SetSpan(new RoundedBackgroundSpan(), stringBuilder.Length() - stringToAppend.Length, stringBuilder.Length(), SpanTypes.ExclusiveExclusive);
just one word: ReplacementSpan

Android paddings for Spannable?

I use this code to set background for piece of text inside TextView:
s.setSpan(new BackgroundColorSpan(getResources().getColor(R.color.selection_blue)), prevIndex, index, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
But I need to set padding for this text too. Is it possible?
You can use ReplacementSpan.
In your Activity:
TextView tagsTextView = (TextView) mView.findViewById(R.id.tagsTextView);
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
SpannableString tag = new SpannableString("TEST");
stringBuilder.append(tag);
stringBuilder.setSpan(new TagSpan(getResources().getColor(R.color.blue), getResources().getColor(R.color.white)), stringBuilder.length() - tag.length(), stringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tagsTextView.setText(stringBuilder, TextView.BufferType.SPANNABLE);
TagSpan.java
public class TagSpan extends ReplacementSpan {
private static final float PADDING = 50.0f;
private RectF mRect;
private int mBackgroundColor;
private int mForegroundColor;
public TagSpan(int backgroundColor, int foregroundColor) {
this.mRect = new RectF();
this.mBackgroundColor = backgroundColor;
this.mForegroundColor = foregroundColor;
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
// Background
mRect.set(x, top, x + paint.measureText(text, start, end) + PADDING, bottom);
paint.setColor(mBackgroundColor);
canvas.drawRect(mRect, paint);
// Text
paint.setColor(mForegroundColor);
int xPos = Math.round(x + (PADDING / 2));
int yPos = (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2)) ;
canvas.drawText(text, start, end, xPos, yPos, paint);
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
return Math.round(paint.measureText(text, start, end) + PADDING);
}
}

Categories

Resources