Android实现抖音话题输入框

在这里插入图片描述

TopicEditText


import android.content.Context;
import android.graphics.Color;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.ReplacementTransformationMethod;
import android.text.style.BackgroundColorSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;


import com.hjq.toast.Toaster;
import com.jrzfveapp.network.api.VideoTagInfo;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * <pre>
 *     抖音话题编辑框
 *     作者:Caowj
 *     日期:2023/3/13 0014 17:24
 * </pre>
 */
public class TopicEditText extends androidx.appcompat.widget.AppCompatEditText {
    private int preTextLength = 0;//EditText之前的长度

    // 话题文本高亮颜色
    private final int mForegroundColor = Color.parseColor("#00F1F5");

    // 话题背景高亮颜色
    private final int mBackgroundColor = Color.parseColor("#EE82EE");

    private final String PREFIX_TAG = "#";
    private final String END_TAG = " ";
    private String tagRegex = DEF_REGEX;

    //    public static final String DEF_REGEX = "#[^#]{1,15}? |#[^#]{1,15}?$";     //默认的正则表达式
    public static final String DEF_REGEX = "#[^#|\\s]+?\\s|#[^#|\\s]+?$";     //默认的正则表达式
    private boolean isChanged;
    /**
     * 通过正则表达式解析文本,提取话题集合
     * 1、包含开头#,不包含末尾空格
     * 2、包含自定义话题
     */
    private List<String> tagList = new ArrayList<>();
    /**
     * 选中的系统提供的话题(有Id,没有#)
     */
    private List<VideoTagInfo> tagInfoList = new ArrayList<>();
    /**
     * 记录选中的话题(包含#)
     */
    private String selectTagStr;

    public void setTagInfoList(List<VideoTagInfo> tagInfoList) {
        this.tagInfoList = tagInfoList;
    }

    public List<VideoTagInfo> getTagInfoList() {
        return tagInfoList;
    }

    public TopicEditText(Context context) {
        super(context);
        initView();
    }

