How to create a secondary line(underline) in TextView?
For example
line1:
|word1 word2 word3 word4| //size17
| word2 description | //size 6
line2:
|word4 word5 word6 word7 | //size17
| word7 description | //size6
EDIT 27.05:
preview version. How to improve?
public class DescriptionSpan extends ReplacementSpan {
String description;
Paint descriptionPaint;
public DescriptionSpan(Paint paint, String description) {
descriptionPaint = paint;
this.description = description;
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
float delta = getShift(paint, text.subSequence(start, end).toString(), description);
if (delta >= 0) {
canvas.drawText(text, start, end, x, y, paint);
canvas.drawText(description, 0, description.length(), x + delta, y + descriptionPaint.getTextSize(), descriptionPaint);
} else {
canvas.drawText(text, start, end, x - delta, y, paint);
canvas.drawText(description, 0, description.length(), x, y + descriptionPaint.getTextSize(), descriptionPaint);
}
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return (int) Math.max(paint.measureText(text, start, end), descriptionPaint.measureText(description));
}
private float getShift(Paint paint, String text, String description) {
return (paint.measureText(text) - descriptionPaint.measureText(description)) / 2;
}
I don't like writing answers without code; however, I've spent way too long on this (I can't resist a challenge when it comes to coding) and I think I have enough information to put you in the right direction without a working code sample.
The way it's supposed to work is that in getSize() you would set the FontMetricsInt that are passed in to the proper heights for your span, as well as return the span width.
But here's the thing: The only concrete implementation of ReplacementSpan is ImageSpan which extends DynamicDrawableSpan (which is a ReplacementSpan). If you look at DynamicDrawableSpan, it sets the descent and bottom to zero and the ascent and top to the size of the drawable. More on this in a second.
When I took your code and tried to add the font metrics logic to getSize(), I ran into a couple problems.
Changes in ascent/top are honored, but any change to the bottom -- which would need to the adjusted to allow room for your description -- is not honored. It seems that the logic is tailored to accommodate ImageSpan/DynamicDrawableSpan, and everything else is treated as an untested corner case.
ReplacementSpan seems to be targeted for single-line texts. The ascent & top are correctly set up in the first paragraph line, but after the line wraps, the normal ascent/top are restored.
The span is generally treated as a non-breaking word, but I did run into an intermittent bug where the span itself was wrapped onto a second line. Obviously this would be a problem with calculating where to put all your texts.
So if you really wanted to do this with spans, I think what you would have to do is have another custom paragraph-style span that implements LineHeightSpan. For this you would implement chooseHeight(). How this works is that chooseHeight() would be called for each line in the paragraph. Your implementation would look for a DescriptionSpan on that line and set the font metrics to accommodate the description text. So you would wrap each paragraph that had DescriptionSpans with this paragraph-style span. With this span, your DescriptionSpan wouldn't alter the font metrics in getSize(), and you'd just assume you have space for the description text in draw(). (I hope the canvas isn't clipped to the span bounds. That custom view idea just keeps looking better and better.)
I would love to get the code working and post it, but I spend too much time banging my head on ReplacementSpan font metrics.
You can use a SpannableStringBuilder for this. Something like below :
SpannableStringBuilder span = new SpannableStringBuilder();
span.append("First Line");
span.setSpan(new RelativeSizeSpan(.8f),0,firstLineLength);
span.append("Second Line");
span.setSpan(new RelativeSizeSpan(1f),firstLineLength,firstLineLength+secondLineLength);
textView.setText(span);
Related
I am trying to merge two textview with two different styles.
In above pic, How to put that curved border textview at end of above text. Means at end of (6).There is an way to do it in html but border not displaying in html.
String html = "<p style=\"border:1px; border-style:solid; border-color:#FF0000; padding: 1em;\"> B1-993m </p>";
holder.book2.setText(Html.fromHtml(
html,
Html.FROM_HTML_MODE_LEGACY));
Above code works if we put it in html file but not working in android.it seems in android some html styles not supported.is there any way to do it or something wrong in my html with android.
Flexlayout :
Not sure about the HTML part i don't have much idea about HTML.
What i understood from your question i think this can be done by SpannableString . All you have to do is create a Custom ReplacementSpan. To apply the Span you need to have the idea about the span indexes i.e where exactly you want to show the box .
I have tried following way . Its just a sample class to get the idea you can modify the drawing as per your need color and style everything.
public class BoxedSpan extends ReplacementSpan {
#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(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRoundRect(rect, 10f, 10f, paint);
paint.setStyle(Paint.Style.FILL);
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(measureText(paint, text, start, end));
}
private float measureText(Paint paint, CharSequence text, int start, int end) {
return paint.measureText(text, start, end);
}
}
here is the sample code i tried :-
SpannableStringBuilder stringBuilder = new
SpannableStringBuilder("Here is some text B1-38 B2-36");
stringBuilder.setSpan(new BoxedSpan(), 18, 23, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
stringBuilder.setSpan(new BoxedSpan(), 24, stringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(stringBuilder);
Looks like it pretty much does the job . Give it a try.
First of all, you can try to use ConstraintLayout, it will help you arrange all view elements properly, and those two rounded TextView can be done with adding rounded background to them. You don't need any html marking.
You might be better suited to using a background drawable with rounded edges for the B1-38
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 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>
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);
I have long text containing name that looks like "something-something". This long text is shown in TextView. The problem is "something-something" got line breaked.
I have found unicode character U+2011 NON-BREAKING HYPHEN. But it looks like this unicode character is supported in font since Android 3.0. However I'm supporting Android 2.1 where replacement character is shown instead.
I have looked at class Spannable, but I did not find how to define nonbreaking block of text. Maybe I overlook something.
I solved breaking of the text block by implementing ReplacementSpan to render text in single block. Here is the code:
public class NonbreakingSpan extends ReplacementSpan {
#Override
public void draw(
Canvas canvas,
CharSequence text, int start, int end,
float x, int top, int y, int bottom,
Paint paint) {
canvas.drawText(text, start, end, x, y, paint);
}
#Override
public int getSize(
Paint paint,
CharSequence text, int start, int end,
FontMetricsInt fm) {
return Math.round(paint.measureText(text, start, end));
}
}