发布动态的话题?记录EditText中如何输入自定义#话题

前言

之前的文章我们都讲到了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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值