Alternative to ReplacementSpan in Android - 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);

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);
}
}

Text is not fitting inside a replacement span in android

I want to create a custom background around a block of text. For that, I have come up with a replacement span. Everything was right but the background color is not correctly fitting up with each text. line spacing between each text is 48dp. If I increase the spacing, then the span color is increasing and vice versa. Currently, it seems like the bottom portion of the background fills until the starting of the next line.
This is the code I'm using:
public class CoolBackgroundColorSpan extends ReplacementSpan {
private final int mBackgroundColor;
private final int mTextColor;
private final float mCornerRadius;
private final float mPaddingStart;
private final float mPaddingEnd;
private final float mMarginStart;
// private float mheight;
public CoolBackgroundColorSpan(int mBackgroundColor, int mTextColor, float mCornerRadius, float mPaddingStart, float mPaddingEnd, float mMarginStart) {
super();
this.mBackgroundColor = mBackgroundColor;
this.mTextColor = mTextColor;
this.mCornerRadius = mCornerRadius;
this.mPaddingStart = mPaddingStart;
this.mPaddingEnd = mPaddingEnd;
this.mMarginStart = mMarginStart;
}
#Override
public int getSize(#NonNull Paint paint, CharSequence text, int start, int end, #Nullable Paint.FontMetricsInt fm) {
return (int) (mPaddingStart + paint.measureText(text.subSequence(start, end).toString()) + mPaddingEnd);
}
#Override
public void draw(#NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, #NonNull Paint paint) {
float width = paint.measureText(text.subSequence(start, end).toString());
RectF rect = new RectF(x - mPaddingStart + mMarginStart, top, x + width + mPaddingEnd + mMarginStart, bottom);
paint.setColor(mBackgroundColor);
canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, paint);
paint.setColor(mTextColor);
canvas.drawText(text, start, end, x + mMarginStart, y, paint);
}
I have used LineHeightSpan. But this background color has a movement. This will move to the corresponding text below with each interval of time, like in karaoke. So that time, the line heigth is also increasing.
Since no one has answered yet, I'll post the answer
public void draw(#NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, #NonNull Paint paint) {
float width = paint.measureText(text.subSequence(start, end).toString());
RectF rect = new RectF(x - mPaddingStart + mMarginStart, top, x + width + mPaddingEnd + mMarginStart,bottom);
paint.setColor(mBackgroundColor);
canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, paint);
paint.setColor(mTextColor);
canvas.drawText(text, start, end, x + mMarginStart, y + (YourValue), paint);
}
Here give the value in (YourValue, lets say 11)

ReplacementSpan cuts line in TextView

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);
}
}

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 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