Основная проблема заключается в том, что для ReplacementSpan
не задана высота. Как указано в источник ReplacementSpan
:
Если диапазон охватывает весь текст, а высота не задана, для диапазона не будет вызываться метод draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)}.
Это повторение того, что опубликовал Archit Sureja. В своем исходном посте я обновил высоту ReplacementSpan
в getSize()
, но теперь я реализую интерфейс LineHeightSpan.WithDensity
, чтобы сделать то же самое. (Спасибо vovahost здесь за эту информацию.)
Однако есть дополнительные проблемы, которые вы подняли и которые необходимо решить.
Проблема, поднятая вашим проектом, заключается в том, что точка не помещается в TextView
, в котором она должна находиться. То, что вы видите, это усечение точки. Что делать, если размер точки превышает либо ширину текста, либо его высоту?
Во-первых, что касается высоты, метод chooseHeight()
интерфейса LineHeightSpan.WithDensity
корректирует то, что считается нижней частью шрифта TextView
, добавляя размер точки к эффективной высоте шрифта. Для этого к низу шрифта добавляется высота точки:
fontMetricsInt.bottom = fm.bottom + mDotSize.toInt();
(Это изменение по сравнению с последней итерацией этого ответа, в которой использовалось заполнение TextView
. После этого изменения TextView
больше не нужен классу UnderDotSpan
. Хотя я добавил TextView
, на самом деле это не нужно.)
Последняя проблема заключается в том, что точка обрезается в начале и в конце, если она шире текста. clipToPadding="false"
здесь не работает, потому что точка обрезается не потому, что она обрезается до заполнения, а потому, что она обрезается до того, что, как мы сказали, ширина текста находится в getSize()
. Чтобы исправить это, я изменил метод getSize()
, чтобы определить, когда точка шире размера текста, и увеличить возвращаемое значение, чтобы оно соответствовало ширине точки. Новое значение, называемое mStartShim
, — это величина, которую необходимо применить к рисованию текста и точки, чтобы все подошло.
Последняя проблема заключается в том, что центр точки — это радиус точки ниже нижней части текста, а не диаметр, поэтому код для рисования точки был изменен в draw()
на:
canvas.drawCircle(x + textSize / 2, bottom.toFloat(), mDotSize / 2, paint)
(Я также изменил код, чтобы сделать перевод Canvas
вместо добавления смещений. Эффект тот же.)
Вот результат:
![введите здесь описание изображения](https://i.stack.imgur.com/snG54.png)
activity_main.xml
‹android.support.constraint.ConstraintLayout android:layout_width=match_parent android:layout_height=match_parent android:background=@android:color/darker_gray›
<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" />
‹/android.support.constraint.ConstraintLayout›
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) {
}
}
Для более общего случая размещения небольшого рисунка под текстом работает следующий класс, основанный на 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) {
}
}
Использование следующего рисуемого XML с UnderDrawableSpan
приводит к такому результату:. (Ширина и высота рисунка установлены на 12dp
. Размер шрифта текста 24sp
.)
![введите здесь описание изображения](https://i.stack.imgur.com/dfGkY.png)
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>
У меня была возможность вернуться к этому вопросу и ответить недавно. Я публикую более гибкую версию кода UnderDrawableSpan. На GitHub есть демонстрационный проект.
UnderDrawableSpan.kt (обновлено)
/**
* Place a drawable at the bottom center of text within a span. Because this class is extended
* from [ReplacementSpan], the span must reside on a single line and cannot span lines.
*/
class UnderDrawableSpan(
context: Context, drawable: Drawable, drawableWidth: Int, drawableHeight: Int, margin: Int
) : ReplacementSpan(), LineHeightSpan.WithDensity {
// The image to draw under the spanned text. The image and text will be horizontally centered.
private val mDrawable: Drawable
// The width if the drawable in dip
private var mDrawableWidth: Int
// The width if the drawable in dip
private var mDrawableHeight: Int
// Margin in dip to place around the drawable
private var mMargin: Int
// Amount to offset the text from the start.
private var mTextOffset = 0f
// Amount to offset the drawable from the start.
private var mDrawableOffset = 0f
// Descent specified in font metrics of the TextPaint.
private var mBaseDescent = 0f
init {
val metrics: DisplayMetrics = context.resources.displayMetrics
mDrawable = drawable
mDrawableWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, drawableWidth.toFloat(), metrics
).toInt()
mDrawableHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, drawableHeight.toFloat(), metrics
).toInt()
mMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, margin.toFloat(), metrics
).toInt()
}
override fun draw(
canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int,
bottom: Int, paint: Paint
) {
canvas.drawText(text, start, end, x + mTextOffset, y.toFloat(), paint)
mDrawable.setBounds(0, 0, mDrawableWidth, mDrawableHeight)
canvas.save()
canvas.translate(x + mDrawableOffset + mMargin, y + mBaseDescent + mMargin)
mDrawable.draw(canvas)
canvas.restore()
}
// ReplacementSpan override to determine the width that the text and drawable should occupy.
// The computed width is determined by the greater of the text width and the drawable width
// plus the requested margins.
override fun getSize(
paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?
): Int {
val textWidth = paint.measureText(text, start, end)
val additionalWidthNeeded = mDrawableWidth + mMargin * 2 - textWidth
// 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.
return if (additionalWidthNeeded >= 0) {
// Drawable is wider than text, so we need to offset the text to center it.
mTextOffset = additionalWidthNeeded / 2
textWidth + additionalWidthNeeded
} else {
// Text is wider than the drawable, so we need to offset the drawable to center it.
// We do not need to expand the width.
mDrawableOffset = -additionalWidthNeeded / 2
textWidth
}.toInt()
}
// Determine the height for the ReplacementSpan.
override fun chooseHeight(
text: CharSequence?, start: Int, end: Int, spanstartv: Int, lineHeight: Int,
fm: Paint.FontMetricsInt, paint: TextPaint
) {
// The text height must accommodate the size of the drawable. To make the accommodation,
// change the bottom of the font so there is enough room to fit the drawable between the
// font bottom and the font's descent.
val tpMetric = paint.fontMetrics
mBaseDescent = tpMetric.descent
val spaceAvailable = fm.descent - mBaseDescent
val spaceNeeded = mDrawableHeight + mMargin * 2
if (spaceAvailable < spaceNeeded) {
fm.descent += (spaceNeeded - spaceAvailable).toInt()
fm.bottom = fm.descent + (tpMetric.bottom - tpMetric.descent).toInt()
}
}
// StaticLayout prefers LineHeightSpan.WithDensity over this function.
override fun chooseHeight(
charSequence: CharSequence?, i: Int, i1: Int, i2: Int, i3: Int, fm: Paint.FontMetricsInt
) = throw IllegalStateException("LineHeightSpan.chooseHeight() called but is not supported.")
}
person
Cheticamp
schedule
03.01.2018
Html.toHtml
? - person Zoe   schedule 25.12.2017<p dir="ltr">1</p>
. Я надеюсь использовать этот код так же, как и другие обычные промежутки. - person android developer   schedule 25.12.2017Html.toHtml
поддерживает только встроенные диапазоны, а не пользовательские - person pskink   schedule 25.12.2017