前言
之前的文章我们都讲到了WX盆友圈动态列表的效果,九宫格控件的实现【传送门】。那么索性就把发布动态的流程也讲一下。
发布其实就是输入文本框,然后选择图片或视频,然后传到资源服务器,数据提交给后端。主要麻烦的一个点就是我们的输入框文本需要支持话题的发布。
如果已有同样的话题,或者相似的话题,会给出提示是否选择这个话题,输入的话题高亮变色显示。那么难点就是如何在一个EditText中操作话题进行一些逻辑判断了。
大致效果如下:
细分一下技术点,其实就是如何找到EditText文本中的话题,如何改变话题的颜色(前景和背景,看需求而定),如果找到类似的话题我们选择了话题就替换当前的话题,如果删除话题是否需要选中整个话题整体删除。
一、如何找到话题
如何在EditText中找到话题的文本呢?其实我们就是用就是正则表达式,我们发布的话题的规则是#后面加文本。所以我们的正则规则
private final String inputReg = "(\\#[\u4e00-\u9fa5a-zA-Z]+\\d{0,100})[\\w\\s]";
复制代码
当我们找到这个正则之后,我们封装成一个对象,使用一个容器保存起来。
public class TopicBean {
private String topicRule = "#";// 匹配规则
private String topicText;// 高亮文本
public int start;
public int end;
}
复制代码
如何使用这个正则呢?我是在 addTextChangedListener 中,当文本变化的时候去匹配一下当前文本是否匹配到了正则,是否包含话题数据。
private List<TopicBean> mTopicList = new ArrayList<>(); // 话题集合
this.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editablede) {
Editable editable = getText();
int length = editablede.toString().length();
//赋值话题列表数据
Matcher matcher = pattern.matcher(editable);
mTopicList.clear();
while (matcher.find()) {
TopicBean tObject = new TopicBean();
tObject.setTopicText(editable.toString().substring(matcher.start(), matcher.end()).trim());
tObject.start = matcher.start();
tObject.end = matcher.end();
mTopicList.add(tObject);
}
//记录上一次的长度
preTextLength = length;
//刷新页面
refreshEditTextUI(editable.toString());
}
});
复制代码
遍历寻找话题,并且封装为话题对象,放入集合保存,以便于后期取用。
二、如何改变话题颜色
既然我们找到了话题,并且放入了集合保存,我们在显示的时候就需要变色高亮显示出来, refreshEditTextUI(editable.toString());
中我们需要额外的处理话题的前景颜色和背景颜色。
private void refreshEditTextUI(String content) {
/*
* 重新设置span
*/
Editable editable = getText();
int textLength = editable.length();
int findPosition = 0;
if (mTopicList != null && mTopicList.size() > 0) {
for (int i = 0; i < mTopicList.size(); i++) {
final TopicBean object = mTopicList.get(i);
// 文本
String objectText = object.getTopicText();
while (findPosition <= textLength) {
// 获取文本开始下标
findPosition = content.indexOf(objectText, findPosition);
if (findPosition != -1) {
// 设置话题内容前景色高亮
ForegroundColorSpan colorSpan = new ForegroundColorSpan(mForegroundColor);
editable.setSpan(colorSpan, findPosition, findPosition + objectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
findPosition += objectText.length();
} else {
break;
}
}
}
}
}
复制代码
设置颜色的方式我们使用的富文本的方式,转换为 editable 之后我们 setSpan 设置为我们自定义的Span即可,我们的需求是只需要改前景的文本颜色,并没有修改背景颜色,如果有需求大家可以自行修改,加一个 BackgroundColorSpan 即可。
如果对富文本有不了解可以参考我之前的文章【传送门】。
三、是否需要删除整体话题
还是在文本变化的监听中,我们还可以在 afterTextChanged 监听中处理删除话题的逻辑,如果当前是话题,是否需要选中话题。
@Override
public void afterTextChanged(Editable editablede) {
Editable editable = getText();
int length = editablede.toString().length();
//赋值话题列表数据
Matcher matcher = pattern.matcher(editable);
mTopicList.clear();
while (matcher.find()) {
TopicBean tObject = new TopicBean();
tObject.setTopicText(editable.toString().substring(matcher.start(), matcher.end()).trim());
tObject.start = matcher.start();
tObject.end = matcher.end();
mTopicList.add(tObject);
}
Log.w("mTObjectsList", mTopicList.toString());
//先添加话题再处理删除逻辑,只是判断删除#号
if (length < preTextLength) {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
if (getText().length() > 0 && getText().toString().contains("#") && selectionStart > 0 && selectionStart <= getText().length()) {
char charAt = getText().toString().charAt(selectionStart - 1);
String value = String.valueOf(charAt);
if (value.equals("#")) {
ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.BLACK);
editable.setSpan(colorSpan, selectionStart - 1, selectionStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/*
* 如果光标起始和结束不在同一位置,删除文本
*/
if (selectionStart != selectionEnd) {
// 查询文本是否属于话题对象,若是移除列表数据
String tagetText = getText().toString().substring(selectionStart, selectionEnd);
Log.w("tagetText:",tagetText);
for (int i = 0; i < mTopicList.size(); i++) {
TopicBean object = mTopicList.get(i);
if (tagetText.equals(object.getTopicText())) {
mTopicList.remove(object);
}
}
return;
}
int lastPos = 0;
if (mTopicList != null && mTopicList.size() > 0) {
// 遍历判断光标的位置
for (int i = 0; i < mTopicList.size(); i++) {
String objectText = mTopicList.get(i).getTopicText();
lastPos = getText().toString().indexOf(objectText, lastPos);
if (lastPos != -1) {
if (selectionStart != 0 && selectionStart >= lastPos && selectionStart <= (lastPos + objectText.length())) {
// 选中话题
setSelection(lastPos, lastPos + objectText.length());
// 设置背景色
editable.setSpan(new BackgroundColorSpan(mBackgroundColor), lastPos, lastPos + objectText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return;
}
lastPos += objectText.length();
}
}
}
}
复制代码
效果就是当我们删除的文本是话题中的文本的时候,我们就选中整个话题,再次删除就删除整个话题
如果不想整体删除呢?那么只需要把 查询文本是否属于话题对象,若是移除列表数据
以下的代码注释掉即可,就是默认的的删除效果。
四、如何回调当前选中的话题
由于一个EditText中可能有多个话题,我们需要回调的是当前输入的选中的话题,那么就需要拿到当前的光标焦点拿到start位置和我们缓存的集合话题对象中的话题的start做比对。
并且如果有多个话题,EditText中可是可以自由的选择当前的光标位置的,如果要正确的拿到当前选中的话题,我们不能再文本监听中做处理,最好是在 onSelectionChanged 的监听中处理当前的话题。
/**
* 监听光标的位置,若光标处于话题内容中间则移动光标到话题结束位置
*/
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (mTopicList == null || mTopicList.size() == 0) {
if (mListener != null) {
mListener.onTopicRemove();
}
return;
}
int startPosition = 0;
int endPosition = 0;
String objectText = "";
for (int i = 0; i < mTopicList.size(); i++) {
objectText = mTopicList.get(i).getTopicText();
int length = getText().toString().length();
while (true) {
// 获取话题文本开始下标
startPosition = getText().toString().indexOf(objectText, startPosition);
endPosition = startPosition + objectText.length();
if (startPosition == -1) {
break;
}
if (selStart > startPosition && selStart <= endPosition) {
// 若光标处于话题内容中间则移动光标到话题结束位置
// setSelection(endPosition);
//回调出去并调用搜索展示下拉框选择
if (mListener != null) {
String finalObjectText = objectText;
int finalI = i;
CommUtils.getHandler().removeCallbacksAndMessages(null);
CommUtils.getHandler().postDelayed(new Runnable() {
@Override
public void run() {
mListener.onTopicEnter(finalObjectText, finalI, getCurrentCursorLine(selStart));
}
}, 300);
}
break;
} else {
if (mListener != null) {
mListener.onTopicRemove();
}
}
startPosition = endPosition;
}
}
}
//回调
private OnTopicEnterListener mListener;
public void setOnTopicEnterListener(OnTopicEnterListener listener) {
mListener = listener;
}
public interface OnTopicEnterListener {
void onTopicEnter(String topic, int topicPosition, int lines);
void onTopicRemove();
}
复制代码
我们定义了光标的监听,并且定义了回调,当我们输入话题,或者选中话题,我们就能通过接口回调到外部处理。
而外部就是我们弹出的弹窗效果,其实弹窗没有什么神器,就是一个固定宽高的RV列表,当回调了当前的话题,我们查询服务器有没有类似的推荐话题,然后显示到RV即可。
难点就是如果有多行的文本,那么话题选中之后的RV推荐列表该如何显示,这里我用到的是行数,计算出行数,由于行高是固定的,就能计算出RV推荐话题列表的偏移值了。
mPresenter.searchTopic(topic).observe(PostNewsFeedFragment.this, list -> {
if (!CheckUtil.isEmpty(list)) {
//先定位
mTopicLayoutParams.topMargin = CommUtils.dip2px(((lines - 1) 18) + 50);
mRvPopupTopic.setLayoutParams(mTopicLayoutParams);
//展示列表
mRvPopupTopic.setVisibility(View.VISIBLE);
mTopicDatas.clear();
mTopicDatas.addAll(list);
} else {
//如果没有对应的关键字不展示列表
mRvPopupTopic.setVisibility(View.GONE);
mTopicDatas.clear();
}
mTopicAdapter.notifyDataSetChanged();
});
复制代码
效果如下:
如果是第三行,那么推荐的话题列表就会定位到第三行的位置。
那么当我们选择了推荐的话题之后,我们又如何替换到原来的话题为当前选中的话题呢?
五、如何替换新话题
这又是一个新的逻辑,我们需要找到老的话题的位置,然后replace换为新的话题。
//替换其中的一个话题
public void setReplaceTopic(String topic, int position) {
if (CheckUtil.isEmpty(getText().toString())) return;
// 原先内容
Editable editable = getText();
//找到老数据
TopicBean tObject = mTopicList.get(position);
String oldTopic = tObject.getTopicText();
int start = tObject.start;
int end = tObject.end;
// 光标位置
int selectionStart = getSelectionStart();
YYLogUtils.w("替换其中的一个话题:"+topic);
//重新设置焦点之后会自动赋值内存的,不需要手动赋值了
if (selectionStart >= 0 && start >= 0 && end > start) {
//随意插入
editable.replace(start, end, topic + " ");
// 移动光标到添加的内容后面
setSelection(getSelectionStart());
}
}
复制代码
需要注意的是,当我们替换之后,为什么没有赋值存到话题集合中,是因为会触发到文本监听的回到,在内部已经存到容器中了,这里就无需重复存话题对象了。
如此就能达到文章开头的选中效果了。
结语
涉及到的一些知识,文本Span的前景与背景变色,光标的监听,editable的编辑,文本的监听等,整合起来就能实现对应的需求。
总的来说其实也不是很难,明确需求之后分解为一步一步的小需求,然后一步一步的实现小需求,串联起来就是我们最终的效果。
由于一些隐私问题就没有很方便的直接在我的Demo中完整贴出。如果大家对代码有需求的话,全部的代码其实都已经在文中贴出了,大家细心整合一下就是完整的代码了。
作者:newki
链接:https://juejin.cn/post/7153932700066250760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。