draw Method for Spannable String doesn't get called on Marshmallow - android

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!

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.

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

How to create gmail-like recipient (user) custom view

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.

Border in Clickable object in SpannableString

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

Categories

Resources