微博的文本編輯和顯示(emoji表情,@某人、鏈接高亮點擊)

 日常開發的過程中我們經常會需要實現類似微博的文本輸入框,可以自定義的emoji、@某人高亮顯示、快捷刪除、文本顯示錶情、@人和鏈接點解等效果。本人躺屍過各種坑後來一波,廢話不說,先看效果:

 

 

Demo 傳送門: https://github.com/CarGuo/RickText


下放開始代碼高能預警,主要通過上面demo進行說明,那麼來吧,互相傷害。(ノಠ益ಠ)ノ彡┻━┻


SmileUtils 自定義表情的輸入與顯示邏輯的邏輯

 
1、開始

  首先你的先有一個女朋友,額···是需要定義一個Map來關聯特殊的文本格式和表情資源

private static final Map<Pattern, Integer> emoticons = new HashMap<Pattern, Integer>();

 將文本作爲一個正則匹配的項,然後作爲一個key存入到Map中,對應關聯表情的圖片資源R.drawable.xxx。
這樣可以更快速的配♂對

emoticons .put(Pattern.compile(Pattern.quote(smile.get(i))), resource.get(i));

 參考【chao xi】其他APP的做法,一般文本使用[xxx]這樣的方式,表情也是對應使用xx1-xxx100這樣的命令,可以方便操作,這樣我們就得到了一個關聯了表情和文本的Map了\(^o^)/目前還不能吃。

2、獲取文本對應的表情資源用於顯示

 正常情況下,我們都需要一個類似GridView一樣的控件來顯示錶情,點擊對應的表情,獲取Map關聯的文本,然後顯示的時候,通過[xxx]這樣的文本來獲取到對應的表情。

/**
 * * 文本對應的資源
 * * @param string 需要轉化文本 
 * * @return 
 */
public static int getRedId(String string) {    
  for (Map.Entry<Pattern, Integer> entry : emoticons.entrySet()) {       
     Matcher matcher = entry.getKey().matcher(string);        
          while (matcher.find()) {            
              return entry.getValue();       
          }   
      }    
      return -1;
}

 這裏通過對對應的輸入文本正則取的對應的表情id,就這麼取出來了。

3、將表情插♂入到輸入框裏(。・・)ノ

 對gridView增加了item點擊事件,根據點擊的文本,轉化爲表情資源,然後生成ImageSpan,加入到Spannable裏面。

 ImageSpan是什麼鬼?

 ImageSpan 可以根據設定好的文本長度,對對應的文本進行替換顯示。因爲考慮到字數限制還有大小問題,下面還有對應參數,大小一般我設置的是20dp(夠大了吧= =),插♀入的時候注意當前的光標位置喲,而Android的文本輸入框一般對於ImageSpan 的回退都是整個刪除的。

 之後SpannableString來存儲對應的ImageSpan 和文本中間的關係,最後利用SpannableStringBuilder 將生成好的SpannableString插入到輸入框中。

/**
 * 文本轉化表情處理
 *
 * @param editText  要顯示的EditText
 * @param maxLength 最長高度
 * @param size      顯示大小
 * @param name      需要轉化的文本
 */
public static void insertIcon(EditText editText, int maxLength, int size, String name) {

    String curString = editText.toString();
    if ((curString.length() + name.length()) > maxLength) {
        return;
    }

    int resId = SmileUtils.getRedId(name);

    Drawable drawable = editText.getResources().getDrawable(resId);
    if (drawable == null)
        return;

    drawable.setBounds(0, 0, size, size);//這裏設置圖片的大小
    ImageSpan imageSpan = new ImageSpan(drawable);
    SpannableString spannableString = new SpannableString(name);
    spannableString.setSpan(imageSpan, 0, spannableString.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);


    int index = Math.max(editText.getSelectionStart(), 0);
    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(editText.getText());
    spannableStringBuilder.insert(index, spannableString);

    editText.setText(spannableStringBuilder);
    editText.setSelection(index + spannableString.length());
}

4、表情顯示框的刪除表情
 
右下角那個

 一般在表情選擇框中,最後面都會有一個返回按鍵,這個返回的圖片資源這裏給它取了一個特殊的名字delete_expression,在每一頁的最後一個加上它,同時對於這個按鍵的點擊做特殊的處理:

 這裏判斷如果是返回按鍵圖片的話,每次逐個刪除[xxx]這樣的塊

