Закругленный фоновый текст, такой как Instagram, ReplacementSpan не работает должным образом

Я пытался сделать что-то похожее на Instagram ниже -

Это то, что я сделал

Но я хочу такие кривые, как Instagram - Это то что я хочу

Теперь я застрял в еще одной проблеме - когда я печатаю. текст не переходит автоматически на следующую строку, я должен нажать return, как обычно, editText работает с фиксированной шириной. (Короче multiline не работает нормально с ReplacementSpan)

Ниже приведен пример кода для того, что я сделал -

public class EditextActivity extends AppCompatActivity {

    EditText edittext;
    RoundedBackgroundSpan roundedBackgroundSpan;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.editext_screen);
        edittext=(EditText)findViewById(R.id.edittext);
       // edittext.setText("Hello My name is Karandeep Atwal.\n\n Hii this is test");
        roundedBackgroundSpan= new RoundedBackgroundSpan(Color.RED,Color.WHITE);
        edittext.getText().setSpan(roundedBackgroundSpan, 0, edittext.getText().length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
    }


    public class RoundedBackgroundSpan extends ReplacementSpan implements LineHeightSpan {

        private static final int CORNER_RADIUS = 15;
        private static final int PADDING_X = 10;

        private int   mBackgroundColor;
        private int   mTextColor;

        /**
         * @param backgroundColor background color
         * @param textColor text color
         */
        public RoundedBackgroundSpan(int backgroundColor, int textColor) {
            mBackgroundColor = backgroundColor;
            mTextColor = textColor;
        }

        @Override
        public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
            return (int) (PADDING_X + paint.measureText(text,start, end) + PADDING_X);
        }

        @Override
        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
            float width = paint.measureText(text,start, end);
            RectF rect = new RectF(x, top, x + width + 2 * PADDING_X, bottom);
            paint.setColor(mBackgroundColor);
            canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);
            paint.setColor(mTextColor);
            canvas.drawText(text, start, end, x + PADDING_X, y, paint);
        }

        @Override
        public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fontMetricsInt) {

        }
    }

}

Ниже мой xml -

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    android:layout_gravity="center"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:padding="5dp"
        android:background="@drawable/border"
        android:id="@+id/edittext"
        android:layout_centerInParent="true"
        android:textColor="@android:color/black"
        android:gravity="center"
        android:hint="hi"
        android:singleLine="false"
        android:inputType="textMultiLine"
        android:textSize="30sp"
        android:maxWidth="100dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

Вот что я получаю, когда набираю текст setSpan -

Это нормальное поведение для фиксированной ширины, что я хочу -

5 ответов

Решение

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    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"
    android:background="@android:color/holo_purple"
    tools:context="com.tttzof.demotext.MainActivity">

    <EditText
        android:id="@+id/editText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Enter text"
        android:textSize="30sp"
        android:gravity="center"
        android:textColor="@android:color/black"
        android:background="@android:color/transparent"
        android:layout_gravity="center"/>

</FrameLayout>

MainActivity.java

import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextWatcher;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final EditText editText = (EditText) findViewById(R.id.editText);

        int padding = dp(8);
        int radius = dp(5);

        final Object span = new BackgroundColorSpan(
                        Color.WHITE,
                        (float)padding,
                        (float) radius
        );

        editText.setShadowLayer(padding, 0f, 0f, 0);
        editText.setPadding(padding, padding, padding, padding);

        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                s.setSpan(span, 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        });
    }

    private int dp(int value) {
        return (int) (getResources().getDisplayMetrics().density * value + 0.5f);
    }
}

BackgroundColorSpan.java

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.text.style.LineBackgroundSpan;

public class BackgroundColorSpan implements LineBackgroundSpan {
    private float padding;
    private float radius;

    private RectF rect = new RectF();
    private Paint paint = new Paint();
    private Paint paintStroke = new Paint();
    private Path path = new Path();

    private float prevWidth = -1f;
    private float prevLeft = -1f;
    private float prevRight = -1f;
    private float prevBottom = -1f;
    private float prevTop = -1f;


    public BackgroundColorSpan(int backgroundColor,
                               float padding,
                               float radius) {
        this.padding = padding;
        this.radius = radius;

        paint.setColor(backgroundColor);
        //paintStroke.setStyle(Paint.Style.STROKE);
        //paintStroke.setStrokeWidth(5f);
        paintStroke.setColor(backgroundColor);
    }

