TextView拦截混合型url

1,534 阅读6分钟

TextView是拦截url

  1. 对富文本进行反编译
  2. 去除无用标签
  3. 将String 转换成Spanned
  4. 手动拦截url设置ClickSpan

一、富文本反编译

在web评论的富文本数据存储到数据库后,保存到数据库的格式可能是这样的:

<p>【产品名称】某某某<p>
<p>【产品名称】某某某<p>

上面的文字去掉转义之后在html中的样子:
<p>【产品名称】某某某<p>

【产品名称】某某某

在html中我们不需要转义,但是在Android中使用TextView是需要进行转义的。

public static void main(String[] args) {
    // 反转义
	String str = StringEscapeUtils.unescapeHtml4("&lt;p&gt;【产品名称】艾酷维多种维生素锌软糖&lt;/p&gt;");
	System.out.println(str);
	// 转义
	String str2 = StringEscapeUtils.escapeHtml4("<p>【产品名称】艾酷维多种维生素锌软糖</p>");
	System.out.println(str2);
}

StringEscapeUtils 是apache工具包common-text中有一个很有用的处理字符串的工具类,其中之一就是StringEscapeUtils,利用它能很方便的进行html、xml、java等的转义与反转义

api 'org.apache.commons:commons-text:1.9'

String sql="1' or '1'='1";
System.out.println("防SQL注入:"+StringEscapeUtils.escapeSql(sql));	//防SQL注入

System.out.println("转义HTML,注意汉字:"+StringEscapeUtils.escapeHtml("<font>chen磊  xing</font>")); 	//转义HTML,注意汉字
System.out.println("反转义HTML:"+StringEscapeUtils.unescapeHtml("<font>chen磊  xing</font>"));	//反转义HTML

System.out.println("转成Unicode编码:"+StringEscapeUtils.escapeJava("陈磊兴")); 	//转义成Unicode编码

System.out.println("转义XML:"+StringEscapeUtils.escapeXml("<name>陈磊兴</name>")); 	//转义xml
System.out.println("反转义XML:"+StringEscapeUtils.unescapeXml("<name>陈磊兴</name>")); 	//转义xml

二、去除无用标签

<a href=\\\"https://beta.wegene.com/page/about\\\">https://beta.wegene.com/page/about</a>22222<a href=\\\"https://beta.wegene.com/uploads/answer/20201201/4b87cee9a0e3fe91daaecac54b6f3f82.jpeg\\\" target=\\\"_blank\\\" data-fancybox-group=\\\"thumb\\\" rel=\\\"lightbox\\\"><img src=\\\"https://beta.wegene.com/uploads/answer/20201201/4b87cee9a0e3fe91daaecac54b6f3f82.jpeg\\\" class=\\\"img-polaroid\\\" title=\\\"yyy2.jpeg\\\" alt=\\\"yyy2.jpeg\\\" /></a>

在项目中存在使用了多个不同内容的a标签,那么有些类型的a标签是不需要所以将其需要将其去除点

// 去掉无用<a>   
public static String trimALabel(String source) {
    Pattern p = Pattern.compile("<a href=\"((?!>).*?) rel=\"lightbox\">");//将无用标签去掉
    Matcher m = p.matcher(source);
    return m.replaceAll("");
}

