2019-06-25 Edittext提交处理换行和空格,兼容@

2019-06-25  本文已影响0人  兣甅

1.需要实现的效果

(1)输入框开头禁止输入空字符
(2)提交时增加多个换行和空格的情况处理优化为1个换行或者2个换行
(3)末尾的空字符要处理掉,但是如果最后是@附带的空格不进行处理

2.使用方法

(1)为了防止将前面的字符删除后空字符跑到第一位,所以需要添加监听


监听到第一位是空字符进行处理

(2)为了保证提交的内容和@的内容位置一致,需要成对的调用


处理为2个换行的情况
处理为1个换行的情况

附:处理换行+空格的源代码:

处理的工具
package com.aimymusic.android.utils

import android.text.*
import android.widget.EditText
import com.aimymusic.ambase.extention.toast
import com.aimymusic.android.application.App
import com.aimymusic.android.comm.ui.view.spedit.mention.data.MentionUser
import com.aimymusic.android.comm.ui.view.spedit.view.SpXEditText
import com.aimymusic.android.repository.bean.dyn.AtBean
import java.util.ArrayList

/**
 * Created by sunhapper on 2019/1/29 .
 */
class SpeditUtil private constructor() {
  private object SingletonHolder {
    val holder = SpeditUtil()
  }

  companion object {
    val instance = SingletonHolder.holder
  }

  /**
   * 插入@用户的信息
   */
  fun insertUser(
    editText: SpXEditText,
    userName: String,
    uid: Long,
    maxLen: Int
  ) {
    editText.text?.let { ed ->
      val insertUser = MentionUser(userName, uid)
      if (maxLen - AimyInputHelper.getRealLength(ed.toString()) <
          AimyInputHelper.getRealLength(insertUser.displayText)
      ) {
        App.INSTANCE.toast("字数超出限制")
      } else if (!hasIn(editText, uid)) {
        insertUserSpan(ed, insertUser.spanStringFore)
      } else {
        App.INSTANCE.toast("你已经@过Ta啦!")
      }
    }
  }

  /**
   * 获取@列表,在输入框内容处理为一行输入框后的@数据,需要配合getEditTextEnter1使用
   */
  fun getAtList1Enter(editText: SpXEditText): List<AtBean> {
    return getAtList(editText, true)
  }

  /**
   * 获取@列表,在输入框内容处理为一行输入框后的@数据,需要配合getEditTextEnter2使用
   */
  fun getAtList2Enter(editText: SpXEditText): List<AtBean> {
    return getAtList(editText, false)
  }

  /**
   * 获取多个回车合并为1个回车的字符串
   */
  fun getEditTextEnter1(editText: EditText): Editable {
    return getEditText(editText, true)
  }

  /**
   * 获取多个回车合并为2个回车的字符串
   */
  fun getEditTextEnter2(editText: EditText): Editable {
    return getEditText(editText, false)
  }

  /**使用AimyInputHelper会导致输入2个英文字母无法输入,删除@的时候还出现@效果消失的情况,所以单独写一个*/
  fun setInputFilter(
    edit: SpXEditText,
    maxLen: Int
  ) {
    edit.filters = arrayOf(SpeditFilter(maxLen))
  }

  /**
   * 获取at的列表(文字处理后的),需要配合getEditTextEnter1或getEditTextEnter2使用
   */
  private fun getAtList(
    editText: SpXEditText,
    oneEnter: Boolean
  ): List<AtBean> {
    val list = ArrayList<AtBean>()
    val result = if (oneEnter) getEditTextEnter1(editText) else getEditTextEnter2(editText)
    val dataSpans = result.getSpans(0, result.length, MentionUser::class.java)
    for (user in dataSpans) {
      list.add(
          AtBean(
              user.id, result.getSpanStart(user),
              user.displayText.length, 0
          )
      )
    }
    return list
  }

  /**
   * 判断输入框中是否已经存在@的用户
   */
  private fun hasIn(
    editText: SpXEditText,
    uid: Long
  ): Boolean {
    val has = false
    editText.text?.let { ed ->
      val dataSpans = ed.getSpans(0, editText.length(), MentionUser::class.java)
      for ((_, id) in dataSpans) {
        if (id == uid) {
          return true
        }
      }
    }
    return has
  }