    @Override
    public void drawBackground(
                    final Canvas c,
                    final Paint p,
                    final int left,
                    final int right,
                    final int top,
                    final int baseline,
                    final int bottom,
                    final CharSequence text,
                    final int start,
                    final int end,
                    final int lnum) {

        float width = p.measureText(text, start, end) + 2f * padding;
        float shift = (right - width) / 2f;

        rect.set(shift, top, right - shift, bottom);

        if (lnum == 0) {
            c.drawRoundRect(rect, radius, radius, paint);
        } else {
            path.reset();
            float dr = width - prevWidth;
            float diff = -Math.signum(dr) * Math.min(2f * radius, Math.abs(dr/2f))/2f;
            path.moveTo(
                            prevLeft, prevBottom - radius
            );

            path.cubicTo(
                            prevLeft, prevBottom - radius,
                            prevLeft, rect.top,
                            prevLeft + diff, rect.top
            );
            path.lineTo(
                            rect.left - diff, rect.top
            );
            path.cubicTo(
                            rect.left - diff, rect.top,
                            rect.left, rect.top,
                            rect.left, rect.top + radius
            );
            path.lineTo(
                            rect.left, rect.bottom - radius
            );
            path.cubicTo(
                            rect.left, rect.bottom - radius,
                            rect.left, rect.bottom,
                            rect.left + radius, rect.bottom
            );
            path.lineTo(
                            rect.right - radius, rect.bottom
            );
            path.cubicTo(
                            rect.right - radius, rect.bottom,
                            rect.right, rect.bottom,
                            rect.right, rect.bottom - radius
            );
            path.lineTo(
                            rect.right, rect.top + radius
            );
            path.cubicTo(
                            rect.right, rect.top + radius,
                            rect.right, rect.top,
                            rect.right + diff, rect.top
            );
            path.lineTo(
                            prevRight - diff, rect.top
            );
            path.cubicTo(
                            prevRight - diff, rect.top,
                            prevRight, rect.top,
                            prevRight, prevBottom - radius
            );
            path.cubicTo(
                            prevRight, prevBottom - radius,
                            prevRight, prevBottom,
                            prevRight - radius, prevBottom

            );
            path.lineTo(
                            prevLeft + radius, prevBottom
            );
            path.cubicTo(
                            prevLeft + radius, prevBottom,
                            prevLeft, prevBottom,
                            prevLeft, rect.top - radius
            );
            c.drawPath(path, paintStroke);
        }

        prevWidth = width;
        prevLeft = rect.left;
        prevRight = rect.right;
        prevBottom = rect.bottom;
        prevTop = rect.top;
    }
}

Улучшение BackgroundColorSpan от @tttzof351 для поддержки выравнивания:

import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.text.style.LineBackgroundSpan
import kotlin.math.abs
import kotlin.math.sign

