Why is my spannable not shown?

Background

I am trying to use a simple SpannableString on a TextView based on the UnderDotSpan class that I found ( here ).

The original UnderDotSpan simply places a dot of a certain size and color below the text itself (without overlapping). I try to use it in normal mode first, and then use a personalized drawable method instead of a dot.

Problem

Unlike normal range usage, this one just doesn't show anything. Even the text.

Here's how to do it for the normal range:

 val text = "1" val timeSpannable = SpannableString(text) timeSpannable.setSpan(ForegroundColorSpan(0xff00ff00.toInt()), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.setText(timeSpannable); 

it will display green "1" in the TextView.

But when I try the following spannable, it (all TextView text: text and dot) doesn't display at all:

 val text = "1" val spannable = SpannableString(text) spannable.setSpan(UnderDotSpan(this@MainActivity, 0xFF039BE5.toInt(), textView.currentTextColor), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.setText(spannable, TextView.BufferType.SPANNABLE) // this also didn't work: textView.setText(spannable) 

It is strange that in one project that I use, it works fine inside RecyclerView, and in another it doesn’t.

Here's the UnderDotSpan code:

 class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan() { companion object { @JvmStatic private val DEFAULT_DOT_SIZE_IN_DP = 4 } constructor(context: Context, dotColor: Int, textColor: Int) : this(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_DOT_SIZE_IN_DP.toFloat(), context.resources.displayMetrics), dotColor, textColor) {} override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?) = Math.round(paint.measureText(text, start, end)) override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { if (TextUtils.isEmpty(text)) { return } val textSize = paint.measureText(text, start, end) paint.color = mDotColor canvas.drawCircle(x + textSize / 2, bottom + mDotSize, mDotSize / 2, paint) paint.color = mTextColor canvas.drawText(text, start, end, x, y.toFloat(), paint) } } 

Note that TextView does not have any special properties, but I will show it anyway:

 <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.user.myapplication.MainActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/> </android.support.constraint.ConstraintLayout> 

What i tried

I tried spreading from other span classes, and also tried setting text in TextView in other ways.

I also tried other span classes that I created, based on the UnderDotSpan class. Example:

 class UnderDrawableSpan(val drawable: Drawable, val drawableWidth: Int = drawable.intrinsicWidth, val drawableHeight: Int = drawable.intrinsicHeight, val margin: Int = 0) : ReplacementSpan() { override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int = Math.round(paint.measureText(text, start, end)) override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { if (TextUtils.isEmpty(text)) return val textSize = paint.measureText(text, start, end) canvas.drawText(text, start, end, x, y.toFloat(), paint) canvas.save() canvas.translate(x + textSize / 2f - drawableWidth / 2f, y.toFloat() + margin) if (drawableWidth != 0 && drawableHeight != 0) drawable.setBounds(0, 0, drawableWidth, drawableHeight) drawable.draw(canvas) canvas.restore() } } 

During debugging, I found that the draw function is not even called, and getSize is called (and returns a value> 0).

Question

Why can't a range be displayed in a TextView?

What happened to the way I used it?

How can I fix it and use this range?

How could this work in other, more complex cases?

+8
android textview spanned spannable
source share
4 answers

The main problem is that the height is not set for ReplacementSpan . As pointed out in the source for ReplacementSpan :

If the span spans the entire text and the height is not set, draw (Canvas, CharSequence, int, int, float, int, int, int, Paint)} will not be called for the range.

This is a repeat of what Archit Sureja posted. In my original post, I updated the ReplacementSpan height in getSize() , but now I am implementing the LineHeightSpan.WithDensity interface to do the same. (Thanks to vovahost here for this information.)

There are, however, additional issues that you have raised that need to be addressed.

The problem posed by your project is that the point does not fit inside the TextView where it should be. What you see is a truncation of a point. What if the point size exceeds the width of the text or its height?

First, with respect to height, the chooseHeight() method of the chooseHeight() interface adjusts what is considered the bottom of the TextView font, adding the dot size to the effective font height. To do this, the height of the point is added to the bottom of the font:

 fontMetricsInt.bottom = fm.bottom + mDotSize.toInt(); 

(This is a change from the last iteration of this answer that used the TextView add-on. Since this change, the TextView no longer required by the UnderDotSpan class. Although I added the TextView , it really is not needed.)

The last problem is that the point ends at the beginning and at the end, if it is larger than the text. clipToPadding="false" does not work here because the point is disabled not because it is attached to the add-on, but because it is cropped to what we said, the width of the text is in getSize() . To fix this, I modified the getSize() method to determine when the point is larger than the text dimension and increase the return value according to the width of the point. The new value, called mStartShim , is the amount that must be applied to the drawing of text and dots to make things fit.

The last problem is that the center of the point is the radius of the point below the bottom of the text, not the diameter, so the code for drawing the point was changed in draw() to:

 canvas.drawCircle(x + textSize / 2, bottom.toFloat(), mDotSize / 2, paint) 