  /**
   * 插入@的用户
   */
  private fun insertUserSpan(
    editable: Editable,
    text: CharSequence
  ) {
    var start = Selection.getSelectionStart(editable)
    var end = Selection.getSelectionEnd(editable)
    if (end < start) {
      val temp = start
      start = end
      end = temp
    }
    editable.replace(start, end, text)
  }

  /**
   * 限制输入长度和禁止开头输入空字符
   */
  private class SpeditFilter(max: Int) : InputFilter {
    private val maxLen: Int = max
    override fun filter(
      source: CharSequence?,
      start: Int,
      end: Int,
      dest: Spanned?,
      dstart: Int,
      dend: Int
    ): CharSequence {
      source?.let {
        if (AimyInputHelper.getRealLength(source) +
            (if (dest == null) 0 else AimyInputHelper.getRealLength(dest)) > maxLen
        ) {
          App.INSTANCE.toast("字数超出限制")
          return ""
        }
      }
      return if (dstart == 0 && !source.isNullOrEmpty()) {
        delStartEmptyChar(source)//禁止在开头输入空格和换行
      } else {
        source ?: ""
      }
    }

    /**
     * 去掉前面空字符
     */
    private fun delStartEmptyChar(cs: CharSequence): CharSequence {
      return if (cs.startsWith(" ") || cs.startsWith("\n")) {
        return delStartEmptyChar(cs.subSequence(1, cs.length))
      } else {
        cs
      }
    }
  }

  /**
   * 获取输入框中的字符串,
   * @param oneEnter true多个回车合并成一个,false多个回车合并成2个
   */
  private fun getEditText(
    editText: EditText,
    oneEnter: Boolean
  ): Editable {
    //①去掉前面的空字符
    val cs1 = delStartEmptyChar(editText.editableText)
    //②去掉后面的空字符
    val cs2 = delEndEmptyChar(cs1)
    //③去掉回车间的空字符
    val cs3 = delEnterMidSpace(cs2)
    return if (oneEnter) {
      multiEnterTo1(cs3)
    } else {
      multiEnterTo2(cs3)
    }
  }

  /**
   * 多行回车变成1个回车
   */
  private fun multiEnterTo1(cs: Editable): Editable {
    return if (cs.contains("\n\n")) {//包含多个回车都处理掉
      val index = cs.indexOf("\n\n")
      return multiEnterTo1(cs.replace(index, index + 2, "\n"))
    } else {
      cs
    }
  }

  /**
   * 多行回车变成2个回车(中间空一行)
   */
  private fun multiEnterTo2(cs: Editable): Editable {
    return if (cs.contains("\n\n\n")) {//包含多个回车都处理掉
      val index = cs.indexOf("\n\n\n")
      return multiEnterTo2(cs.replace(index, index + 3, "\n\n"))
    } else {
      cs
    }
  }

  /**
   * 防止出现"回车+空格+回车"
   */
  private fun delEnterMidSpace(cs: Editable): Editable {
    return if (cs.contains("\n")) {
      val delArray = mutableListOf<Pair<Int, Int>>()
      val arrayCS = cs.split("\n")
      val sbTemp = SpannableStringBuilder()
      for (shortCS in arrayCS) {
        if (sbTemp.isNotEmpty()) sbTemp.append("\n")
        if (shortCS.isNotEmpty() && shortCS.trim().isBlank()) {
          delArray.add(Pair(sbTemp.length, sbTemp.length + shortCS.length))
        }
        sbTemp.append(shortCS)
      }
      var csTemp = cs
      for (i in delArray.size - 1 downTo 0) {
        val pair = delArray[i]
        csTemp = csTemp.delete(pair.first, pair.second)
      }
      csTemp
    } else {
      cs
    }
  }

  /**
   * 去掉前面空字符
   */
  fun delStartEmptyChar(cs: Editable): Editable {
    return if (cs.startsWith(" ") || cs.startsWith("\n")) {
      return delStartEmptyChar(cs.delete(0, 1))
    } else {
      cs
    }
  }

