概述
在使用微博的时候,我们都会发现这两个功能
- EditText可输入
@好友
,#话题#
和链接links
- 动态中可展示
@好友
,#话题#
和链接links
很容易想到是通过Span
和 正则表达式
来实现的,但是其中还涉及到了一些细节需要处理
EditText
输入部分的细节功能
- 关键词变色高亮
- 删除的时候要选中整体删除
- 焦点及光标不可以落在关键词中间
- 一般都会带有附加信息
对于最后一点,是基于以下考虑,比如
用户名可相同,id不相同
一般都是类似这样的处理,
<name id="xxx">@杨幂</name>
或者(@杨幂,id=xxx)
根据不同的id,跳转界面,比如用户详情页(
获取详细用户信息
)。
参考实现:
优化实现
最后一点针对MentionEditText
做了一些优化,源码:Mentions中的EditText部分。
抽象其功能主要涉及到这几个方面:
- 界面显示的CharSequence
- 带有附加字段的CharSequence
- 高亮颜色
先来看一下用法,再看实现吧:
- User
public class User implements InsertData{
//...
@Override public CharSequence charSequence() {
return "@"+userName; //provide the CharSequence insert to edittext
}
@Override public FormatRange.FormatData formatData() {
return new UserConvert(this);//provide the formater for the insert data
}
@Override public int color() {
return Color.MAGENTA;//provide the range color
}
private class UserConvert implements FormatRange.FormatData {
public static final String USER_FORMART = "(@%s,id=%s)";
private final User user;
public UserConvert(User user) {
this.user = user;
}
@Override public CharSequence formatCharSequence() {//format
return String.format(USER_FORMART, user.getUserName(), user.getUserId());
}
}
}
- Activity
public class MainActivity extends AppCompatActivity{
@BindView(R.id.mentionedittext) MentionEditText mMentionedittext;
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK && null != data) {
switch (requestCode) {
case REQUEST_USER_APPEND:
User user = (User) data.getSerializableExtra(UserList.RESULT_USER);
mMentionedittext.insert(user);//insert data to edittext
break;
//...
}
}
super.onActivityResult(requestCode, resultCode, data);
}
}
- 获取发送到服务器的数据
CharSequence convertMetionString = mMentionedittext.getFormatCharSequence();// 按照上面的format格式,这里会得到 (@xxx,id=xxx-xx-x)
接下来看实现
!. 我们首先提供一个接口,定义插入的数据,如下:
public interface InsertData {
CharSequence charSequence(); //提供界面显示的CharSequence
FormatRange.FormatData formatData();//提供CharSequence的转换器
int color();//提供高亮显示的颜色
}
!!. 为什么定义这儿接口,先看一下字符的插入过程
// 伪代码
public void insert(InsertData insertData) {
//1.插入需要显示在界面上的CharSequence
//2.将插入的CharSequence及其属性用一个类管理起来
//3.将字符串变色
}
}
如上,第二步中可能需要的信息可能包括: 字符的起始位置,字符,转换后的字符…
但是后面发现,字符接口已经提供
、、、而转换后的字符
(可能需要经过不同的转换),还是提供一个转换器,让用户自己实现。
!!!. 因此有了转换接口,如下:
public interface FormatData {
CharSequence formatCharSequence();//转换为带有特殊字段的CharSequence
}
!!!!. 那么上面的插入代码即可采用如下方式写:
public void insert(InsertData insertData) {
if (null != insertData) {
CharSequence charSequence = insertData.charSequence();
Editable editable = getText();
int start = getSelectionStart();//获取插入的开始位置
int end = start + charSequence.length();//获取文本长度
editable.insert(start, charSequence);//插入
FormatRange.FormatData format = insertData.formatData();
Range range = new FormatRange(start, end, format);
mRangeManager.add(range);//将相关信息存储起来,用于获取转换信息
int color = insertData.color();
editable.setSpan(new ForegroundColorSpan(color), start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);//关键字变色
}
}
!!!!!. 那么我们看一下,上面这些东西如何组合成我们想要的最终信息,上面将Range
信息存储起来了,这里将其连接起来即可构成我们想要的信息,如下:
// mRangeManager中有一个ArrayList<Range>
public CharSequence getFormatCharSequence(String text) {
if (isEmpty()) {
return text;
}
int lastRangeTo = 0;
ArrayList<? extends Range> ranges = get();
Collections.sort(ranges);//Range 实现了Comparable,并且按照 start 排序
StringBuilder builder = new StringBuilder("");
CharSequence newChar;
for (Range range : ranges) {
if (range instanceof FormatRange) {
FormatRange formatRange = (FormatRange) range;
FormatRange.FormatData convert = formatRange.getConvert();
newChar = convert.formatCharSequence();
builder.append(text.substring(lastRangeTo, range.getFrom()));//将第一个 `Range` 之前的 字符县存入
builder.append(newChar); // 将 转换后的字符 存入
lastRangeTo = range.getTo();
}
}
builder.append(text.substring(lastRangeTo));//存入最后一个 `Range` 之后的字符
return builder.toString();
}
如上,即实现了功能。
TextView
因为发出去的数据结构做了一些改变,展示的时候也需要作相应的处理
- 界面不能
将附加信息
显示出来,可以仿照Html
类的实现 - 点击
Span
的时候,获取附加信息,如获取xml
的Attribute
信息 - 点击
Span
之外的地方,响应相应事件,这里有坑,LinkMovementMethod
会拦截事件 - 支持图文混排
emoji
,支持ellipse
,使用SpanableString
时,ellipse
会失效。
参考实现:
优化实现:
其实相比前面的EditText
,这个相对简单多了。
!. 首先定义接口:
public interface ParserConverter {
Spanned convert(CharSequence source);//将CharSequence转化为需要显示的Spanned,类似 Html.fromHtml()
}
!!. MentionTextView
继承 TextView
,通过接口转换
public class MentionTextView extends TextView {
//...
@Override public void setText(CharSequence text, BufferType type) {
if (!TextUtils.isEmpty(text) && null != mParserConverter) {
text = mParserConverter.convert(text);
}
super.setText(text, type);
setMovementMethod(new LinkMovementMethod());
}
//...
}
!!!. 使用
mMentiontextview.setParserConverter(mUserParser);
CharSequence convertMetionString = mMentionedittext.getFormatCharSequence();
mMentiontextview.setText(convertMetionString);
!!! Parser实现
先看一下,我们的Parser
需要实现的功能有如下几个:
- @功能
- #tag#功能
- links功能:如微博将
http链接
替换为网页链接
,并可以跳转
步骤1.
public class LinkUtil {
// 获取网页链接,动态替换
private static final Pattern URL_PATTERN = Pattern.compile(
"((http|https|ftp|ftps):\\/\\/)?([a-zA-Z0-9-]+\\.){1,5}(com|cn|net|org|hk|tw)((\\/(\\w|-)+(\\.([a-zA-Z]+))?)+)?(\\/)?(\\??([\\.%:a-zA-Z0-9_-]+=[#\\.%:a-zA-Z0-9_-]+(&)?)+)?");
public static String replaceUrl(String source) {
Matcher matcher = URL_PATTERN.matcher(source);
if (matcher.find()) {
String url = matcher.group();
source = source.replace(url, "<a href=" + "\'" + url + "\'" + ">网页链接</a>");
}
return source;
}
}
步骤2:
public class Parser implements ParserConverter {
public Parser() {
}
@Override public Spanned convert(CharSequence source) {
if (TextUtils.isEmpty(source)) return new SpannableString("");
String sourceString = source.toString();
sourceString = LinkUtil.replaceUrl(sourceString);
return Html.fromHtml(sourceString, null, new HtmlTagHandler());
}
}
如上,功能实现,详细代码见: Mentions