class BackgroundColorSpan(backgroundColor: Int,
                          private val padding: Int,
                          private val radius: Int) : LineBackgroundSpan {
    private val rect = RectF()
    private val paint = Paint()
    private val paintStroke = Paint()
    private val path = Path()
    private var prevWidth = -1f
    private var prevLeft = -1f
    private var prevRight = -1f
    private var prevBottom = -1f
    private var prevTop = -1f

    private val ALIGN_CENTER = 0
    private val ALIGN_START = 1
    private val ALIGN_END = 2

    init {
        paint.color = backgroundColor
        paintStroke.color = backgroundColor
    }

    private var align = ALIGN_CENTER

    fun setAlignment(alignment: Int) {
        align = alignment
    }

    override fun drawBackground(
            c: Canvas,
            p: Paint,
            left: Int,
            right: Int,
            top: Int,
            baseline: Int,
            bottom: Int,
            text: CharSequence,
            start: Int,
            end: Int,
            lnum: Int) {


        val width = p.measureText(text, start, end) + 2f * padding
        val shiftLeft: Float
        val shiftRight: Float


        when (align) {
            ALIGN_START -> {
                shiftLeft = 0f - padding
                shiftRight = width + shiftLeft
            }

            ALIGN_END -> {
                shiftLeft = right - width + padding
                shiftRight = (right + padding).toFloat()
            }
            else -> {
                shiftLeft = (right - width) / 2
                shiftRight = right - shiftLeft
            }
        }

        rect.set(shiftLeft, top.toFloat(), shiftRight, bottom.toFloat())


        if (lnum == 0) {
            c.drawRoundRect(rect, radius.toFloat(), radius.toFloat(), paint)
        } else {
            path.reset()
            val difference = width - prevWidth
            val diff = -sign(difference) * (2f * radius).coerceAtMost(abs(difference / 2f)) / 2f
            path.moveTo(
                    prevLeft, prevBottom - radius
            )

            if (align != ALIGN_START) {
                path.cubicTo(//1
                        prevLeft, prevBottom - radius,
                        prevLeft, rect.top,
                        prevLeft + diff, rect.top
                )
            } else {
                path.lineTo(prevLeft, prevBottom + radius)
            }
            path.lineTo(
                    rect.left - diff, rect.top
            )
            path.cubicTo(//2
                    rect.left - diff, rect.top,
                    rect.left, rect.top,
                    rect.left, rect.top + radius
            )
            path.lineTo(
                    rect.left, rect.bottom - radius
            )
            path.cubicTo(//3
                    rect.left, rect.bottom - radius,
                    rect.left, rect.bottom,
                    rect.left + radius, rect.bottom
            )
            path.lineTo(
                    rect.right - radius, rect.bottom
            )
            path.cubicTo(//4
                    rect.right - radius, rect.bottom,
                    rect.right, rect.bottom,
                    rect.right, rect.bottom - radius
            )
            path.lineTo(
                    rect.right, rect.top + radius
            )

            if (align != ALIGN_END) {
                path.cubicTo(//5
                        rect.right, rect.top + radius,
                        rect.right, rect.top,
                        rect.right + diff, rect.top
                )
                path.lineTo(
                        prevRight - diff, rect.top
                )
                path.cubicTo(//6
                        prevRight - diff, rect.top,
                        prevRight, rect.top,
                        prevRight, prevBottom - radius
                )

            } else {
                path.lineTo(prevRight, prevBottom - radius)
            }
            path.cubicTo(//7
                    prevRight, prevBottom - radius,
                    prevRight, prevBottom,
                    prevRight - radius, prevBottom
            )

            path.lineTo(
                    prevLeft + radius, prevBottom
            )

            path.cubicTo(//8
                    prevLeft + radius, prevBottom,
                    prevLeft, prevBottom,
                    prevLeft, rect.top - radius
            )
            c.drawPath(path, paintStroke)

        }
        prevWidth = width
        prevLeft = rect.left
        prevRight = rect.right
        prevBottom = rect.bottom
        prevTop = rect.top
    }
}

Полученные результаты:

Внедряю новые RoundedBackgroundSpan.kt класс расширяется LineBackgroundSpan, потому что он может рисовать декоративный слой для текста построчно.

class RoundedBackgroundSpan(
  backgroundColor: Int,
  private val padding: Float,
  private val radius: Float
) : LineBackgroundSpan {

  companion object {
    private const val NO_INIT = -1f
  }

  private val rect = RectF()
  private val paint = Paint().apply {
    color = backgroundColor
    isAntiAlias = true
  }
  private val path = Path()

  private var prevWidth = NO_INIT
  private var prevRight = NO_INIT

  override fun drawBackground(
    c: Canvas,
    p: Paint,
    left: Int,
    right: Int,
    top: Int,
    baseline: Int,
    bottom: Int,
    text: CharSequence,
    start: Int,
    end: Int,
    lineNumber: Int
  ) {

    val actualWidth = p.measureText(text, start, end) + 2f * padding
    val widthDiff = abs(prevWidth - actualWidth)

    val width = if (lineNumber == 0) {
      actualWidth
    } else if ((actualWidth < prevWidth) && (widthDiff < 2f * radius)) {
      prevWidth
    } else if ((actualWidth > prevWidth) && (widthDiff < 2f * radius)) {
      actualWidth + (2f * radius - widthDiff)
    } else {
      actualWidth
    }

    val shiftLeft = 0f - padding
    val shiftRight = width + shiftLeft

    rect.set(shiftLeft, top.toFloat(), shiftRight, bottom.toFloat())

    c.drawRoundRect(rect, radius, radius, paint)

    if (lineNumber > 0) {
      drawCornerType1(c, rect, radius)

      when {
        prevWidth < width -> drawCornerType2(c, rect, radius)
        prevWidth > width -> drawCornerType3(c, rect, radius)
        else              -> drawCornerType4(c, rect, radius)
      }
    }

    prevWidth = width
    prevRight = rect.right
  }

  private fun drawLeftCorner(c: Canvas, rect: RectF, radius: Float) {
    path.reset()
    path.moveTo(rect.left, rect.top + radius)
    path.lineTo(rect.left, rect.top - radius)
    path.lineTo(rect.left + radius, rect.top)
    path.lineTo(rect.left, rect.top + radius)

    c.drawPath(path, paint)
  }

  private fun drawTopCorner(c: Canvas, rect: RectF, radius: Float) {
    path.reset()
    path.moveTo(prevRight + radius, rect.top)
    path.lineTo(prevRight - radius, rect.top)
    path.lineTo(prevRight, rect.top - radius)
    path.cubicTo(
      prevRight, rect.top - radius,
      prevRight, rect.top,
      prevRight + radius, rect.top
    )

    c.drawPath(path, paint)
  }

  private fun drawBottomCorner(c: Canvas, rect: RectF, radius: Float) {
    path.reset()
    path.moveTo(rect.right + radius, rect.top)
    path.lineTo(rect.right - radius, rect.top)
    path.lineTo(rect.right, rect.top + radius)
    path.cubicTo(
      rect.right, rect.top + radius,
      rect.right, rect.top,
      rect.right + radius, rect.top
    )

    c.drawPath(path, paint)
  }

  private fun drawRightCorner(c: Canvas, rect: RectF, radius: Float) {
    path.reset()
    path.moveTo(rect.right, rect.top - radius)
    path.lineTo(rect.right, rect.top + radius)
    path.lineTo(rect.right - radius, rect.top)
    path.lineTo(rect.right, rect.top - radius)

    c.drawPath(path, paint)
  }
}