  /**
   * 去掉后面空字符
   */
  private fun delEndEmptyChar(cs: Editable): Editable {
    return when {
      //裁掉末尾是回车的字符
      cs.endsWith("\n") ->
        return delEndEmptyChar(cs.delete(cs.length - 1, cs.length))
      //末尾是空格的字符
      cs.endsWith(" ") -> {
        val spans = cs.getSpans(0, cs.length, MentionUser::class.java)
        if (spans.isNotEmpty()) {
          val endStr = spans[spans.size - 1].displayText
          if (cs.endsWith(endStr)) return cs
        }
        //如果是普通的空格,则删除
        delEndEmptyChar(cs.delete(cs.length - 1, cs.length))
      }
      //正常字符,直接返回
      else -> cs
    }
  }
}
AtBean
data class AtBean(
  var uid: Long,
  var index: Int? = 0,
  var len: Int? = 0,
  var type: Int? = 0
)

附@效果:点击查看原文

原文修改代码

(1)MentionUser -->增加选中背景变色

package com.aimymusic.android.comm.ui.view.spedit.mention.data

import android.graphics.Color
import android.text.*
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import com.aimymusic.android.comm.ui.view.spedit.mention.span.BreakableSpan
import com.aimymusic.android.comm.ui.view.spedit.mention.span.IntegratedSpan

/**
 * Created by sunhapper on 2019/1/30 .
 * 使用三星输入法IntegratedSpan完整性不能保证,所以加上BreakableSpan使得@mention完整性被破坏时删除对应span
 */
data class MentionUser(
  var name: String,
  var id: Long = 0
) : IntegratedSpan, BreakableSpan {
  private val colorAt = Color.parseColor("#FF30D18B")
  private val colorBg = Color.parseColor("#3372D2FF")
  private var styleSpanBg: Any = BackgroundColorSpan(colorBg)
  private var styleSpanFore: Any? = null
  var isSpanBg: Boolean = false
  val spanStringFore: Spannable
    get() {
      styleSpanFore = ForegroundColorSpan(colorAt)
      val spannableString = SpannableString(displayText)
      spannableString.setSpan(
          styleSpanFore, 0, spannableString.length,
          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
      )
      spannableString.setSpan(
          this, 0, spannableString.length,
          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
      )
      val stringBuilder = SpannableStringBuilder()
      isSpanBg = false
      return stringBuilder.append(spannableString)
    }
  val spanStringBg: Spannable
    get() {
      styleSpanFore = ForegroundColorSpan(colorAt)
      val spannableString = SpannableString(displayText)
      spannableString.setSpan(
          styleSpanFore, 0, spannableString.length,
          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
      )
      spannableString.setSpan(
          styleSpanBg, 0, spannableString.length,
          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
      )
      spannableString.setSpan(
          this, 0, spannableString.length,
          Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
      )
      val stringBuilder = SpannableStringBuilder()
      isSpanBg = true
      return stringBuilder.append(spannableString)
    }
  val displayText: CharSequence
    get() = "@$name "

  override fun isBreak(text: Spannable): Boolean {
    val spanStart = text.getSpanStart(this)
    val spanEnd = text.getSpanEnd(this)
    val isBreak = spanStart >= 0 && spanEnd >= 0 && !text.subSequence(
        spanStart,
        spanEnd
    ).toString().contentEquals(
        displayText
    )
    if (isBreak && styleSpanFore != null) {
      text.removeSpan(styleSpanFore)
      text.removeSpan(styleSpanBg)
      styleSpanFore = null
    }
    return isBreak
  }

  fun removeBgSpan(text: Spannable): Boolean {
    if (isSpanBg) {
      text.removeSpan(styleSpanBg)
      isSpanBg = false
      return true
    }
    return false
  }
}

(2)SpanChangedWatcher -->去除选中状态

package com.aimymusic.android.comm.ui.view.spedit.mention.watcher;

import android.text.*;
import com.aimymusic.android.comm.ui.view.spedit.mention.data.MentionUser;
import com.aimymusic.android.comm.ui.view.spedit.mention.span.BreakableSpan;
import com.aimymusic.android.comm.ui.view.spedit.mention.span.IntegratedSpan;

