I'm trying to create a custom class that extends ImageSpan because I need some kind of margin/padding on the spans.
What I figured I need to do is to override the getSize function to return a bigger width so the spans get graphically spaced.
The problem is that as soon as I override the getSize function my view gets completely screwed up. My educated guess is then that I'm doing something stupid inside that funcion, but I can't get what.
Custom class code:
class PaddingImageSpan(drawable: Drawable, private val offset: Float = 0f) : ImageSpan(drawable) {
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int {
val width = paint.measureText(text, start, end)
val fontMetricsInt = paint.fontMetricsInt
if (fm != null){
fm.ascent = fontMetricsInt.ascent
fm.bottom = fontMetricsInt.bottom
fm.descent = fontMetricsInt.descent
fm.leading = fontMetricsInt.leading
fm.top = fontMetricsInt.top
}
println(width)
return width.roundToInt()
}
}
I figured it out. I'm posting the solution so if someone looks for it he can find it!
My problem was I was using the text metrics instead of the drawable metrics.
This is the correct code:
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int {
val rect = drawable.bounds
if (fm != null) {
fm.ascent = -rect.bottom
fm.descent = 0
fm.top = fm.ascent
fm.bottom = 0
}
return rect.right// + offset
}
That said, the cleaner way that I could come up with to space spannable is not by working on the spannable class but changing the setBounds() values.
Related
I am now trying to resolve the conflict of gesture and DrawLayout.
According to the document, to resolve the conflict, I should call DrawerLayout.setSystemGestureExclusionRects to specify the exclusion area Rect.
So I write the code like this:
override fun onDraw(c: Canvas?) {
super.onDraw(c)
updateGestureExclusion()
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
if (!changed) {
return
}
updateGestureExclusion()
}
private fun updateGestureExclusion() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return
}
systemGestureExclusionRects = listOf(createSystemGestureExclusionRect())
}
private fun createSystemGestureExclusionRect(): Rect {
val screenSize = ScreenUtils.getScreenSize()
val height = screenSize.y
val heightInDp = SizeUtils.px2dp(height.toFloat())
return if (heightInDp <= 200) {
Rect(0, 0, SizeUtils.dp2px(56f), height)
} else {
val topInDp = (heightInDp - 200) / 2f
val top = SizeUtils.dp2px(topInDp)
val bottom = top + SizeUtils.dp2px(199f)
Rect(0, top, SizeUtils.dp2px(56f), bottom)
}
}
I clip an 56dp * 200dp at the center-left of screen as systemGestureExclusionRect. Unforetunately, For Realme RMX2020 device, Android System recognize the area of left-top screen as systemGestureExclusionRect. Even if I change the exclusionRect, Android System still recognize the area of left-top screen as systemGestureExclusionRect.
I also checked the size of the rect area and it did not exceed the limit in the document.
What's wrong with my code and how to resolve the conflict correctly? Thank you! :)
Hi Guys ? So I have a one TextView and I have a Word of value that I have to set some styles only on value, like
the problem is, I need to put space on top and bottom of this value inside the textview to be it a little separeted the rest of String.
I need to let it like that
My code now is this
private fun configTitleSpan(
title: String,
textToSpanValue: String
): SpannableString {
val startIndex = title.indexOf(textToSpanValue)
val endIndex = title.indexOf(textToSpanValue).plus(textToSpanValue.length)
if (startIndex <= 0 || endIndex <= 0) return title.toSpan()
return SpannableString(title)
.apply {
setSpan(
RelativeSizeSpan(SIZE_SPAN),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
.apply {
setSpan(
TypefaceSpan(DEFAULT_FONT_STYLE),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
.apply {
setSpan(
MarginTopBottomSpan(HEIGHT_SIZE_SPAN, HEIGHT_SIZE_SPAN),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
I can put the value formatted but the space on top and bottom that I need I can`t put it.
I can solve it following this answer, I made some adjusts and works fine.
Using LineHeightSpan
private class MarginTopBottomSpan(
private val topMargin: Int,
private val bottomMargin: Int
) : LineHeightSpan {
override fun chooseHeight(
text: CharSequence,
start: Int,
end: Int,
spanstartv: Int,
v: Int,
fm: FontMetricsInt
) {
fm.bottom += bottomMargin
fm.descent += bottomMargin
fm.top += topMargin * VALUE_NEGATIVE
fm.ascent += topMargin * VALUE_NEGATIVE
}
companion object {
private const val VALUE_NEGATIVE = -1
}
}
It takes a lot of time in view.onMeasure when textview set text(spannable) has many ReplacementSpans on android 10 phone, so it is occurred ANR.
I tested on Samsung G981N and G975N phones(android 10)
According to android profiler, android.graphics.text.LineBreaker.nComputeLineBreaks runs most of running times.
Here the image, and below the code I tested.
(compileSdkVersion 29)
(replace A*100 to K*2 using ReplacementSpan)
(textLength = 3000 runs normally, but 30000 occurred ANR)
private fun test() {
val text = StringBuffer()
val textLength = 30000
val spanLength = 100
var count = 0
while (count < textLength) {
text.append("A")
count++
}
val spannable = text.toSpannable()
var index = 0
while (index < (textLength / spanLength)) {
val span = TestReplacementSpan()
spannable.setSpan(span, index * spanLength, index * spanLength + spanLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
index++
}
binding.textview.text = spannable
}
class TestReplacementSpan : ReplacementSpan() {
private val replaceText = "KK"
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: FontMetricsInt?): Int {
if (fm != null) {
paint.getFontMetricsInt(fm)
}
return Math.round(paint.measureText(replaceText, 0, replaceText.length))
}
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
canvas.drawText(replaceText, 0, replaceText.length, Math.max(x, 0f), y.toFloat(), paint)
}
}
add below code and it is solved
textview.breakStrategy = Layout.BREAK_STRATEGY_SIMPLE
How to use BulletSpan(gapWidth, color, bulletRadius), below api level 28? I am unable to set bulletRadius below api level 28. Any help would be appreciated.
So the method with radius parameters do not appear until API level 28. For previous APIs, you can refer to this article.
Basically what the author did was porting the API 28+ BulletSpan to your app project so you can use the ported version to achieve setting the radius.
Writing a Custom Bullet Span works like a charm, because you will get the canvas in your hand. With this you can paint a bullet of any kind/size to your view.
open class CharBulletSpan(var charCode: String, gapWidth: Int, internal val bulletColor: Int, val bulletSize: Int, val alignment: Layout.Alignment,val typeface: Typeface) : BulletSpan(gapWidth, bulletColor) {
private var isSpanStart = true
private val space = gapWidth
var alpha = 0f
override fun getLeadingMargin(first: Boolean): Int { // Returns the amount of indentation as set by the LeadingMarginSpan.class
if (!first) {
return 0
}
return super.getLeadingMargin(first)
}
override fun drawLeadingMargin(canvas: Canvas, paint: Paint, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int, text: CharSequence, start: Int, end: Int, first: Boolean, layout: Layout?) {
//This is where the magic happens. Let us get the x - translation for the bullet since we will be painting on the canvas for different text alignments
isSpanStart = (text as Spanned).getSpanStart(this) == start
val xPos = getBulletXPos(layout!!, start, x)
val selectionStart = Selection.getSelectionStart(text)
val selectionEnd = Selection.getSelectionEnd(text)
//The following block is just for a fancy purpose. When we type text and press enter and the cursor is in a new line, we can apply an alpha value to the bullet/ set a transparency. If text is typed in that line , the bullet's alpha value can be changed.
if (!text.isNullOrEmpty()) {
if (start != end && (text.subSequence(start, end).isNotEmpty() && text.subSequence(start, end) != "\n")) {
this.alpha = 255f
}
if (start == end) {
if ((start == 1 && selectionStart == 0) || (start == selectionStart && end == selectionEnd)) { // first line
this.alpha = 150f
if (!isCursorVisible) {
this.alpha = 0f
}
} else if (selectionStart != start) {
this.alpha = 0f
}
}
} else if (text != null && text.isEmpty() && start == 0 && start == end) {
this.alpha = 255f
}
if (isSpanStart) {
// Now we shall fire the bullet
renderCharBullet(canvas, paint, xPos, dir, top, baseline, bottom, text,charCode)
}
}
private fun getBulletXPos(layout: Layout, start: Int, x: Int): Int {
val width = layout.width
val lineNo = layout.getLineForOffset(start)
val lineLeft = layout.getLineLeft(lineNo)
val lineWidth = layout.getLineWidth(lineNo)
return when (alignment) {
Layout.Alignment.NORMAL -> x
Layout.Alignment.OPPOSITE -> x + (width - lineWidth).toInt()
Layout.Alignment.ALIGN_CENTER -> lineLeft.toInt() - space
else -> x
}
}
private fun renderCharBullet(canvas: Canvas?, paint: Paint?, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int, text: CharSequence?, charCode: String) {
val rectF = Rect()
val newPaint = Paint()
newPaint.typeface = typeface
newPaint.textSize = bulletSize
//Constructing a new paint to compute the y - translation of the bullet for the current line
if (!text.isNullOrEmpty()) {
newPaint.getTextBounds(text.subSequence(0, 1).toString(), 0, text.subSequence(0, 1).length, rectF)
}
val oldStyle = paint?.style
paint?.textSize = bulletSize
paint?.typeface = typeface
paint?.style = Paint.Style.FILL
paint?.color = bulletColor
paint?.alpha = alpha.toInt()
if (canvas!!.isHardwareAccelerated) {
canvas.save()
canvas.translate((x + dir).toFloat(), baseline - rectF.height().div(2.0f))
canvas.drawText(charCode, 0f, rectF.height().div(2.0f), paint!!)
canvas.restore()
}
paint?.style = oldStyle
}
}
I have EditText with content: #<m id="36c03920-f411-4919-a175-a5b1eb616592">Full name</m>, how are you doing?. I would like to hide tag with id from the user and keep only #Full name, how are you doing? when displaying text. Still getText should return full content.
I found ReplacementSpan useful to this approach. At first step, tried replacing only </m> but text is drawn twice in two lines. First line starts with # and second one starts with <. Also cannot insert new text. getSize is called multiple times and draw is called twice.
Is my approach correct? Should I try to found different solution and store ids in separate collection and do post processing on getText()?
Code:
inner class RemoveTagSpan : ReplacementSpan() {
private val regexEndTag = Regex("</m>")
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
text?.replace(regexEndTag, "")?.let { canvas.drawText(it, start, it.length - 1, x, y.toFloat(), paint) }
}
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: FontMetricsInt?): Int {
return text?.replace(regexEndTag, "")?.let { paint.measureText(it).roundToInt() } ?: 0
}
}
Not sure if this will give you exactly what you want, but generally displaying markup language in a TextView or EditText(?) can be done in this way:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
textView.setText(Html.fromHtml("#<m id="36c03920-f411-4919-a175-a5b1eb616592">Full name</m>, how are you doing?", Html.FROM_HTML_MODE_COMPACT));
} else {
textView.setText(Html.fromHtml("#<m id="36c03920-f411-4919-a175-a5b1eb616592">Full name</m>, how are you doing?"));