Почему мой spannable не отображается?

Фон

Я пытаюсь использовать простой SpannableString в TextView на основе класса UnderDotSpan, который я нашел (здесь).

Оригинальный UnderDotSpan просто помещает точку определенного размера и цвета под самим текстом (не перекрывая). Я пытаюсь сначала использовать его в обычном режиме, а затем использовать настраиваемый рисунок вместо точки.

Проблема

В отличие от обычного использования диапазона, этот просто ничего не показывает. Даже не текст.

Вот как это делается для нормального диапазона:

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

он покажет зеленую «1» в TextView.

Но когда я пробую следующий spannable, он (все содержимое TextView: текст и точка) вообще не отображается:

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)

Странно то, что в одном проекте, который я использую, он отлично работает внутри RecyclerView, а в другом — нет.

Вот код 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)
    }

}

Обратите внимание, что у TextView нет особых свойств, но я все равно покажу его:

<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>

Что я пробовал

Я попытался расшириться от других классов span, а также попытался установить текст в TextView другими способами.

Я также пробовал другие созданные мной классы span на основе класса UnderDotSpan. Пример:

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()
    }

}

Во время отладки я обнаружил, что функция draw даже не вызывается, а getSize вызывается (и возвращает значение >0).

Вопрос

Почему диапазон не отображается в TextView?

Что не так с тем, как я его использовал?

Как я могу это исправить и использовать этот диапазон?

Почему это может работать в других, более сложных случаях?


person android developer    schedule 25.12.2017    source источник
comment
Вы пробовали Html.toHtml?   -  person Zoe    schedule 25.12.2017
comment
@Zoe Нет, но теперь, когда у меня есть, отображается другая проблема: вместо текста 1 и точки отображается просто <p dir="ltr">1</p> . Я надеюсь использовать этот код так же, как и другие обычные промежутки.   -  person android developer    schedule 25.12.2017
comment
@Zoe Html.toHtml поддерживает только встроенные диапазоны, а не пользовательские   -  person pskink    schedule 25.12.2017
comment
@pskink Знаете ли вы, возможно, причину, по которой то, что я написал, не работает (по крайней мере, в этом случае)?   -  person android developer    schedule 25.12.2017
comment
Когда вы говорите, что он вообще не отображается, означает ли это, что точка не отображается, или это означает, что точка и текст не отображаются? ?   -  person Ben P.    schedule 27.12.2017
comment
@БенП. Все содержимое TextView. И текст, и точка. Обновлю вопрос.   -  person android developer    schedule 27.12.2017
comment
можете ли вы попробовать сделать ширину TextView равной match_parent (только для целей тестирования)?   -  person aelimill    schedule 28.12.2017
comment
@aelimill Я не хочу этого в реальном приложении, но даже при использовании match_parent (как для ширины, так и для высоты) ни текст, ни точка не отображаются.   -  person android developer    schedule 28.12.2017


Ответы (4)


Основная проблема заключается в том, что для 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 вместо добавления смещений. Эффект тот же.)

Вот результат:

введите здесь описание изображения

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.)