/**
 * Created by sunhapper on 2019/1/24 .
 */
public class SpanChangedWatcher implements SpanWatcher {
    @Override
    public void onSpanAdded(Spannable text, Object what, int start, int end) {

    }

    @Override
    public void onSpanRemoved(Spannable text, Object what, int start, int end) {

    }

    @Override
    public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) {
        if (what == Selection.SELECTION_END && oend != nstart) {
            IntegratedSpan[] spans = text.getSpans(nstart, nend, IntegratedSpan.class);
            if (spans != null && spans.length > 0) {
                IntegratedSpan integratedSpan = spans[0];
                int spanStart = text.getSpanStart(integratedSpan);
                int spanEnd = text.getSpanEnd(integratedSpan);
                int index = (Math.abs(nstart - spanEnd) > Math.abs(nstart - spanStart)) ? spanStart : spanEnd;
                Selection.setSelection(text, Selection.getSelectionStart(text), index);
            }
        }
        if (what == Selection.SELECTION_START && oend != nstart) {
            IntegratedSpan[] spans = text.getSpans(nstart, nend, IntegratedSpan.class);
            if (spans != null && spans.length > 0) {
                IntegratedSpan integratedSpan = spans[0];
                int spanStart = text.getSpanStart(integratedSpan);
                int spanEnd = text.getSpanEnd(integratedSpan);
                int index = (Math.abs(nstart - spanEnd) > Math.abs(nstart - spanStart)) ? spanStart : spanEnd;
                Selection.setSelection(text, index, Selection.getSelectionEnd(text));
            }
        }
        if (what instanceof BreakableSpan && ((BreakableSpan) what).isBreak(text)) {
            text.removeSpan(what);
        }
      //去除选中状态
      if (what == Selection.SELECTION_START && ostart != nstart && (nstart == nend)) {
        MentionUser[] spans = text.getSpans(0, text.length(), MentionUser.class);
        for (MentionUser span : spans) {
          if (span.removeBgSpan(text)) break;
        }
      }
    }
}

(3)SelectDeleteKeyEventProxy --> 点击删除时变色

package com.aimymusic.android.comm.ui.view.spedit.view;

import android.text.Editable;
import android.text.Selection;
import android.view.KeyEvent;
import com.aimymusic.android.comm.ui.view.spedit.mention.data.MentionUser;
import com.aimymusic.android.comm.ui.view.spedit.mention.span.IntegratedSpan;

/**
 * Created by sunhapper on 2019/1/25 .
 */
public class SelectDeleteKeyEventProxy implements KeyEventProxy {
    @Override
    public boolean onKeyEvent(KeyEvent keyEvent, Editable text) {
        if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DEL && keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
            int selectionStart = Selection.getSelectionStart(text);
            int selectionEnd = Selection.getSelectionEnd(text);
            if (selectionEnd != selectionStart) {
                return false;
            }
            IntegratedSpan[] integratedSpans = text.getSpans(selectionStart, selectionEnd, IntegratedSpan.class);
            if (integratedSpans != null && integratedSpans.length > 0) {
                IntegratedSpan span = integratedSpans[0];
                int spanStart = text.getSpanStart(span);
                int spanEnd = text.getSpanEnd(span);
                if (spanEnd == selectionStart) {
                    //使用setSelection会造成点击字符串后部光标移到末尾之后再点删除,经常光标会跑到字符串前面而没有选中效果,原因未知
                    //bug环境 小米note miui9.2 android 6.0.1
                    //手上设备不多暂时只有这一台设备出现此问题。。。
                    //Selection.setSelection(text, spanStart, spanEnd);
                    if (span instanceof MentionUser) {
                      if (((MentionUser) span).isSpanBg()) {
                        text.replace(spanStart, spanEnd, "");
                      } else {
                        text.replace(spanStart, spanEnd, ((MentionUser) span).getSpanStringBg());
                      }
                    }
                    return true;
                }
            }
        }
        return false;
    }
}
上一篇 下一篇

猜你喜欢

热点阅读