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);
}
}
}
Related
I have a Calendar inside my Application. The Calendar is a GridView with Buttons for every date. I tried to color them with the following class
public class CircleSpan extends ReplacementSpan {
private final float mPadding;
private final int mCircleColor;
private final int mTextColor;
public CircleSpan(Context context) {
super();
TypedArray ta = context.getTheme().obtainStyledAttributes(new int[]{
R.color.current_day,
android.R.attr.textColorPrimaryInverse
});
mCircleColor = ta.getColor(0, ContextCompat.getColor(context, R.color.current_day));
//noinspection ResourceType
mTextColor = ta.getColor(1, 0);
ta.recycle();
mPadding = context.getResources().getDimension(R.dimen.padding_circle);
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
Log.d("CircleSpan", "getSize");
return Math.round(paint.measureText(text, start, end) + mPadding * 2); // left + right
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
if (TextUtils.isEmpty(text)) {
Log.d("CircleSpan", "empty draw");
return;
}
float textSize = paint.measureText(text, start, end);
paint.setColor(mCircleColor);
canvas.drawCircle(x + textSize / 2 + mPadding,
(top + bottom) / 2, // center Y
(text.length() == 1 ? textSize : textSize / 2) + mPadding,
paint);
paint.setColor(mTextColor);
canvas.drawText(text, start, end, mPadding + x, y, paint);
Log.d("CircleSpan", "draw");
}
}
I created the class and tested it with a Lollipop Test Device and everything worked fine. After that I put the Application on my device with Marshmallow. The entries inside the Calendar which should have a color weren't visible anymore. I found out that the draw method inside my CircleSpan class didn't even got called.
With a little "hack" i was able to get it working but I'm really not satisfied by the solution. It consists of a TextView that is not visible on the end of the screen which also gets colored with the CircleSpan. The difference consists of an extension of the text and just color everything except the extension:
// Absolutly hacked
SpannableString spannable1 = new SpannableString(theday + " ");
spannable1.setSpan(new CircleSpan(gridcell.getContext(), ColorType.NONE),
0, theday.length() - 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mHack.setText(spannable1, TextView.BufferType.SPANNABLE);
As long the "hack" is inside the App everything else got colored like I coded it. But I'm really not sure why. I've read about the ReplacementSpan in the Android documentation: ReplacementSpan getSize
But the clue
Returns the width of the span. Extending classes can set the height of the span by updating attributes of Paint.FontMetricsInt. If the span covers the whole text, and the height is not set, draw(Canvas, CharSequence, int, int, float, int, int, int, Paint) will not be called for the span.
doesen't help me. Does anyone has an idea how its possible to color the entries of my calendar with my class and without the "hack"? And why is the problem just on Marshmallow Devices? I'm not sure about Nougat and whats happening on devices below Lollipop.
I hope everyone can understand my poor English. Thanks in advance!
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
}
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);
}
Currently I have a SpannableString object with multiple Clickable objects set to it. So one string has many Clickable objects and depending on which word/section the User clicks the app will go on and do the processing of that click event. The other day I had asked here on stackoverflow about getting rid of the blue underline on part of the word in the SpannableString and the answer was to sub class the ClickableSpan class, and override the updateDrawState method and setting the underlineText to false which worked.
My Problem:
Is it possible to put a border around the Clickable object in the SpannableString? So basically each Clickable object/string has to have there own border.
I thought maybe the updateDrawState method maybe able to help but it didn't. Does anybody know how this can be achieved?
Thanks.
I extended ReplacementSpan to make an outlined span. Unfortunately, I can't manage to make them wrap, but if you're only looking to apply your outline to a couple words, it should work fine. To make this clickable, you'd just use the subclass you mentioned setSpan(ClickableSpanWithoutUnderline...) before you set this one.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_replacement_span);
final Context context = this;
final TextView tv = (TextView) findViewById(R.id.tv);
Spannable span = Spannable.Factory.getInstance().newSpannable("Some string");
span.setSpan(new BorderedSpan(context), 0, span.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(span, TextView.BufferType.SPANNABLE);
}
public static class BorderedSpan extends ReplacementSpan {
final Paint mPaintBorder, mPaintBackground;
int mWidth;
Resources r;
int mTextColor;
public BorderedSpan(Context context) {
mPaintBorder = new Paint();
mPaintBorder.setStyle(Paint.Style.STROKE);
mPaintBorder.setAntiAlias(true);
mPaintBackground = new Paint();
mPaintBackground.setStyle(Paint.Style.FILL);
mPaintBackground.setAntiAlias(true);
r = context.getResources();
mPaintBorder.setColor(Color.RED);
mPaintBackground.setColor(Color.GREEN);
mTextColor = Color.BLACK;
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
//return text with relative to the Paint
mWidth = (int) paint.measureText(text, start, end);
return mWidth;
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
canvas.drawRect(x, top, x + mWidth, bottom, mPaintBackground);
canvas.drawRect(x, top, x + mWidth, bottom, mPaintBorder);
paint.setColor(mTextColor); //use the default text paint to preserve font size/style
canvas.drawText(text, start, end, x, y, paint);
}
}