最近慢慢习惯了新环境,也渐渐的变得忙碌起来。以前暴雷的事情有同窗仍是比较关注,我想说的是,已经一而再再而三的展期了,老赖加上老赖平台,结果是至关明确的,不说了,说多了都是泪。java
前两天接到一个需求,须要完成如下效果。git
下面是实现的效果图,@用户和连接会高亮显示,能够点击,包含展开和回收功能。如下作了不一样状况下的显示效果:github
Demo下载正则表达式
扫描二维码下载 bash
主流思路有两个:一个是曲线救国,另外一个是对着TextView直接撸。app
用两个TextView来分别显示,上面的主要负责显示内容,下面的负责展开和收回的功能。这种方式实现起来的好处是实现比较简单,缺点是很难作到如图所示在文字的最后添加展开和收回两个字,也就是很难还原设计稿;并且对于内容仍是须要额外处理@用户和连接的操做,不太方便。ide
所谓“对着TextView直接撸”就是自定义View继承TextView,在自定义View里面去处理全部的逻辑,好处是用起来方便点,并且也能尽可能还原设计稿。在这里咱们采用第二种方式,第一种方式提供一个思路,你们感兴趣的能够本身试试。字体
在开始写代码以前,咱们须要考虑几个点ui
其实这个问题算是整个实现中最难的一个吧!在此以前也是让我头疼的一个问题,不事后来我遇到了DynamicLayout,使用它咱们能够获取行的最后位置,行的开始位置,行的行宽以及指定内容的所占的行数。spa
//用来计算内容的大小
DynamicLayout mDynamicLayout =
new DynamicLayout(mFormatData.formatedContent, mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, 1.2f, 0.0f,
true);
//获取行数
int mLineCount = mDynamicLayout.getLineCount();
int index = currentLines - 1;
//获取指定行的最后位置
int endPosition = mDynamicLayout.getLineEnd(index);
//获取指定行的开始位置
int startPosition = mDynamicLayout.getLineStart(index);
//获取指定行的行宽
float lineWidth = mDynamicLayout.getLineWidth(index);
复制代码
下面这个图会对上面的参数进行简单的说明:
/** * 计算原内容被裁剪的长度 * * @param endPosition * @param startPosition * @param lineWidth * @param endStringWith * @param offset * @return */
private int getFitPosition(int endPosition, int startPosition, float lineWidth, float endStringWith, float offset, String aimContent) {
//最后一行须要添加的文字的字数
int position = (int) ((lineWidth - (endStringWith + offset)) * (endPosition - startPosition)/ lineWidth);
if (position < 0) return endPosition;
//计算最后一行须要显示的正文的长度
float measureText = mPaint.measureText(
(aimContent.substring(startPosition, startPosition + position)));
//若是最后一行须要显示的正文的长度比最后一行的长减去“展开”文字的长度要短就能够了 不然加个空格继续算
if (measureText <= lineWidth - endStringWith) {
return startPosition + position;
} else {
return getFitPosition(endPosition, startPosition, lineWidth, endStringWith, offset + mPaint.measureText(" "));
}
}
复制代码
使用正则表达式对原内容进行匹配,下面是正则表达式:
@[\w\p{InCJKUnifiedIdeographs}-]{1,26}
复制代码
将匹配到内容作一下记录,最后再使用SpannableStringBuilder对匹配到的内容设置可点击的span并设置其余颜色等具体样式。在如下代码中,咱们将匹配到的信息的内容和位置信息保存下来,后面会用到的。对于@用户这块,后面会提到怎么添加高亮显示和添加点击事件。
//对@用户 进行正则匹配
Pattern pattern = Pattern.compile(regexp_mention, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(newResult.toString());
List<FormatData.PositionData> datasMention = new ArrayList<>();
while (matcher.find()) {
//将匹配到的内容进行统计处理
datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), LinkType.MENTION_TYPE));
}
复制代码
在开始的时候,找了不少的匹配文字中连接的正则表达式,后来发现好多都有问题。联想到TextView自己就有对连接跳转的支持,就想着TextView的内部必定有相关的正则来匹配,后来查看TextView的源码,发现还真有。
对于连接,后面会提到怎么添加高亮显示和添加点击事件。下面是匹配连接的代码:
List<FormatData.PositionData> datas = new ArrayList<>();
//对连接进行正则匹配
Pattern pattern = AUTOLINK_WEB_URL;
Matcher matcher = pattern.matcher(content);
StringBuffer newResult = new StringBuffer();
int start = 0;
int end = 0;
int temp = 0;
while (matcher.find()) {
start = matcher.start();
end = matcher.end();
newResult.append(content.toString().substring(temp, start));
//将匹配到的内容进行统计处理
datas.add(new FormatData.PositionData(newResult.length() + 1, newResult.length() + 2 + TARGET.length(), matcher.group(), LinkType.LINK_TYPE));
newResult.append(" " + TARGET + " ");
temp = end;
}
复制代码
除了对连接进行匹配之外,咱们还须要将识别到的连接用掩码隐藏起来。如何掩码呢?也就是把原文中的连接用“网页连接”替换掉。那么如何替换掉呢?上面的代码中咱们会获取到对应的连接以及连接所在的位置,那么咱们只须要使用“网页连接”替换掉匹配到的连接便可。
//newResult是最终会显示在页面上的内容容器
newResult.append(content.toString().substring(end, content.toString().length()));
复制代码
对于@用户,连接和“展开”或者“收回”三者的实现,最终都是使用SpannableStringBuilder来处理。以前咱们在对原内容进行解析的时候,将匹配到的连接或者@用户进行了存储,而且存储了他们所在的位置(start,end)以及类型。
//定义类型的枚举类型
public enum LinkType {
//普通连接
LINK_TYPE,
//@用户
MENTION_TYPE
}
复制代码
有了这些数据的集合,咱们只须要遍历这些数据,并分别对这些数据进行setSpan处理,而且在setSpan的过程当中设置字体颜色,以及点击事件的回调便可。
//处理连接或者@用户
private void dealLinksOrMention(FormatData formatData,SpannableStringBuilder ssb) {
List<FormatData.PositionData> positionDatas = formatData.getPositionDatas();
HH:
for (FormatData.PositionData data : positionDatas) {
if (data.getType().equals(LinkType.LINK_TYPE)) {
int fitPosition = ssb.length() - getHideEndContent().length();
if (data.getStart() < fitPosition) {
SelfImageSpan imageSpan = new SelfImageSpan(mLinkDrawable, ImageSpan.ALIGN_BASELINE);
//设置连接图标
ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//设置连接文字样式
int endPosition = data.getEnd();
if (fitPosition > data.getStart() + 1 && fitPosition < data.getEnd()) {
endPosition = fitPosition;
}
if (data.getStart() + 1 < fitPosition) {
ssb.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
if (linkClickListener != null)
linkClickListener.onLinkClickListener(LinkType.LINK_TYPE, data.getUrl());
}
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(mLinkTextColor);
ds.setUnderlineText(false);
}
}, data.getStart() + 1, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
} else {
int fitPosition = ssb.length() - getHideEndContent().length();
if (data.getStart() < fitPosition) {
int endPosition = data.getEnd();
if (fitPosition < data.getEnd()) {
endPosition = fitPosition;
}
ssb.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
if (linkClickListener != null)
linkClickListener.onLinkClickListener(LinkType.MENTION_TYPE, data.getUrl());
}
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(mLinkTextColor);
ds.setUnderlineText(false);
}
}, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
}
}
/** * 设置 "展开" * @param ssb * @param formatData */
private void setExpandSpan(SpannableStringBuilder ssb,FormatData formatData){
int index = currentLines - 1;
int endPosition = mDynamicLayout.getLineEnd(index);
int startPosition = mDynamicLayout.getLineStart(index);
float lineWidth = mDynamicLayout.getLineWidth(index);
String endString = getHideEndContent();
//计算原内容被截取的位置下标
int fitPosition =
getFitPosition(endPosition, startPosition, lineWidth, mPaint.measureText(endString), 0);
ssb.append(formatData.formatedContent.substring(0, fitPosition));
//在被截断的文字后面添加 展开 文字
ssb.append(endString);
int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length();
ssb.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
action();
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mExpandTextColor);
ds.setUnderlineText(false);
}
}, ssb.length() - TEXT_EXPEND.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
复制代码
在处理这一块的时候有个细节须要注意,那就是假如在文字切割后的末尾正好有个一个连接,而这个地方又要显示“展开”或者“收回”,这个地方要特别注意连接setSpan的范围,一不注意就可能连同把后面的“展开”或者“收回”也一块儿设置了,致使事件不对。处理“收回”是差很少的,就不贴代码了。最后还有一个附加功能就是在最后添加时间串的功能,其实也就是在“展开”和“收回”前面加一个串,作好这方面的判断就行了,代码里面已经作了处理。具体能够去Github上面去看。
Github地址: ExpandableTextView
若是链接失效就直接点击这个连接吧!github.com/MZCretin/Ex…
您的star就是对我最大的鼓励!
我就是比较喜欢用代码解决生活中的问题,感受很开心,哈哈哈。也但愿你们关注个人简书,掘金,Github和CSDN。
简书首页,连接是 www.jianshu.com/u/123f97613…
掘金首页,连接是 juejin.im/user/5838d5…
Github首页,连接是 github.com/MZCretin
CSDN首页,连接是 blog.csdn.net/u010998327
我是Cretin,一个可爱的小男孩。