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

Fancy Rich Text Editing on Android - Invited Open Source Software Foundation Talk

David Wu
December 28, 2011

Fancy Rich Text Editing on Android - Invited Open Source Software Foundation Talk

Android is an open platform that aims to allow for innovation at every level of its software stack. To serve this purpose it has provided a rich and powerful suite of framework APIs to make app development easy for the developer. In particular, the View system in Android offers a collection of commonly used widgets and layouts, with which app developers are able to build rich GUI applications. However, customizing widgets and layouts to tailor apps to user needs can often be neglected or difficult.

This talk aims to discuss the importance of customizing for user experience. We will provide tips and tricks on how to use customized Spans to do fancy rich text editing in Android.

David Wu

December 28, 2011
Tweet

More Decks by David Wu

Other Decks in Programming

Transcript

  1. Fancy Rich Text Editing on Android David Wu @wuman Open

    Source Apps on Mobile - Ҏ։ݯೈᱪଧ଄ߦಈੜ׆ Open Source Software Foundation December 28, 2011 1 Wednesday, December 28, 11
  2. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 10 Wednesday, December 28, 11
  3. int getSpanStart(Object) int getSpanEnd(Object) T[] getSpans(int start, int end, Class<T>)

    void setSpan(Object, int, int, int flags) void removeSpan(Object) CharSequence Interface Implementation Spanned SpannedString Spannable SpannableString Editable SpannableStringBuilder Editable insert(int where, CharSequence) Editable replace(int, int, CharSequence) Editable delete(int, int) void clear() void clearSpans() immutable markup, immutable text mutable markup, immutable text immutable markup, immutable text 11 Wednesday, December 28, 11
  4. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 13 Wednesday, December 28, 11
  5. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 16 Wednesday, December 28, 11
  6. Html.fromHtml() <br /> <p /> <div /> <strong />, <b

    /> <em />, <cite />, <dfn />, <i /> <big /> <small /> <font color=”” face=””/> <blockquote /> <tt /> / / monospace <a href=””/> <u /> <sup /> <sub /> <h1 /> .. <h6 /> <img /> 20 Wednesday, December 28, 11
  7. Problems with Html.fromHtml() Native implementation of <img /> handles only

    images with dimensions known ahead of time. text.append(“\uFFFC”); text.setSpan(new ImageSpan(drawable, src), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Unable to intercept or override handling of common tags Small objects created and recycled frequently may impact scrolling performance 22 Wednesday, December 28, 11
  8. Html.fromHtml() ImageGetter should return a Drawable which, upon remote image

    retrieval, triggers the containing TextView to relayout. Make a copy of the Html class and TagSoup library to include in your own application. Object pool (check out my other presentation on performance and memory improvements) 23 Wednesday, December 28, 11
  9. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 24 Wednesday, December 28, 11
  10. Problem with Clicks Clicking on the trailing white space of

    any line ending with a ClickableSpan will still invoke its ClickableSpan.onClick(). 25 Wednesday, December 28, 11
  11. MovementMethod TextView uses the MovementMethod helper class to help it

    handle user events on its content. TextView.setMovementMethod() Without a MovementMethod, TextView will not draw highlighted link selections nor handle user events. 26 Wednesday, December 28, 11
  12. MovementMethod BaseMovementMethod ArrowKeyMovementMethod ScrollingMovementMethod LinkMovementMethod Interface Implementation onKeyDown() onKeyUp() onKeyOther()

    onTakeFocus() onTouchEvent() onTrackballEvent() up() down() left() right() home() bottom() leftWord() rightWord() lineStart() lineEnd() pageUp() pageDown() cursor movement and selection scroll text buffer traverses links in text buffer and scrolls if necessary 27 Wednesday, December 28, 11
  13. Problem with LinkMovementMethod public boolean onTouchEvent(...) { ... Layout layout

    = widget.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); .... if ( action == MotionEvent.ACTION_UP ) { link[0].onClick(widget); } ... } 28 Wednesday, December 28, 11
  14. Layout TextView uses the Layout class to manage text layout.

    Layout is a base abstract class. TextView uses StaticLayout normally for immutable text. TextView uses DynamicLayout for mutable text such as Spannable and its derivatives. 29 Wednesday, December 28, 11
  15. Layout getLine*() / / Width, Start, End, Top, Ascent, Bounds,

    etc. getOffset*() getParagraph*() getText() getWidth(), getHeight() getPaint() 30 Wednesday, December 28, 11
  16. Fix for LinkMovementMethod public boolean onTouchEvent(...) { ... Layout layout

    = widget.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); int maxLineRight = layout.getLineWidth(line) if ( x <= maxLineRight + slop ) { ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); } .... if ( action == MotionEvent.ACTION_UP ) { link[0].onClick(widget); } 31 Wednesday, December 28, 11
  17. Another problem with LinkMovementMethod LinkMovementMethod also handles highlighted link selection

    when the link is being clicked. There is a bug that makes the selection stay there even after the click event is consumed. Trial and error will enable you to fix this code. 32 Wednesday, December 28, 11
  18. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 33 Wednesday, December 28, 11
  19. Spans We have already seen Spans are objects that change

    the style or behavior of a segment of the text buffer. 34 Wednesday, December 28, 11
  20. CharacterStyle MetricAffectingSpan ClickableSpan UnderlineSpan StrikethroughSpan BackgroundColorSpan ForegroundColorSpan MaskFilterSpan RasterizerSpan URLSpan

    TextAppearanceSpan TypefaceSpan StyleSpan AbsoluteSizeSpan RelativeSizeSpan SuperscriptSpan SubscriptSpan ReplacementSpan DynamicDrawableSpan ImageSpan UpdateAppearance 35 Wednesday, December 28, 11
  21. CharacterStyle MetricAffectingSpan ClickableSpan UnderlineSpan StrikethroughSpan BackgroundColorSpan ForegroundColorSpan MaskFilterSpan RasterizerSpan URLSpan

    TextAppearanceSpan TypefaceSpan StyleSpan AbsoluteSizeSpan RelativeSizeSpan SuperscriptSpan SubscriptSpan ReplacementSpan DynamicDrawableSpan ImageSpan UpdateLayout 36 Wednesday, December 28, 11
  22. Many more Span interfaces and classes ParcelableSpan ParagraphStyle AlignmentSpan LeadingMarginSpan

    LineBackgroundSpan TabStopSpan WrapTogetherSpan LineHeightSpan DrawableMarginSpan IconMarginSpan QuoteSpan BulletSpan EasyEditSpan 37 Wednesday, December 28, 11
  23. TextWatcher and SpanWatcher void afterTextChanged(Editable) void beforeTextChanged(CharSequence, int, int, int)

    void onTextChanged(CharSequence, int, int, int) void onSpanAdded(Spannable, Object, int, int) void onSpanChanged(Spannable, Object, int, int, int, int) void onSpanRemoved(Spannable, Object, int, int) 38 Wednesday, December 28, 11
  24. TextWatcher and SpanWatcher Used in two different places Detected and

    invoked by Spannable or Editable TextView also allows external objects to add/remove TextWatchers as listeners: void TextView.addTextChangedListener(TextWatcher) This is accomplished by TextView itself setting a ChangeWatcher into the current Spannable upon setText(). 39 Wednesday, December 28, 11
  25. TextWatcher and SpanWatcher In short, TextView itself listens for changes

    in both text and markup via its own ChangeWatcher object. It then redirects text changes to external listeners. It does the following things (if applicable) upon a span change: Invalidate the cursor Notifies a selection change Invalidate() and checkForResize() Inform the IMS and the current extract editor Use a bookkeeping flag to remind itself later at onDraw() a new highlighted selection path should be used. 40 Wednesday, December 28, 11
  26. Problem with Layout Layout.getSelectionPath(int start, int end, Path dest) uses

    a naive way of calculating the path. 41 Wednesday, December 28, 11
  27. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 42 Wednesday, December 28, 11
  28. Inline Emoticons Implement them as a subclass of DynamicDrawableSpan holding

    an AnimationDrawable Drawbacks/limitations: Need to separate layers of an animated GIF yourself Only works for a pre-defined set of animated GIFs 43 Wednesday, December 28, 11
  29. Inline Emoticons AnimationDrawables come with start() and stop() methods. They

    need to be stopped or else you will have a unstoppable Looper event. What’s worse is that it’s usually also going to be a memory leak because the Drawable holds a reference to a Context. You do that with a SpanWatcher. This way you can stop the animation onSpanRemoved(). You might also want to stop the AnimationDrawable when the associated TextView is detached from the window. You can find out about this via the View.onDetachedFromWindow() callback. 44 Wednesday, December 28, 11
  30. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 45 Wednesday, December 28, 11
  31. TextWatcher application Use it to update the word count or

    the remaining character count 46 Wednesday, December 28, 11
  32. MultiAutoCompleteTextView.Tokenizer @Override public int findTokenStart(CharSequence text, int cursor) { int

    start = cursor; while (start > 0 && text.charAt(start - 1) != ' ') { start--; } while (start < cursor && text.charAt(start) == ' ') { start++; } if (start < cursor && text.charAt(start) == '@') { start++; } else { start = cursor; } return start; } 49 Wednesday, December 28, 11
  33. MultiAutoCompleteTextView.Tokenizer @Override public CharSequence terminateToken(CharSequence text) { if (text instanceof

    Spanned) { SpannableString spanText = new SpannableString(text); TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spanText, 0); setAutoComplete(false); return spanText; } else { setAutoComplete(false); return text; } } 50 Wednesday, December 28, 11
  34. MultiAutoCompleteTextView.Tokenizer private void setAutoComplete(boolean enabled) { int inputType = mEditbox.getInputType();

    if (enabled) { inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; } else { inputType &= (~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); } mEditbox.setRawInputType(inputType); } 51 Wednesday, December 28, 11
  35. Agenda Some examples Background knowledge about Spanned Linkify Html.fromHtml() MovementMethod

    Dissecting the TextView and styled Spans Emoticons Bonus: TextWatcher and Tokenizer applications 52 Wednesday, December 28, 11