三、将String转换成Spanned

  1. 使用Html.fromHtml(text)将text转换成Spanned

     Spanned spanned = null;
     text = text.replaceAll("\n", "<br />");
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
         // flags
         // FROM_HTML_MODE_COMPACT:html块元素之间使用一个换行符分隔
         // FROM_HTML_MODE_LEGACY:html块元素之间使用两个换行符分隔
         spanned = Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT);
     } else {
         spanned = Html.fromHtml(text);
     }
    
  2. Html.fromHtml()->HtmlToSpannedConverter

     public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter,
             TagHandler tagHandler) {
         Parser parser = new Parser();
         try {
             parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
         } catch (org.xml.sax.SAXNotRecognizedException e) {
             // Should not happen.
             throw new RuntimeException(e);
         } catch (org.xml.sax.SAXNotSupportedException e) {
             // Should not happen.
             throw new RuntimeException(e);
         }
    
         HtmlToSpannedConverter converter =
                 new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);
         return converter.convert();
     }
    
  3. convert()

     public Spanned convert() {
    
         // 1 先XMLReader将Text加入到mSpannableStringBuilder
         mReader.setContentHandler(this);
         try {
             mReader.parse(new InputSource(new StringReader(mSource)));
         } catch (IOException e) {
             // We are reading from a string. There should not be IO problems.
             throw new RuntimeException(e);
         } catch (SAXException e) {
             // TagSoup doesn't throw parse exceptions.
             throw new RuntimeException(e);
         }
    
         // 2 处理SpannableStringBuilder多余的字符。
         // Fix flags and range for paragraph-type markup.
         Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
         for (int i = 0; i < obj.length; i++) {
             int start = mSpannableStringBuilder.getSpanStart(obj[i]);
             int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
    
             // If the last line of the range is blank, back off by one.
             if (end - 2 >= 0) {
                 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
                     mSpannableStringBuilder.charAt(end - 2) == '\n') {
                     end--;
                 }
             }
    
             if (end == start) {
                 mSpannableStringBuilder.removeSpan(obj[i]);
             } else {
                 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
             }
         }
    
         return mSpannableStringBuilder;
     }
    
  4. XMLFilterImpl或ParserAdapter中方法

     // 处理html中的各种标签
     public void startElement (String uri, String localName, String qName,
                   Attributes atts)
     throws SAXException
     {
     if (contentHandler != null) {
         contentHandler.startElement(uri, localName, qName, atts);
     }
     }
     
     // 将text转换成字符数组后在添加到char
     public void characters (char ch[], int start, int length)
     throws SAXException
     {
     if (contentHandler != null) {
         contentHandler.characters(ch, start, length);
     }
     }
    
  5. Html中处理字符的方法 将字符加入到SpannableStringBuilder中

     public void characters(char ch[], int start, int length) throws SAXException {
         StringBuilder sb = new StringBuilder();
    
         /*
          * Ignore whitespace that immediately follows other whitespace;
          * newlines count as spaces.
          */
    
         for (int i = 0; i < length; i++) {
             char c = ch[i + start];
    
             if (c == ' ' || c == '\n') {
                 char pred;
                 int len = sb.length();
    
                 if (len == 0) {
                     len = mSpannableStringBuilder.length();
    
                     if (len == 0) {
                         pred = '\n';
                     } else {
                         pred = mSpannableStringBuilder.charAt(len - 1);
                     }
                 } else {
                     pred = sb.charAt(len - 1);
                 }
    
                 if (pred != ' ' && pred != '\n') {
                     sb.append(' ');
                 }
             } else {
                 sb.append(c);
             }
         }
    
         mSpannableStringBuilder.append(sb);
     }
    
  6. HtmlToSpannedConverter中处理标签的方法,比如b标签,br标签等

     private void handleStartTag(String tag, Attributes attributes) {
         if (tag.equalsIgnoreCase("br")) {
             // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
             // so we can safely emit the linebreaks when we handle the close tag.
         } else if (tag.equalsIgnoreCase("p")) {
             startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
             startCssStyle(mSpannableStringBuilder, attributes);
         } else if (tag.equalsIgnoreCase("ul")) {
             startBlockElement(mSpannableStringBuilder, attributes, getMarginList());
         } else if (tag.equalsIgnoreCase("li")) {
             startLi(mSpannableStringBuilder, attributes);
         } else if (tag.equalsIgnoreCase("div")) {
             startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());
         } else if (tag.equalsIgnoreCase("span")) {
             startCssStyle(mSpannableStringBuilder, attributes);
         } else if (tag.equalsIgnoreCase("strong")) {
             start(mSpannableStringBuilder, new Bold());
         } else if (tag.equalsIgnoreCase("b")) {
             start(mSpannableStringBuilder, new Bold());
         } else if (tag.equalsIgnoreCase("em")) {
             start(mSpannableStringBuilder, new Italic());
         } else if (tag.equalsIgnoreCase("cite")) {
             start(mSpannableStringBuilder, new Italic());
         } else if (tag.equalsIgnoreCase("dfn")) {
             start(mSpannableStringBuilder, new Italic());
         } else if (tag.equalsIgnoreCase("i")) {
             start(mSpannableStringBuilder, new Italic());
         } else if (tag.equalsIgnoreCase("big")) {
             start(mSpannableStringBuilder, new Big());
         } else if (tag.equalsIgnoreCase("small")) {
             start(mSpannableStringBuilder, new Small());
         } else if (tag.equalsIgnoreCase("font")) {
             startFont(mSpannableStringBuilder, attributes);
         } else if (tag.equalsIgnoreCase("blockquote")) {
             startBlockquote(mSpannableStringBuilder, attributes);
         } else if (tag.equalsIgnoreCase("tt")) {
             start(mSpannableStringBuilder, new Monospace());
         } else if (tag.equalsIgnoreCase("a")) {
             startA(mSpannableStringBuilder, attributes);
         } else if (tag.equalsIgnoreCase("u")) {
             start(mSpannableStringBuilder, new Underline());
         } else if (tag.equalsIgnoreCase("del")) {
             start(mSpannableStringBuilder, new Strikethrough());
         } else if (tag.equalsIgnoreCase("s")) {
             start(mSpannableStringBuilder, new Strikethrough());
         } else if (tag.equalsIgnoreCase("strike")) {
             start(mSpannableStringBuilder, new Strikethrough());
         } else if (tag.equalsIgnoreCase("sup")) {
             start(mSpannableStringBuilder, new Super());
         } else if (tag.equalsIgnoreCase("sub")) {
             start(mSpannableStringBuilder, new Sub());
         } else if (tag.length() == 2 &&
                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
             startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1');
         } else if (tag.equalsIgnoreCase("img")) {
             startImg(mSpannableStringBuilder, attributes, mImageGetter);
         } else if (mTagHandler != null) {
             mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
         }
     }
    

