Share
Embed
Crazy Fancy Android Text
Talk given about styling text on Android @ DevFestMN, February 2019.
Video: https://youtu.be/4cweBUdOvww
Daniel Lew
February 02, 2019
Transcript
63 slides
-
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 -
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" /> -
Definitions Span - markup objects that can style text Spannable
- text with mutable spans Spanned - text with spans -
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> == -
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> -
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 ) -
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 ) -
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 ) -
HTML Drawbacks • Limited tag support • Strings support different
tags than Html.fromHtml() • Details: https://blog.danlew.net/2011/04/13/html_in_text_views/ -
Hard Mode: Build Your Own • Code only • Verbose
• Flexible & powerful -
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) -
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) -
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) -
Hard Mode: Build Your Own Class Mutable Text Mutable Markup
SpannedString No No SpannableString No Yes SpannableStringBuilder Yes Yes -
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) -
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) -
Span Flags • Flags define behavior • Can control precise
behavior… • …But just use 0 or SPAN_EXCLUSIVE_EXCLUSIVE -
Bad Spans • BulletSpan - Sucks until API 28! •
QuoteSpan - Sucks until API 28! • URLSpan - Crashes when there’s no intent handler! -
Custom Span Process • Pick a Span to extend •
Decipher how it works • Get frustrated by limited Span API -
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 } ... } -
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 } ... } -
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 } } -
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 } } -
Custom Span Limitations • Bound by API limitations • Cannot
Parcelable custom spans • Cannot copy/paste your custom span • Choose wisely what you extend -
Example Custom Spans • BetterURLSpan : URLSpan • BulletSpanCompat :
BulletSpan • QuoteSpanCompat : QuoteSpan • CodeBackgroundSpan : LineBackgroundSpan • ListItemSpan : BulletSpan • ThematicBreakSpan : ReplacementSpan -
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) -
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) == -
Construction Optimization Class Mutable Text Mutable Markup SpannableString No Yes
SpannableStringBuilder Yes Yes -
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) -
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/ -
Replacement Spans • Cannot attach to empty space • Options
• Invisible character • Fallback text if replacement span fails • Space character -
Testing Spans • Comparing two Spannables is difficult because… •
Span API is weird • Spans do not implement equals() • Not all Span attributes are public -
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) } -
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) } -
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)) } -
Resources • Florina Muntenescu: https://medium.com/@florina.muntenescu • Optimizing spans • Styling
internationalized text • …And more! • Google IO Talk: https://youtu.be/x-FcOX6ErdI