введите здесь описание изображения

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
comment
Я все еще вижу его усеченным. Посмотрите: i.stack.imgur.com/GXVza.png. Даже если я добавлю больше отступов внизу, он все равно будет усечен. Я даже пытался использовать «android:clipToPadding=false android:clipChildren=false» как для TextView, так и для его родителя. - person android developer; 03.01.2018
comment
@androiddeveloper Это странно. Усеченная точка кажется намного больше, чем 4dp, которую вы указываете, особенно по сравнению с размером текста. Я вернулся и включил ваш класс UnderDotSpan в свою демонстрацию. Результаты были такими же. Я обновил свой ответ точным кодом, который я запускаю с обновленным изображением. Возможно, это поможет вам определить проблему. Я бы проверил mDotSize в методе рисования, чтобы убедиться, что это именно то, что вы ожидаете. - person Cheticamp; 03.01.2018
comment
Я не понимаю, почему атрибуты отсечения не помогают. Я также не понимаю, почему написанный мной код в некоторых случаях работает, а в некоторых нет. Пожалуйста, дайте мне знать, если у вас есть стабильное решение. - person android developer; 03.01.2018
comment
@androiddeveloper Можете ли вы представить код, который работает не так, как ожидалось, и опубликовать его здесь? - person Cheticamp; 03.01.2018
comment
хорошо, обновленный вопрос, чтобы иметь полную ссылку на проект. Поиск проекта доступен здесь - person android developer; 03.01.2018
comment
@androiddeveloper Этот проект вообще не отображает текст - условие, которое я воспроизвел. Я искал проект, который отображает усеченную точку. - person Cheticamp; 03.01.2018
comment
Вот с чем я работаю. Вместо этого вы можете просто поместить туда свой код (некоторые из них закомментированы, так что вы можете раскомментировать их). Пожалуйста, начните оттуда. - person android developer; 03.01.2018
comment
@androiddeveloper См. переписать - person Cheticamp; 03.01.2018
comment
Кажется, работает хорошо. Что ты сделал? Чего не хватало? Что случилось? Что бы вы изменили, чтобы UnderDrawableSpan тоже работал? А пока я дарую награду. Если вам удастся ответить на все из них, вы также получите принятый ответ и +1 :) - person android developer; 04.01.2018
comment
@androiddeveloper .UnderDrawableSpan работает как задумано и как вы задокументировали. Чего не хватало, так это некоторого кода в большом проекте календаря, где вы нашли UnderDrawableSpan для установки высоты пролета. Я не отслеживал это, но высоту просто нужно установить перед рисованием. Проблем с отсечением не существует, пока точка соответствует заданным границам TextView. Проблемы возникли из-за того, что точка была 16dp, а не 4dp, как в том же проекте календаря. - person Cheticamp; 04.01.2018
comment
@androiddeveloper Что касается UnderDrawableSpan, я бы взял обновленный UnderDotSpan и заменил draw(). Кроме того, перейдите в TextView и работайте оттуда. В основном все должно оставаться по-прежнему. Я предполагаю, что рисунок не слишком велик. - person Cheticamp; 04.01.2018
comment
Пожалуйста, покажите части, которые вы изменили. Я не понимаю, что ты сделал, чтобы заставить его работать. Предыдущий код даже не показывал текст. Пожалуйста, также покажите модифицированный UnderDrawableSpan, который заставит его работать. Вы уверены, что можно изменить заполнение TextView в диапазоне? Это безопасно? Это обычное дело? - person android developer; 04.01.2018
comment
@androiddeveloper Вы правы, оспаривая использование заполнения TextView, чтобы точка подходила. Я изменил это - смотрите обновление. Я также добавил комментарии в код, чтобы выделить изменения, которые я внес в UnderDotSpan. По сути, этот класс создает новый, расширенный тип шрифта, который является просто шрифтом TextView с добавлением точки под ним. - person Cheticamp; 04.01.2018
comment
Вроде тоже неплохо. Вы заменили canvas.drawText(text, start, end, x + startShim, y.toFloat(), paint) кодом, который перемещает текст над точкой. Но не могли бы вы показать, как заставить работать UnderDrawableSpan? - person android developer; 04.01.2018
comment
@androiddeveloper Что ты рисуешь для UnderDrawableSpan? Он работает так же, как UnderDotSpan, только с возможностью рисования, а не с кругом? - person Cheticamp; 04.01.2018
comment
Просто простой рисунок. Например, это может быть градиентDrawable, который я выбираю для установки его размера во время выполнения. - person android developer; 05.01.2018
comment
Работает, но я не вижу, чтобы mMargin использовался... Кроме того, что такое shim? - person android developer; 07.01.2018
comment
@androiddeveloper Если изображение шире текста, текст необходимо сместить, чтобы все выровнялось. mStartShim это сумма. Использование поля было неопределенным, но я оставил его в коде, чтобы вы могли применять его по мере необходимости. - person Cheticamp; 07.01.2018
comment
Это поле между текстом и точкой/рисунком. Странно, что я не добавил это в точечный код. Был уверен, что знаю. В настоящее время вы помещаете их прямо под текстом, без пробелов, верно? Если это так, я думаю, что могу добавить использование маржи самостоятельно. Это будет просто добавление к оси Y, верно? - person android developer; 07.01.2018
comment
@androiddeveloper Это правильно. В настоящее время рисуемый объект находится прямо под текстом, поэтому поле смещает его вниз, поэтому это корректировка того, где рисуемый объект размещается на оси Y. - person Cheticamp; 07.01.2018
comment
Должен ли я поставить его в canvas.translate или canvas.drawText , или это не имеет значения? - person android developer; 07.01.2018
comment
@androiddeveloper Я думаю, что вам придется увеличить высоту на сумму вашей маржи. Итак, в chooseHeight() у вас будет fontMetricsInt.bottom = fm.bottom + mDrawableHeight + mMargin. Тогда в draw() у вас будет canvas.translate(offset, bottom - mDrawableHeight - mMargin)). Я думаю, что это должно работать нормально. - person Cheticamp; 07.01.2018
comment
Я думаю, что ваш расчет неверен. Если я сделаю это, я получу перекрывающийся текст (скажем, с шириной и высотой 16 и полем 8 в параметрах) - person android developer; 08.01.2018
comment
@androiddeveloper Это неверно. Добавьте отступ к высоте (ontMetricsInt.bottom = fm.bottom + mDrawableHeight + mMargin), но перевод не нужен, кроме того, что уже есть. Я обновил код в ответе, чтобы добавить его на поля. - person Cheticamp; 08.01.2018
comment
Хороший. Спасибо за все ваше время. Вы заслуживаете того, чтобы этот ответ был принят, а также +1. :) - person android developer; 08.01.2018
comment
Просто чтобы вы знали, я не думаю, что принято использовать единицы DP в качестве параметра классов. Почти в каждом API Android это чистые пиксели, и если вы хотите использовать DP, вы делаете это вне вызова. - person android developer; 08.01.2018
comment
@androiddeveloper Хороший вопрос. У вас был размер точки по умолчанию, выраженный в dp, поэтому я просто выбрал его. - person Cheticamp; 08.01.2018
comment
Я только установил его как размер по умолчанию. У того, кто использует класс, есть вариант: если не указано, используется размер по умолчанию (DP), а если указано, то в PX. - person android developer; 08.01.2018
comment
@androiddeveloper Я обобщил класс здесь, если вам интересно. - person Cheticamp; 09.01.2018
comment
Очень хороший образец репо. Пожалуйста, обновите код и здесь. - person android developer; 09.01.2018