四、拦截url

// Spannable中有获取到UrlSpan可以将UrlSpan提取出来,只有标签中才会有通过UrlSpan识别出来。而没有标签的urlSpan如何识别出来呢

if (spanned instanceof Spannable) {
    int end = spanned.length();
    Spannable spannable = (Spannable) spanned;
    URLSpan[] labelSpans = spannable.getSpans(0, end, URLSpan.class);
    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(spanned);
    // 循环遍历并拦截 所有http://开头的链接
    for (URLSpan uri : labelSpans) {
        String url = uri.getURL();
        // 将 @username 中的uid提取出来,
        Matcher m = Pattern.compile("<a href=\"" + url + "\"((?!>).*?)data-id=\"(.*?)\">").matcher(text);
        String uid = null;
        if (m.find()) {
            if (m.groupCount() > 1)
                uid = m.group(2);
        }
        if (url.startsWith("http")) {
            spannableStringBuilder.setSpan(new ALabelUrlSpan(getContext(), url, uid, colorRes),
                    spannable.getSpanStart(uri),
                    spannable.getSpanEnd(uri), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
            spannableStringBuilder.removeSpan(uri);
        }
    }
    if (Linkify.addLinks(spannable, Linkify.WEB_URLS)) {
        final URLSpan[] urlSpans = spannable.getSpans(0, spannable.length(), URLSpan.class);
        for (URLSpan uri : urlSpans) {
            String url = uri.getURL();
            if (url.startsWith("http")) {
                spannableStringBuilder.setSpan(new ALabelUrlSpan(getContext(), url, null, colorRes),
                        spannable.getSpanStart(uri),
                        spannable.getSpanEnd(uri), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
                spannableStringBuilder.removeSpan(uri);
            }
        }

        if ((labelSpans != null && labelSpans.length > 0) || (urlSpans != null && urlSpans.length > 0)) {
            setMovementMethod(AlabelLinkMovementMethod.getInstance());
        }
    } else {
        if (labelSpans != null && labelSpans.length > 0) {
            setMovementMethod(AlabelLinkMovementMethod.getInstance());
        }
    }
    append(spannableStringBuilder);
} else {
    append(spanned);
}

识别不带标签的url

TextVew中带有url,需要跳转到浏览器或者拦截的话首先需要设置autoLink="web"

<com.wegene.commonlibrary.view.ALabelTextView
    android:id="@+id/tv_text"
    style="@style/ALabelTextStyle"
    android:layout_width="0dp"
    android:autoLink="web"
    android:layout_marginTop="@dimen/dp_5"
    android:layout_marginRight="@dimen/sp_15"
    app:layout_constraintLeft_toLeftOf="@id/tv_name"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/v_report_case"
    tools:text="秋风萧瑟洪波涌起日月之行若出其中,星河灿烂若出其里" />

这样设置了TextView中点击连接可以跳转到外部浏览器,那需要拦截如何处理呢。

  1. addLinks判断Links是否有Links

     public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
         return addLinks(text, mask, null, null);
     }
     
    
  2. 带有web的标签 WEB_URLS

     private static boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask,
             @Nullable Context context, @Nullable Function<String, URLSpan> urlSpanFactory) {
         if (text != null && containsUnsupportedCharacters(text.toString())) {
             android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
             return false;
         }
    
         if (mask == 0) {
             return false;
         }
    
         final URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
    
         for (int i = old.length - 1; i >= 0; i--) {
             text.removeSpan(old[i]);
         }
    
         final ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
    
         // 执行带有Web的标签
         if ((mask & WEB_URLS) != 0) {
             gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL,
                 new String[] { "http://", "https://", "rtsp://" },
                 sUrlMatchFilter, null);
         }
    
         if ((mask & EMAIL_ADDRESSES) != 0) {
             gatherLinks(links, text, Patterns.AUTOLINK_EMAIL_ADDRESS,
                 new String[] { "mailto:" },
                 null, null);
         }
    
         if ((mask & PHONE_NUMBERS) != 0) {
             gatherTelLinks(links, text, context);
         }
    
         if ((mask & MAP_ADDRESSES) != 0) {
             gatherMapLinks(links, text);
         }
    
         pruneOverlaps(links);
    
         if (links.size() == 0) {
             return false;
         }
    
         for (LinkSpec link: links) {
             applyLink(link.url, link.start, link.end, text, urlSpanFactory);
         }
    
         return true;
     }
     
    
  3. 处理Spannable中的连接

     private static final void gatherLinks(ArrayList<LinkSpec> links,
             Spannable s, Pattern pattern, String[] schemes,
             MatchFilter matchFilter, TransformFilter transformFilter) {
         Matcher m = pattern.matcher(s);
    
         while (m.find()) {
             int start = m.start();
             int end = m.end();
    
             if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
                 LinkSpec spec = new LinkSpec();
                 String url = makeUrl(m.group(0), schemes, m, transformFilter);
    
                 spec.url = url;
                 spec.start = start;
                 spec.end = end;
    
                 links.add(spec);
             }
         }
     }
    
  4. 添加UrlSpan的到Spannale中

     private static void applyLink(String url, int start, int end, Spannable text,
             @Nullable Function<String, URLSpan> urlSpanFactory) {
         if (urlSpanFactory == null) {
             urlSpanFactory = DEFAULT_SPAN_FACTORY;
         }
         final URLSpan span = urlSpanFactory.apply(url);
         text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
     }
     
    
  5. 如果有UrlSpan则手动处理

     if (Linkify.addLinks(spannable, Linkify.WEB_URLS)) {
         final URLSpan[] urlSpans = spannable.getSpans(0, spannable.length(), URLSpan.class);
         for (URLSpan uri : urlSpans) {
             String url = uri.getURL();
             if (url.startsWith("http")) {
                 spannableStringBuilder.setSpan(new ALabelUrlSpan(getContext(), url, null, colorRes),
                         spannable.getSpanStart(uri),
                         spannable.getSpanEnd(uri), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
                 spannableStringBuilder.removeSpan(uri);
             }
         }
    
         if ((labelSpans != null && labelSpans.length > 0) || (urlSpans != null && urlSpans.length > 0)) {
             setMovementMethod(AlabelLinkMovementMethod.getInstance());
         }
     }
    
  6. 这里添加了ClickSpan 而TextView中的ClickSpan如何处理是否会重复。

    在上面的步骤中我们使用了TextView.append()将text加入文本中

    TextView中的append方法

    public final void append(CharSequence text) { append(text, 0, text.length()); }

    public void append(CharSequence text, int start, int end) { if (!(mText instanceof Editable)) { setText(mText, BufferType.EDITABLE); }

     ((Editable) mText).append(text, start, end);
    
     if (mAutoLinkMask != 0) {
         boolean linksWereAdded = Linkify.addLinks(mSpannable, mAutoLinkMask);
         // Do not change the movement method for text that support text selection as it
         // would prevent an arbitrary cursor displacement.
         if (linksWereAdded && mLinksClickable && !textCanBeSelected()) {
             setMovementMethod(LinkMovementMethod.getInstance());
         }
     }
    

    }

在这段代码中又调用了Linkify.addLinks(mSpannable, mAutoLinkMask),又多添加了一次urlSpan

点击onTouchEvent(MotionEvent event)

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int action = event.getActionMasked();
    if (mEditor != null) {
        mEditor.onTouchEvent(event);

        if (mEditor.mSelectionModifierCursorController != null
                && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
            return true;
        }
    }

    final boolean superResult = super.onTouchEvent(event);

    /*
     * Don't handle the release after a long press, because it will move the selection away from
     * whatever the menu action was trying to affect. If the long press should have triggered an
     * insertion action mode, we can now actually show it.
     */
    if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
        mEditor.mDiscardNextActionUp = false;

        if (mEditor.mIsInsertionActionModeStartPending) {
            mEditor.startInsertionActionMode();
            mEditor.mIsInsertionActionModeStartPending = false;
        }
        return superResult;
    }

    final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
            && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();

    if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
            && mText instanceof Spannable && mLayout != null) {
        boolean handled = false;

        if (mMovement != null) {
            handled |= mMovement.onTouchEvent(this, mSpannable, event);
        }

        final boolean textIsSelectable = isTextSelectable();
        if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
            // The LinkMovementMethod which should handle taps on links has not been installed
            // on non editable text that support text selection.
            // We reproduce its behavior here to open links for these.
            ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(),
                getSelectionEnd(), ClickableSpan.class);

            if (links.length > 0) {
                links[0].onClick(this);
                handled = true;
            }
        }

        if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
            // Show the IME, except when selecting in read-only text.
            final InputMethodManager imm = getInputMethodManager();
            viewClicked(imm);
            if (isTextEditable() && mEditor.mShowSoftInputOnFocus && imm != null) {
                imm.showSoftInput(this, 0);
            }

            // The above condition ensures that the mEditor is not null
            mEditor.onTouchUpEvent(event);

            handled = true;
        }

        if (handled) {
            return true;
        }
    }

    return superResult;
}

在手指抬起的时候最终调用

/// 获取选择地方的ClikSpan
ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(),
    getSelectionEnd(), ClickableSpan.class);

// 如果一个url对应有多个ClickSpan选择执行第一个ClickSpan。
if (links.length > 0) {
    links[0].onClick(this);
    handled = true;
}

也就说第一个是我们添加的ALabelUrlSpan,那么就执行这个,所以不会有两个点击事件。append()方法不能重写,因为append()中的mText等参数是私有的所以不能重写。

// 这样可以就可以不添加两个ClickSpan
setAutoLinkMask(0);
append(spannableStringBuilder);
setAutoLinkMask(Linkify.WEB_URLS);

这是我写的第一篇文章,写的很潦草