Background
I'm trying to use a simple SpannableString
on a TextView
, based on an UnderDotSpan class I've found (here).
The original UnderDotSpan just puts a dot of a specific size and color below the text itself (not overlapping). What I'm trying is to first use it normally, and then use a customized drawable instead of a dot.
The problem
As opposed to a normal span usage, this one just doesn't show anything. Not even the text.
Here's how it's done for a normal span:
val text = "1"
val timeSpannable = SpannableString(text)
timeSpannable.setSpan(ForegroundColorSpan(0xff00ff00.toInt()), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(timeSpannable);
it will show a green "1" in the TextView.
But when I try the next spannable, it (entire TextView content: text and dot) doesn't show up 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)
Weird thing is that in one project that I use, it works fine inside RecyclerView , and in another, it doesn't.
Here's the code of UnderDotSpan :
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 the TextView doesn't 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've tried
I tried to extend from other span classes, and also tried to set the text to the TextView in other ways.
I've also tried other span classes I've made, 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()
}
}
While debugging, I've found that draw
function isn't even called, while getSize
do get called (and returns a >0 value).
The question
Why can't the span be shown on the TextView ?
What's wrong with the way I've used it?
How can I fix it, and use this span ?
How come it might work in other, more complex cases?
The basic problem is that the height is not set for the
ReplacementSpan
. As stated in the the source forReplacementSpan
:This is a repeat of what Archit Sureja posted. In my original post, I updated the height of the
ReplacementSpan
ingetSize()
but I now implement theLineHeightSpan.WithDensity
interface to do the same. (Thanks to vovahost here for this information.)There are, however, additional issues that you have brought up that need to be addressed.
The issue raised by your supplied project is that the dot does not fit within the
TextView
in which it must reside. What you are seeing is truncation of the dot. What to do if the size of the dot exceeds either the width of the text or its height?First, regarding the height, the
chooseHeight()
method of interfaceLineHeightSpan.WithDensity
adjusts what is considered the bottom of theTextView
font by adding in the size of the dot to the font's effective height. To do this, the height of the dot is added to the the bottom of the font:(This is a change from the last iteration of this answer which used the
TextView
's padding. Since this change, theTextView
no longer is needed by theUnderDotSpan
class. Although I had added theTextView
, it is not really needed.)The last issue is that the dot is cutoff at the start and end if it is wider than the text.
clipToPadding="false"
doesn't work here because the dot is cutoff not because it is being clipped to the padding but because it is being clipped to what we said the text width is ingetSize()
. To fix this, I modified thegetSize()
method to detect when the dot is wider than the text measurement and to increase the returned value to match the dot's width. A new value calledmStartShim
is the amount that must be applied to the drawing of the text and the dot to make things fit.The final issue is that the center of the dot is the radius of the dot below the bottom of the text and not the diameter, so the code to draw the dot was changed in
draw()
to:(I also changed the code to do
Canvas
translation instead of adding the offsets. The effect is the same.)Here is the result:
activity_main.xml
MainActivity.java
UnderDotSpan.kt
For the more general case of placing a small drawable under the text the following class works and is based upon
UnderDotSpan
:UnderDrawableSpan.java
Use of the following drawable XML with
UnderDrawableSpan
produces this result:. (Width and height of the drawable is set to12dp
. Font size of the text is24sp
.)gradient_drawable.xml
your span is not showing because draw method is not called due to height is not set.
please refer this link
Paint.FontMetricsInt object's all variable that we are getting is 0 so there is no hight, so draw method is not called.
For How Paint.FontMatricsInt work you can refer this link.
So we are setting Paint.FontMetricsInt with help of paint object that we are getting in getSize's arguments.
Here is My code I change few things related to set height.
Final output which I getting is like below
use this to draw circle below text
and last one IMP
Once text is set on
textview
then used:Example: