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);
}
}
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.
My goal is to create this badge-like view used in Gmail and Foursquare as given below.
So far I have created ReplacementSpan to handle foreground and background color of individual view.
public class SearchTagSpan extends ReplacementSpan {
private int backgroundColor;
private int forgroundColor;
public SearchTagSpan() {
backgroundColor = -1;
forgroundColor = Color.BLACK;
}
public SearchTagSpan(int backgroundColor, int forgroundColor) {
this.backgroundColor = backgroundColor;
this.forgroundColor = forgroundColor;
}
#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.drawRect(rect, paint);
paint.setColor(forgroundColor);
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);
}
}
Both background and foreground color is applied correctly however when the view becomes multiple-line the background of each badge stretch to fill the line spacing specified in the TextView layout. If I remove line spacing then the background of the first and second line are touching each other as shown in the image below.
Am I going the right direction here? Is there something I missed?
Take a look at these
https://plus.google.com/+RomanNurik/posts/WUd7GrfZfiZ
and
https://github.com/splitwise/TokenAutoComplete
and
https://github.com/kpbird/chips-edittext-library
For that you need ActionBar.
This supports above +3.0
If you need same in lower version of android then you can use appCompat support library in order to support lower version of android.
Kindly refer .. http://developer.android.com/guide/topics/ui/actionbar.html
and best working example tutorial over here ... http://www.androidhive.info/2013/11/android-working-with-action-bar/
Hope it helps you.
I have a TextView which has a SpannableString inside it to highlight searched terms. Like so :
<TextView android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/textBox"
android:textSize="16sp"
android:paddingTop="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:lineSpacingExtra="5dp"
android:textColor="#color/TextGray"/>
As can be seen I am using android:lineSpacingExtra to give the lines a nice spacing, however it is causing the SpannableString background to be too tall. I would like to keep the spacing between the lines but make the SpannableString shorter.
How is this possible?
You can create your own span by extending ReplacementSpan. In draw method, you can take into account the fontSpacing which you can get from Paint parameter.
Like this:
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.RectF;
import android.text.style.ReplacementSpan;
public class BetterHighlightSpan extends ReplacementSpan {
private int backgroundColor;
public BetterHighlightSpan(int backgroundColor) {
super();
this.backgroundColor = backgroundColor;
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
return Math.round(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) {
// save current color
int oldColor = paint.getColor();
// calculate new bottom position considering the fontSpacing
float fontSpacing = paint.getFontSpacing();
float newBottom = bottom - fontSpacing;
// change color and draw background highlight
RectF rect = new RectF(x, top, x + paint.measureText(text, start, end), newBottom);
paint.setColor(backgroundColor);
canvas.drawRect(rect, paint);
// revert color and draw text
paint.setColor(oldColor);
canvas.drawText(text, start, end, x, y, paint);
}
}
You can use it like this:
TextView textView = (TextView) findViewById(R.id.textView);
SpannableStringBuilder builder = new SpannableStringBuilder("here some text and more of it");
builder.setSpan(new BetterHighlightSpan(Color.CYAN), 4, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(builder);
I couldn't much test it but you can improve it.
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);
}
}
}