自定義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

相關文章
相關標籤/搜索