(I also changed the code to make a Canvas translation instead of adding offsets. The effect is the same.)

Here is the result:

enter image description here

activity_main.xml

 <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:background="@android:color/white" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> 

MainActivity.java

 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val text = "1" val spannable = SpannableString(text) spannable.setSpan(UnderDotSpan(this@MainActivity, 0xFF039BE5.toInt(), textView.currentTextColor), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) textView.setText(spannable, TextView.BufferType.SPANNABLE) } } 

UnderDotSpan.kt

 // From the original UnderDotSpan: Also implement the LineHeightSpan.WithDensity interface to // compute the height of our "dotted" font. class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan(), LineHeightSpan.WithDensity { companion object { @JvmStatic private val DEFAULT_DOT_SIZE_IN_DP = 16 } // Additional horizontal space to the start, if needed, to fit the dot var mStartShim = 0; constructor(context: Context, dotColor: Int, textColor: Int) : this(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_DOT_SIZE_IN_DP.toFloat(), context.resources.displayMetrics), dotColor, textColor) // ReplacementSpan override to determine the size (length) of the text. override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { val baseTextWidth = paint.measureText(text, start, end) // If the width of the text is less than the width of our dot, increase the text width // to match the dot width; otherwise, just return the width of the text. mStartShim = if (baseTextWidth < mDotSize) ((mDotSize - baseTextWidth) / 2).toInt() else 0 return Math.round(baseTextWidth + mStartShim * 2) } override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { if (TextUtils.isEmpty(text)) { return } val textSize = paint.measureText(text, start, end) paint.color = mDotColor canvas.save() // Draw the circle in the horizontal center and under the text. Add in the // offset (mStartShim) if we had to increase the length of the text to accommodate our dot. canvas.translate(mStartShim.toFloat(), -mDotSize / 2) // Draw a circle, but this could be any other shape or drawable. It just has // to fit into the allotted space which is the size of the dot. canvas.drawCircle(x + textSize / 2, bottom.toFloat(), mDotSize / 2, paint) paint.color = mTextColor // Keep the starting shim, but reset the y-translation to write the text. canvas.translate(0f, mDotSize / 2) canvas.drawText(text, start, end, x, y.toFloat(), paint) canvas.restore() } // LineHeightSpan.WithDensity override to determine the height of the font with the dot. override fun chooseHeight(charSequence: CharSequence, i: Int, i1: Int, i2: Int, i3: Int, fontMetricsInt: Paint.FontMetricsInt, textPaint: TextPaint) { val fm = textPaint.fontMetricsInt fontMetricsInt.top = fm.top fontMetricsInt.ascent = fm.ascent fontMetricsInt.descent = fm.descent // Our "dotted" font now must accommodate the size of the dot, so change the bottom of the // font to accommodate the dot. fontMetricsInt.bottom = fm.bottom + mDotSize.toInt(); fontMetricsInt.leading = fm.leading } // LineHeightSpan.WithDensity override that is needed to satisfy the interface but not called. override fun chooseHeight(charSequence: CharSequence, i: Int, i1: Int, i2: Int, i3: Int, fontMetricsInt: Paint.FontMetricsInt) { } } 

For a more general case of placing a small picture in the text, the following class works and is based on UnderDotSpan :

