2019-06-25 Edittext提交处理换行和空格,兼容@
2019-06-25 本文已影响0人
兣甅
1.需要实现的效果
(1)输入框开头禁止输入空字符
(2)提交时增加多个换行和空格的情况处理优化为1个换行或者2个换行
(3)末尾的空字符要处理掉,但是如果最后是@附带的空格不进行处理
2.使用方法
(1)为了防止将前面的字符删除后空字符跑到第一位,所以需要添加监听

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


附:处理换行+空格的源代码:
处理的工具
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;
}
}