private void processImage(boolean opening, Editable output) { Object[] spans = output.getSpans(0, output.length(), ImageSpan.class); if (spans.length == 0) { return; } ImageSpan imageSpan = (ImageSpan) spans[0]; String imageUrl = imageSpan.getSource(); // Handle image video thumbnail clicked if (imageUrl.startsWith(BBCodeParser.VIDEO_URL_PREFIX)) { VideoClickableSpan clickableSpan = mVideoClickableSpanProvider.get(); clickableSpan.setVideoUrl(imageUrl.substring(BBCodeParser.VIDEO_URL_PREFIX.length())); output.setSpan( clickableSpan, output.getSpanStart(imageSpan), output.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return; } // Not Image Video Thumbnail, This is normal image if (TextUtils.isEmpty(imageUrl) || !RichTextUtils.isUrl(imageUrl)) { return; } ImageClickableSpan clickableSpan = mImageClickableSpanProvider.get(); clickableSpan.setImageUrl(imageUrl); output.setSpan( clickableSpan, output.getSpanStart(imageSpan), output.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); }
@Override public void onTextChanged(CharSequence s, int start, int before, int count) { Editable text = getText(); if (text == null) return; clearSelections(); updateHint(); TokenImageSpan[] spans = text.getSpans(start - before, start - before + count, TokenImageSpan.class); for (TokenImageSpan token : spans) { int position = start + count; if (text.getSpanStart(token) < position && position <= text.getSpanEnd(token)) { // We may have to manually reverse the auto-complete and remove the extra ,'s int spanStart = text.getSpanStart(token); int spanEnd = text.getSpanEnd(token); removeToken(token, text); // The end of the span is the character index after it spanEnd--; if (spanEnd >= 0 && text.charAt(spanEnd) == ',') { text.delete(spanEnd, spanEnd + 1); } if (spanStart >= 0 && text.charAt(spanStart) == ',') { text.delete(spanStart, spanStart + 1); } } } }
private void updateHint() { Editable text = getText(); CharSequence hintText = getHint(); if (text == null || hintText == null) { return; } // Show hint if we need to if (prefix.length() > 0) { HintSpan[] hints = text.getSpans(0, text.length(), HintSpan.class); HintSpan hint = null; int testLength = prefix.length(); if (hints.length > 0) { hint = hints[0]; testLength += text.getSpanEnd(hint) - text.getSpanStart(hint); } if (text.length() == testLength) { hintVisible = true; if (hint != null) { return; // hint already visible } // We need to display the hint manually Typeface tf = getTypeface(); int style = Typeface.NORMAL; if (tf != null) { style = tf.getStyle(); } ColorStateList colors = getHintTextColors(); HintSpan hintSpan = new HintSpan(null, style, (int) getTextSize(), colors, colors); text.insert(prefix.length(), hintText); text.setSpan( hintSpan, prefix.length(), prefix.length() + getHint().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); setSelection(prefix.length()); } else { if (hint == null) { return; // hint already removed } // Remove the hint. There should only ever be one int sStart = text.getSpanStart(hint); int sEnd = text.getSpanEnd(hint); text.removeSpan(hint); text.replace(sStart, sEnd, ""); hintVisible = false; } } }
private static void endSpan(Class<?> type, Editable output) { int length = output.length(); Object span = getLast(output, type); int start = output.getSpanStart(span); output.removeSpan(span); if (start != length) output.setSpan(span, start, length, SPAN_EXCLUSIVE_EXCLUSIVE); }
public void handleTag( final boolean opening, final String tag, final Editable output, final XMLReader xmlReader) { if (TAG_DEL.equalsIgnoreCase(tag)) { if (opening) startSpan(new StrikethroughSpan(), output); else endSpan(StrikethroughSpan.class, output); return; } if (TAG_UL.equalsIgnoreCase(tag) || TAG_OL.equalsIgnoreCase(tag)) { if (opening) { listElements.add(new ListSeparator(TAG_OL.equalsIgnoreCase(tag))); } else if (!listElements.isEmpty()) { listElements.removeLast(); } if (!opening && listElements.isEmpty()) output.append('\n'); return; } if (TAG_LI.equalsIgnoreCase(tag) && opening && !listElements.isEmpty()) { listElements.getLast().append(output, listElements.size()); return; } if (TAG_CODE.equalsIgnoreCase(tag)) { if (opening) startSpan(new TypefaceSpan("monospace"), output); else endSpan(TypefaceSpan.class, output); } if (TAG_PRE.equalsIgnoreCase(tag)) { output.append('\n'); if (opening) startSpan(new TypefaceSpan("monospace"), output); else endSpan(TypefaceSpan.class, output); } if ((TAG_ROOT.equalsIgnoreCase(tag) || TAG_HTML.equalsIgnoreCase(tag)) && !opening) { // Remove leading newlines while (output.length() > 0 && output.charAt(0) == '\n') output.delete(0, 1); // Remove trailing newlines int last = output.length() - 1; while (last >= 0 && output.charAt(last) == '\n') { output.delete(last, last + 1); last = output.length() - 1; } QuoteSpan[] quoteSpans = output.getSpans(0, output.length(), QuoteSpan.class); for (QuoteSpan span : quoteSpans) { int start = output.getSpanStart(span); int end = output.getSpanEnd(span); output.removeSpan(span); output.setSpan(new ReplySpan(), start, end, SPAN_EXCLUSIVE_EXCLUSIVE); } } }
@Override public void afterTextChanged(Editable s) { TokenImageSpan[] spans = s.getSpans(0, s.length(), TokenImageSpan.class); for (TokenImageSpan token : currentTokens) { if (!Arrays.asList(spans).contains(token)) { spanWatcher.onSpanRemoved(s, token, s.getSpanStart(token), s.getSpanEnd(token)); } } }
private void removeSpan(TokenImageSpan span) { Editable text = getText(); if (text == null) return; // If the spanwatcher has been removed, we need to also manually trigger onSpanRemoved TokenSpanWatcher[] spans = text.getSpans(0, text.length(), TokenSpanWatcher.class); if (spans.length == 0) { spanWatcher.onSpanRemoved(text, span, text.getSpanStart(span), text.getSpanEnd(span)); } else if (Build.VERSION.SDK_INT < 14) { // HACK: Need to manually trigger on Span removed if there is only 1 object // not sure if there's a cleaner way if (objects.size() == 1) { spanWatcher.onSpanRemoved(text, span, text.getSpanStart(span), text.getSpanEnd(span)); } } // Add 1 to the end because we put a " " at the end of the spans when adding them text.delete(text.getSpanStart(span), text.getSpanEnd(span) + 1); }
private void renewColor() { if (DBG) { Log.d(LOG_TAG, "--- renewColor:"); } if (mView instanceof View) { ImageSpan parent = getParentSpan(); Editable text = ((EditStyledText) mView).getText(); int start = text.getSpanStart(parent); ForegroundColorSpan[] spans = text.getSpans(start, start, ForegroundColorSpan.class); if (spans.length > 0) { renewColor(spans[spans.length - 1].getForegroundColor()); } } }
/** * Adds {@link StrikethroughSpan} to {@literal <strike>} tag. * * <p>See android.text.HtmlToSpannedConverter#handleStartTag(java.lang.String, * org.xml.sax.Attributes) See android.text.HtmlToSpannedConverter#handleEndTag(java.lang.String) */ private void handleStrike(boolean opening, Editable output) { int len = output.length(); if (opening) { output.setSpan(new Strike(), len, len, Spannable.SPAN_MARK_MARK); } else { Strike strike = getLastSpan(output, Strike.class); int where = output.getSpanStart(strike); output.removeSpan(strike); if (where != len) { output.setSpan(new StrikethroughSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } }
public void onClearStyles() { if (DBG) { Log.d(LOG_TAG, "--- onClearStyles"); } Editable txt = mEST.getText(); int len = txt.length(); Object[] styles = txt.getSpans(0, len, Object.class); for (Object style : styles) { if (style instanceof ParagraphStyle || style instanceof QuoteSpan || style instanceof CharacterStyle) { if (style instanceof ImageSpan) { int start = txt.getSpanStart(style); int end = txt.getSpanEnd(style); txt.replace(start, end, ""); } txt.removeSpan(style); } } mEST.setBackgroundDrawable(mEST.mDefaultBackground); mEST.mBackgroundColor = DEFAULT_BACKGROUND_COLOR; }
private void process(boolean opening, Editable output, CharacterStyle... spanStyles) { int len = output.length(); if (opening) { for (CharacterStyle style : spanStyles) { output.setSpan(style, len, len, Spannable.SPAN_MARK_MARK); } } else { for (CharacterStyle style : spanStyles) { Object obj = getLast(output, style.getClass()); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { if (style instanceof VideoClickableSpan) { // Need to set the video url to this span. String url = output.subSequence(where, len).toString(); ((VideoClickableSpan) style).setVideoUrl(url); } output.setSpan(style, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } }
@Override protected void onSelectionChanged(int selStart, int selEnd) { if (hintVisible) { // Don't let users select the hint selStart = 0; } // Never let users select text selEnd = selStart; if (tokenClickStyle != null && tokenClickStyle.isSelectable()) { Editable text = getText(); if (text != null) { clearSelections(); } } if (prefix != null && (selStart < prefix.length() || selEnd < prefix.length())) { // Don't let users select the prefix setSelection(prefix.length()); } else { Editable text = getText(); if (text != null) { // Make sure if we are in a span, we select the spot 1 space after the span end TokenImageSpan[] spans = text.getSpans(selStart, selEnd, TokenImageSpan.class); for (TokenImageSpan span : spans) { int spanEnd = text.getSpanEnd(span); if (selStart <= spanEnd && text.getSpanStart(span) < selStart) { setSelection(spanEnd + 1); return; } } } super.onSelectionChanged(selStart, selEnd); } }
@Override public void afterTextChanged(Editable editable) { modified = true; // should be unaffected by textWatcherEnabled if (!textWatcherEnabled) return; // Add style as the user types if a toggle button is enabled int position = Selection.getSelectionStart(DroidWriterEditText.this.getText()); if (position < 0) { position = 0; } if (position > 0) { CharacterStyle[] appliedStyles = editable.getSpans(position - 1, position, CharacterStyle.class); StyleSpan currentBoldSpan = null; StyleSpan currentItalicSpan = null; UnderlineSpan currentUnderlineSpan = null; // Look for possible styles already applied to the entered text for (int i = 0; i < appliedStyles.length; i++) { if (appliedStyles[i] instanceof StyleSpan) { if (((StyleSpan) appliedStyles[i]).getStyle() == android.graphics.Typeface.BOLD) { // Bold style found currentBoldSpan = (StyleSpan) appliedStyles[i]; } else if (((StyleSpan) appliedStyles[i]).getStyle() == android.graphics.Typeface.ITALIC) { // Italic style found currentItalicSpan = (StyleSpan) appliedStyles[i]; } } else if (appliedStyles[i] instanceof UnderlineSpan) { // Underlined style found currentUnderlineSpan = (UnderlineSpan) appliedStyles[i]; } } // Handle the bold style toggle button if it's present if (boldToggle != null) { if (boldToggle.isChecked() && currentBoldSpan == null) { // The user switched the bold style button on and the // character doesn't have any bold // style applied, so we start a new bold style span. The // span is inclusive, // so any new characters entered right after this one // will automatically get this style. editable.setSpan( new StyleSpan(android.graphics.Typeface.BOLD), position - 1, position, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } else if (!boldToggle.isChecked() && currentBoldSpan != null) { // The user switched the bold style button off and the // character has bold style applied. // We need to remove the old bold style span, and define // a new one that end 1 position right // before the newly entered character. int boldStart = editable.getSpanStart(currentBoldSpan); int boldEnd = editable.getSpanEnd(currentBoldSpan); editable.removeSpan(currentBoldSpan); if (boldStart <= (position - 1)) { editable.setSpan( new StyleSpan(android.graphics.Typeface.BOLD), boldStart, position - 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } // The old bold style span end after the current cursor // position, so we need to define a // second newly created style span too, which begins // after the newly entered character and // ends at the old span's ending position. So we split // the span. if (boldEnd > position) { editable.setSpan( new StyleSpan(android.graphics.Typeface.BOLD), position, boldEnd, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } } } // Handling italics and underlined styles is the same as // handling bold styles. // Handle the italics style toggle button if it's present if (italicsToggle != null && italicsToggle.isChecked() && currentItalicSpan == null) { editable.setSpan( new StyleSpan(android.graphics.Typeface.ITALIC), position - 1, position, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } else if (italicsToggle != null && !italicsToggle.isChecked() && currentItalicSpan != null) { int italicStart = editable.getSpanStart(currentItalicSpan); int italicEnd = editable.getSpanEnd(currentItalicSpan); editable.removeSpan(currentItalicSpan); if (italicStart <= (position - 1)) { editable.setSpan( new StyleSpan(android.graphics.Typeface.ITALIC), italicStart, position - 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } // Split the span if (italicEnd > position) { editable.setSpan( new StyleSpan(android.graphics.Typeface.ITALIC), position, italicEnd, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } } // Handle the underlined style toggle button if it's present if (underlineToggle != null && underlineToggle.isChecked() && currentUnderlineSpan == null) { editable.setSpan( new UnderlineSpan(), position - 1, position, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } else if (underlineToggle != null && !underlineToggle.isChecked() && currentUnderlineSpan != null) { int underLineStart = editable.getSpanStart(currentUnderlineSpan); int underLineEnd = editable.getSpanEnd(currentUnderlineSpan); editable.removeSpan(currentUnderlineSpan); if (underLineStart <= (position - 1)) { editable.setSpan( new UnderlineSpan(), underLineStart, position - 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } // We need to split the span if (underLineEnd > position) { editable.setSpan( new UnderlineSpan(), position, underLineEnd, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); } } } }
public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { int selStart, selEnd; int pref = 0; if (view != null) { pref = TextKeyListener.getInstance().getPrefs(view.getContext()); } { int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); selStart = Math.min(a, b); selEnd = Math.max(a, b); } int activeStart = content.getSpanStart(TextKeyListener.ACTIVE); int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE); // now for the multitap cases... // Try to increment the character we were working on before // if we have one and it's still the same key. int rec = (content.getSpanFlags(TextKeyListener.ACTIVE) & Spannable.SPAN_USER) >>> Spannable.SPAN_USER_SHIFT; if (activeStart == selStart && activeEnd == selEnd && selEnd - selStart == 1 && rec >= 0 && rec < sRecs.size()) { if (keyCode == KeyEvent.KEYCODE_STAR) { char current = content.charAt(selStart); if (Character.isLowerCase(current)) { content.replace(selStart, selEnd, String.valueOf(current).toUpperCase()); removeTimeouts(content); new Timeout(content); // for its side effects return true; } if (Character.isUpperCase(current)) { content.replace(selStart, selEnd, String.valueOf(current).toLowerCase()); removeTimeouts(content); new Timeout(content); // for its side effects return true; } } if (sRecs.indexOfKey(keyCode) == rec) { String val = sRecs.valueAt(rec); char ch = content.charAt(selStart); int ix = val.indexOf(ch); if (ix >= 0) { ix = (ix + 1) % (val.length()); content.replace(selStart, selEnd, val, ix, ix + 1); removeTimeouts(content); new Timeout(content); // for its side effects return true; } } // Is this key one we know about at all? If so, acknowledge // that the selection is our fault but the key has changed // or the text no longer matches, so move the selection over // so that it inserts instead of replaces. rec = sRecs.indexOfKey(keyCode); if (rec >= 0) { Selection.setSelection(content, selEnd, selEnd); selStart = selEnd; } } else { rec = sRecs.indexOfKey(keyCode); } if (rec >= 0) { // We have a valid key. Replace the selection or insertion point // with the first character for that key, and remember what // record it came from for next time. String val = sRecs.valueAt(rec); int off = 0; if ((pref & TextKeyListener.AUTO_CAP) != 0 && TextKeyListener.shouldCap(mCapitalize, content, selStart)) { for (int i = 0; i < val.length(); i++) { if (Character.isUpperCase(val.charAt(i))) { off = i; break; } } } if (selStart != selEnd) { Selection.setSelection(content, selEnd); } content.setSpan(OLD_SEL_START, selStart, selStart, Spannable.SPAN_MARK_MARK); content.replace(selStart, selEnd, val, off, off + 1); int oldStart = content.getSpanStart(OLD_SEL_START); selEnd = Selection.getSelectionEnd(content); if (selEnd != oldStart) { Selection.setSelection(content, oldStart, selEnd); content.setSpan( TextKeyListener.LAST_TYPED, oldStart, selEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); content.setSpan( TextKeyListener.ACTIVE, oldStart, selEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | (rec << Spannable.SPAN_USER_SHIFT)); } removeTimeouts(content); new Timeout(content); // for its side effects // Set up the callback so we can remove the timeout if the // cursor moves. if (content.getSpanStart(this) < 0) { KeyListener[] methods = content.getSpans(0, content.length(), KeyListener.class); for (Object method : methods) { content.removeSpan(method); } content.setSpan(this, 0, content.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } return true; } return super.onKeyDown(view, content, keyCode, event); }
protected void handleFocus(boolean hasFocus) { if (!hasFocus) { setSingleLine(true); Editable text = getText(); if (text != null && lastLayout != null) { // Display +x thingy if appropriate int lastPosition = lastLayout.getLineVisibleEnd(0); TokenImageSpan[] tokens = text.getSpans(0, lastPosition, TokenImageSpan.class); int count = objects.size() - tokens.length; if (count > 0) { lastPosition++; CountSpan cs = new CountSpan( count, getContext(), getCurrentTextColor(), (int) getTextSize(), (int) maxTextWidth()); text.insert(lastPosition, cs.text); float newWidth = Layout.getDesiredWidth( text, 0, lastPosition + cs.text.length(), lastLayout.getPaint()); // If the +x span will be moved off screen, move it one token in if (newWidth > maxTextWidth()) { text.delete(lastPosition, lastPosition + cs.text.length()); if (tokens.length > 0) { TokenImageSpan token = tokens[tokens.length - 1]; lastPosition = text.getSpanStart(token); cs.setCount(count + 1); } else { lastPosition = prefix.length(); } text.insert(lastPosition, cs.text); } text.setSpan( cs, lastPosition, lastPosition + cs.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } else { setSingleLine(false); Editable text = getText(); if (text != null) { CountSpan[] counts = text.getSpans(0, text.length(), CountSpan.class); for (CountSpan count : counts) { text.delete(text.getSpanStart(count), text.getSpanEnd(count)); text.removeSpan(count); } if (hintVisible) { setSelection(prefix.length()); } else { setSelection(text.length()); } TokenSpanWatcher[] watchers = getText().getSpans(0, getText().length(), TokenSpanWatcher.class); if (watchers.length == 0) { // Someone removes watchers? I'm pretty sure this isn't in this code... -mgod text.setSpan(spanWatcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); } } } }