    public TopicEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public TopicEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }


    /**
     * 插入话题
     *
     * @param tagInfo 话题内容(包含#)
     */
    public void insertTopic(VideoTagInfo tagInfo, int maxLength) {
        String topicStr = tagInfo.getContent();
        if (TextUtils.isEmpty(topicStr)) {
            return;
        }
        topicStr = PREFIX_TAG + topicStr;
        int index = getSelectionStart();
        Editable editable = getText();
        if (topicStr.length() >= maxLength - editable.length()) {
            Toaster.show("超出最大长度了~");
            return;
        }

        if (topicStr.length() > 1) {
            editable.insert(index, topicStr.trim() + END_TAG);
            tagInfoList.add(tagInfo);
        } else {
            // 自定义话题(输入#)
            editable.insert(index, topicStr.trim());
        }
        setSelection(index + topicStr.length() + END_TAG.length());
    }

    private void initView() {
        addTextChangedListener(new TextWatcher() {
            
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            
            public void afterTextChanged(Editable editable) {
                if (TextUtils.isEmpty(editable)) {
                    preTextLength = 0;
                    tagList.clear();
                    tagInfoList.clear();
                    return;
                }

                if (isChanged) return;

                String inputStr = editable.toString().replace("#", PREFIX_TAG);

                int length = inputStr.length();
                Log.d("caowj", "\n====afterTextChanged()====length:" + length + ",,preTextLength:" + preTextLength);
                if (length > preTextLength) {
                    // 刚刚执行了增加操作
                    tagList = new ArrayList<>();//提取话题列表
                    if (TextUtils.isEmpty(tagRegex)) tagRegex = DEF_REGEX;
                    Pattern pattern = Pattern.compile(tagRegex);
                    Matcher matcher = pattern.matcher(inputStr);
                    SpannableStringBuilder builder = new SpannableStringBuilder(inputStr);
                    while (matcher.find()) {
                        int pos = matcher.start();
                        int end = matcher.end();
                        Log.d("caowj", "匹配开始位置:" + pos + "-->" + end);
                        String item = matcher.group(0);
                        // 话题颜色高亮
                        SpannableString span = TopicSpan.getSpan(mForegroundColor, item, item, null);
                        builder = builder.delete(pos, pos + span.length());
                        builder.insert(pos, span);
                        tagList.add(item);
                    }

                    if (tagList.isEmpty()) {
                        selectTagStr = "";
                        return;
                    }

                    isChanged = true;
                    int preIndex = getSelectionStart();
                    Log.d("caowj", "设置高亮前:" + getSelectionStart() + "-->" + getSelectionEnd());
                    setText(builder);// 会导致光标回到首位
                    Log.d("caowj", "设置高亮后:" + getSelectionStart() + "-->" + getSelectionEnd());
                    isChanged = false;
                    setSelection(preIndex);//重新setText()后会导致光标回到首位,需要重新设置光标位置。
                } else if (preTextLength > length) {
//                    // 删除数据了,需要更新tagList
//                    tagList = new ArrayList<>();//提取话题列表
//                    if (TextUtils.isEmpty(tagRegex)) tagRegex = DEF_REGEX;
//                    Pattern pattern = Pattern.compile(tagRegex);
//                    Matcher matcher = pattern.matcher(inputStr);
//                    while (matcher.find()) {
//                        String item = matcher.group(0);
//                        tagList.add(item);
//                    }
                }

                preTextLength = length;
            }
        });

        // 替换输入的中文#
        setTransformationMethod(new ReplacementTransformationMethod() {
            
            protected char[] getOriginal() {
                char[] ori = {'#'};
                return ori;
            }

            
            protected char[] getReplacement() {
                char[] rep = {'#'};
                return rep;
            }
        });

        setOnKeyListener(new OnKeyListener() {
            
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DEL) {
                    if (tagList.isEmpty()) {
                        selectTagStr = "";
                        return false;
                    }

                    if (!TextUtils.isEmpty(selectTagStr) && !tagInfoList.isEmpty() && !getText().toString().contains(selectTagStr)) {
                        // 刚刚删除了选中的话题
                        Log.d("caowj", "刚刚删除了官方的话题:" + selectTagStr);
                        for (int i = 0; i < tagInfoList.size(); i++) {
                            if (selectTagStr.replace(PREFIX_TAG, "").trim().equals(tagInfoList.get(i).getContent())) {
                                tagInfoList.remove(i);
                            }
                        }
                    }

                    int selectionStart = getSelectionStart();
                    int selectionEnd = getSelectionEnd();
                    if (selectionStart < selectionEnd) {
                        Log.d("caowj", "删除已选中的部分");
                        return false;
                    }

                    int lastPos = 0;

                    // 遍历判断光标的位置
                    for (int i = 0; i < tagList.size(); i++) {
                        String topicTxt = tagList.get(i);
                        if (!topicTxt.endsWith(END_TAG)) {
                            // 正常删除
                            Log.d("caowj", "正常的删除操作");
                        } else {
                            lastPos = getText().toString().indexOf(topicTxt, lastPos);

                            if (lastPos != -1) {
                                if (selectionStart != 0 && selectionStart > lastPos && selectionStart <= (lastPos + topicTxt.length())) {
                                    Log.d("caowj", "选中了话题:" + topicTxt);
                                    // 选中话题
                                    setSelection(lastPos, lastPos + topicTxt.length());
                                    // 记录选中的话题
                                    selectTagStr = topicTxt;
                                    // 设置背景色
                                    getText().setSpan(new BackgroundColorSpan(mBackgroundColor), lastPos, lastPos + topicTxt.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                                    return true;
                                }
                                lastPos += topicTxt.length();
                            }
                        }
                    }

                }
                return false;
            }
        });
    }

    /**
     * 监听光标的位置,若光标处于话题内容中间则移动光标到话题结束位置
     */
    
    protected void onSelectionChanged(int selStart, int selEnd) {
        super.onSelectionChanged(selStart, selEnd);
        if (selStart != selEnd) {
            Log.d("caowj", "选中了范围,无需移到光标");
            return;
        }
        if (tagList == null || tagList.size() == 0) {
            return;
        }
        Log.d("caowj", "======onSelectionChanged...话题数量:" + tagList.size());
        Log.d("caowj", "\n======onSelectionChanged====selStart=" + selStart + ",,selEnd=" + selEnd);

        int startPosition = 0;
        int endPosition;
        String topicTxt;

        int length = getText().toString().length();
        for (int i = 0; i < tagList.size(); i++) {
            topicTxt = tagList.get(i);

            if (!topicTxt.endsWith(END_TAG)) {
                //  结尾编辑中的话题,无需定位到结尾
                Log.w("caowj", "结尾编辑中的话题");
            } else {
                while (true) {
                    // 获取话题文本开始下标
                    startPosition = getText().toString().indexOf(topicTxt, startPosition);
                    endPosition = startPosition + topicTxt.length();

                    if (startPosition < 0) {
                        break;
                    }
//                    Log.d("caowj", "startPosition=" + startPosition + ",endPosition=" + endPosition);
                    // 若光标处于话题内容中间,则移动光标到话题结束位置
                    if (selStart > startPosition && selStart <= endPosition) {
                        Log.d("caowj", "设置光标位置,length=" + length + ",,startPosition=" + startPosition + ",,endPosition=" + endPosition);

                        setSelection(endPosition);
                        break;
                    }
                    startPosition = endPosition;
                }
            }
        }
    }

    /**
     * 获取话题的列表(包含自定义的话题)
     *
     * @return
     */
    public List<String> getTagList() {
        List list = new ArrayList();
        if (TextUtils.isEmpty(tagRegex)) tagRegex = DEF_REGEX;
        Editable editable = getText();
        Pattern pattern = Pattern.compile(tagRegex);
        Matcher matcher = pattern.matcher(editable);

        if (editable.toString().contains(PREFIX_TAG)) {
            //提取话题列表
            while (matcher.find()) {
                list.add(matcher.group(0).replace(PREFIX_TAG, "").replace(END_TAG, ""));
            }
        }
        return list;
    }
}

