自定义EditText轻松实现群聊@说起(@mention) #微博话题#等功能

开发聊天功能,须要在群聊中实现@xxx功能,网上没有找到现成的东西能够直接拿来用的,那就本身撸一个好了react

项目地址 github.com/sunhapper/S…
用法说明 SpEditTool使用指南
欢迎star,提PR、issuegit

ScreenShot

ScreenShot

功能分析

  • 能够插入@xxx这样的特殊字符串
  • 须要有高亮等效果
  • 特殊字符串做为一个总体,要一块儿删除,光标不能进入特殊字符串内部
  • 特殊字符串应当对应一个自定义的数据结构保存@的对象的id,名字等信息

实现思路

继承EditText

原本不想使用继承这样侵入的方式去实现,可是须要监听光标的变化,而sdk并无提供设置光标监听的方法。github

记录特殊字符串的位置和表明的信息

这个是实现功能的关键点,总结了下网上的方案正则表达式

MentionEditTextbash

这个库中使用了正则表达式去匹配字符串中的特殊字符串,并且必须严格的@开头空格结尾,这种方式对于特殊字符串中间带@或者空格的的状况没法处理,对只想把@视为普通字符的状况也没法处理数据结构

RichEditorapp

这个库本身维护了一个List,记录了特殊字符串的内容,在删除或者光标变化时遍历这个List判断光标是否处在特殊字符串的位置 最初本身咋一看以为能够知足需求,在List的元素中加一个字段就能够记录@xxx的数据结构了,可是简单用了以后发现一个很严重的问题:像@11 @1这样前面是相同内容的字符串处理的时候遍历算出的位置是不对的,并且很容易触发setSelection的递归调用致使StackOverflowide

SpEditToolpost

本身写的库,容我自卖自诩一下 这里利用了Spannable的setSpan方法为对应的特殊字符串设置一个Object做为标记,好处有这么两点ui

  • 这个标记的位置是由EditText中的Editable对象来维护的,插入字符,删除特殊字符串位置自动就会变化,虽然偷懒,可是效果不错
  • 由于标记和特殊字符串是一一对应的,因此不管文本框的内容如何变化都不用担忧匹配出错

主要代码:

/**
   * 插入特殊字符串,提供给外部调用
   * @param showContent 特殊字符串显示在文本框中的内容
   * @param rollBack 是否往前删除一个字符,由于@的时候可能留了一个字符在输入框里
   * @param customData 特殊字符串的数据结构
   * @param customSpan 特殊字符串的样式
   */
  public void insertSpecialStr(String showContent, boolean rollBack, Object customData,
      Object customSpan) {
    if (TextUtils.isEmpty(showContent)) {
      return;
    }
    int index = getSelectionStart();
    Editable editable = getText();
    SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(editable);
    //SpData中保存了显示内容和对应数据结构
    SpData spData = new SpData();
    spData.setShowContent(showContent);
    spData.setCustomData(customData);
    SpannableString spannableString = new SpannableString(showContent);
    spannableString
        .setSpan(spData, 0, spannableString.length(),
            SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
    //设置自定义样式
    if (customSpan != null) {
      spannableString
          .setSpan(customSpan, 0, spannableString.length(),
              SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    //是否回删一个字符
    if (rollBack) {
      spannableStringBuilder.delete(index - 1, index);
      index--;
    }
    spannableStringBuilder.insert(index, spannableString);
    setText(spannableStringBuilder);
    //将光标置到插入内容末尾
    setSelection(index + spannableString.length());
  }
复制代码

获取插入的特殊字符串

使用Spanned接口的getSpans方法

public SpData[] getSpDatas() {
    Editable editable = getText();
    SpData[] spanneds = editable.getSpans(0, getText().length(), SpData.class);
    if (spanneds != null && spanneds.length > 0) {
      for (SpData spData : spanneds) {
        int start = editable.getSpanStart(spData);
        int end = editable.getSpanEnd(spData);
        //设置当前特殊字符串的起止位置
        spData.setEnd(end);
        spData.setStart(start);
      }
      sortSpans(editable, spanneds, 0, spanneds.length - 1);//获取到的数据多是没排过序的,因此快排排个序再返回
      return spanneds;
    } else {
      return new SpData[]{};
    }
  }
复制代码

监听光标改变

覆盖onSelectionChanged方法

/**
   * 监听光标位置,对插入的特殊字符一块儿删除
   */
  @Override
  protected void onSelectionChanged(int selStart, int selEnd) {
    super.onSelectionChanged(selStart, selEnd);
    SpData[] spDatas = getSpDatas();
    for (int i = 0; i < spDatas.length; i++) {
      SpData spData = spDatas[i];
      int startPostion = spData.start;
      int endPostion = spData.end;
      if (changeSelection(selStart, selEnd, startPostion, endPostion, false)) {
        return;
      }
    }
  }
复制代码

监听删除事件

使用EditText的setOnKeyListener,监听删除事件,若是碰到特殊字符串总体删除

setOnKeyListener(new OnKeyListener() {
      @Override
      public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
        return onDeleteEvent();
        }

        return false;
      }
    });
复制代码
private boolean onDeleteEvent() {
    int selectionStart = getSelectionStart();
    int selectionEnd = getSelectionEnd();
    if (selectionEnd!=selectionStart){
      return false;
    }
    SpData[] spDatas = getSpDatas();
    for (int i = 0; i < spDatas.length; i++) {
      SpData spData = spDatas[i];
      int rangeStart = spData.start;
      if (selectionStart == spData.end) {
        getEditableText().delete(rangeStart, selectionEnd);
        return true;
      }

    }
    return false;
  }
复制代码

响应文本框中@的输入

EditText能够添加一个TextWatcher监听文本的变化(并非必要的,能够本身在外部处理)

addTextChangedListener(new TextWatcher() {
      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) {
      }

      @Override
      public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
        //reactKeys是须要响应的字符列表,不单单能够响应@
        for (Character character : reactKeys) {
          if (count == 1 && !TextUtils.isEmpty(charSequence)) {
            char mentionChar = charSequence.toString().charAt(start);
            if (character.equals(mentionChar) && mKeyReactListener != null) {
             handKeyReactEvent(character);//在EditText内部,因此用回调的方式通知外部有特殊的字符被输入
              return;
            }
          }
        }
      }

      @Override
      public void afterTextChanged(Editable s) {

      }
    });
复制代码
private void handKeyReactEvent(final Character character) {
    post(new Runnable() {
      @Override
      public void run() {
        mKeyReactListener.onKeyReact(character.toString());
      }
    });
  }
复制代码

Tips:post(Runnable runnabe)

onTextChanged中使用post(Runnable runnabe)去调用外部回调,是由于在onTextChanged执行时,最初插入的@等字符的onSelectionChanged回调还没走

假设输入了@,不使用post(Runnable runnabe),直接调用onKeyReact,在回调中插入@sunhapper字符串并设置光标位置,onSelectionChanged调用顺序为onSelectionChanged(10,10)-->onSelectionChanged(1,1)致使光标位置位于插入字符串前面而不是后面,不符合预期

使用post(Runnable runnabe)可让当前线程的代码执行完再去调用onKeyReact,onSelectionChanged调用顺序为onSelectionChanged(1,1)-->onSelectionChanged(10,10),光标位置符合预期

总结

  • 继承EditText
  • 利用setSpan方法将自定义的数据结构和样式和插入的文本绑定
  • 利用getSpans方法获取插入的数据
  • 监听光标变化,主动改变光标位置,防止光标进入特殊字符串内部
  • 监听删除事件,对特殊字符串总体删除

完成以上几步,一个支持插入@ #话题#等各类要高亮要总体删除的EditText就完成了

欢迎你们使用已有的轮子
项目地址github.com/sunhapper/S… 欢迎star,提PR、issue

相关文章
相关标签/搜索