ваш диапазон не отображается, потому что метод рисования не вызывается из-за того, что высота не установлена.

пожалуйста, обратитесь по этой ссылке

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

GetSize() — возвращает ширину диапазона. Расширяющие классы могут устанавливать высоту диапазона, обновляя атрибуты Paint.FontMetricsInt. Если диапазон охватывает весь текст, а высота не задана, для диапазона не будет вызываться метод draw(Canvas, CharSequence, int, int, float, int, int, int, Paint).

Все переменные объекта Paint.FontMetricsInt, которые мы получаем, равны 0, поэтому нет высоты, поэтому метод рисования не вызывается.

Чтобы узнать, как работает Paint.FontMatricsInt, вы можете обратиться по этой ссылке.

Значение вершины, восхождения, базовой линии, спуск, низ и интерлиньяж в Android FontMetrics

Итак, мы устанавливаем Paint.FontMetricsInt с помощью объекта рисования, который мы получаем в аргументах getSize.

Вот мой код. Я меняю несколько вещей, связанных с высотой набора.

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)
    }
}

Окончательный результат, который я получаю, выглядит следующим образом:

введите здесь описание изображения

ОБНОВЛЕННЫЙ ОТВЕТ

используйте это, чтобы нарисовать круг под текстом

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)
    }
}

и последний IMP

val text = "1\n" вместо val text = "1"