TopicSpan

package com.jrzfveapp.widgets;

import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;

/**
 * <pre>
 *     TopicSpan
 *     作者:Caowj
 *     日期:2023/3/13 0014 14:39
 * </pre>
 */
public class TopicSpan<T> extends ClickableSpan {

    int defColor = Color.parseColor("#00F1F5");
    int color = defColor;
    float textSize = -1;
    boolean underline = false;
    boolean bolder = false;
    T data;
    Callback<T> callback;


    public TopicSpan(int color, float textSize, boolean underline, boolean bolder, T data, Callback<T> callback) {
        this.color = color == -1 ? defColor : color;
        this.textSize = textSize;
        this.underline = underline;
        this.bolder = bolder;
        this.data = data;
        this.callback = callback;
    }

    
    public void onClick(View widget) {
        if (callback != null) {
            callback.onClick(data);
        }
    }

    
    public void updateDrawState(TextPaint ds) {
        super.updateDrawState(ds);

        ds.setColor(color);
        if (textSize > -1) {
            ds.setTextSize(textSize);
        }
        ds.setFakeBoldText(bolder);
        ds.setUnderlineText(underline);
        ds.clearShadowLayer();
    }

    public static class Builder<T> {
        int color = Color.parseColor("#1fabf3");
        float textSize = -1;
        boolean underline = false;
        boolean bolder = false;
        T data;
        Callback<T> callback;

        public Builder(int color) {
            this.color = color;
        }

        public Builder color(int color) {
            this.color = color;
            return this;
        }

        public Builder textSize(float textSize) {
            this.textSize = textSize;
            return this;
        }

        public Builder underline(boolean underline) {
            this.underline = underline;
            return this;
        }

        public Builder data(T data) {
            this.data = data;
            return this;
        }

        public Builder bolder(boolean bolder) {
            this.bolder = bolder;
            return this;
        }

        public Builder callback(Callback<T> callback) {
            this.callback = callback;
            return this;
        }

        public TopicSpan build() {
            return new TopicSpan(color, textSize, underline, bolder, data, callback);
        }

    }

    public interface Callback<T> {
        void onClick(T data);
    }

    public static <T> SpannableString getSpan(int color, String displayText, T data, Callback<T> callback) {
        SpannableString span = new SpannableString(displayText);
        TopicSpan tag = new Builder<T>(color)
                .data(data)
                .callback(callback).build();
        span.setSpan(tag, 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return span;
    }

    public static <T> SpannableString getSpan(int color, float textSize, String displayText, boolean bolder, T data, Callback<T> callback) {
        SpannableString span = new SpannableString(displayText);
        TopicSpan tag = new Builder<T>(color)
                .data(data)
                .bolder(bolder)
                .textSize(textSize)
                .callback(callback).build();
        span.setSpan(tag, 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return span;
    }

}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值