Crazy Fancy Android Text

D225ebf0faa666ac7655cc7e4689283c?s=47 Daniel Lew
February 02, 2019

Crazy Fancy Android Text

Talk given about styling text on Android @ DevFestMN, February 2019.

Video: https://youtu.be/4cweBUdOvww

D225ebf0faa666ac7655cc7e4689283c?s=128

Daniel Lew

February 02, 2019
Tweet

Transcript

  1. 2.
  2. 3.

    The Trouble with Trello (Markdown) • Basic nesting of elements

    would fail • Image loading was iffy at best • Endless URL parsing issues • Code blocks would fail to render due to cool wind breezes • HTML-escaped entities would scramble text • Russian text crashes the app
  3. 7.

    Styling an Entire TextView <TextView android:text="Hello, Normal World!" /> <TextView

    android:text="Hello, Bold World!" android:textStyle="bold" /> <TextView android:text="Hello, Italic World!" android:textStyle="italic" />
  4. 8.
  5. 9.
  6. 11.

    Definitions Span - markup objects that can style text Spannable

    - text with mutable spans Spanned - text with spans
  7. 13.

    Easy Mode: HTML <string name="multi_style_text"> <sup>Whoa</sup> <i>this</i> <sub>is</sub> <b>wild</b>! \n\n

    <font fgcolor="red">How did you do that?</font> \n\n <strike>I cheated</strike> Spans! </string> ==
  8. 14.

    Easy Mode: HTML <string name="multi_style_text"> <sup>Whoa</sup> <i>this</i> <sub>is</sub> <b>wild</b>! \n\n

    <font fgcolor="red">How did you do that?</font> \n\n <strike>I cheated</strike> Spans! </string>
  9. 15.

    Easy Mode: HTML textView.text = HtmlCompat.fromHtml( "<sup>Whoa</sup> <i>this</i> <sub>is</sub> <b>wild</b>!"

    + "\n\n" + "<font fgcolor=\"red\">How did you do that?</font>" + "\n\n" + "<strike>I cheated</strike> Spans!”, 0 )
  10. 16.

    Easy Mode: HTML == textView.text = HtmlCompat.fromHtml( "<sup>Whoa</sup> <i>this</i> <sub>is</sub>

    <b>wild</b>!" + "\n\n" + "<font fgcolor=\"red\">How did you do that?</font>" + "\n\n" + "<strike>I cheated</strike> Spans!”, 0 )
  11. 18.

    Easy Mode: HTML != <string name="multi_style_text"> <sup>Whoa</sup> <i>this</i> <sub>is</sub> <b>wild</b>!

    \n\n <font fgcolor="red">How did you do that?</font> \n\n <strike>I cheated</strike> Spans! </string> textView.text = HtmlCompat.fromHtml( "<sup>Whoa</sup> <i>this</i> <sub>is</sub> <b>wild</b>!" + "\n\n" + "<font fgcolor=\"red\">How did you do that?</font>" + "\n\n" + "<strike>I cheated</strike> Spans!”, 0 )
  12. 19.

    HTML Drawbacks • Limited tag support • Strings support different

    tags than Html.fromHtml() • Details: https://blog.danlew.net/2011/04/13/html_in_text_views/
  13. 21.

    Hard Mode: Build Your Own == val text = SpannableString("Whoa

    this is wild!\n\nHow did you do that?\n\nI cheated Spans!") text.setSpan(SuperscriptSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.ITALIC), 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(SubscriptSpan(), 10, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.BOLD), 13, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(ForegroundColorSpan(Color.RED), 20, 40, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StrikethroughSpan(), 42, 51, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  14. 22.

    Hard Mode: Build Your Own val text = SpannableString("Whoa this

    is wild!\n\nHow did you do that?\n\nI cheated Spans!") text.setSpan(SuperscriptSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.ITALIC), 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(SubscriptSpan(), 10, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.BOLD), 13, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(ForegroundColorSpan(Color.RED), 20, 40, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StrikethroughSpan(), 42, 51, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  15. 23.

    Hard Mode: Build Your Own val text = SpannableString("Whoa this

    is wild!\n\nHow did you do that?\n\nI cheated Spans!") text.setSpan(SuperscriptSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.ITALIC), 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(SubscriptSpan(), 10, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.BOLD), 13, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(ForegroundColorSpan(Color.RED), 20, 40, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StrikethroughSpan(), 42, 51, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  16. 24.

    Hard Mode: Build Your Own Class Mutable Text Mutable Markup

    SpannedString No No SpannableString No Yes SpannableStringBuilder Yes Yes
  17. 25.

    Hard Mode: Build Your Own val text = SpannableString("Whoa this

    is wild!\n\nHow did you do that?\n\nI cheated Spans!") text.setSpan(SuperscriptSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.ITALIC), 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(SubscriptSpan(), 10, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.BOLD), 13, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(ForegroundColorSpan(Color.RED), 20, 40, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StrikethroughSpan(), 42, 51, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  18. 26.

    Hard Mode: Build Your Own val text = SpannableString("Whoa this

    is wild!\n\nHow did you do that?\n\nI cheated Spans!") text.setSpan(SuperscriptSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.ITALIC), 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(SubscriptSpan(), 10, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StyleSpan(Typeface.BOLD), 13, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(ForegroundColorSpan(Color.RED), 20, 40, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(StrikethroughSpan(), 42, 51, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  19. 28.

    Span Flags • Flags define behavior • Can control precise

    behavior… • …But just use 0 or SPAN_EXCLUSIVE_EXCLUSIVE
  20. 35.

    Bad Spans • BulletSpan - Sucks until API 28! •

    QuoteSpan - Sucks until API 28! • URLSpan - Crashes when there’s no intent handler!
  21. 37.

    Custom Span Process • Pick a Span to extend •

    Decipher how it works • Get frustrated by limited Span API
  22. 44.

    Thematic Break Span class ThematicBreakSpan(val lineColor: Int, val lineHeight: Int,

    val verticalPadding: Int) : ReplacementSpan() { override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { if (fm != null) { fm.ascent = -lineHeight - (2 * verticalPadding) fm.descent = 0 fm.top = fm.ascent fm.bottom = 0 } return 1 } ... }
  23. 45.

    Thematic Break Span class ThematicBreakSpan(val lineColor: Int, val lineHeight: Int,

    val verticalPadding: Int) : ReplacementSpan() { override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { if (fm != null) { fm.ascent = -lineHeight - (2 * verticalPadding) fm.descent = 0 fm.top = fm.ascent fm.bottom = 0 } return 1 } ... }
  24. 46.

    Thematic Break Span class ThematicBreakSpan(val lineColor: Int, val lineHeight: Int,

    val verticalPadding: Int) : ReplacementSpan() { override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { val color = paint.color val style = paint.style paint.color = lineColor paint.style = Paint.Style.FILL canvas.drawRect(etc etc etc) paint.color = color paint.style = style } }
  25. 47.

    Thematic Break Span class ThematicBreakSpan(val lineColor: Int, val lineHeight: Int,

    val verticalPadding: Int) : ReplacementSpan() { override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { val color = paint.color val style = paint.style paint.color = lineColor paint.style = Paint.Style.FILL canvas.drawRect(etc etc etc) paint.color = color paint.style = style } }
  26. 48.

    Custom Span Limitations • Bound by API limitations • Cannot

    Parcelable custom spans • Cannot copy/paste your custom span • Choose wisely what you extend
  27. 49.

    Example Custom Spans • BetterURLSpan : URLSpan • BulletSpanCompat :

    BulletSpan • QuoteSpanCompat : QuoteSpan • CodeBackgroundSpan : LineBackgroundSpan • ListItemSpan : BulletSpan • ThematicBreakSpan : ReplacementSpan
  28. 51.

    Reusing Spans? val boldSpan = StyleSpan(Typeface.BOLD) val text = SpannableString("Alice

    threw the ball to Bob") text.setSpan(boldSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(boldSpan, 24, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
  29. 52.

    Reusing Spans? val boldSpan = StyleSpan(Typeface.BOLD) val text = SpannableString("Alice

    threw the ball to Bob") text.setSpan(boldSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) text.setSpan(boldSpan, 24, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) ==
  30. 55.

    Construction Optimization • SpannableStringBuilder was taking 98% of construction time

    • Faster method: val sb = StringBuilder() val spans = mutableListOf<SpanInfo>() data class SpanInfo(val what: Any, val start: Int, val end: Int, val flags: Int)
  31. 56.

    Construction Optimization Construction Layout SpannableStringBuilder 51.3ms 9.4ms StringBuilder + SpannableString

    1.5ms 16.4ms https://blog.danlew.net/2018/08/30/exploring-spannable-performance/
  32. 57.

    Replacement Spans • Cannot attach to empty space • Options

    • Invisible character • Fallback text if replacement span fails • Space character
  33. 58.

    Testing Spans • Comparing two Spannables is difficult because… •

    Span API is weird • Spans do not implement equals() • Not all Span attributes are public
  34. 59.

    Testing Spans val spanObjects = spanned.getSpans(0, spanned.length, Any::class.java) spanObjects.forEach {

    span -> val start = expected.getSpanStart(span) val end = expected.getSpanEnd(span) val flags = expected.getSpanFlags(span) }
  35. 60.

    Testing Spans val spanObjects = spanned.getSpans(0, spanned.length, Any::class.java) spanObjects.forEach {

    span -> val start = spanned.getSpanStart(span) val end = spanned.getSpanEnd(span) val flags = spanned.getSpanFlags(span) }
  36. 61.

    Testing Spans fun assertSpanEquals(expected: RelativeSizeSpan, actual: RelativeSizeSpan) { assertEquals(expected.sizeChange, actual.sizeChange)

    } fun assertSpanEquals(expected: LeadingMarginSpan.Standard, actual:LeadingMarginSpan.Standard) { assertEquals(expected.getLeadingMargin(true), actual.getLeadingMargin(true)) assertEquals(expected.getLeadingMargin(false), actual.getLeadingMargin(false)) }
  37. 62.

    Resources • Florina Muntenescu: https://medium.com/@florina.muntenescu • Optimizing spans • Styling

    internationalized text • …And more! • Google IO Talk: https://youtu.be/x-FcOX6ErdI