Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Crazy Fancy Android Text

Daniel Lew
February 02, 2019

Crazy Fancy Android Text

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

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

Daniel Lew

February 02, 2019
Tweet

More Decks by Daniel Lew

Other Decks in Programming

Transcript

  1. 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
  2. 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" />
  3. Definitions Span - markup objects that can style text Spannable

    - text with mutable spans Spanned - text with spans
  4. 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> ==
  5. 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>
  6. 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 )
  7. 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 )
  8. 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 )
  9. HTML Drawbacks • Limited tag support • Strings support different

    tags than Html.fromHtml() • Details: https://blog.danlew.net/2011/04/13/html_in_text_views/
  10. 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)
  11. 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)
  12. 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)
  13. Hard Mode: Build Your Own Class Mutable Text Mutable Markup

    SpannedString No No SpannableString No Yes SpannableStringBuilder Yes Yes
  14. 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. 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. Span Flags • Flags define behavior • Can control precise

    behavior… • …But just use 0 or SPAN_EXCLUSIVE_EXCLUSIVE
  17. Bad Spans • BulletSpan - Sucks until API 28! •

    QuoteSpan - Sucks until API 28! • URLSpan - Crashes when there’s no intent handler!
  18. Custom Span Process • Pick a Span to extend •

    Decipher how it works • Get frustrated by limited Span API
  19. 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 } ... }
  20. 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 } ... }
  21. 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 } }
  22. 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 } }
  23. Custom Span Limitations • Bound by API limitations • Cannot

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

    BulletSpan • QuoteSpanCompat : QuoteSpan • CodeBackgroundSpan : LineBackgroundSpan • ListItemSpan : BulletSpan • ThematicBreakSpan : ReplacementSpan
  25. 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)
  26. 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) ==
  27. 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)
  28. 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/
  29. Replacement Spans • Cannot attach to empty space • Options

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

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

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