EditText实现 @ 功能 (二)

记得两周前写过关于@功能的简单实现, 那次的实现思路是监听删除字符串, 如果删除一个字符并且该字符是unicode编码为 8197 的字符, 就查找@符号并删除整个@字符串. 刚好上周做到了@的需求, 完全推翻了上篇博客的思路

首先说下要实现的功能
  • 输入 @ 字符, 弹出一界面选择好友(监听回调)
  • 选择好友后更新 @ 的内容
  • 删除 @ 内容将一次删除 @+好友名 所有字符
  • @好友名要高亮
  • 要可以点击外部按钮输入 @ 并高亮, 并能区分是点击按钮还是键盘输入 @
  • @好友 之间插入字符 @ 将失效
  • @好友 之间插入 @ , 前一个 @ 将失效
  • 记录 @好友 开始位置和结束位置以及跟用户ID对应, 获取在发送文本时发送至服务器, 以供其他界面展示 @内容
  • 同一个用户id不能多次@

废话不多说, 先上效果图:

在这里插入图片描述

实现思路
  • 继承 TextWatcher , 重写其中方法, 监听字符变化
  • 插入@符号时记录userId以及@长度(length)和开始位置(index), 定义集合, 记录所有@
  • 增删字符时通过监听 EditText 字符变化修改, 修改@对应索引, 当字符变化完毕后更新 spannable 使文字高亮
  • 删除字符增加字符时判断如果变化在@+好友中间, 删除对应@对象
  • 暴露给外部获取@字符信息(userId index 和length)
使用方法:
    private ImpeccableAtTextWatcher atwatcher = null;
    private void initEditText(EditText editText){
        ImpeccableAtTextWatcher.AtListener listener = new ImpeccableAtTextWatcher.AtListener() {
            @Override
            public void triggerAt() {
                //在此处跳转好友列表界面
                //... 从好友界面返回将选择的好友添加至EditText
                //键盘输入@使用该方法添加
                atwatcher.insertTextForAt("张三", 23);

                //外部点击按钮跳转好友列表返回添加使用方法 insertTextForAtIndex()
//                atwatcher.insertTextForAtIndex("张三", 23);
            }
        };
        atwatcher = new ImpeccableAtTextWatcher(editText, Color.RED, listener);
        editText.addTextChangedListener(atwatcher);
    }
代码实现

其实主要就是通过 TextWatcher 的3个监听方法来完成的, 没什么难点, 下边是完整代码:

public class ImpeccableAtTextWatcher implements TextWatcher {
	public static final String TAG = "AtTextWatcher";

	private int color;
	private char atEndFlag = (char) 8197;
	private AtListener mListener;
	private int atIndex = -1;
	private int endFlagIndex = -1;
	private EditText et;
	private List<AtUserBean> callUsers;
	private List<Object> cacheSpans = new ArrayList<>();

	private boolean needNotifyAtColor;
	private boolean closeListener;

	public ImpeccableAtTextWatcher(EditText et, int highLightColor, AtListener listener) {
		this.et = et;
		this.color = highLightColor;
		this.mListener = listener;
	}

	public List<AtUserBean> getCallUsers() {
		return callUsers;
	}

	/**
	 * 点击 @ 按钮使用此方法插入带 @ 的高亮字符
	 */
	public void insertTextForAtIndex(CharSequence nickName, long userId) {
		if (containsUser(userId))//去重逻辑
			return;

		int selectionStart = et.getSelectionStart();
		int selectionEnd = et.getSelectionEnd();

		if (selectionEnd > selectionStart) {
			selectionStart = selectionEnd;
		}
		int atIndex = selectionStart;

		closeListener = true;
		et.getText().insert(atIndex, "@");
		closeListener = false;

		this.atIndex = atIndex;// -- \\外部传入的是光标的位置, 而成员变量为 @ 字符的位置
		StringBuilder sb = new StringBuilder();
		insertTextForAtInternal(et, nickName, sb, userId);
	}

	public boolean containsUser(long userId) {
		if (callUsers == null || callUsers.isEmpty())
			return false;

		for (AtUserBean callUser : callUsers) {
			if (callUser != null && callUser.userId == userId)
				return true;
		}
		return false;
	}

	public void insertTextForAt(CharSequence nickName, long userId) {
		if (atIndex == -1)
			return;

		containsUser(userId);
		StringBuilder sb = new StringBuilder();
		insertTextForAtInternal(et, nickName, sb, userId);
	}

	private void insertTextForAtInternal(EditText et, CharSequence text, StringBuilder sb, long userId) {
		sb.append(text);
		sb.append(atEndFlag);
		text = sb.toString();
/*        SpannableString ss = new SpannableString(text);
        if(color != 0) {
            ss.setSpan(new ForegroundColorSpan(color), 0 , ss.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }*/

		Editable text1 = et.getText();
		int start = atIndex;
		int length = text.length() + 1;
		text1.insert(atIndex + 1, text);
		disposeCallusers(start, length, userId);
		notifyAtColor();
	}