person Archit Sureja    schedule 29.12.2017
comment
Во-первых, этот код почему-то ставит точку над текстом, а не под ним. Что значит высота не установлена? Где в вашем коде это установлено? Что вы изменили в коде? Почему код, который я использовал, работает в одних случаях, а в других нет? Что нужно сделать, чтобы исправить и UnderDrawableSpan? - person android developer; 31.12.2017
comment
Я получаю точки под текстом. посмотрите мой снимок экрана, который я недавно добавил. Во-вторых, как я сказал в своем ответе, мы получаем 0 во всех переменных Paint.FontMatricsInt, которые устанавливают высоту вашего текстового представления. Вы можете сослаться на ссылку, которую я недавно добавил в ответ. Таким образом, высота текстового просмотра не устанавливается автоматически, что происходит в других расширяемых классах. - person Archit Sureja; 02.01.2018
comment
Я не вижу точки под текстом в вашем примере. Я вижу одно поверх другого. Я имел в виду по оси Y. Не ось Z. Другими словами, это означает, что вам нужно немного посмотреть вниз, чтобы увидеть точку. Что касается проблемы, то я не понимаю, как это работает в некоторых случаях. Все, что вы сделали, это изменили реализацию getSize? - person android developer; 02.01.2018
comment
Я изменил путь canvas.drawCircle. так что он идет поверх другого по оси Y, пожалуйста, проверьте обновленный ответ. Что касается проблемы, я тоже не понимаю, как это работает в некоторых случаях, может быть, в некоторых случаях высота устанавливается андроидом, глубоко не исследовал. - person Archit Sureja; 02.01.2018
comment
Я не хочу, чтобы TextView имел match_parent в качестве ширины (или высоты). Точка должна быть под текстом, независимо от того, насколько большой текст. Кроме того, в новом коде точка усечена в исходном регистре (см. здесь: i.stack.imgur .com/BbLlj.png ) и вообще не показывает точку в новом коде размещения точки под текстом (см. здесь: i.stack.imgur.com/DXXqi.png). Использование интерлиньяжа (что означает paint.fontMetricsInt.leading) также приводило к усеченной точке (см. здесь: i.stack.imgur.com /epE8e.png ) - person android developer; 02.01.2018
comment
Я также пытался избежать обрезки TextView, используя android:clipChildren="false" android:clipToPadding="false" для него и его родителя, но это не сработало. - person android developer; 02.01.2018
comment
можете ли вы использовать текстовый архит суреджа \n как это означает с newLine . Если вы можете, вы можете использовать его с fm?.leading = asd.leading + mDotSize.toInt() и canvas.drawCircle(x + textSize / 2, bottom.toFloat()+mDotSize / 2, mDotSize / 2, paint) - person Archit Sureja; 02.01.2018
comment
Давайте продолжим обсуждение в чате. - person Archit Sureja; 02.01.2018
comment
Я не понимаю ваших новых инструкций. Пожалуйста, просто используйте UnderDotDrawable, как я. Текст очень короткий, и точка все равно должна отображаться, как в репозитории (здесь: github.com/ hidroh/calendar , см. скриншот здесь: github.com/ hidroh/calendar/blob/master/screenshots/2.png ). - person android developer; 02.01.2018
comment
Все равно плохой результат. Точка усечена (см. здесь: i.stack.imgur.com/mZTAW.png ). И необходимость в \n тоже должна быть устранена. Я не хочу, чтобы TextView влиял на что-либо вокруг него больше, чем следовало бы. - person android developer; 02.01.2018
comment
чтобы удалить \n, вы должны указать фиксированный размер, например github.com/hidroh/calendar/blob/master/app/src/main/res/layout/ и используйте метод getsize(), который был в моем основном ответе (не в обновленном ответе). - person Archit Sureja; 02.01.2018
comment
Продолжим обсуждение в чате. chat.stackoverflow.com/rooms/162327/ - person Archit Sureja; 02.01.2018
comment
Хорошо, но мне нужно, чтобы это было wrap_content. Текст может быть коротким, а может быть длинным. Использование даже match_parent из исходного кода репо, похоже, не работает, поэтому я до сих пор не понимаю, почему это работает в их случае, а не здесь. Однако, как вы написали, если я использую в их коде wrap_content, это не работает. - person android developer; 02.01.2018
comment
Если текст длинный, то как вы хотите обрабатывать. означает, что если текст занимает две ссылки, вам нужно поставить точку под второй строкой или первой строкой. и мы можем сделать обсуждение в чате. - person Archit Sureja; 02.01.2018
comment
Под всем текстом диапазона, но в моем случае это не имеет значения, потому что текст всегда должен быть достаточно коротким, чтобы находиться в одной строке. Мне это нужно в случае, очень похожем на интерфейс календаря, который я показал в репозитории. - person android developer; 02.01.2018

После того, как текст установлен на textview, используется:

textview.setMovementMethod(LinkMovementMethod.getInstance());

Пример:

tvDescription.setText(hashText);
tvDescription.setMovementMethod(LinkMovementMethod.getInstance());
person Nitin Verma    schedule 02.01.2018
comment
Не работает. Тем не менее я не вижу ни текста, ни точки под ним. - person android developer; 02.01.2018

person    schedule
comment
Я не вижу, чтобы здесь использовался UnderDotSpan, и вместо этого вы добавили другие стили... - person android developer; 02.01.2018