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. Crazy Fancy Android Text DevFest MN (2019) @danlew42

  2. None
  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
  4. Text -> Abstract Syntax Tree -> Fancy TextView

  5. Simple Styles

  6. Styling an Entire TextView

  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" />
  8. None
  9. None
  10. Spannable Span Span Span Span Span Span

  11. Definitions Span - markup objects that can style text Spannable

    - text with mutable spans Spanned - text with spans
  12. Constructing Spannables

  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> ==
  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>
  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 )
  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 )
  17. Easy Mode: HTML !=

  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 )
  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/
  20. Hard Mode: Build Your Own • Code only • Verbose

    • Flexible & powerful
  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)
  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)
  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)
  24. Hard Mode: Build Your Own Class Mutable Text Mutable Markup

    SpannedString No No SpannableString No Yes SpannableStringBuilder Yes Yes
  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)
  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)
  27. Hard Mode: Build Your Own text.setSpan( SuperscriptSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE

    ) Span Start End WTF?
  28. Span Flags • Flags define behavior • Can control precise

    behavior… • …But just use 0 or SPAN_EXCLUSIVE_EXCLUSIVE
  29. Multiple Spans

  30. Built-In Spans N

  31. Span Types • Appearance-only spans • Metric-affecting spans • Paragraph-affecting

    spans
  32. Appearance-only Spans • StyleSpan • UnderlineSpan • ForegroundColorSpan • BackgroundColorSpan

    • URLSpan
  33. Metric-affecting Spans • SuperscriptSpan • RelativeSizeSpan • ImageSpan

  34. Paragraph-affecting Spans • LeadingMarginSpan • DrawableMarginSpan • BulletSpan • QuoteSpan

  35. Bad Spans • BulletSpan - Sucks until API 28! •

    QuoteSpan - Sucks until API 28! • URLSpan - Crashes when there’s no intent handler!
  36. Custom Spans

  37. Custom Span Process • Pick a Span to extend •

    Decipher how it works • Get frustrated by limited Span API
  38. Thematic Break Span

  39. Thematic Break Span ≈ ≈ ReplacementSpan

  40. Thematic Break Span ReplacementSpan

  41. Thematic Break Span ReplacementSpan

  42. Thematic Break Span ReplacementSpan

  43. Thematic Break Span ReplacementSpan

  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 } ... }
  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 } ... }
  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 } }
  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 } }
  48. Custom Span Limitations • Bound by API limitations • Cannot

    Parcelable custom spans • Cannot copy/paste your custom span • Choose wisely what you extend
  49. Example Custom Spans • BetterURLSpan : URLSpan • BulletSpanCompat :

    BulletSpan • QuoteSpanCompat : QuoteSpan • CodeBackgroundSpan : LineBackgroundSpan • ListItemSpan : BulletSpan • ThematicBreakSpan : ReplacementSpan
  50. Odds & Ends

  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)
  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) ==
  53. Spacing Newlines Newlines Newlines w/ RelativeSizeSpan

  54. Construction Optimization Class Mutable Text Mutable Markup SpannableString No Yes

    SpannableStringBuilder Yes Yes
  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)
  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/
  57. Replacement Spans • Cannot attach to empty space • Options

    • Invisible character • Fallback text if replacement span fails • Space character
  58. Testing Spans • Comparing two Spannables is difficult because… •

    Span API is weird • Spans do not implement equals() • Not all Span attributes are public
  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) }
  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) }
  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)) }
  62. Resources • Florina Muntenescu: https://medium.com/@florina.muntenescu • Optimizing spans • Styling

    internationalized text • …And more! • Google IO Talk: https://youtu.be/x-FcOX6ErdI
  63. Questions? @danlew42