String filename = smileImageExpressionAdapter.getItem(position);
try {
    if (filename != "delete_expression") { // 不是刪除鍵,顯示錶情
        /**插入表情*/
        SmileUtils.insertIcon(editTextEmoji, 2000, ScreenUtils.dip2px(getContext(), 20), filename);

    } else { // 刪除文字或者表情
        if (!TextUtils.isEmpty(editTextEmoji.getText())) {

            int selectionStart = editTextEmoji.getSelectionStart();// 獲取光標的位置
            if (selectionStart > 0) {
                String body = editTextEmoji.getText().toString();
                String tempStr = body.substring(0, selectionStart);
                int i = tempStr.lastIndexOf("[");// 獲取最後一個表情的位置
                if (i != -1) {
                    CharSequence cs = tempStr.substring(i, selectionStart);
                    if (SmileUtils.containsKey(cs.toString()))
                        editTextEmoji.getEditableText().delete(i, selectionStart);
                    else
                        editTextEmoji.getEditableText().delete(selectionStart - 1,
                                selectionStart);
                } else {
                    editTextEmoji.getEditableText().delete(selectionStart - 1, selectionStart);
                }
            }
        }

    }

} catch (Exception e) {
    e.printStackTrace();
}

5、批量處理顯示文本,適合插入文本到EditText和TextView中
 
 對於文本我們最後都處理爲Spannable 返回,顯示的時候只需要setText即可。

 這裏使用的是通過CharSequence 生成一個新的Spannable ,對這個Spananle進行key的正則匹配一個一個替換需要顯示爲表情的文本。

/**
 * replace existing spannable with smiles
 *
 * @param context   上下文
 * @param spannable 顯示的span
 * @return 是否添加
 */
