Закругленный фоновый текст, такой как 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 ответов
<?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: библиотека
Если вас интересуют подробности, прочитайте статью