Android利用SpannableString實(shí)現(xiàn)格式化微博內(nèi)容
前言
在Android開發(fā)中,有許多信息展示需要通過TextView來展現(xiàn),如果只是普通的信息展現(xiàn),使用TextView setText(CharSequence str)設(shè)置即可,但是當(dāng)在TextView里的這段內(nèi)容需要截取某一部分字段,可以被點(diǎn)擊以及響應(yīng)響應(yīng)的操作,這時(shí)候就需要用到SpannableString了,SpannableString 配合 TextView 可以輕松實(shí)現(xiàn)對(duì)特定的文本做特定處理,例如可以修改文字顏色、背景色、將文字替換為圖片實(shí)現(xiàn),點(diǎn)擊效果等。
首先看看最終實(shí)現(xiàn)的效果圖:
第一個(gè)卡片內(nèi)的微博是原始文本信息,第二個(gè)卡片內(nèi)的微博是第一個(gè)格式化后的文本內(nèi)容,將微博內(nèi)的”話題”、”表情”、”網(wǎng)頁鏈接”、以及”@用戶”都進(jìn)行了處理,并可以點(diǎn)擊,使其和官方微博展示的樣式保持一致。
要實(shí)現(xiàn)的效果:
- 將話題進(jìn)行變色并且可以點(diǎn)擊提示對(duì)應(yīng)的話題文本內(nèi)容
- 將圖片表情替換掉對(duì)應(yīng)的表情關(guān)鍵字顯示
- 將鏈接地址替換成一個(gè)鏈接的圖片和”網(wǎng)頁鏈接”四個(gè)字顯示
- 將@的用戶進(jìn)行變色并且可以點(diǎn)擊提示對(duì)應(yīng)的話題文本內(nèi)容
需要:
- 使用正則表達(dá)式提取文本內(nèi)對(duì)應(yīng)的”話題”、”表情”、”網(wǎng)頁鏈接”、以及”@用戶”內(nèi)容
- 使用 SpannableString 格式化提取到的文本
- 給格式化的部分添加點(diǎn)擊事件
定義正則表達(dá)式
首先定義”話題”、”表情”、”網(wǎng)頁鏈接”、以及”@用戶”對(duì)應(yīng)的正則表達(dá)式和對(duì)應(yīng)的 Pattern。SCHEME 下文會(huì)提到具體的用處的。
public class WeiboPattern { // #話題# public static final String REGEX_TOPIC = "#[\\p{Print}\\p{InCJKUnifiedIdeographs}&&[^#]]+#"; // [表情] public static final String REGEX_EMOTION = "\\[(\\S+?)\\]"; // url public static final String REGEX_URL = "http://[a-zA-Z0-9+&@#/%?=~_\\\\-|!:,\\\\.;]*[a-zA-Z0-9+&@#/%=~_|]"; // @人 public static final String REGEX_AT = "@[\\w\\p{InCJKUnifiedIdeographs}-]{1,26}"; public static final Pattern PATTERN_TOPIC = Pattern.compile(REGEX_TOPIC); public static final Pattern PATTERN_EMOTION = Pattern.compile(REGEX_EMOTION); public static final Pattern PATTERN_URL = Pattern.compile(REGEX_URL); public static final Pattern PATTERN_AT = Pattern.compile(REGEX_AT); public static final String SCHEME_TOPIC = "topic:"; public static final String SCHEME_URL = "url:"; public static final String SCHEME_AT = "at:"; }
提取匹配部分并使用 SpannableString 格式化
我將此過程寫到一個(gè)方法內(nèi)了,下面直接上代碼,代碼中有詳細(xì)的注釋解釋:
/** * 格式化微博文本 * * @param context 上下文 * @param source 源文本 * @param textView 目標(biāo) TextView * @return SpannableStringBuilder */ public static SpannableStringBuilder formatWeiBoContent(Context context, String source, TextView textView) { // 獲取到 TextView 的文字大小,后面的 ImageSpan 需要用到該值 int textSize = (int) textView.getTextSize(); // 若要部分 SpannableString 可點(diǎn)擊,需要如下設(shè)置 textView.setMovementMethod(LinkMovementMethod.getInstance()); // 將要格式化的 String 構(gòu)建成一個(gè) SpannableStringBuilder SpannableStringBuilder value = new SpannableStringBuilder(source); // 使用正則匹配話題 Linkify.addLinks(value, WeiboPattern.PATTERN_TOPIC, WeiboPattern.SCHEME_TOPIC); // 使用正則匹配鏈接 Linkify.addLinks(value, WeiboPattern.PATTERN_URL, WeiboPattern.SCHEME_URL); // 使用正則匹配@用戶 Linkify.addLinks(value, WeiboPattern.PATTERN_AT, WeiboPattern.SCHEME_AT); // 自定義的匹配部分的點(diǎn)擊效果 MyClickableSpan clickSpan; // 獲取上面到所有 addLinks 后的匹配部分(這里一個(gè)匹配項(xiàng)被封裝成了一個(gè) URLSpan 對(duì)象) URLSpan[] urlSpans = value.getSpans(0, value.length(), URLSpan.class); // 遍歷所有的 URLSpan for (final URLSpan urlSpan : urlSpans) { // 點(diǎn)擊匹配部分效果 clickSpan = new MyClickableSpan() { @Override public void onClick(View view) { ToastUtils.makeShort(urlSpan.getURL()); } }; // 話題 if (urlSpan.getURL().startsWith(WeiboPattern.SCHEME_TOPIC)) { int start = value.getSpanStart(urlSpan); int end = value.getSpanEnd(urlSpan); value.removeSpan(urlSpan); // 格式化話題部分文本 value.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } // @用戶 if (urlSpan.getURL().startsWith(WeiboPattern.SCHEME_AT)) { int start = value.getSpanStart(urlSpan); int end = value.getSpanEnd(urlSpan); value.removeSpan(urlSpan); // 格式化@用戶部分文本 value.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } // 鏈接 if (urlSpan.getURL().startsWith(WeiboPattern.SCHEME_URL)) { int start = value.getSpanStart(urlSpan); int end = value.getSpanEnd(urlSpan); value.removeSpan(urlSpan); SpannableStringBuilder urlSpannableString = getUrlTextSpannableString(context, urlSpan.getURL(), textSize); value.replace(start, end, urlSpannableString); // 格式化鏈接部分文本 value.setSpan(clickSpan, start, start + urlSpannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } // 表情需要單獨(dú)格式化 Matcher emotionMatcher = WeiboPattern.PATTERN_EMOTION.matcher(value); while (emotionMatcher.find()) { String emotion = emotionMatcher.group(); int start = emotionMatcher.start(); int end = emotionMatcher.end(); int resId = EmotionUtils.getImageByName(emotion); if (resId != -1) { // 表情匹配 L.e("find emotion: " + emotion); Drawable drawable = context.getResources().getDrawable(resId); drawable.setBounds(0, 0, (int) (textSize * 1.3), (int) (textSize * 1.3)); // 自定義的 VerticalImageSpan ,可解決默認(rèn)的 ImageSpan 不垂直居中的問題 VerticalImageSpan imageSpan = new VerticalImageSpan(drawable); value.setSpan(imageSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } return value; }
private static SpannableStringBuilder getUrlTextSpannableString(Context context, String source, int size) { SpannableStringBuilder builder = new SpannableStringBuilder(source); String prefix = " "; builder.replace(0, prefix.length(), prefix); Drawable drawable = context.getResources().getDrawable(R.drawable.ic_status_link); drawable.setBounds(0, 0, size, size); builder.setSpan(new VerticalImageSpan(drawable), prefix.length(), source.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); builder.append(" 網(wǎng)頁鏈接"); return builder; }
getUrlTextSpannableString()
:方法是用來返回一個(gè)圖標(biāo)+”網(wǎng)頁鏈接” SpannableString,用于替換鏈接文本
上面將”話題”、”表情”、”網(wǎng)頁鏈接”都用了addLinks方法來標(biāo)記的,然后統(tǒng)一處理。表情則是單獨(dú)處理的。
表情則使用如下方法事先做好映射:
public class EmotionUtils { public static LinkedHashMap<String, Integer> sMap; static { sMap = new LinkedHashMap<>(); sMap.put("[doge]", R.drawable.d_doge); sMap.put("[污]", R.drawable.d_wu); } public static int getImageByName(String name) { Integer integer = sMap.get(name); return integer == null ? -1 : integer; } }
還有剛才說到的自定義 MyClickableSpan 修改默認(rèn)的樣式:
public class MyClickableSpan extends ClickableSpan { @Override public void onClick(View view) { } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(0xff03A9F4); ds.setUnderlineText(false); } }
另外,由于默認(rèn)的 ImageSpan 在 TextView 有使用android:lineSpacingExtra屬性時(shí),不會(huì)垂直居中,所以使用到了網(wǎng)上的一個(gè)繼承自 ImageSpan 的 VerticalImageSpan 可以做到保持圖片在 TextView 內(nèi)保持垂直居中:
public class VerticalImageSpan extends ImageSpan { public VerticalImageSpan(Drawable drawable) { super(drawable); } /** * update the text line height */ @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt) { Drawable drawable = getDrawable(); Rect rect = drawable.getBounds(); if (fontMetricsInt != null) { Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt(); int fontHeight = fmPaint.descent - fmPaint.ascent; int drHeight = rect.bottom - rect.top; int centerY = fmPaint.ascent + fontHeight / 2; fontMetricsInt.ascent = centerY - drHeight / 2; fontMetricsInt.top = fontMetricsInt.ascent; fontMetricsInt.bottom = centerY + drHeight / 2; fontMetricsInt.descent = fontMetricsInt.bottom; } return rect.right; } /** * see detail message in android.text.TextLine * * @param canvas the canvas, can be null if not rendering * @param text the text to be draw * @param start the text start position * @param end the text end position * @param x the edge of the replacement closest to the leading margin * @param top the top of the line * @param y the baseline * @param bottom the bottom of the line * @param paint the work paint */ @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { Drawable drawable = getDrawable(); canvas.save(); Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt(); int fontHeight = fmPaint.descent - fmPaint.ascent; int centerY = y + fmPaint.descent - fontHeight / 2; int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2; canvas.translate(x, transY); drawable.draw(canvas); canvas.restore(); } }
然后直接調(diào)用該方法格式化:
mTextView.setText(formatWeiBoContent(this,mTextView.getText().toString(),mTextView))
最終的效果圖和文章開頭效果一樣了,并且可以點(diǎn)擊,這里展示了點(diǎn)擊”網(wǎng)頁鏈接”時(shí)彈出的 Toast 提示:
總結(jié)
本文僅介紹了 SpannableString 常用的一些場景,例如修改特定文本的顏色,替換特定文本,特定文本的點(diǎn)擊事件,但是 SpannableString 的強(qiáng)大遠(yuǎn)不止如此。SpannableString 的更多用法可閱讀官方文檔。好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Android中的SpannableString與SpannableStringBuilder詳解
- TextView使用SpannableString設(shè)置復(fù)合文本 SpannableString實(shí)現(xiàn)TextView的鏈接效果
- Android SpannableString設(shè)置超鏈接、顏色、字體等屬性
- Android TextView中文字通過SpannableString設(shè)置屬性用法示例
- Android TextView使用SpannableString設(shè)置復(fù)合文本的方法詳解
- Android RichText 讓Textview輕松的支持富文本(圖像ImageSpan、點(diǎn)擊效果等等類似QQ微信聊天)
- 利用SpannableString和ImageSpan在textview中插入圖片的方法
相關(guān)文章
Android高仿微信對(duì)話列表滑動(dòng)刪除效果
這篇文章主要為大家詳細(xì)介紹了Android高仿微信對(duì)話列表滑動(dòng)刪除效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08Kotlin1.6.20新功能Context?Receivers使用技巧揭秘
這篇文章主要為大家介紹了Kotlin1.6.20功能Context?Receivers使用揭秘,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06FragmentStatePagerAdapter保存恢復(fù)下拉刷新Fragment內(nèi)存數(shù)據(jù)
這篇文章主要為大家介紹了FragmentStatePagerAdapter保存恢復(fù)下拉刷新Fragment內(nèi)存數(shù)據(jù)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Compose開發(fā)之動(dòng)畫藝術(shù)探索及實(shí)現(xiàn)示例
這篇文章主要為大家介紹了Compose開發(fā)之動(dòng)畫藝術(shù)探索及實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09