Skip to content

Android 富文本显示

initLiu edited this page Apr 29, 2016 · 2 revisions

重写了SpannableString类,添加了parseLinkSpan()方法对链接进行解析。 `

mTxt = (TextView) findViewById(R.id.txt);
mTxt.setSpannableFactory(MySpannableString.SPANNABLE_FACTORY);
mTxt.setMovementMethod(LinkMovementMethod.getInstance());
mTxt.setText(new MySpannableString("asdfasdfwww.baidu.com#asdfasdf97898#asdf@asdf", MySpannableString.GRAB_LINKS))

package com.example.test11;

import java.lang.reflect.Array;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.text.GetChars;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.util.Patterns;
import android.view.View;

public class MySpannableString implements CharSequence, GetChars, Spannable {

public static final int NO_ACTION = 0;
public static final int GRAB_LINKS = 0x00000001;

private String mText;
private Object[] mSpans;
private int[] mSpanData;
private int mSpanCount;

/* package */ static final Object[] EMPTY = new Object[0];

private static final int START = 0;
private static final int END = 1;
private static final int FLAGS = 2;
private static final int COLUMNS = 3;

/**
 * Regular expression to match all IANA top-level domains for WEB_URL. List accurate as of 2010/02/05. List taken
 * from: http://data.iana.org/TLD/tlds-alpha-by-domain.txt This pattern is auto-generated by
 * frameworks/base/common/tools/make-iana-tld-pattern.py
 */
public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = "(?:"
        + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
        + "|(?:biz|b[abdefghijmnorstvwyz])"
        + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
        + "|d[ejkmoz]"
        + "|(?:edu|e[cegrstu])"
        + "|f[ijkmor]"
        + "|(?:gov|g[abdefghilmnpqrstuwy])"
        + "|h[kmnrtu]"
        + "|(?:info|int|i[delmnoqrst])"
        + "|(?:jobs|j[emop])"
        + "|k[eghimnprwyz]"
        + "|l[abcikrstuvy]"
        + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
        + "|(?:name|net|n[acefgilopruz])"
        + "|(?:org|om)"
        + "|(?:pro|p[aefghklmnrstwy])"
        + "|qa"
        + "|r[eosuw]"
        + "|s[abcdeghijklmnortuvyz]"
        + "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
        + "|u[agksyz]"
        + "|v[aceginu]"
        + "|w[fs]"
        + "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-zckzah)"
        + "|y[etu]" + "|z[amw]))";

/**
 * Good characters for Internationalized Resource Identifiers (IRI). This comprises most common used Unicode
 * characters allowed in IRI as detailed in RFC 3987. Specifically, those two byte Unicode characters are not
 * included.
 */
public static final String GOOD_IRI_CHAR = "a-zA-Z0-9";//\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";

/**
 * Regular expression pattern to match most part of RFC 3987 Internationalized URLs, aka IRIs. Commonly used Unicode
 * characters are added.
 */
public static final Pattern WEB_URL = Pattern
        .compile("((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
                + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
                + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?"
                + "((?:(?<!["+GOOD_IRI_CHAR+"])(?:["
                + GOOD_IRI_CHAR
                + "]["
                + GOOD_IRI_CHAR
                + "\\-]{0,64}\\.){1,16}" // named host
                + TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL
                + "|(?:(?:25[0-5]|2[0-4]" // or ip address
                + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]"
                + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]"
                + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + "|[1-9][0-9]|[0-9])))"
                + "(?:\\:\\d{1,5})?)" // plus option port number
                + "(\\/(?:(?:([" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params
                + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))|(\\[(?=(?:([" + GOOD_IRI_CHAR + "\\;\\/\\?                        \\:\\@\\&\\=\\#\\~" // plus option query params
                + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2})))))*)?" + "(?:\\b|$)");// and finally, a
                                                                                               // word boundary or
                                                                                               // end of
                                                                                               // input. This is to
                                                                                               // stop foo.sure from
                                                                                               // matching as foo.su
/**
 * 话题正则表达式
 */
public static final String TOPIC = "#[a-zA-Z0-9]+#";

/**
 * @ 正则表达式
 */
public static final String AT = "@[a-zA-Z0-9]+";

static final String LINK_REGEX = AT + "|" + TOPIC + "|" + WEB_URL.pattern();

static final Pattern LINK_PATTERN = Pattern.compile(LINK_REGEX);

public static final Spannable.Factory SPANNABLE_FACTORY = new Spannable.Factory() {

	@Override
	public Spannable newSpannable(CharSequence source) {
		if (source instanceof MySpannableString) {
			try {
				return (MySpannableString) ((MySpannableString) source).clone();
			} catch (CloneNotSupportedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return super.newSpannable(source);
	}
};

public MySpannableString(CharSequence source) {
	this(source, NO_ACTION);
}

public MySpannableString(CharSequence source, int grabFlag) {
	this(source, 0, source.length(), grabFlag);
}

private MySpannableString(CharSequence source, int start, int end, int grabFlag) {
	if (start == 0 && end == source.length())
		mText = source.toString();
	else
		mText = source.toString().substring(start, end);

	int inital = growSize(3);
	mSpans = new Object[inital];
	mSpanData = new int[inital * 3];

	if ((GRAB_LINKS & grabFlag) == GRAB_LINKS) {
		if (mText != null && mText.length() < 1000) {
			parseLinkSpan();//解析连接
		}
	}

	if (source instanceof Spanned) {
		Spanned sp = (Spanned) source;
		Object[] spans = sp.getSpans(start, end, Object.class);

		for (int i = 0; i < spans.length; i++) {
			int st = sp.getSpanStart(spans[i]);
			int en = sp.getSpanEnd(spans[i]);
			int fl = sp.getSpanFlags(spans[i]);

			if (st < start)
				st = start;
			if (en > end)
				en = end;

			setSpan(spans[i], st - start, en - start, fl);
		}
	}
}

private void parseLinkSpan() {
	Matcher m = LINK_PATTERN.matcher(mText);
	int s;
	int e;
	String sub_str;
	while (m.find()) {
		s = m.start();
		e = m.end();
		sub_str = mText.substring(s, e);
		addSpan(new LinkSpan(sub_str), s, e, SPAN_EXCLUSIVE_EXCLUSIVE);
	}
}

private void addSpan(Object span, int start, int end, int flags) {
	if (mSpanCount + 1 >= mSpans.length) {
		Object[] newtags = new Object[(growSize(mSpanCount))];
		int[] newdata = new int[newtags.length * 3];

		System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
		System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);

		mSpans = newtags;
		mSpanData = newdata;
	}

	mSpans[mSpanCount] = span;
	mSpanData[mSpanCount * COLUMNS + START] = start;
	mSpanData[mSpanCount * COLUMNS + END] = end;
	mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
	mSpanCount++;
}

private int growSize(int currentSize) {
	return currentSize <= 4 ? 8 : currentSize * 2;
}

public static MySpannableString valueOf(CharSequence source) {
	if (source instanceof MySpannableString) {
		return (MySpannableString) source;
	} else {
		return new MySpannableString(source);
	}
}

@Override
public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
	int count = 0;

	int spanCount = mSpanCount;
	Object[] spans = mSpans;
	int[] data = mSpanData;
	Object[] ret = null;
	Object ret1 = null;

	for (int i = 0; i < spanCount; i++) {
		int spanStart = data[i * COLUMNS + START];
		int spanEnd = data[i * COLUMNS + END];

		if (spanStart > queryEnd) {
			continue;
		}
		if (spanEnd < queryStart) {
			continue;
		}

		if (spanStart != spanEnd && queryStart != queryEnd) {
			if (spanStart == queryEnd) {
				continue;
			}
			if (spanEnd == queryStart) {
				continue;
			}
		}

		// verify span class as late as possible, since it is expensive
		if (kind != null && !kind.isInstance(spans[i])) {
			continue;
		}

		if (count == 0) {
			ret1 = spans[i];
			count++;
		} else {
			if (count == 1) {
				ret = (Object[]) Array.newInstance(kind, spanCount - i + 1);
				ret[0] = ret1;
			}

			int prio = data[i * COLUMNS + FLAGS] & Spanned.SPAN_PRIORITY;
			if (prio != 0) {
				int j;

				for (j = 0; j < count; j++) {
					int p = getSpanFlags(ret[j]) & Spanned.SPAN_PRIORITY;

					if (prio > p) {
						break;
					}
				}

				System.arraycopy(ret, j, ret, j + 1, count - j);
				ret[j] = spans[i];
				count++;
			} else {
				ret[count++] = spans[i];
			}
		}
	}

	if (count == 0) {
		return (T[]) Array.newInstance(kind, 0);
	}
	if (count == 1) {
		ret = (Object[]) Array.newInstance(kind, 1);
		ret[0] = ret1;
		return (T[]) ret;
	}
	if (count == ret.length) {
		return (T[]) ret;
	}

	Object[] nret = (Object[]) Array.newInstance(kind, count);
	System.arraycopy(ret, 0, nret, 0, count);
	return (T[]) nret;
}

@Override
public int getSpanStart(Object what) {
	int count = mSpanCount;
	Object[] spans = mSpans;
	int[] data = mSpanData;

	for (int i = count - 1; i >= 0; i--) {
		if (spans[i] == what) {
			return data[i * COLUMNS + START];
		}
	}

	return -1;
}

@Override
public int getSpanEnd(Object what) {
	int count = mSpanCount;
	Object[] spans = mSpans;
	int[] data = mSpanData;

	for (int i = count - 1; i >= 0; i--) {
		if (spans[i] == what) {
			return data[i * COLUMNS + END];
		}
	}

	return -1;
}

@Override
public int getSpanFlags(Object what) {
	int count = mSpanCount;
	Object[] spans = mSpans;
	int[] data = mSpanData;

	for (int i = count - 1; i >= 0; i--) {
		if (spans[i] == what) {
			return data[i * COLUMNS + FLAGS];
		}
	}

	return 0;
}

@Override
public int nextSpanTransition(int start, int limit, Class kind) {
	int count = mSpanCount;
	Object[] spans = mSpans;
	int[] data = mSpanData;

	if (kind == null) {
		kind = Object.class;
	}

	for (int i = 0; i < count; i++) {
		int st = data[i * COLUMNS + START];
		int en = data[i * COLUMNS + END];

		if (st > start && st < limit && kind.isInstance(spans[i]))
			limit = st;
		if (en > start && en < limit && kind.isInstance(spans[i]))
			limit = en;
	}

	return limit;
}

@Override
public boolean equals(Object o) {
	if (o instanceof Spanned && toString().equals(o.toString())) {
		Spanned other = (Spanned) o;
		// Check span data
		Object[] otherSpans = other.getSpans(0, other.length(), Object.class);
		if (mSpanCount == otherSpans.length) {
			for (int i = 0; i < mSpanCount; ++i) {
				Object thisSpan = mSpans[i];
				Object otherSpan = otherSpans[i];
				if (thisSpan == this) {
					if (other != otherSpan || getSpanStart(thisSpan) != other.getSpanStart(otherSpan)
							|| getSpanEnd(thisSpan) != other.getSpanEnd(otherSpan)
							|| getSpanFlags(thisSpan) != other.getSpanFlags(otherSpan)) {
						return false;
					}
				} else if (!thisSpan.equals(otherSpan) || getSpanStart(thisSpan) != other.getSpanStart(otherSpan)
						|| getSpanEnd(thisSpan) != other.getSpanEnd(otherSpan)
						|| getSpanFlags(thisSpan) != other.getSpanFlags(otherSpan)) {
					return false;
				}
			}
			return true;
		}
	}
	return false;
}

@Override
public int hashCode() {
	int hash = toString().hashCode();
	hash = hash * 31 + mSpanCount;
	for (int i = 0; i < mSpanCount; ++i) {
		Object span = mSpans[i];
		if (span != this) {
			hash = hash * 31 + span.hashCode();
		}
		hash = hash * 31 + getSpanStart(span);
		hash = hash * 31 + getSpanEnd(span);
		hash = hash * 31 + getSpanFlags(span);
	}
	return hash;
}

@Override
public void setSpan(Object what, int start, int end, int flags) {
	int nstart = start;
	int nend = end;

	checkRange("setSpan", start, end);

	if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
		if (start != 0 && start != length()) {
			char c = charAt(start - 1);

			if (c != '\n')
				throw new RuntimeException(
						"PARAGRAPH span must start at paragraph boundary" + " (" + start + " follows " + c + ")");
		}

		if (end != 0 && end != length()) {
			char c = charAt(end - 1);

			if (c != '\n')
				throw new RuntimeException(
						"PARAGRAPH span must end at paragraph boundary" + " (" + end + " follows " + c + ")");
		}
	}

	int count = mSpanCount;
	Object[] spans = mSpans;
	int[] data = mSpanData;

	for (int i = 0; i < count; i++) {
		if (spans[i] == what) {
			int ostart = data[i * COLUMNS + START];
			int oend = data[i * COLUMNS + END];

			data[i * COLUMNS + START] = start;
			data[i * COLUMNS + END] = end;
			data[i * COLUMNS + FLAGS] = flags;

			sendSpanChanged(what, ostart, oend, nstart, nend);
			return;
		}
	}

	if (mSpanCount + 1 >= mSpans.length) {
		Object[] newtags = new Object[(growSize(mSpanCount))];
		int[] newdata = new int[newtags.length * 3];

		System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
		System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);

		mSpans = newtags;
		mSpanData = newdata;
	}

	mSpans[mSpanCount] = what;
	mSpanData[mSpanCount * COLUMNS + START] = start;
	mSpanData[mSpanCount * COLUMNS + END] = end;
	mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
	mSpanCount++;

	if (this instanceof Spannable)
		sendSpanAdded(what, nstart, nend);
}

@Override
public void removeSpan(Object what) {
	int count = mSpanCount;
	Object[] spans = mSpans;
	int[] data = mSpanData;

	for (int i = count - 1; i >= 0; i--) {
		if (spans[i] == what) {
			int ostart = data[i * COLUMNS + START];
			int oend = data[i * COLUMNS + END];

			int c = count - (i + 1);

			System.arraycopy(spans, i + 1, spans, i, c);
			System.arraycopy(data, (i + 1) * COLUMNS, data, i * COLUMNS, c * COLUMNS);

			mSpanCount--;

			sendSpanRemoved(what, ostart, oend);
			return;
		}
	}
}

@Override
public void getChars(int start, int end, char[] dest, int off) {
	mText.getChars(start, end, dest, off);
}

@Override
public int length() {
	// TODO Auto-generated method stub
	return mText.length();
}

@Override
public char charAt(int index) {
	// TODO Auto-generated method stub
	return mText.charAt(index);
}

@Override
public CharSequence subSequence(int start, int end) {
	// TODO Auto-generated method stub
	return null;
}

@Override
public String toString() {
	return mText.toString();
}

private void sendSpanAdded(Object what, int start, int end) {
	SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
	int n = recip.length;

	for (int i = 0; i < n; i++) {
		recip[i].onSpanAdded((Spannable) this, what, start, end);
	}
}

private void sendSpanRemoved(Object what, int start, int end) {
	SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
	int n = recip.length;

	for (int i = 0; i < n; i++) {
		recip[i].onSpanRemoved((Spannable) this, what, start, end);
	}
}

private void sendSpanChanged(Object what, int s, int e, int st, int en) {
	SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en), SpanWatcher.class);
	int n = recip.length;

	for (int i = 0; i < n; i++) {
		recip[i].onSpanChanged((Spannable) this, what, s, e, st, en);
	}
}

private static String region(int start, int end) {
	return "(" + start + " ... " + end + ")";
}

private void checkRange(final String operation, int start, int end) {
	if (end < start) {
		throw new IndexOutOfBoundsException(operation + " " + region(start, end) + " has end before start");
	}

	int len = length();

	if (start > len || end > len) {
		throw new IndexOutOfBoundsException(operation + " " + region(start, end) + " ends beyond length " + len);
	}

	if (start < 0 || end < 0) {
		throw new IndexOutOfBoundsException(operation + " " + region(start, end) + " starts before 0");
	}
}

@Override
protected Object clone() throws CloneNotSupportedException {
	MySpannableString clone = (MySpannableString) super.clone();
	clone.mSpanData = new int[mSpanData.length];
	System.arraycopy(mSpanData, 0, clone.mSpanData, 0, mSpanData.length);

	clone.mSpans = new Object[mSpans.length];
	System.arraycopy(mSpans, 0, clone.mSpans, 0, mSpans.length);
	return clone;
}

/**
 * 标识链接的span
 * 
 */
public class LinkSpan extends ClickableSpan {
	// Modify by shawn for IVR
	protected String mUrl;

	public LinkSpan(String url) {
		this.mUrl = url;
	}

	@Override
	public void onClick(View widget) {
		String url = mUrl;
		Matcher m = Patterns.WEB_URL.matcher(url);
		// 是个链接
		if (m.find()) {
			return;
		}

		m = Pattern.compile(TOPIC).matcher(url);
		// 是个话题
		if (m.find()) {
			return;
		}

		m = Pattern.compile(AT).matcher(url);
		// 是@
		if (m.find()) {
			return;
		}
	}
}

}

`