textview.settext text has many replacementspans on android 10 occured ANR - android

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

Related

SpannableString with space on top and bottom of just Word betwenn anothers

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
}
}

Custom ImageSpan not displaying properly

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.

How to move a word in an android textview above the next word using text span?

I'm trying to move a word above the next word in an android textview like in the attached image (example image). I have managed to shift the word upwards (like a superscript) with spannablestringbuilder, but I can't find a way to shift the right part of the text left in order to fill the gap. Does anyone have any idea how can this be done?
This is the function I've written so far:
/**
* Adds clickable spans for words that are contained between "[" and "]"
*
* #param imString The string on which to apply clickable spans
*/
private fun addClickablePart(imString: String): SpannableStringBuilder
{
var string = imString
val spannableStringBuilder = SpannableStringBuilder((string.replace("[", "")).replace("]", ""))
var startIndex = string.indexOf("[")
while (startIndex != -1)
{
string = string.replaceFirst("[", "")
val endIndex = string.indexOf("]", startIndex)
string = string.replaceFirst("]", "")
val clickString = string.substring(startIndex, endIndex)
spannableStringBuilder.setSpan(
object: ClickableSpan()
{
override fun onClick(view: View)
{
HelperFunction.showToast(this#SongActivity, clickString)
}
override fun updateDrawState(text: TextPaint)
{
super.updateDrawState(text)
text.isUnderlineText = false
text.color = ContextCompat.getColor(this#SongActivity, R.color.colorAccent)
text.textSize = HelperFunction.spToPx(this#SongActivity, 12).toFloat()
text.baselineShift += (text.ascent()).toInt() // move chord upwards
text.typeface = Typeface.create(ResourcesCompat.getFont(this#SongActivity, R.font.roboto_mono), Typeface.BOLD) // set text to bold
}
},
startIndex, endIndex, 0)
startIndex = string.indexOf("[", endIndex)
}
return spannableStringBuilder
}
You can use HTML in your TextView to achieve this, for example the HTML/CSS code for the superscript notation would be:
<!DOCTYPE html>
<html>
<head>
<style>
sup {
vertical-align: super;
font-size: medium;
color: red;
position: relative; left: -2.5em; top: -0.5em;
}
</style>
</head>
<body>
<p>word <sup>topword</sup></p>
</body>
</html>
Please note that this is just an example and you need to fix the alignment.
To display it in a TextView use the following code:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
textView.setText(Html.fromHtml(htmlVal, Html.FROM_HTML_MODE_COMPACT));
} else {
textView.setText(Html.fromHtml(htmlVal));
}
I've managed to solve the problem using ReplacementSpan. I'm posting the code below.
This is the custom ReplacementSpan class which draws the words on the canvas at the desired positions:
inner class ChordSpan: ReplacementSpan()
{
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: FontMetricsInt?): Int
{
val mText = text!!.subSequence(start, end).toString().replace("[", "")
var chordString = ""
var regularString = mText
if (mText.contains("]"))
{
chordString = mText.substringBefore("]")
regularString = mText.substringAfter("]")
}
val chordStringTextPaint = getChordStringTextPaint(paint)
val regularStringTextPaint = getRegularStringTextPaint(paint)
return max(chordStringTextPaint.measureText(chordString), regularStringTextPaint.measureText(regularString)).toInt()
}
private fun getChordStringTextPaint(paint: Paint): TextPaint
{
val textPaint = TextPaint(paint)
textPaint.textSize = textPaint.textSize / 1.5F
textPaint.typeface = Typeface.DEFAULT_BOLD
textPaint.color = ContextCompat.getColor(this#SongActivity, R.color.colorAccent)
return textPaint
}
private fun getRegularStringTextPaint(paint: Paint): TextPaint
{
return TextPaint(paint)
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint)
{
val mText = text!!.subSequence(start, end).toString().replace("[", "")
var chordString = ""
var regularString = mText
if (mText.contains("]"))
{
chordString = mText.substringBefore("]")
regularString = mText.substringAfter("]")
}
val chordStringTextPaint = getChordStringTextPaint(paint)
val regularStringTextPaint = getRegularStringTextPaint(paint)
canvas.drawText(chordString, x, y.toFloat(), chordStringTextPaint)
canvas.drawText(regularString, x, y.toFloat() + (bottom - top) / 2.5F, regularStringTextPaint)
}
}
And this is the function that applies the spans on the hole text:
private fun formatDisplayOfLyricsWithChords(string: String): SpannableString
{
val mString = "$string\n\n"
val endOfStringIndex = mString.length
val spannableString = SpannableString(mString)
var startIndex = 0
while (startIndex != -1 && startIndex != endOfStringIndex)
{
var possibleEndIndex = mString.indexOf("[", startIndex + 1)
if (possibleEndIndex == -1)
{
possibleEndIndex = endOfStringIndex + 1
}
var endOfRowIndex = mString.indexOf("\n", startIndex + 1)
if (endOfRowIndex == -1)
{
endOfRowIndex = endOfStringIndex + 1
}
val endIndex = minOf(possibleEndIndex, endOfRowIndex, endOfStringIndex)
spannableString.setSpan(ChordSpan(), startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
if (mString[startIndex] == '[')
{
val startIndexClick = startIndex
val endIndexClick = mString.indexOf("]", startIndex + 1)
val chord = mString.substring(startIndexClick + 1, endIndexClick)
spannableString.setSpan(
object: ClickableSpan()
{
override fun onClick(view: View)
{
handleClickOnChord(chord)
}
},
startIndexClick, endIndexClick, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
startIndex = endIndex
if (startIndex == endOfRowIndex)
{
startIndex++
}
}
return spannableString
}
I took inspiration from this answer from a similar question: https://stackoverflow.com/a/24091284/8211969

How to set bullet radius in BulletSpan below api level 28?

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
}
}

Implement autoSizeTextType in android Edittext

Android Oreo buildToolsVersion provides a simplified way to autoresize textsize in AppCompatTextView as follows
<android.support.v7.widget.AppCompatTextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:maxWidth="300dp"
android:background="#android:color/holo_green_light"
app:autoSizeTextType="uniform"
app:autoSizeMinTextSize="5sp"
app:autoSizeMaxTextSize="50sp"
app:autoSizeStepGranularity="4sp"
/>
Can a similar implementation be applied to AppCompatEditText since it is basically an extension of TextView? Simply applying autoSizeTextType to AppCompatEditText doesn't seem to work. Is there a way to make this work out?
No you cannot. Please see here; it is disabled for all AppCompatEditText since it is not supported.
I had an specific case which was a single line EditText, so I'm posting my solution just in case to be helpful to someone...
private val initialTextSize: Float by lazy {
resources.getDimensionPixelSize(R.dimen.default_text_size).toFloat()
}
private fun updateTextSize(s: CharSequence) {
// Auto resizing text
val editWidth = myEditText.width
if (editWidth > 0) { // when the screen is opened, the width is zero
var textWidth = myEditText.paint.measureText(s, 0, s.length)
if (textWidth > editWidth) {
var fontSize = initialTextSize
while (textWidth > editWidth && fontSize > 12) { // minFontSize=12
fontSize -= 1
myEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
textWidth = myEditText.paint.measureText(s, 0, s.length)
}
// As long the text grows, the font size decreases,
// so here I'm increasing it to set the correct text s
} else {
var fontSize = myEditText.textSize
while (textWidth <= editWidth && fontSize < initialTextSize) {
fontSize = min(fontSize + 1, initialTextSize)
myEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
textWidth = myEditText.paint.measureText(s, 0, s.length)
}
}
}
}
Then you must call this function from a TextWatcher attached to the EditText.
private fun createTextWatcher() = object : SimpleTextWatcher() {
override fun beforeTextChanged(s: CharSequence?,
start: Int, count: Int, after: Int
) {
super.beforeTextChanged(s, start, count, after)
s?.let { updateTextSize(it) }
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
// Do nothing
}
override fun afterTextChanged(s: Editable?) {
super.afterTextChanged(s)
s?.let { updateTextSize(it) }
}
}
My suggestion would be use: --> https://github.com/AndroidDeveloperLB/AutoFitTextView correction https://github.com/ViksaaSkool/AutoFitEditText
See this thread for more info: --> Auto-fit TextView for Android
General Article on mention library by author: --> https://viksaaskool.wordpress.com/2014/11/16/using-auto-resize-to-fit-edittext-in-android/

Categories

Resources