单词高亮的TextView控件。额,为什么做这个…. 好吧,之前面试时公司要求的题目

  • 特定词组高亮显示(中文/英文)
  • 单词自动换行
  • 高亮词组保持在同一行显示

继承自 View 实现,文本都是使用画布画上去。使用两支画笔表示默认文本和高亮文本。

文本分组实现

ExtendText 表示文本单元是否高亮

1
2
3
4
5
6
7
8
private class ExtendText {
String textUnit;
boolean isHighlight;
ExtendText(String textUnit, boolean isHighlight) {
this.textUnit = textUnit;
this.isHighlight = isHighlight;
}
}
  1. 将原文中匹配给定的高亮词组的前后加 # 号
  2. 根据 # 号将原文 split 成数组
  3. 遍历数组,构造 ExtendText 数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void setDisplayedText(final String text, final List<String> highlighted) {
if (TextUtils.isEmpty(text)) {
return;
}
String s = text;
String[] words;
extendTexts.clear();
if (highlighted != null) {
for (String str : highlighted) {
s = s.replaceAll(str, "#" + str + "#");
}
words = s.split("#");
for (String str : words) {
boolean isHighlight = highlighted.contains(str);
if (isHighlight) {
ExtendText t = new ExtendText(str, true);
extendTexts.add(t);
} else {
for (String word : Arrays.asList(str.split(" "))) {
ExtendText tt = new ExtendText(word + " ", false);
extendTexts.add(tt);
}
}
}
} else {
words = s.split(" ");
for (String str : words) {
ExtendText t = new ExtendText(str, false);
extendTexts.add(t);
}
}
requestLayout();
invalidate();
}

控件不同情况下尺寸确定

在这里也学习了自定义 View 里的尺寸测量方法

View 的测量模式有3种:

  • UNSPECIFIED: 表示视图的尺寸未指明,比如 wrap_content 模式下,如果此时父容器也是wrap_content(比如父容器的 ScrollView),则需要自己计算 View 的实际占用值
  • AT_MOST: 表示视图的尺寸最多达到多少,比如 match_content,一般取测量值和View实际占用值的最小值
  • EXACTLY: 表示视图的尺寸是确定的,比如 layout_width="100dp",一般直接返回测量值

首先是 onMeasure 里根据测量值和测量模式获取实际需要绘制的宽高

1
2
3
4
5
6
7
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = measureWithSize(MeasureSpec.getSize(widthMeasureSpec), widthMeasureSpec);
width -= (getPaddingRight() + getPaddingLeft());
int height = measureHeightSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}

测量宽度

1
2
3
4
5
6
7
8
9
10
11
12
private int measureWithSize(int defaultSize, int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
return Math.min(defaultSize, specSize);
case MeasureSpec.EXACTLY:
return specSize;
}
return defaultSize;
}

测量高度

高度的测量比宽度多了UNSPECIFIED模式下,自己测量了View需要的高度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private int measureHeightSize(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//确保layout_height为wrap_content时所占高度合适
int wrapHeight = (int) (getPaddingTop() + dfPaint.getTextSize() + getPaddingBottom());
float x_draw = getPaddingLeft();
for (ExtendText t : extendTexts) {
Paint paint = t.isHighlight ? hlPaint : dfPaint;
float textLen = paint.measureText(t.textUnit);
if (x_draw + textLen > width) {
x_draw = getPaddingLeft();
wrapHeight += paint.getTextSize();
}
x_draw += textLen;
}
result = wrapHeight;
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
//父控件为Scrollview情况,直接使用wrapHeight
break;
case MeasureSpec.AT_MOST:
result = Math.min(result, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
}
return result;
}

绘制时自动换行

使用 Pain.measureText测量画笔绘制文本将要的宽度,然后与空间的宽度比较判断是否需要换行,换行就增加 y 方向的坐标值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float x_draw = getPaddingLeft();
float y_draw = getPaddingTop() + dfPaint.getTextSize();
for (ExtendText t : extendTexts) {
Paint paint = t.isHighlight ? hlPaint : dfPaint;
float textLen = paint.measureText(t.textUnit);
if (x_draw + textLen > width) {
x_draw = getPaddingLeft();
y_draw += paint.getTextSize();
}
canvas.drawText(t.textUnit, x_draw, y_draw, paint);
x_draw += textLen;
}
}


View

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!