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

Crazy Fancy Android Text

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Daniel Lew Daniel Lew
February 02, 2019

Crazy Fancy Android Text

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

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

Avatar for Daniel Lew

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