	private void notifyAtColor() {
		needNotifyAtColor = false;
		if (color == 0)
			return;
		Editable text = et.getText();
		if (!cacheSpans.isEmpty()) {
			for (Object span : cacheSpans) {
				text.removeSpan(span);
			}
			cacheSpans.clear();
		}


		if (callUsers == null || callUsers.isEmpty())
			return;

		SpannableStringBuilder sb;
		if (text instanceof SpannableStringBuilder) {
			sb = (SpannableStringBuilder) text;
		} else {
			sb = new SpannableStringBuilder(text);
		}

		for (AtUserBean callUser : callUsers) {
			if (callUser != null) {
				ForegroundColorSpan span = new ForegroundColorSpan(color);
				sb.setSpan(span, callUser.range.getStart(), callUser.range.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
				cacheSpans.add(span);
			}
		}

		if (!(text instanceof SpannableStringBuilder)) {
			et.setText(sb);
		}
	}

	private AtUserBean disposeCallusers(int start, int length, long userId) {
		if (callUsers == null) {
			callUsers = new ArrayList<>();
		}
		AtUserBean atInfo = new AtUserBean(userId, start, length);
		callUsers.add(atInfo);
		return atInfo;
	}

	@Override
	public void beforeTextChanged(CharSequence s, int start, int count, int after) {
		if (count == 1) {//删除一个字符
			char c = s.charAt(start);
			if (c == atEndFlag) {
				endFlagIndex = start;
				return;
			}
		}

		if (count != 0) {//删除字符
			int end = start + count;
			disposeDelAt(start, end);
		}

		if (after != 0) {//插入字符
			//此处传入 count 而不是 end 是因为 beforeTextChanged 时还未真实插入字符, 故 end 无用
			disposeInsertAt(start, after);
		}
	}

	private void disposeInsertAt(int start, int count) {
		if (callUsers == null || callUsers.isEmpty())
			return;

		Iterator<AtUserBean> it = callUsers.iterator();

		while (it.hasNext()) {
			AtUserBean next = it.next();
			if (next == null || next.range == null)
				continue;

			if (start > next.range.getStart() && start < next.range.getEnd()) {
				it.remove();
				needNotifyAtColor = true;
				continue;
			} else {
				//未影响高亮区域, 不做处理
			}

			if (start >= next.range.getEnd()) {
				//未影响该处高亮@字符, 不处理
			} else {
				//受到插入影响, 需要处理
				//如能走到此处, 则插入位置的肯定不在高亮区域内

				//加上插入的字符数量
				next.range.from += count;
			}
		}
	}

	private void disposeDelAt(int start, int end) {
		if (callUsers == null || callUsers.isEmpty())
			return;

		Iterator<AtUserBean> it = callUsers.iterator();

		while (it.hasNext()) {
			AtUserBean next = it.next();
			if (next == null || next.range == null)
				continue;

			if (end <= next.range.getStart() || start >= next.range.getEnd()) {
				//未删除高亮区域, 不做处理
			} else {
				it.remove();
				needNotifyAtColor = true;
				continue;
			}

			if (start >= next.range.getEnd()) {
				//未影响该处高亮@字符, 不处理
			} else {
				//受到删除影响, 需要处理
				//如能走到此处, 则删除的肯定不包含高亮区域

				//减去删除的字符数量
				next.range.from -= (end - start);
			}
		}
	}

	/**
	 * @param s      新文本内容,即文本改变之后的内容
	 * @param start  被修改文本的起始偏移量
	 * @param before 被替换旧文本长度
	 * @param count  替换的新文本长度
	 */
	@Override
	public void onTextChanged(CharSequence s, int start, int before, int count) {
		if (count == 1) {//新增(输入)一个字符
			char c = s.charAt(start);
			if (c == '@') {
				atIndex = start;
				if (mListener != null && !closeListener) {
					mListener.triggerAt();
				}
			}
		}
	}

	@Override
	public void afterTextChanged(Editable s) {
		Log.i(TAG, "afterTextChanged() called with: s = [" + s + "]");
		if (endFlagIndex != -1) {
			int index = endFlagIndex;
			while ((index -= 1) != -1) {
				char c = s.charAt(index);
				if (c == '@') {
					break;
				}
			}
			int endFlagIndex = this.endFlagIndex;
			this.endFlagIndex = -1;
			//endFlagIndex 是@字符串结束符号位置, 所以真实结束位置需要加1
			if (index != -1 && contains(index, endFlagIndex + 1))
				s.delete(index, endFlagIndex);
		}

		if (needNotifyAtColor) {
			notifyAtColor();
		}
	}

	/**
	 * 删除的位置是否包含高亮字符
	 */
	private boolean contains(int start, int end) {
		if (callUsers == null || callUsers.isEmpty())
			return false;

		for (AtUserBean at : callUsers) {
			if (at.range == null)
				continue;
			if (at.range.getStart() == start && at.range.getEnd() == end)
				return true;
		}

		return false;
	}

	/**
	 * 输入 @ 监听
	 */
	public interface AtListener {
		/**
		 * 通过软键盘输入@字符触发的监听
		 */
		void triggerAt();
	}
}

AtUserBean类代码:

public class AtUserBean{
    public long userId;
    public AtRange range;

    public AtUserBean(long userId, int start, int length) {
        this.userId = userId;
        range = new AtRange();
        range.from = start;
        range.length = length;
    }

    public static class AtRange{
        public int from;
        public int length;

        public int getStart(){
            return from;
        }

        public int getEnd(){
            return getStart() + length;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_27070117/article/details/89711428