Search code examples
androidtextviewandroid-spannablebullet-span

BulletSpan which has newline is not working in Android


i'm using BulletSpan which is customized. i want to display long text that has '\n'. every lines are fine except for the text line which has '\n'. bulleetspan can't apply the indent to the newline text. this is the result.

enter image description here

and the last text is one text. and the text has '\n' inside. enter image description here

and the code is..

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    val source = listOf("Spans are powerful markup objects that you can use to style text at a character or paragraph level.",
    "By attaching spans to text objects, you can change text in a variety of ways, ",
    "including adding color, making the text clickable,\scaling the text size,\nand drawing text in a customized way.")

    val sb = SpannableStringBuilder()

    for (i in source.indices) {
        val length = sb.length
        sb.append(source[i])
        sb.append("\n")
        sb.setSpan(CustomBulletSpan(
            bulletRadius = dip(8),
            gapWidth = dip(14),
            mColor = color(),
            mWantColor = true
        ), length, length + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
    }

    binding.tvResult.text = sb

}

private fun dip(dp: Int): Int {
    return TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        dp.toFloat(),
        resources.displayMetrics
    ).toInt()
}

private fun color(): Int {
    return ContextCompat.getColor(applicationContext, R.color.gray);
} 
}

and the customBullentSpan code is..

class CustomBulletSpan(
val bulletRadius: Int = STANDARD_BULLET_RADIUS,
val gapWidth: Int = STANDARD_GAP_WIDTH,
val mColor: Int = STANDARD_COLOR,
val mWantColor: Boolean = false
) : LeadingMarginSpan {

companion object {
    // Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
    private const val STANDARD_BULLET_RADIUS = 4
    private const val STANDARD_GAP_WIDTH = 2
    private const val STANDARD_COLOR = 0
}

private var mBulletPath: Path? = null


override fun getLeadingMargin(first: Boolean): Int {
    return 2 * bulletRadius + gapWidth
}

override fun drawLeadingMargin(
    c: Canvas,
    p: Paint,
    x: Int,
    dir: Int,
    top: Int,
    baseline: Int,
    bottom: Int,
    text: CharSequence,
    start: Int,
    end: Int,
    first: Boolean,
    layout: Layout?
) {
    if ((text as Spanned).getSpanStart(this) == start) {
        val style = p.style
        p.style = Paint.Style.FILL
        var oldColor = 0
        if (mWantColor) {
            oldColor = p.color
            p.color = mColor
        }

        val yPosition = if (layout != null) {
            val line = layout.getLineForOffset(start)
            layout.getLineBaseline(line).toFloat() - bulletRadius * 1.3f
        } else {
            (top + bottom) / 1.3f
        }

        val xPosition = (x + dir * bulletRadius).toFloat()

        if (c.isHardwareAccelerated) {
            if (mBulletPath == null) {
                mBulletPath = Path()
                mBulletPath!!.addCircle(0.0f, 0.0f, bulletRadius.toFloat(), Path.Direction.CW)
            }
            c.save()
            c.translate(xPosition, yPosition)
            c.drawPath(mBulletPath!!, p)
            c.restore()
        } else {
            c.drawCircle(xPosition, yPosition, bulletRadius.toFloat(), p)
        }

        if (mWantColor) {
            p.color = oldColor
        }

        p.style = style
    }
 }
}

how can i solve this problem??


Solution

  • You could just get the string and split by \n and apply span

        var len = 0
    
        for (i in source.indices) {
    
            if (source[i].contains("\n")) {
                val splitted = source[i].split("\n")
                for (k in splitted.indices) {
                    len = sb.length
                    sb.append(splitted[k])
                    sb.append("\n")
                    sb.setSpan(
                            CustomBulletSpan(
                                    bulletRadius = dip(8),
                                    gapWidth = dip(14),
                                    mColor = color(),
                                    mWantColor = true
                            ), len, len + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
                    )
                }
            } else {
                len = sb.length
                sb.append(source[i])
                sb.append("\n")
                sb.setSpan(
                        CustomBulletSpan(
                                bulletRadius = dip(8),
                                gapWidth = dip(14),
                                mColor = color(),
                                mWantColor = true
                        ), len, len + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
                )
    
            }
        }
    

    The other way is to split and add it to new list and iterate over the same and apply spans

      val newList = mutableListOf<String>()
        for (item in source) {
           if(item.contains("\n")) {
               val split = item.split("\n")
               for (splitItem in split){
                   newList.add(splitItem)
               }
           } else{
               newList.add(item)
           }
        }
    

    enter image description here