像 Instagram 这样的圆形背景文本,ReplacementSpan 不能按要求工作
Rounded Background text like Instagram, ReplacementSpan not working as required
我正在尝试做类似于下面 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
-
键入时得到的结果
这是我想要的固定宽度的正常行为 -
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;
}
}
我实现了 new RoundedBackgroundSpan.kt
class extends 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
@tttzof351 增强 BackgroundColorSpan
以支持对齐:
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
}
}
结果:
修改了@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
}
}
我正在尝试做类似于下面 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
-
这是我想要的固定宽度的正常行为 -
<?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;
}
}
我实现了 new RoundedBackgroundSpan.kt
class extends 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
@tttzof351 增强 BackgroundColorSpan
以支持对齐:
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
}
}
结果:
修改了@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
}
}