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 for ReplacementSpan
:
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.
This is a repeat of what Archit Sureja posted. In my original post, I updated the height of the ReplacementSpan
in getSize()
but I now implement the LineHeightSpan.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 interface LineHeightSpan.WithDensity
adjusts what is considered the bottom of the TextView
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:
fontMetricsInt.bottom = fm.bottom + mDotSize.toInt();
(This is a change from the last iteration of this answer which used the TextView
's padding. Since this change, the TextView
no longer is needed by the UnderDotSpan
class. Although I had added the TextView
, 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 in getSize()
. To fix this, I modified the getSize()
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 called mStartShim
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:
canvas.drawCircle(x + textSize / 2, bottom.toFloat(), mDotSize / 2, paint)
(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
<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's 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 the more general case of placing a small drawable under the text the following class works and is based upon 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's 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) {
}
}
Use of the following drawable XML with UnderDrawableSpan
produces this result:. (Width and height of the drawable is set to 12dp
. Font size of the text is 24sp
.)
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>
your span is not showing because draw method is not called due to height is not set.
please refer this link
https://developer.android.com/reference/android/text/style/ReplacementSpan.html
GetSize() - 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.
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.
Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics
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.
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)
}
}
Final output which I getting is like below
UPDATED ANSWER
use this to draw circle below 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 one IMP
val text = "1\n"
instead ofval text = "1"
Once text is set on textview
then used:
textview.setMovementMethod(LinkMovementMethod.getInstance());
Example:
tvDescription.setText(hashText);
tvDescription.setMovementMethod(LinkMovementMethod.getInstance());
/*
* 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());
}
来源:https://stackoverflow.com/questions/47967798/how-come-my-spannable-isnt-shown