UnderDrawableSpan.java

 public class UnderDrawableSpan extends ReplacementSpan implements LineHeightSpan.WithDensity { final private Drawable mDrawable; final private int mDrawableWidth; final private int mDrawableHeight; final private int mMargin; // How much we need to jog the text to line up with a larger-than-text-width drawable. private int mStartShim = 0; UnderDrawableSpan(Context context, Drawable drawable, int drawableWidth, int drawableHeight, int margin) { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); mDrawable = drawable; mDrawableWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) drawableWidth, metrics); mDrawableHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) drawableHeight, metrics); mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) margin, metrics); } @Override public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { if (TextUtils.isEmpty(text)) { return; } float textWidth = paint.measureText(text, start, end); float offset = mStartShim + x + (textWidth - mDrawableWidth) / 2; mDrawable.setBounds(0, 0, mDrawableWidth, mDrawableHeight); canvas.save(); canvas.translate(offset, bottom - mDrawableHeight); mDrawable.draw(canvas); canvas.restore(); canvas.save(); canvas.translate(mStartShim, 0); canvas.drawText(text, start, end, x, y, paint); canvas.restore(); } // ReplacementSpan override to determine the size (length) of the text. @Override public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { float baseTextWidth = paint.measureText(text, start, end); // If the width of the text is less than the width of our drawable, increase the text width // to match the drawable width; otherwise, just return the width of the text. mStartShim = (baseTextWidth < mDrawableWidth) ? (int) (mDrawableWidth - baseTextWidth) / 2 : 0; return Math.round(baseTextWidth + mStartShim * 2); } // LineHeightSpan.WithDensity override to determine the height of the font with the dot. @Override public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt, TextPaint textPaint) { Paint.FontMetricsInt fm = textPaint.getFontMetricsInt(); fontMetricsInt.top = fm.top; fontMetricsInt.ascent = fm.ascent; fontMetricsInt.descent = fm.descent; // Our font now must accommodate the size of the drawable, so change the bottom of the // font to accommodate the drawable. fontMetricsInt.bottom = fm.bottom + mDrawableHeight + mMargin; fontMetricsInt.leading = fm.leading; } // Required but not used. @Override public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) { } } 

Using the following extractable XML with UnderDrawableSpan produces this result :. (The width and height of the drawable are set to 12dp . The font size of the text is 24sp .)

enter image description here

gradient_drawable.xml

 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <size android:width="4dp" android:height="4dp" /> <gradient android:type="radial" android:gradientRadius="60%p" android:endColor="#e96507" android:startColor="#ece6e1" /> </shape> 
+4
source share

your range is not displayed because the draw method is not being called due to a height not set.

refer to this link

https://developer.android.com/reference/android/text/style/ReplacementSpan.html

GetSize () - returns the width of the range. Extending classes can set the range height by updating the Paint.FontMetricsInt attributes. If the span spans the entire text and the height is not specified, draw (Canvas, CharSequence, int, int, float, int, int, int, Paint) will not be called for the range.

Paint.FontMetricsInt object: all the variables that we get are 0, so there is no height, so the draw method is not called.

For Paint.FontMatricsInt to work, you can link to this link.

The value of the top, bottom, bottom, bottom, bottom and leading in FontMetrics from Android

So, we set Paint.FontMetricsInt with the paint object, which we get in the getSize arguments.

Here is my code. I am changing a few things related to setting the height.

 class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan() { companion object { @JvmStatic private val DEFAULT_DOT_SIZE_IN_DP = 16 } constructor(context: Context, dotColor: Int, textColor: Int) : this(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_DOT_SIZE_IN_DP.toFloat(), context.resources.displayMetrics), dotColor, textColor) {} override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { val asd = paint.getFontMetricsInt() fm?.leading = asd.leading fm?.top = asd.top fm?.bottom = asd.bottom fm?.ascent = asd.ascent fm?.descent = asd.descent return Math.round(measureText(paint, text, start, end)) } override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { if (TextUtils.isEmpty(text)) { return } val textSize = paint.measureText(text, start, end) paint.color = mDotColor canvas.drawCircle(x + textSize / 2, (bottom /2).toFloat(), mDotSize / 2, paint) paint.color = mTextColor canvas.drawText(text, start, end, x, y.toFloat(), paint) } private fun measureText(paint: Paint, text: CharSequence, start: Int, end: Int): Float { return paint.measureText(text, start, end) } } 

The final output that I get looks below

enter image description here

UPDATED RESPONSE

use this to draw a circle under the text

 class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan() { companion object { @JvmStatic private val DEFAULT_DOT_SIZE_IN_DP = 4 } constructor(context: Context, dotColor: Int, textColor: Int) : this(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_DOT_SIZE_IN_DP.toFloat(), context.resources.displayMetrics), dotColor, textColor) {} override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { val asd = paint.getFontMetricsInt() fm?.leading = asd.leading + mDotSize.toInt() fm?.top = asd.top fm?.bottom = asd.bottom fm?.ascent = asd.ascent fm?.descent = asd.descent return Math.round(paint.measureText(text, start, end)) } override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { if (TextUtils.isEmpty(text)) { return } val textSize = paint.measureText(text, start, end) paint.color = mDotColor canvas.drawCircle(x + textSize / 2, bottom + mDotSize, mDotSize / 2, paint) paint.color = mTextColor canvas.drawText(text, start, end, x, y.toFloat(), paint) } } 

and last IMP

val text = "1\n" instead of val text = "1"

+1
source share

Once the text is set to textview and then used:

 textview.setMovementMethod(LinkMovementMethod.getInstance()); 

Example:

 tvDescription.setText(hashText); tvDescription.setMovementMethod(LinkMovementMethod.getInstance()); 
-2
source share
 /* * Set text with hashtag and mentions on TextView * */ public void setTextOnTextView(String description, TextView tvDescription) { SpannableString hashText = new SpannableString(description); Pattern pattern = Pattern.compile("@([A-Za-z0-9_-]+)"); Matcher matcher = pattern.matcher(hashText); while (matcher.find()) { final StyleSpan bold = new StyleSpan(android.graphics.Typeface.BOLD); // Span to make text bold hashText.setSpan(bold, matcher.start(), matcher.end(), 0); } Pattern patternHash = Pattern.compile("#([A-Za-z0-9_-]+)"); Matcher matcherHash = patternHash.matcher(hashText); while (matcherHash.find()) { final StyleSpan bold = new StyleSpan(android.graphics.Typeface.BOLD); // Span to make text bold hashText.setSpan(bold, matcherHash.start(), matcherHash.end(), 0); } tvDescription.setText(hashText); tvDescription.setMovementMethod(LinkMovementMethod.getInstance()); } 
-3
source share

All Articles