Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Some Examples 2 Wednesday, December 28, 11

Slide 3

Slide 3 text

http:/ /www.roguso.com 3 Wednesday, December 28, 11

Slide 4

Slide 4 text

Fancy Markup 4 Wednesday, December 28, 11

Slide 5

Slide 5 text

Font size/color Typeface Style Images Qualifier Clickable links 5 Wednesday, December 28, 11

Slide 6

Slide 6 text

Direct User Interaction 6 Wednesday, December 28, 11

Slide 7

Slide 7 text

Emoticons that finally work! Animated gif that’s supposed to animate 7 Wednesday, December 28, 11

Slide 8

Slide 8 text

URL shortening Emoticons Image upload progress updates 8 Wednesday, December 28, 11

Slide 9

Slide 9 text

Behind the scenes 9 Wednesday, December 28, 11

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

int getSpanStart(Object) int getSpanEnd(Object) T[] getSpans(int start, int end, Class) 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

Slide 12

Slide 12 text

TextView.setText(CharSequence, BufferType.SPANNABLE) EditText.setText(CharSequence, BufferType.EDITABLE) 12 Wednesday, December 28, 11

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Linkify textView.setText(someContent) Linkify.addLinks(textView, Linkify.ALL) Linkify.ALL Linkify.EMAIL_ADDRESSES Linkify.MAP_ADDRESSES Linkify.PHONE_NUMBERS Linkify.WEB_URLS 14 Wednesday, December 28, 11

Slide 15

Slide 15 text

Linkify 15 Wednesday, December 28, 11

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Spanned Html.fromHtml(String) Spanned Html.fromHtml(String, ImageGetter, TagHandler) 17 Wednesday, December 28, 11

Slide 18

Slide 18 text

ImageGetter Drawable getDrawable(String src) 18 Wednesday, December 28, 11

Slide 19

Slide 19 text

TagHandler void handleTag( boolean opening, String tag, Editable output, XMLReader xmlReader) 19 Wednesday, December 28, 11

Slide 20

Slide 20 text

Html.fromHtml()

Slide 21

Slide 21 text

Html.fromHtml() 21 Wednesday, December 28, 11

Slide 22

Slide 22 text

Problems with Html.fromHtml() Native implementation of 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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Layout getLine*() / / Width, Start, End, Top, Ascent, Bounds, etc. getOffset*() getParagraph*() getText() getWidth(), getHeight() getPaint() 30 Wednesday, December 28, 11

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Many more Span interfaces and classes ParcelableSpan ParagraphStyle AlignmentSpan LeadingMarginSpan LineBackgroundSpan TabStopSpan WrapTogetherSpan LineHeightSpan DrawableMarginSpan IconMarginSpan QuoteSpan BulletSpan EasyEditSpan 37 Wednesday, December 28, 11

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Problem with Layout Layout.getSelectionPath(int start, int end, Path dest) uses a naive way of calculating the path. 41 Wednesday, December 28, 11

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

TextWatcher application Use it to update the word count or the remaining character count 46 Wednesday, December 28, 11

Slide 47

Slide 47 text

MultiAutoCompleteTextView.Tokenizer 47 Wednesday, December 28, 11

Slide 48

Slide 48 text

MultiAutoCompleteTextView.Tokenizer int findTokenStart(CharSequence, int cursor) int findTokenEnd(CharSequence, int cursor) terminateToken(CharSequence) 48 Wednesday, December 28, 11

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Thank you! [email protected] @wuman 53 Wednesday, December 28, 11