Rounded Background text like Instagram, ReplacementSpan not working as required
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;
}
}
Enhancing BackgroundColorSpan
by @tttzof351 to support alignment:
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
}
}
Results: