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

Android Text: The good 😎, the bad 😳, the ugly 🥴...

Android Text: The good 😎, the bad 😳, the ugly 🥴 (droidcon NYC 2018)

Learn about internals of Android Text stack, starting with TextView and EditText. The talk focuses on how to effectively use various features of text stack in order to improve functionality and performance of your applications.

See related resources at https://github.com/siyamed/droidcon-nyc-18

Siyamed SINIR

August 28, 2018
Tweet

Other Decks in Programming

Transcript

  1. @siyamed Android Text The good , the bad , the

    ugly Siyamed Sinir Android Text @ Google
  2. Performance • No measurement, no improvement • Results differ with

    environment setup, input • Tradeoffs, performance vs ◦ Effort ◦ Design ◦ Maintainability/Extensibility ◦ CPU/Memory ◦ ….
  3. Turn off default hyphenation 2.5x text layout performance improvement Very

    cheap to implement Introduced on M 66% of devices* * https://developer.android.com/about/dashboards/
  4. How to turn off default hyphenation res/values/styles.xml <style name="AppTheme" parent="Theme.AppCompat.Light">

    <item name="android:textViewStyle">@style/MyTextViewStyle</item> </style>
  5. How to turn off default hyphenation res/values/styles.xml <style name="BaseTextViewStyle" parent="android:Widget.TextView"/>

    <style name="MyTextViewStyle" parent="BaseTextViewStyle"> <item name="android:hyphenationFrequency">none</item> </style>
  6. How to turn off default hyphenation res/values/styles.xml <style name="BaseTextViewStyle" parent="android:Widget.TextView"/>

    <style name="MyTextViewStyle" parent="BaseTextViewStyle"> <item name="android:hyphenationFrequency">none</item> </style> // res/values-21/styles.xml <style name="BaseTextViewStyle" parent="android:Widget.Material.TextView"/>
  7. Prefetch & Precomputed Text up to 10x text layout performance

    improvement Very cheap to implement androidx:1.0.0-rc01
  8. Prefetch RecyclerView & PrecomputedText override fun onBindViewHolder(viewHolder: ViewHolder, position: Int)

    { // viewHolder.txtContent.text = dataSet[index] viewHolder.txtContent.setTextFuture( PrecomputedTextCompat.getTextFuture( dataSet[index], viewHolder.txtContent.textMetricsParamsCompat, null/* executor */ ) ) }
  9. Prefetch RecyclerView & PrecomputedText override fun onBindViewHolder(viewHolder: ViewHolder, position: Int)

    { // viewHolder.txtContent.text = dataSet[index] viewHolder.txtContent.setTextFuture( // run when idle PrecomputedTextCompat.getTextFuture( // pre measure text dataSet[index], // with this CharSequence viewHolder.txtContent.textMetricsParamsCompat,// and display config null/* executor */ ) ) }
  10. Native Text measurement Hel + + + + H e

    l o Font H 65 e 48 l 6C l 6C o 6F Word Layout LRU Cache Native 5K items (since Android L)
  11. GoogleSans-Regular.ttf GoogleSans-Italic.ttf GoogleSans-Medium.ttf GoogleSans-MediumItalic.ttf GoogleSans-Bold.ttf GoogleSans-BoldItalic.ttf Roboto-Thin.ttf Roboto-ThinItalic.ttf Roboto-Light.ttf Roboto-LightItalic.ttf

    Roboto-Regular.ttf Roboto-Italic.ttf Roboto-Medium.ttf Roboto-MediumItalic.ttf Roboto-Black.ttf Roboto-BlackItalic.ttf Roboto-Bold.ttf Roboto-BoldItalic.ttf RobotoCondensed-Light.ttf RobotoCondensed-LightItalic.ttf RobotoCondensed-Regular.ttf RobotoCondensed-Italic.ttf RobotoCondensed-Medium.ttf RobotoCondensed-MediumItalic.ttf RobotoCondensed-Bold.ttf RobotoCondensed-BoldItalic.ttf NotoSerif-Regular.ttf NotoSerif-Bold.ttf NotoSerif-Italic.ttf NotoSerif-BoldItalic.ttf DroidSansMono.ttf CutiveMono.ttf ComingSoon.ttf DancingScript-Regular.ttf DancingScript-Bold.ttf CarroisGothicSC-Regular.ttf NotoNaskhArabic-Regular.ttf NotoNaskhArabic-Bold.ttf NotoNaskhArabicUI-Regular.ttf NotoNaskhArabicUI-Bold.ttf NotoSansEthiopic-Regular.ttf NotoSansEthiopic-Bold.ttf NotoSerifEthiopic-Regular.otf NotoSerifEthiopic-Bold.otf NotoSansHebrew-Regular.ttf NotoSansHebrew-Bold.ttf NotoSerifHebrew-Regular.ttf NotoSerifHebrew-Bold.ttf NotoSansThai-Regular.ttf NotoSansThai-Bold.ttf NotoSerifThai-Regular.ttf NotoSerifThai-Bold.ttf NotoSansThaiUI-Regular.ttf NotoSansThaiUI-Bold.ttf NotoSansArmenian-Regular.otf NotoSansArmenian-Medium.otf NotoSansArmenian-Bold.otf NotoSerifArmenian-Regular.otf NotoSerifArmenian-Bold.otf NotoSansGeorgian-Regular.otf NotoSansGeorgian-Medium.otf NotoSansGeorgian-Bold.otf NotoSerifGeorgian-Regular.otf NotoSerifGeorgian-Bold.otf NotoSansDevanagari-Regular.otf NotoSansDevanagari-Medium.otf NotoSansDevanagari-Bold.otf NotoSerifDevanagari-Regular.ttf NotoSerifDevanagari-Bold.ttf NotoSansDevanagariUI-Regular.otf NotoSansDevanagariUI-Medium.otf NotoSansDevanagariUI-Bold.otf NotoSansGujarati-Regular.ttf NotoSansGujarati-Bold.ttf NotoSerifGujarati-Regular.ttf NotoSerifGujarati-Bold.ttf NotoSansGujaratiUI-Regular.ttf NotoSansGujaratiUI-Bold.ttf NotoSansGurmukhi-Regular.ttf NotoSansGurmukhi-Bold.ttf NotoSerifGurmukhi-Regular.otf NotoSerifGurmukhi-Bold.otf NotoSansGurmukhiUI-Regular.ttf NotoSansGurmukhiUI-Bold.ttf NotoSansTamil-Regular.otf NotoSansTamil-Medium.otf NotoSansTamil-Bold.otf NotoSerifTamil-Regular.otf NotoSerifTamil-Bold.otf NotoSansTamilUI-Regular.otf NotoSansTamilUI-Medium.otf NotoSansTamilUI-Bold.otf NotoSansMalayalam-Regular.otf NotoSansMalayalam-Medium.otf NotoSansMalayalam-Bold.otf NotoSerifMalayalam-Regular.ttf NotoSerifMalayalam-Bold.ttf NotoSansMalayalamUI-Regular.otf NotoSansMalayalamUI-Medium.otf NotoSansMalayalamUI-Bold.otf NotoSansBengali-Regular.otf NotoSansBengali-Medium.otf NotoSansBengali-Bold.otf NotoSerifBengali-Regular.ttf NotoSerifBengali-Bold.ttf NotoSansBengaliUI-Regular.otf NotoSansBengaliUI-Medium.otf NotoSansBengaliUI-Bold.otf NotoSansTelugu-Regular.ttf NotoSansTelugu-Bold.ttf NotoSerifTelugu-Regular.ttf NotoSerifTelugu-Bold.ttf NotoSansTeluguUI-Regular.ttf NotoSansTeluguUI-Bold.ttf NotoSansKannada-Regular.ttf NotoSansKannada-Bold.ttf NotoSerifKannada-Regular.ttf NotoSerifKannada-Bold.ttf NotoSansKannadaUI-Regular.ttf NotoSansKannadaUI-Bold.ttf NotoSansOriya-Regular.ttf NotoSansOriya-Bold.ttf NotoSansOriyaUI-Regular.ttf NotoSansOriyaUI-Bold.ttf NotoSansSinhala-Regular.otf NotoSansSinhala-Medium.otf NotoSansSinhala-Bold.otf NotoSerifSinhala-Regular.otf NotoSerifSinhala-Bold.otf NotoSansSinhalaUI-Regular.otf NotoSansSinhalaUI-Medium.otf NotoSansSinhalaUI-Bold.otf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSansKhmer-VF.ttf NotoSerifKhmer-Regular.otf NotoSerifKhmer-Bold.otf NotoSansKhmerUI-Regular.ttf NotoSansKhmerUI-Bold.ttf NotoSansLao-Regular.ttf NotoSansLao-Bold.ttf NotoSerifLao-Regular.ttf NotoSerifLao-Bold.ttf NotoSansLaoUI-Regular.ttf NotoSansLaoUI-Bold.ttf NotoSansMyanmar-Regular.otf NotoSansMyanmar-Medium.otf NotoSansMyanmar-Bold.otf NotoSerifMyanmar-Regular.otf NotoSerifMyanmar-Bold.otf NotoSansMyanmarUI-Regular.otf NotoSansMyanmarUI-Medium.otf NotoSansMyanmarUI-Bold.otf NotoSansThaana-Regular.ttf NotoSansThaana-Bold.ttf NotoSansCham-Regular.ttf NotoSansCham-Bold.ttf NotoSansAhom-Regular.otf NotoSansAdlam-Regular.ttf NotoSansAvestan-Regular.ttf NotoSansBalinese-Regular.ttf NotoSansBamum-Regular.ttf NotoSansBatak-Regular.ttf NotoSansBrahmi-Regular.ttf NotoSansBuginese-Regular.ttf NotoSansBuhid-Regular.ttf NotoSansCanadianAboriginal-Regular.ttf NotoSansCarian-Regular.ttf NotoSansChakma-Regular.ttf NotoSansCherokee-Regular.ttf NotoSansCoptic-Regular.ttf NotoSansCuneiform-Regular.ttf NotoSansCypriot-Regular.ttf NotoSansDeseret-Regular.ttf NotoSansEgyptianHieroglyphs-Regular.ttf NotoSansElbasan-Regular.otf NotoSansGlagolitic-Regular.ttf NotoSansGothic-Regular.ttf NotoSansHanunoo-Regular.ttf NotoSansImperialAramaic-Regular.ttf NotoSansInscriptionalPahlavi-Regular.ttf NotoSansInscriptionalParthian-Regular.ttf NotoSansJavanese-Regular.ttf NotoSansKaithi-Regular.ttf NotoSansKayahLi-Regular.ttf NotoSansKharoshthi-Regular.ttf NotoSansLepcha-Regular.ttf NotoSansLimbu-Regular.ttf NotoSansLinearB-Regular.ttf NotoSansLisu-Regular.ttf NotoSansLycian-Regular.ttf NotoSansLydian-Regular.ttf NotoSansMandaic-Regular.ttf NotoSansMeeteiMayek-Regular.ttf NotoSansNewTaiLue-Regular.ttf NotoSansNKo-Regular.ttf NotoSansOgham-Regular.ttf NotoSansOlChiki-Regular.ttf NotoSansOldItalic-Regular.ttf NotoSansOldPersian-Regular.ttf NotoSansOldSouthArabian-Regular.ttf NotoSansOldTurkic-Regular.ttf NotoSansOsage-Regular.ttf NotoSansOsmanya-Regular.ttf NotoSansPhoenician-Regular.ttf NotoSansRejang-Regular.ttf NotoSansRunic-Regular.ttf NotoSansSamaritan-Regular.ttf NotoSansSaurashtra-Regular.ttf NotoSansShavian-Regular.ttf NotoSansSundanese-Regular.ttf NotoSansSylotiNagri-Regular.ttf NotoSansSyriacEstrangela-Regular.ttf NotoSansSyriacEastern-Regular.ttf NotoSansSyriacWestern-Regular.ttf NotoSansTagalog-Regular.ttf NotoSansTagbanwa-Regular.ttf NotoSansTaiTham-Regular.ttf NotoSansTaiViet-Regular.ttf NotoSansTibetan-Regular.ttf NotoSansTibetan-Bold.ttf NotoSansTifinagh-Regular.ttf NotoSansUgaritic-Regular.ttf NotoSansVai-Regular.ttf NotoSansSymbols-Regular-Subsetted.ttf NotoSansCJK-Regular.ttc NotoSerifCJK-Regular.ttc NotoSansCJK-Regular.ttc NotoSerifCJK-Regular.ttc NotoSansCJK-Regular.ttc NotoSerifCJK-Regular.ttc NotoSansCJK-Regular.ttc NotoSerifCJK-Regular.ttc NotoColorEmoji.ttf NotoSansSymbols-Regular-....ttf NotoSansTaiLe-Regular.ttf NotoSansYi-Regular.ttf NotoSansMongolian-Regular.ttf NotoSansPhagsPa-Regular.ttf NotoSansAnatolianHieroglyphs-....otf NotoSansBassaVah-Regular.otf NotoSansBhaiksuki-Regular.otf NotoSansHatran-Regular.otf NotoSansLinearA-Regular.otf NotoSansManichaean-Regular.otf NotoSansMarchen-Regular.otf NotoSansMeroitic-Regular.otf NotoSansMiao-Regular.otf NotoSansMro-Regular.otf NotoSansMultani-Regular.otf NotoSansNabataean-Regular.otf NotoSansNewa-Regular.otf NotoSansOldNorthArabian-Regular.otf NotoSansOldPermic-Regular.otf NotoSansPahawhHmong-Regular.otf NotoSansPalmyrene-Regular.otf NotoSansPauCinHau-Regular.otf NotoSansSharada-Regular.otf NotoSansSoraSompeng-Regular.otf
  12. (Text)Layout is a blueprint for text This is an example

    text Native measurement / line breaking This is an example text top: x1 descent: y1 top: x2 descent: y2 top padding bottom padding start index LTR 2 lines end index
  13. PrecomputedText Hel + + + + = PrecomputedText.create(“Hello”, params) text

    Native reference word layout result tradeoff: uses memory (20K for 500 chars)
  14. Utility functions you don’t necessarily need TextView to create PrecomputedText

    TextView.getTextMetricsParams AppCompatTextView.getTextMetricsParamsCompat
  15. measure text measure TextView onMeasure() draw text draw TextView onDraw()

    Word Layout LRU Cache So What? draw needs word layout objects
  16. Inflation cost ...creating the necessary TextViews (around 40 for more)

    is responsible for a significant part of the overall view inflation cost (about 30 ms on a Pixel) “ ” An Android Developer
  17. <CardView ...> <TextView android:id="@+id/username_title" .../> Inflation val spannable = SpannableString(username_and_title);

    spannable.setSpan( TextAppearanceSpan(cxt, R.style.Username)... spannable.setSpan( TextAppearanceSpan(cxt, R.style.Title)...
  18. <CardView ...> <TextView android:id="@+id/username_title" .../> Inflation val spannable = SpannableString(username_and_title);

    spannable.setSpan( TextAppearanceSpan(cxt, R.style.Username)... spannable.setSpan( TextAppearanceSpan(cxt, R.style.Title)... tradeoff
  19. CharSequence type TextView guards against external changes if (type ==

    BufferType.EDITABLE ...) { Editable t = mEditableFactory.newEditable(text); .... } else if (precomputed != null) { ... } else if (type == BufferType.SPANNABLE ...) { text = mSpannableFactory.newSpannable(text); } else if (!(text instanceof CharWrapper)) { text = TextUtils.stringOrSpannedString(text); } if mutable then copy copy copy copy
  20. class TransformationMethod ex: All Caps AllCapsTransformationMethod String( “username” ) String(

    “USERNAME”) mText =”username” mTransformed = “USERNAME” <TextView android:text="username" android:textAllCaps="true" .../>
  21. Checking for spans / Iterating fun hasUrlSpan(spanned: Spanned): Boolean {

    val spans: Array<out T> = spanned.getSpans(0, spanned.length, URLSpan.class) return spans.isNotEmpty() }
  22. nextSpanTransition good for iterating, and checking for existence fun hasUrlSpan(spanned:

    Spanned): Boolean { val limit = spanned.length return spanned.nextSpanTransition(0, limit, URLSpan.class) < limit }
  23. override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { val mySpannable =

    SpannableStringBuilder(string); mySpannable.setSpan(...) // ... viewHolder.textView.text = mySpannable } Spannables
  24. configure SpannableFactory class MySpannableFactory : Spannable.Factory() { override fun newSpannable(source:

    CharSequence): Spannable { return source as? Spannable ?: super.newSpannable(source) } } textView.spannableFactory = spannableFactory
  25. Don’t use autoLink in RecyclerView expensive operation runs on UI

    Thread <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:autoLink="web"/>
  26. Instead extract links when preparing the UI data, on a

    background thread val spannable = SpannableString(string) LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS) override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.textView.setText(spannable, BufferType.SPANNABLE) // ... }
  27. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris velit

    sapien, ultrices nec pellentesque eget, viverra nec felis. Proin ante est, malesuada sit amet est a, posuere molestie ipsum. Etiam efficitur orci non elit laoreet pharetra. Mauris tristique maximus pharetra. Etiam faucibus, odio non dictum euismod, eros sapien hendrerit eros, vitae molestie lectus eros nec tortor. Integer luctus in erat eget pulvinar. DynamicLayout
  28. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris velit

    sapien, ultrices nec pellentesque eget, viverra nec felis. Proin ante est, malesuada sit amet est a, posuere molestie ipsum. Etiam efficitur orci non elit laoreet pharetra. Mauris tristique maximus pharetra. Etiam faucibus, odio non dictum euismod, eros sapien hendrerit eros, vitae molestie lectus eros nec tortor. Integer luctus in erat eget pulvinar. DynamicLayout
  29. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris velit

    sapien, ultrices nec pellentesque eget, viverra nec felis. Proin ante est, malesuada sit amet est a, posuere molestie ipsum. Etiam efficitur orci non elit laoreet pharetra. Mauris tristique maximus pharetra. Etiam faucibus, odio non dictum euismod, eros sapien hendrerit eros, vitae molestie lectus eros nec tortor. Integer luctus in erat eget pulvinar. DynamicLayout
  30. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris velit

    sapien, ultrices nec pellentesque eget, viverra nec felis. Proin ante est, malesuada sit amet est a, posuere molestie ipsum. Etiam efficitur orci non elit laoreet pharetra. Mauris tristique maximus pharetra. Etiam faucibus, odio non dictum euismod, eros sapien hendrerit eros, vitae molestie lectus eros nec tortor. Integer luctus in erat eget pulvinar. DynamicLayout StaticLayout StaticLayout
  31. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris velit

    sapien, ultrices nec pellentesque eget, viverra nec felis. Proin ante est, malesuada sit amet est a, posuere molestie ipsum. Etiam efficitur orci non elit laoreet pharetra. Mauris tristique maximus pharetra. Etiam faucibus, odio non dictum euismod, eros sapien hendrerit eros, vitae molestie lectus eros nec tortor. Integer luctus in erat eget pulvinar. DynamicLayout
  32. Editable Text events EditText DynamicLayout SpannableStringBuilder StaticLayout InputFilter TextWatcher KeyListener

    change events before change (control over change) hardware keyboard events (mostly) SpanWatcher changes to spans
  33. glide typing auto suggest hand writing voice translate Input Method,

    not keyboard :) different world than hardware keyboard
  34. InputFilter “replace” CharSequence filter ( CharSequence source, int sourceStart, int

    sourceEnd, Spanned dest, int destStart, int destEnd) source destination sourceStart sourceEnd destStart destEnd (result)
  35. InputFilter length limit editText.filters = arrayOf(InputFilter.LengthFilter(5)) 12345 Label Label u+1F469

    u+1F3FD u+200D u+1F3A4 u+1F468 u+200D u+1F469 u+200D u+1F467 u+200D u+1F466 4 7 ZWJ ZWJ ZWJ ZWJ
  36. InputFilter BreakIterator fun countCharacterBreaks(string: String): Int { val breakIterator =

    BreakIterator.getCharacterInstance() breakIterator.setText(string) var count = 0 while (breakIterator.next() != BreakIterator.DONE) count++ return count }
  37. InputFilter handle emoji editText.filters = arrayOf(InputFilter { source, sStart, sEnd,

    dest, dStart, dEnd -> val destCount = countCharacterBreaks(dest.toString()) val removedCount = countCharacterBreaks(dest.substring(dStart, dEnd)) val addedCount = countCharacterBreaks(source.substring(sStart, sEnd)) })
  38. InputFilter handle emoji editText.filters = arrayOf(InputFilter { source, sStart, sEnd,

    dest, dStart, dEnd -> val destCount = countCharacterBreaks(dest.toString()) val removedCount = countCharacterBreaks(dest.substring(dStart, dEnd)) val addedCount = countCharacterBreaks(source.substring(sStart, sEnd)) var remaining = 5 - (destCount - removedCount) if (remaining <= 0) { return@InputFilter "" //reject } else if (remaining >= addedCount) { return@InputFilter null //apply } })
  39. InputFilter handle emoji editText.filters = arrayOf(InputFilter { source, sStart, sEnd,

    dest, dStart, dEnd -> val destCount = countCharacterBreaks(dest.toString()) val removedCount = countCharacterBreaks(dest.substring(dStart, dEnd)) val addedCount = countCharacterBreaks(source.substring(sStart, sEnd)) var remaining = 5 - (destCount - removedCount) if (remaining <= 0) { return@InputFilter "" //reject } else if (remaining >= addedCount) { return@InputFilter null //apply } else { return@InputFilter trimSource(source, sStart, sEnd, remaining) } })
  40. InputFilter trimSource fun trimSource(source: CharSequence, start: Int, end: Int, max:

    Int): CharSequence { val breakIterator = BreakIterator.getCharacterInstance() breakIterator.setText(source.subSequence(start, end).toString()) var endIndex = start; var remaining = max while(remaining > 0 && breakIterator.next() != BreakIterator.DONE) { remaining-- endIndex = breakIterator.current() } return source.subSequence(start, endIndex) }
  41. final class EmojiInputConnection extends InputConnectionWrapper { //... @Override public boolean

    deleteSurroundingText(final int beforeLength, final int afterLength) { final boolean result = EmojiCompat.handleDeleteSurroundingText(this, getEditable(), beforeLength, afterLength, false /*inCodePoints*/); return result || super.deleteSurroundingText(beforeLength, afterLength); } @Override public boolean deleteSurroundingTextInCodePoints(final int beforeLength, final int afterLength) { final boolean result = EmojiCompat.handleDeleteSurroundingText(this, getEditable(), beforeLength, afterLength, true /*inCodePoints*/); return result || super.deleteSurroundingTextInCodePoints(beforeLength, afterLength); } //... }