1. 程式人生 > >展示固定長度文字,SpannableString匹配連結被截斷問題解決

展示固定長度文字,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個字
這裡寫圖片描述
這裡寫圖片描述

這種情況,我暫時找不到好的解決辦法,如果有,也只能是不停的迴圈。即:截完以後,在看最後一個字是不是關鍵字,直到找到不是關鍵字的位置或者到文字結尾。我認為這樣太麻煩了,代價有點大。

如果大神有好的解決辦法,請指教!!!