public static boolean addSmiles(Context context, Spannable spannable) {
    boolean hasChanges = false;
    for (Map.Entry<Pattern, Integer> entry : emoticons.entrySet()) {
        Matcher matcher = entry.getKey().matcher(spannable);
        while (matcher.find()) {
            boolean set = true;
            for (ImageSpan span : spannable.getSpans(matcher.start(),
                    matcher.end(), ImageSpan.class))
                if (spannable.getSpanStart(span) >= matcher.start()
                        && spannable.getSpanEnd(span) <= matcher.end())
                    spannable.removeSpan(span);
                else {
                    set = false;
                    break;
                }
            if (set) {
                hasChanges = true;
                spannable.setSpan(new ImageSpan(context, entry.getValue()),
                        matcher.start(), matcher.end(),
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }
    return hasChanges;
}

public static Spannable getSmiledText(Context context, CharSequence text) {
    Spannable spannable = spannableFactory.newSpannable(text);
    addSmiles(context, spannable);
    return spannable;
}

TextCommonUtils 處理文本顯示的邏輯

1、URL和純數字

 有時候,一個女朋友是不夠的,額···┑( ̄Д  ̄)┍TextView除了顯示錶情之外還需要對URL和手機號碼實現高亮可點擊,這時候就需要在表情之外增加其他的了邏輯了。

 那麼首先再找一個女朋友,設置TextVidew的AutoLinkMask爲系統識別的URL和Phone,這樣系統就會把對應的女朋友(文本)識別出來處理爲Spanable格式

textView.setAutoLinkMask(Linkify.WEB_URLS | Linkify.PHONE_NUMBERS);
textView.setText(spannable);

 之後我們利用這個特性,對TextView的CharSequence 進行判斷

 if (charSequence instanceof Spannable) {
  int end = charSequence.length();
  Spannable sp = (Spannable) textView.getText();
  URLSpan[] urls = sp.getSpans(0, end, URLSpan.class);
   ····
  String urlString = url.getURL();
}

 我們就拿到了系統幫我們處理好的URL和Phone格式的Span和String了,之後我們就可以挑選要睡誰了。

 這裏我們對文本進行二次處理,先是清除了文本原本的樣式變爲處的,然後根據是否要點擊或者特殊顯示處理,替換成我們自己的樣式,我們可以繼承URLSpan,實現一個我們自己的LinkSpan ,這樣就可以實現點擊效果和別的顏色了。

SpannableStringBuilder style = new SpannableStringBuilder(charSequence);
style.clearSpans();// should clear old spans
···
//正常,不要的
style.setSpan(new StyleSpan(Typeface.NORMAL), sp.getSpanStart(url), sp.getSpanEnd(url), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
···
//url點擊的,留下
 LinkSpan linkSpan = new LinkSpan(context, url.getURL(), color, spanUrlCallBack);
 style.setSpan(linkSpan, sp.getSpanStart(url), sp.getSpanEnd(url), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);

 有時候系統的判斷會有一些小誤差,我們可以通過二次判斷來確認是否爲我們的選美

/**
 * 頂級域名判斷;如果要忽略大小寫,可以直接在傳入參數的時候toLowerCase()再做判斷
 * 處理1. 2. 3.識別成鏈接的問題
 *
 * @param str
 * @return 是否符合url
 */
public static boolean isTopURL(String str) {
    String ss[] = str.split("\\.");
    if (ss.length < 3)
        return false;

    return true;

}

/**
 * 是否數字
 *
 * @param str
 * @return 是否數字
 */
public static boolean isNumeric(String str) {
    Pattern pattern = Pattern.compile("[0-9]*");
    Matcher isNum = pattern.matcher(str);
    if (!isNum.matches()) {
        return false;
    }
    return true;
}

 下方這就是一個完整的處理流程,其中還帶有了At某人高亮的邏輯。額,後面要說的,你怎麼就跑出來了。
(ノಠ益ಠ)ノ彡┻━┻

/**
 * 處理帶URL的邏輯
 *
 * @param context         上下文
 * @param textView        需要顯示的view
 * @param spannable       顯示的spananle
 * @param color           需要顯示的顏色
 * @param spanUrlCallBack 鏈接點擊的返回
 * @return 返回顯示的spananle
 */
private static Spannable resolveUrlLogic(Context context, TextView textView, Spannable spannable, int color, SpanUrlCallBack spanUrlCallBack) {
    CharSequence charSequence = textView.getText();
    if (charSequence instanceof Spannable) {
        int end = charSequence.length();
        Spannable sp = (Spannable) textView.getText();
        URLSpan[] urls = sp.getSpans(0, end, URLSpan.class);
        ClickAtUserSpan[] atSpan = sp.getSpans(0, end, ClickAtUserSpan.class);
        if (urls.length > 0) {
            SpannableStringBuilder style = new SpannableStringBuilder(charSequence);
            style.clearSpans();// should clear old spans
            for (URLSpan url : urls) {
                String urlString = url.getURL();
                if (isNumeric(urlString.replace("tel:", ""))) {
                    style.setSpan(new StyleSpan(Typeface.NORMAL), sp.getSpanStart(url), sp.getSpanEnd(url), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
                } else if (isTopURL(urlString.toLowerCase())) {
                    LinkSpan linkSpan = new LinkSpan(context, url.getURL(), color, spanUrlCallBack);
                    style.setSpan(linkSpan, sp.getSpanStart(url), sp.getSpanEnd(url), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
                } else {
                    style.setSpan(new StyleSpan(Typeface.NORMAL), sp.getSpanStart(url), sp.getSpanEnd(url), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
                }

            }
            for (ClickAtUserSpan atUserSpan : atSpan) {
                style.setSpan(atUserSpan, sp.getSpanStart(atUserSpan), sp.getSpanEnd(atUserSpan), Spanned.SPAN_MARK_POINT);
            }
            SmileUtils.addSmiles(context, style);
            textView.setAutoLinkMask(0);
            return style;
        } else {
            return spannable;
        }
    } else {
        return spannable;
    }
}

2、TextView的@某人顯示效果

 如同上面處理的邏輯,@某人使用的也是一種自定的Span,繼承了ClickableSpan,所以上面在清除樣式後要恢復到原來的狀態。所以@某人和url的顯示有着一個正宮和二奶的關係,這裏是如果@某人和url衝突,優先顯示@人的效果。

 目前@某人的判斷邏輯和微博的還不大一樣(其實我也想一樣的 ̄へ ̄),微博是拿用戶的暱稱直接作爲id可以把帶@直接用正則判斷顯示高亮,而這裏用的是用戶暱稱和用戶id綁定後判斷文本里是否有需要高亮顯示,用的是@xxx (@xxx加一個空格)或者@xxx\b這樣的固定格式。

這裏需要注意的邏輯是

  • @人的在文本中出現的順序和返回的List順序不一定一致
  • @同一個人的名字可能出現多次

所以找女朋友還是以這裏以返回的人list爲主,一個一個到文本中去配對吧。

具體邏輯是

  • 首先通過String的indexOf來判斷文本中是否有該名字的存在(index),首先從0的偏移開始。

  • 如果識別到了,那麼就將這個位置用 Map

textView.setMovementMethod(LinkMovementMethod.getInstance());
整個處理代碼(╮(╯_╰)╭我已經寫的無力了):
/**
 * AT某人的跳轉
 *
 * @param context            上下文
 * @param listUser           需要顯示的AT某人
 * @param content            需要處理的文本
 * @param textView           需要顯示的view
 * @param clickable          AT某人是否可以點擊
 * @param color              需要顯示的顏色
 * @param spanAtUserCallBack AT某人點擊的返回
 * @return 返回顯示的spananle
 */
public static Spannable getAtText(Context context, List<UserModel> listUser, String content, TextView textView, boolean clickable,
                                  int color, SpanAtUserCallBack spanAtUserCallBack) {
    if (listUser == null || listUser.size() <= 0)
        return getEmojiText(context, content);
    Spannable spannableString = new SpannableString(content);
    int indexStart = 0;
    int lenght = content.length();
    boolean hadHighLine = false;
    Map<String, String> map = new HashMap<>();
    for (int i = 0; i < listUser.size(); i++) {
        int index = content.indexOf(listUser.get(i).getUser_name(), indexStart);
        if (index < 0 && indexStart > 0) {
            index = content.indexOf(listUser.get(i).getUser_name());
            if (map.containsKey("" + index)) {
                int tmpIndexStart = (indexStart < lenght) ? Integer.parseInt(map.get("" + index)) : lenght - 1;
                if (tmpIndexStart != indexStart) {
                    indexStart = tmpIndexStart;
                    i--;
                    continue;
                }
            }
        }
        if (index > 0) {
            map.put(index + "", index + "");
            int mathStart = index - 1;
            int indexEnd = index + listUser.get(i).getUser_name().length();
            boolean hadAt = "@".equals(content.substring(mathStart, index));
            int matchEnd = indexEnd + 1;
            if (hadAt && (matchEnd <= lenght || indexEnd == lenght)) {
                if ((indexEnd == lenght) || " ".equals(content.substring(indexEnd, indexEnd + 1)) || "\b".equals(content.substring(indexEnd, indexEnd + 1))) {
                    if (indexEnd > indexStart) {
                        indexStart = indexEnd;
                    }
                    hadHighLine = true;
                    spannableString.setSpan(new ClickAtUserSpan(context, listUser.get(i), color, spanAtUserCallBack), mathStart, (indexEnd == lenght) ? lenght : matchEnd, Spanned.SPAN_MARK_POINT);

                }
            }
        }
    }
    SmileUtils.addSmiles(context, spannableString);
    if (!(textView instanceof EditText) && clickable && hadHighLine)
        textView.setMovementMethod(LinkMovementMethod.getInstance());
    return spannableString;
}

EditTextAtUtils 處理@某人的邏輯

 這裏需要實現的在編輯文本框中需要實現的@某人顯示,類似微博Android端的效果需要注意這幾個:
((ノಠ益ಠ)ノ彡┻━┻哪來那麼多問題)

1)、回退的時候直接刪除整個@塊。

2)、光標不能落入到@塊中,防止在@塊中又插入多一次。

3)、刪除的時候對應刪除list裏面的id和name。

4)、不能直接使用Span來改變顏色,不然某些機器中會導致@塊後面的字體效果直接變爲@一樣的樣式(目前不知道什麼原因)。

5)、監聽輸入@符號。

  • 未能實現的是複製的時候微博可以整個複製,不能複製其中文字,如果有知道實現的大神留言指導下~
    (臣妾不知道如何入♀手啊…..((/- -)/)

好了,開始說實現方法吧:

1、輸入文本中的文本格式爲@名字\b這個的格式,那麼監聽EditText文本變化,判斷如果被刪除的是\b,那麼就把\b到@的文本直接刪除。

2、同樣是在文本框中監聽如果輸入的文本是增加的,而且@符號,那麼就通知跳轉到用戶選擇頁面。

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    beforeCount = s.toString().length();
    if (count == 1) {
        String deleteSb = s.toString().substring(start, start + 1);
        if ("\b".equals(deleteSb)) {
            delIndex = s.toString().lastIndexOf("@", start);
            length = start - delIndex;
        }
    }
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
    String setMsg = s.toString();
    if (delIndex != -1) {
        resolveDeleteName();
        int position = delIndex;
        delIndex = -1;
        editText.getText().replace(position, position + length, "");
        editText.setSelection(position);
    } else {
        if (setMsg.length() >= beforeCount && editText.getSelectionEnd() > 0 && setMsg.charAt(editText.getSelectionEnd() - 1) == '@') {
            if (editTextAtUtilJumpListener != null) {
                editTextAtUtilJumpListener.notifyAt();
            }
        }
    }
}

3、光標處理

 EditText在點擊的時候我們可以獲取到光標落下的位置,這時候我們通過該位置去已有@的list列表裏判斷每個名字所在位置,比對光標位置是不是落在了@塊內,如果是就強行將光標落到@塊的旁邊(= =光標不能插進來)。

if (TextUtils.isEmpty(editText.getText()))
    return;
int selectionStart = editText.getSelectionStart();
if (selectionStart > 0) {
    int lastPos = 0;
    for (int i = 0; i < contactNameList.size(); i++) {
        if ((lastPos = editText.getText().toString().indexOf(
                contactNameList.get(i), lastPos)) != -1) {
            if (selectionStart >= lastPos && selectionStart <= (lastPos + contactNameList.get(i).length())) {
                editText.setSelection(lastPos + contactNameList.get(i).length());
            }
            lastPos += (contactNameList.get(i)).length();
        }
    }
}

4、顯示編輯框中的高亮效果

 這裏不用普通的span,直接使用Html.fromHtml來達到文本變色的效果,將@名字插入到spannableStringBuilder光標的位置中,再在後面補上一個\b。

/**
 * 添加了@的加入
 *
 * @param user_id   用戶id
 * @param user_name 用戶名
 * @param color     類似#f77500的顏色格式
 */
public void resolveText(String user_id, String user_name, String color) {
    contactNameList.add(user_name + "\b");
    contactIdList.add(user_id);

    int index = editText.getSelectionStart();
    SpannableStringBuilder spannableStringBuilder =
            new SpannableStringBuilder(editText.getText());
    //直接用span會導致後面沒文字的時候新輸入的一起變色
    Spanned htmlText = Html.fromHtml(String.format("<font color='%s'>" + user_name + "</font>", color));
    spannableStringBuilder.insert(index, htmlText);
    spannableStringBuilder.insert(index + htmlText.length(), "\b");
    editText.setText(spannableStringBuilder);
    editText.setSelection(index + htmlText.length() + 1);
}

 這是對一連串的輸入文本做@高亮處理(我編不下去了,你們繼續O__O “…)

/**
 * 處理插入的文本
 *
 * @param context  上下文
 * @param text     需要處理的文本
 * @param listUser 需要處理的at某人列表
 * @param editText 需要被插入的editText
 * @param color    類似#f77500的顏色格式
 */
public static void resolveInsertText(Context context, String text, List<UserModel> listUser, String color, EditText editText) {

    //此處保存名字的鍵值
    Map<String, String> names = new HashMap<>();
    if (listUser != null && listUser.size() > 0) {
        for (UserModel userModel : listUser) {
            names.put("@" + userModel.getUser_name(), userModel.getUser_name());
        }
    }
    if (TextUtils.isEmpty(text))
        return;
    //設置表情
    Spannable spannable = TextCommonUtils.getEmojiText(context, text);
    editText.setText(spannable);

    //查找@
    int length = spannable.length();
    Pattern pattern = Pattern.compile("@[^\\s]+\\s?");
    Matcher matcher = pattern.matcher(spannable);
    SpannableStringBuilder spannableStringBuilder =
            new SpannableStringBuilder(spannable);
    for (int i = 0; i < length; i++) {
        if (matcher.find()) {
            String name = text.substring(matcher.start(), matcher.end());
            if (names.containsKey(name.replace("\b", "").replace(" ", ""))) {
                //直接用span會導致後面沒文字的時候新輸入的一起變色
                Spanned htmlText = Html.fromHtml(String.format("<font color='%s'>" + name + "</font>", color));
                spannableStringBuilder.replace(matcher.start(), matcher.start() + name.length(), htmlText);
                int index = matcher.start() + htmlText.length();
                if (index < text.length()) {
                    if (" ".equals(text.subSequence(index - 1, index))) {
                        spannableStringBuilder.replace(index - 1, index, "\b");
                    }
                } else {
                    if (text.substring(index - 1).equals(" ")) {
                        spannableStringBuilder.replace(index - 1, index, "\b");
                    } else {
                        //如果是最後面的沒有空格,補上\b
                        spannableStringBuilder.insert(index, "\b");
                    }
                }
            }
        }
    }
    editText.setText(spannableStringBuilder);
    editText.setSelection(editText.getText().length());
}

就到這了,米娜桑,下面是demo和GitHub,多多指教哈~

Demo : https://github.com/CarGuo/RickText

GitHub : https://github.com/CarGuo

也不知道誰規定的,聽說最後放個圖片是吧~~

相信我,我現在就這麼看着你

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章