TextView是拦截url
- 对富文本进行反编译
- 去除无用标签
- 将String 转换成Spanned
- 手动拦截url设置ClickSpan
一、富文本反编译
在web评论的富文本数据存储到数据库后,保存到数据库的格式可能是这样的:
<p>【产品名称】某某某<p>
<p>【产品名称】某某某<p>
上面的文字去掉转义之后在html中的样子:
<p>【产品名称】某某某<p>
【产品名称】某某某
在html中我们不需要转义,但是在Android中使用TextView是需要进行转义的。
public static void main(String[] args) {
// 反转义
String str = StringEscapeUtils.unescapeHtml4("<p>【产品名称】艾酷维多种维生素锌软糖</p>");
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
-
使用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); } -
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(); } -
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; } -
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); } } -
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); } -
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中点击连接可以跳转到外部浏览器,那需要拦截如何处理呢。
-
addLinks判断Links是否有Links
public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) { return addLinks(text, mask, null, null); } -
带有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; } -
处理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); } } } -
添加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); } -
如果有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()); } } -
这里添加了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);
这是我写的第一篇文章,写的很潦草