打造自定义TextView实现文本两端对齐,并实现长按自由选择文字弹出自定义ActionMenu菜单功能
写在前面的话
从安卓自由开发者转为职业开发者也有一年了,期间在CSDN社区受益很多,也很想写一些自己对于android开发的感悟和理解,可是一直迟迟没有下笔,一方面是项目确实很忙,抽不开时间;另一方面鄙人给自己定下的原则是写东西不要泛而烂,一定要是干货。因此一年来虽然积累了很多自己写的东西,却一直很迟疑这些东西写出来会不会没有营养。今年开始鄙人决定开始陆续写一些东西放在这里,写的如何也请各位过客轻拍和指正。
好了,开始这篇文章的话题。去年鄙人开发了一个阅读类的app,处理文本的时候,想要实现两个功能,一个是文本两端对齐,一个是可以自定义选中文字后弹出的菜单。第一个功能其实可以用webview实现,而原生textview并没有两端对齐的特性。第二个功能android其实提供了相关的api(textview的setCustomActionMenuCallBack()方法),但是国内厂商的定制化rom把这个接口屏蔽掉了(经测试nexus5,nexus5x,nexus6p都可以实现调用该方法自定义菜单,而小米,华为,oppo调用这个方法都不起作用);因此不管用webview还是textview都无法满足我想要实现的功能。在查阅了大量资料和博客后,我决定写一个自定义textview实现上面需要的功能。
先看一张效果图:
完成上述功能,主要有以下三个功能点:
- 文字两端对齐处理
- 长按选择文字并高亮文本
- 选中文字后ActionMenu的弹出
由于文章过长,我分成了两篇,这一篇主要讲部分1,2;下一篇将部分3和其它的一些细节。
由于是自定义textview,首先需要继承原生TextView,然后在此基础上重写一系列方法。其实也可以选择继承EditText,因为EditText也是继承自TextView的。选择TextView继承或EditText继承,没有太大的区别,只有一点细节需要处理。在这里我选择了EditText作为SelectableTextView的继承。
在开始自定义之旅之前,我们需要屏蔽掉EditText原生那套文本选择逻辑,不然在功能上会出现冲突。那么要如何屏蔽呢?阅读EditText和TextView源码后发现,通过重写EditText的getDefaultEditable方法,返回false就会把原生自带的ActionMenu屏蔽掉。
@Override
public boolean getDefaultEditable() {
// 返回false,屏蔽掉系统自带的ActionMenu
return false;
}
至于具体的源码解读在这里不做过多篇墨,如果感兴趣可以去读相关源码。
1.文字两端对齐处理
接下来我们讨论第一部分文字两端对齐的处理。如果对自定义view比较熟悉的话,就应该知道这部分需要在onDraw方法里实现对文字的重新绘制以达到两端对齐的效果。首先先介绍一下需要用到的几个辅助类:
- Layout
- StaticLayout
安卓自带的Layout类可以获得一段文本的行数以及每一行对应字符的开始和结束处的位移:
Layout layout = getLayout();
int textTotalCount = layout.getLineCount();
int lineStart = layout.getLineStart(i);
int lineEnd = layout.getLineEnd(i);
而StaticLayout是我们实现两端对齐的关键。其getDesiredWidth()方法可以计算出一段字符串的实际宽度,不包括字符间距。
// 获取每行字符串的宽度(不包括字符间距)
float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint());
介绍完这两个辅助类,文本两端对齐的基本思路就有了,很简单:
循环遍历文本的每一行,计算每一行文本字符串的宽度;
用控件本身宽度减去该行字符串的宽度获得多余的宽度;
将多余的宽度均分给该行每一个字符,得到一个平均补偿宽度;
绘制该行。每绘制一个字符,接着向右偏移一个补偿宽度。
这样就基本实现了文本的两端对齐。下面我们看代码:
/**
* 重绘文字,两端对齐
*
* @param canvas
*/
private void drawTextWithJustify(Canvas canvas) {
// 文字画笔
TextPaint textPaint = getPaint();
textPaint.setColor(getCurrentTextColor());
textPaint.drawableState = getDrawableState();
String text_str = getText().toString();
// 当前所在行的Y向偏移
int currentLineOffsetY = getPaddingTop();
currentLineOffsetY += getTextSize();
Layout layout = getLayout();
//循环每一行,绘制文字
for (int i = 0; i < layout.getLineCount(); i++) {
int lineStart = layout.getLineStart(i);
int lineEnd = layout.getLineEnd(i);
//获取到TextView每行中的内容
String line_str = text_str.substring(lineStart, lineEnd);
// 获取每行字符串的宽度(不包括字符间距)
float desiredWidth = StaticLayout.getDesiredWidth(text_str, lineStart, lineEnd, getPaint());
if (isLineNeedJustify(line_str)) {
//最后一行不需要重绘
if (i == layout.getLineCount() - 1) {
canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint);
} else {
drawJustifyTextForLine(canvas, line_str, desiredWidth, currentLineOffsetY);
}
} else {
canvas.drawText(line_str, getPaddingLeft(), currentLineOffsetY, textPaint);
}
//更新行Y向偏移
currentLineOffsetY += getLineHeight();
}
}
注意,在28行,对每一行做了一个判断,如果该行以换行符结尾,说明这一行是某一段的最后一行了,不需要两端对齐。
第31行,如果不需要两端对齐,直接用canvas的drawText方法绘制该行;
第33行,调用的drawJustifyTextForLine方法就是处理文本两端对齐的方法,看该方法的代码逻辑:
/**
* 重绘此行,两端对齐
*
* @param canvas
* @param line_str 该行所有的文字
* @param desiredWidth 该行每个文字的宽度的总和
*