美文网首页AndroidAndroid干货android项目知识点
微博的文本编辑和显示(emoji表情,@某人、链接高亮点击)

微博的文本编辑和显示(emoji表情,@某人、链接高亮点击)

作者: 恋猫月亮 | 来源:发表于2016-11-14 22:03 被阅读3218次
    日常开发的过程中我们经常会需要实现类似微博的文本输入框,可以自定义的emoji、@某人高亮显示、快捷删除、文本显示表情、@人和链接点解等效果。本人躺尸过各种坑后来一波,废话不说,先看效果:

    </p>
    <p> 大家好,我是废话[]( ̄▽ ̄)*<p>

    效果
    动图效果<( ̄ˇ ̄)/:https://github.com/CarGuo/RickText/blob/master/device-2016-11-10-220253.mp4_1478787046.gif

    Demo 传送门: https://github.com/CarGuo/RickText

    下放开始代码高能预警,主要通过上面demo进行说明,那么来吧,互相伤害(ノಠ益ಠ)ノ彡┻━┻

    </p>

    SmileUtils 自定义表情的输入与显示逻辑的逻辑

    </p>
    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 可以根据设定好的文本长度,对对应的文本进行替换显示。因为考虑到字数限制还有<a>大小</a>问题,下面还有对应参数,大小一般我设置的是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());
    }
    

    3、表情显示框的删除表情

    右下角那个

    一般在表情选择框中,最后面都会有一个返回按键,这个返回的图片资源这里给它取了一个特殊的名字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();
    }
    

    4、批量处理显示文本,适合插入文本到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<String, String> map记录下来这个位置用于后面判断。

    • 判断这个位置下的名字前面是否有@、后面是否有空格或者\b。(这里注意有时候服务端可能把最后一个空格且截取了)

    • 如果符合条件即可用span替换显示。

    • 记录下来这个index为startIndex,下一个循环从这个startIndex开始indexOf的获取。

    • 如果从这个indexOf开始到结束一直没有,那么@名字可能在startIndex前面,所以从0开始重新取index。

    • 如果拿到了index,还需要判断这个index是不是map里已经处理过的,如果是就往后移startIndex再去取一次判断。

    最后设置下方效果来达到点击跳转。

    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 处理@某人的逻辑

    </p>
    这里需要实现的在编辑文本框中需要实现的@某人显示,类似微博Android端的效果需要注意这几个:
    ((ノಠ益ಠ)ノ彡┻━┻哪来那么多问题)

    1)、回退的时候直接删除整个@块。

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

    3)、删除的时候对应删除list里面的id和name。

    4)、不能直接使用Span来改变颜色,不然某些机器中会导致@块后面的字体效果直接变为@一样的样式(目前不知道什么原因)。

    5)、监听输入@符号。

    • 未能实现的是复制的时候微博可以整个复制,不能复制其中文字,如果有知道实现的大神留言指导下~
      (臣妾不知道如何入♀手啊.....((/- -)/)
    好了,开始说实现方法吧:

    </p>
    1、输入文本中的文本格式为<a>@名字\b</a>这个的格式,那么监听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

    也不知道谁规定的,听说最后放个图片是吧~~

    相信我,我现在就这么看着你

    相关文章

      网友评论

      • 3f8e5fc9d706:删除@XX 和话题时候那个背景颜色怎摸换成其他颜色
        恋猫月亮:@陌宇柒_47a5 这个是调用系统的setSelection,应该是主题那里修改
      • 3f8e5fc9d706:删除@XX 和话题时候那个背景颜色怎摸换掉
      • 1e0410bd5f2f:楼主,这个demo使用textView怎么不能展示多个话题状态啊
        恋猫月亮:@徐miss 多话题???
      • 我一定会学会:看着头晕,还是下载demo看看
      • f726002671d9:感觉这么好的库居然这么少关注😎
      • f726002671d9:很实用😎
      • 恋猫月亮:1.0.5 完成所有预计中的功能了
      • 东之城:我用了 html 高亮 但是 无法做到 左右对齐
        东之城:@恋猫月亮 JustifyTextView 这个效果
        恋猫月亮:@东之尘 左右对齐是??
      • 大空ts翼:微博的可以限制选中的,还有删除是整块删除 :joy:
        恋猫月亮:@大空ts翼 这边也是整块删除,就是复制的时候这一块没实现
      • 恋猫月亮:如今我还是很想知道,微博发帖时,@人的时候在编辑框复制,如何限制必须整个复制的?如何实现??有谁知道吗?
        恋猫月亮: @Xerath 感谢提醒,已经修改后集合都项目中😉
        恋猫月亮: @Xerath 不错不错,就是这个😁
        Xerath:你是说一下选中整个span对象吧?可以重写EditText的onSelectionChanged(int selStart, int selEnd)方法,在这里面处理,单击光标落在span外也可以在这里做。具体可以参考https://github.com/luckyandyzhang/MentionEditText。我也是最近看到的:smile:
      • 67bc61b75d7a:有用。😇
        恋猫月亮:@葱鸡波 thx
      • AchillesL:很赞
        恋猫月亮: @AchillesL 谢谢🤗🤗
      • 梦华芳秋:这个好!
        恋猫月亮:@梦华芳秋 😉😉😉
      • 恋猫月亮:新增1.0.3修正了手机号码不显示高亮问题
      • mcp1993:电话号码颜色和点击都有问题
        恋猫月亮: @mcp1993 谢谢提醒,已经提交修复
        恋猫月亮:@mcp1993 感谢提醒,我看看
        恋猫月亮: @mcp1993 具体什么问题?我看看哈
      • 不会飞的小猪:涨知识了
        恋猫月亮: @不会飞的小猪 互相学习🤗

      本文标题:微博的文本编辑和显示(emoji表情,@某人、链接高亮点击)

      本文链接:https://www.haomeiwen.com/subject/ymghpttx.html