И используйте это:

private fun initSpannableText() {
    val span = RoundedBackgroundSpan(
        backgroundColor = colors.random(),
        padding = dp(5),
        radius = dp(5)
    )

    with(spanText) {
        setShadowLayer(dp(10), 0f, 0f, 0) // it's important for padding working

        text = androidx.core.text.buildSpannedString { inSpans(span) { append(text.toString()) } }
    }
}

Подробнее о реализации в этой статье:https://medium.com/@Semper_Viventem/simple-implementation-of-rounded-background-for-text-in-android-60a7706c0419

Модифицированная версия @Rahul_Tiwari для автоматического масштабирования отступов и углового радиуса при изменении размера текста. Он масштабируется на основе процентного изменения размера текста по умолчанию. Плюс setShadowLayer по мере необходимости. Он также добавляет отступы вверху и внизу текста, чтобы они были одинаковыми со всех сторон.

import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.text.style.LineBackgroundSpan
import android.view.Gravity
import android.widget.TextView
import kotlin.math.abs
import kotlin.math.sign

class BackgroundColorSpan(private val tv: TextView,
                          backgroundColor: Int,
                          private val defaultTextSizePx: Float,
                          private val paddingToTextSizeRatio : Float = 0.125f,
                          gravityAlignment: Int = Gravity.CENTER) : LineBackgroundSpan {
    private val rect = RectF()
    private val paint = Paint()
    private val paintStroke = Paint()
    private val path = Path()
    private var prevWidth = -1f
    private var prevLeft = -1f
    private var prevRight = -1f
    private var prevBottom = -1f
    private var prevTop = -1f

    /***
     * Gravity.CENTER_HORIZONTAL
     * Gravity.LEFT
     * Gravity.RIGHT
     */
    private var gravityAlignment : Int

    init {
        tv.includeFontPadding = false
        paint.color = backgroundColor
        paintStroke.color = backgroundColor
        this.gravityAlignment = gravityAlignment and Gravity.HORIZONTAL_GRAVITY_MASK
    }

    private val paddingForDefaultTextSize: Float get() =  defaultTextSizePx * paddingToTextSizeRatio

    private fun getTextScale(currentPaint: Paint) : Float  = currentPaint.textSize / defaultTextSizePx

    private fun getTagWidth(text: CharSequence, start: Int, end: Int, paint: Paint, padding: Float): Float =
            padding + paint.measureText(text, start, end) + padding

    private fun updatePaddingAndShadowLayerRadius(padding: Float) {
        if (tv.shadowRadius != padding) {
            tv.setShadowLayer(padding/* radius */, 0.toFloat(), 0.toFloat(), 0 /* transparent */)
        }
        val paddingI= padding.toInt()
        if (tv.paddingLeft != paddingI && tv.paddingRight != paddingI){
            tv.setPadding(paddingI, paddingI, paddingI, paddingI)
            tv.setLineSpacing(padding, 1.0f)
        }
    }

    override fun drawBackground(
            c: Canvas,
            p: Paint,
            left: Int,
            right: Int,
            top: Int,
            baseline: Int,
            bottom: Int,
            text: CharSequence,
            start: Int,
            end: Int,
            lnum: Int) {

        val paddingForTextSize = paddingForDefaultTextSize * getTextScale(p)
        updatePaddingAndShadowLayerRadius(paddingForTextSize)
        val width = getTagWidth(text, start, end, p, paddingForTextSize)
        val shiftLeft: Float
        val shiftRight: Float
        val fm = p.fontMetrics
        val tagBottom: Float = baseline + fm.descent + paddingForTextSize
        val topPadding = if (lnum == 0 ) paddingForTextSize else 0f
        val tagTop: Float = baseline + fm.ascent - topPadding

        val tagHeight = tagBottom - tagTop
        val radius = tagHeight / 10


        when (gravityAlignment) {
            Gravity.LEFT -> {
                shiftLeft = 0f - paddingForTextSize
                shiftRight = width + shiftLeft
            }

            Gravity.RIGHT -> {
                shiftLeft = right - width + paddingForTextSize
                shiftRight = (right + paddingForTextSize)
            }
            else -> {
                shiftLeft = (right - width) / 2
                shiftRight = right - shiftLeft
            }
        }

        rect.set(shiftLeft, tagTop, shiftRight, tagBottom)


        if (lnum == 0) {
            c.drawRoundRect(rect, radius, radius, paint)
        } else {
            path.reset()
            val difference = width - prevWidth
            val diff = -sign(difference) * (2f * radius).coerceAtMost(abs(difference / 2f)) / 2f
            path.moveTo(
                    prevLeft, prevBottom - radius
            )

            if (gravityAlignment != Gravity.LEFT) {
                path.cubicTo(//1
                        prevLeft, prevBottom - radius,
                        prevLeft, rect.top,
                        prevLeft + diff, rect.top
                )
            } else {
                path.lineTo(prevLeft, prevBottom + radius)
            }
            path.lineTo(
                    rect.left - diff, rect.top
            )
            path.cubicTo(//2
                    rect.left - diff, rect.top,
                    rect.left, rect.top,
                    rect.left, rect.top + radius
            )
            path.lineTo(
                    rect.left, rect.bottom - radius
            )
            path.cubicTo(//3
                    rect.left, rect.bottom - radius,
                    rect.left, rect.bottom,
                    rect.left + radius, rect.bottom
            )
            path.lineTo(
                    rect.right - radius, rect.bottom
            )
            path.cubicTo(//4
                    rect.right - radius, rect.bottom,
                    rect.right, rect.bottom,
                    rect.right, rect.bottom - radius
            )
            path.lineTo(
                    rect.right, rect.top + radius
            )

            if (gravityAlignment != Gravity.RIGHT) {
                path.cubicTo(//5
                        rect.right, rect.top + radius,
                        rect.right, rect.top,
                        rect.right + diff, rect.top
                )
                path.lineTo(
                        prevRight - diff, rect.top
                )
                path.cubicTo(//6
                        prevRight - diff, rect.top,
                        prevRight, rect.top,
                        prevRight, prevBottom - radius
                )

            } else {
                path.lineTo(prevRight, prevBottom - radius)
            }
            path.cubicTo(//7
                    prevRight, prevBottom - radius,
                    prevRight, prevBottom,
                    prevRight - radius, prevBottom
            )

            path.lineTo(
                    prevLeft + radius, prevBottom
            )

            path.cubicTo(//8
                    prevLeft + radius, prevBottom,
                    prevLeft, prevBottom,
                    prevLeft, rect.top - radius
            )
            c.drawPath(path, paintStroke)

        }
        prevWidth = width
        prevLeft = rect.left
        prevRight = rect.right
        prevBottom = rect.bottom
        prevTop = rect.top
    }
}

Чтобы избежать использования сложного алгоритма создания пути, вы можете использовать:

      paint.pathEffect = CornerPathEffect(cornerRadius)

Это заменяет все острые углы закругленными углами с указанным радиусом. ВместоSpanвы можете создатьDrawableи используйте его для установкиbackground:

      class ShapeBackgroundDrawable(
   private val shape: Path
) : ColorDrawable() {

   override fun draw(canvas: Canvas) {
       canvas.drawPath(shape, paint)
   }
}

textView.background = ShapeBackgroundDrawable(path)

результат

Мы создали библиотеку, которая также поддерживает Jetpack Compose: библиотека
Если вас интересуют подробности, прочитайте статью

Другие вопросы по тегам