展示固定長度文字,SpannableString匹配連結被截斷問題解決
最近專案中遇到這樣的一個問題:動態列表中,每個item,文字只展示120字,超過120字,就是前120字+“…”來展示。但是,文字中有表情、連結等。
以前為了省事,就直接粗暴的擷取前120字,然後跟省略號。然後去匹配表情和連結等,這樣,就造成了一個問題,如果擷取的位置遇到了連結,就會把連結截斷,造成連結的不完整。
解決的辦法就是,先匹配,把連結處理完,然後再去擷取
為了簡單處理,我下面用50字來模擬。即:一個文字,最多展示50字,超過了就有省略號。
特別說明:上面說的所謂上限(最多50或者120字),其實是一個粗略值,是可以小範圍浮動的數值。實際中沒有使用者會去數字數,發現字數多了去找客服反映的。
首先,我先寫個基本的匹配方法,把架子搭起來。
自定義MyTextView
package com.chen.demo;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method .LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.regex.Matcher ;
import java.util.regex.Pattern;
public class MyTextView extends TextView {
private Context mContext;
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
/**
* 處理資料
*
* @param str 要被處理的資料
*/
public void handleData(String str) {
Log.e("handleData=str",str);
String content = str;
if (content == null) {
//防止matcher造成空指標
setText("");
return;
}
//處理匹配的url
Pattern p = Pattern.compile(Util.UrlRegex);
Matcher m = p.matcher(content);
ArrayList<String> urlList = new ArrayList<String>();
while (m.find()) {
String urlStr = m.group();
if (urlStr.contains("http://") || urlStr.contains("https://")) {
while (urlStr.endsWith(",") || urlStr.endsWith(",") || urlStr.endsWith(".") || urlStr.endsWith("。") || urlStr.endsWith(";") || urlStr.endsWith(";") || urlStr.endsWith("!") || urlStr.endsWith("!") || urlStr.endsWith("?") || urlStr.endsWith("?") || urlStr.endsWith("#") || urlStr.endsWith("@")) {
urlStr = urlStr.substring(0, urlStr.length() - 1);
}
urlList.add(urlStr);
content = content.replace(urlStr, Util.ReplaceString);
}
}
SpannableString spannableString = new SpannableString(content);
//處理表情相關
String emoji_string = "\\[(.+?)\\]";
Pattern emoji_patten = Pattern.compile(emoji_string);
Matcher matcher = emoji_patten.matcher(content);
while (matcher.find()) {
Drawable drawable = mContext.getResources().getDrawable(R.mipmap.emoji_weixiao);
drawable.setBounds(0, 0, Util.dp2px(mContext, 20), Util.dp2px(mContext, 20));
ImageSpan imgSpan = new ImageSpan(drawable);
spannableString.setSpan(imgSpan, matcher.start(),
matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (urlList.size() > 0) {
int urlStartNew = 0;
int urlStartOld = 0;
String urlTemp = content;
for (int i = 0; i < urlList.size(); i++) {
final String regexUrl = urlList.get(i);
spannableString.setSpan(new ClickableSpan() {
@Override
public void updateDrawState(TextPaint ds) {
// TODO Auto-generated method stub
super.updateDrawState(ds);
ds.setColor(0xff0000ff);
ds.setUnderlineText(false);
}
@Override
public void onClick(View widget) {
Toast.makeText(mContext, regexUrl, Toast.LENGTH_SHORT).show();
}
}, urlStartOld + urlTemp.indexOf(Util.ReplaceString), urlStartOld + urlTemp.indexOf(Util.ReplaceString) + Util.ReplaceString.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Drawable d = getResources().getDrawable(R.mipmap.web_link);
try {
d.setBounds(0, 0, Util.dp2px(mContext, 20), Util.dp2px(mContext, 20));
spannableString.setSpan(new ImageSpan(d), urlStartOld + urlTemp.indexOf(Util.ReplaceString), urlStartOld + urlTemp.indexOf(Util.ReplaceString) + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} catch (Exception e) {
//異常以後,就不加小圖片了
}
setMovementMethod(LinkMovementMethod.getInstance());
urlStartNew = urlTemp.indexOf(Util.ReplaceString) + Util.ReplaceString.length();
urlStartOld += urlStartNew;
urlTemp = urlTemp.substring(urlStartNew);
}
}
Log.e("handleData=spannableString",spannableString+"");
//TODO 下面說到的程式碼,放這裡
setText(spannableString);
}
}
然後,在程式碼中用一下:
public class MainActivity extends Activity {
private MyTextView my_text_view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
my_text_view = (MyTextView) findViewById(R.id.my_text_view);
String s="哈哈哈[\\圖片]噢噢噢http://www.baidu.com僅僅";
my_text_view.handleData(s);
}
}
日誌和效果圖:
08-30 09:54:52.006 11513-11513/? E/handleData=str: 哈哈哈[\圖片]噢噢噢http://www.baidu.com僅僅
08-30 09:54:52.011 11513-11513/? E/handleData=spannableString: 哈哈哈[\圖片]噢噢噢*檢視連結僅僅
說明:我對連結的處理是這樣的,把連結換成“*檢視連結”,然後找到這句話,把其中的星號替換成連結icon
OK,工具都準備好了,現在需要做的,就是在這個基礎上進行文字的擷取。
首先我們要明確的是,要解決什麼問題?
要解決:拿到指定長度的文字,且,不能造成連結的缺失,要保證連結的完整。
從上面程式碼上看出,我們要關係的一句話是“*檢視連結”
思路:如果文字長度>50,取出第50個字,看它是否在“*檢視連結”中,如果在,就加幾個字,補齊。否則,就擷取前50個字,然後後面跟省略號。
現在,我們假設,如果我拿到了第50個字是 *
那麼,如圖:
如果文字長度大於等於 54個字,我們就取前54個字,依次類推,如果第50個字是 “接”,就取前50個字
還有一種特殊情況:第50個字在我們的判斷條件中,如:第50個字是“查”,但是這個字是使用者自輸的,不是因為匹配連結出現的,且文字長度是51,就不能補齊了,就要在這裡擷取
整理後,判斷條件為:
boolean isNeedAppend=false;
int length = spannableString.length();
if (length > 50) {
//比50大,取出第50個字元
CharSequence cs = spannableString.subSequence(49, 50);
Log.e("第50個字",cs+"");
if ("*檢視連結".contains(cs)) {
int cutNum = 0;
if (TextUtils.equals("*", cs) && length >= 54) {
cutNum = 54;
} else if (TextUtils.equals("查", cs) && length >= 53) {
cutNum = 53;
} else if (TextUtils.equals("看", cs) && length >= 52) {
cutNum = 52;
} else if (TextUtils.equals("鏈", cs) && length >= 51) {
cutNum = 51;
} else if (TextUtils.equals("接", cs) && length >= 50) {
cutNum = 50;
}else{
//這種情況是,如果使用者自己輸入了“*檢視連結”或者其中一部分,導致第50個字在判斷裡面,但是後面的文字不夠長,補不齊“檢視連結”
//例如:第50個字是查,但是最長只有51個字
cutNum=50;
}
spannableString = (SpannableString) spannableString.subSequence(0, cutNum);
} else {
//結尾處沒有我們關係的關鍵字
//拿到0-49,共50個字
spannableString = (SpannableString) spannableString.subSequence(0, 50);
}
}
setText(spannableString);
if(isNeedAppend){
append("...");
}
說明:注意一下最後的append的方法,要加省略號,只能這樣加,如果用spannableString+”…”,會導致Textview載入的變成普通的string,之前的匹配,會全部失效
如果,使用者自己輸了關鍵字,出現在位置50,並且足夠長呢?這樣,就有漏洞了,具體的,請看下面的測試資料分析
測試資料
注:連結在替換後,只算5個字“*檢視連結”的長度是5
1、文字不夠50字。
2、文字長度超過50字,且第50個字不是關鍵字
重頭戲來了
3、第50個字是關鍵字,且,這個關鍵字是因為連結匹配出現的,不是使用者自己輸的
4、第50個字是關鍵字,且,這個字是使用者自己輸入的,不是因為連結匹配出現的
5、第50個字是關鍵字,且,這個字是使用者自己輸入的,同時,文字足夠長,不會被擷取前50個字
這種情況,我暫時找不到好的解決辦法,如果有,也只能是不停的迴圈。即:截完以後,在看最後一個字是不是關鍵字,直到找到不是關鍵字的位置或者到文字結尾。我認為這樣太麻煩了,代價有點大。
如果大神有好的解決辦法,請指教!!!