打造原生的图文混排控件

随着互联网的发展,信息展示的元素越来越丰富,无论是PC端,还是移动端,图文混排已经成为一种通用的信息展示方式,但在各个平台却都没有提供这种原生的控件。为了更方便地在开发中展示丰富的文本信息,便自定义了这个图文混排控件。

思想

普通的图文混排,无非就两种元素:文字和图片。根据这种特点,我们可以自定义协议,如图片的的网址以<img>img_url</img>来引用。根据这种协议就可以将整个文章进行分段,然后动态地创建控件(TextView和ImageView)加载数据即可。

协议

文本不需要协议规定,图片网址用<img>img_url</img>的形式引用(协议自己定义)。
PS:对于文章中文字部分的<img>img_url</img>要做特殊处理。

实现

根据上面的思想,将很容易实现原生的图片混排控件,我们只需要自定义一个LinearLayout, 在其中动态加载布局即可。

step1: 将文章进行分段

没错,使用正则表达式根据上面的协议便可对文章进行分段:

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 final String imageRegex = "<img>(.*?)</img>";

/**
* 设置图文混排控件要显示的内容
* @param content 要显示的内容
*/
public void setContent(String content) {
// 格式化字符串(替换特殊符号)
String text = null;
// 设置子View水平居中
setGravity(Gravity.CENTER_HORIZONTAL);
Pattern pattern = Pattern.compile(imageRegex);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
// 加载文字
text = content.substring(startPos, matcher.start());
if (!TextUtils.isEmpty(text)) {
appendTextView(clearNewlineChar(text));
}
// 加载图片
appendImageView(content.substring(matcher.start() + 5, matcher.end() - 6));
startPos = matcher.end();
}
// 加载最后一个图片后面的文字
text = content.substring(startPos);
if (!TextUtils.isEmpty(text)) {
appendTextView(clearNewlineChar(text));
}
}

step2: 加载分段后的文字部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 动态添加文本内容
* @param content
*/
private void appendTextView(String content) {
if (!TextUtils.isEmpty(content)) {
TextView textView = new TextView(context);
textView.setTextIsSelectable(true);
textView.setText(content);
textView.setGravity(Gravity.LEFT);
textView.getPaint().setTextSize(42);
textView.setLineSpacing(0, 1.4f);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.bottomMargin = dpToPx(12);
params.leftMargin = dpToPx(10);
params.rightMargin = dpToPx(10);
textView.setLayoutParams(params);
addView(textView);
}
}

step3: 加载分段后的图片部分

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 动态添加图片
* @param imageUrl
*/
private void appendImageView(String imageUrl) {
ImageView imageView = new ImageView(context);
final int screenWidth = getDeviceScreenWidth();
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(screenWidth, (int) (screenWidth * 2.0 / 3));
params.bottomMargin = dpToPx(12);
imageView.setLayoutParams(params);
ImageLoader.getInstance().displayImage(imageUrl, imageView);
addView(imageView);
}

源码

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
public class MixedTextImageLayout extends LinearLayout {

private int startPos = 0;
private Context context;
private final String articleRegex = "((<img>(.*?)</img>)|(\\{poi\\}(.*?)\\{/poi\\}))";
private final String imageRegex = "<img>(.*?)</img>";

public MixedTextImageLayout(Context context) {
super(context);
this.context = context;
setOrientation(VERTICAL);
}

public MixedTextImageLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
setOrientation(VERTICAL);
}

public MixedTextImageLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.context = context;
setOrientation(VERTICAL);
}

/**
* 设置图文混排控件要显示的内容
* @param content 要显示的内容
*/
public void setContent(String content) {
// 格式化字符串(替换特殊符号)
String text = null;
setGravity(Gravity.CENTER_HORIZONTAL);
Pattern pattern = Pattern.compile(imageRegex);
Matcher matcher = pattern.matcher(clearNeedlessChars(content));
while (matcher.find()) {
text = content.substring(startPos, matcher.start());
if (!TextUtils.isEmpty(text)) {
appendTextView(clearNewlineChar(text));
}
appendImageView(content.substring(matcher.start() + 5, matcher.end() - 6));
startPos = matcher.end();
}
text = content.substring(startPos);
if (!TextUtils.isEmpty(text)) {
appendTextView(clearNewlineChar(text));
}
}

/**
* 动态添加文本内容
* @param content
*/
private void appendTextView(String content) {
if (!TextUtils.isEmpty(content)) {
TextView textView = new TextView(context);
textView.setTextIsSelectable(true);
textView.setText(content);
textView.setGravity(Gravity.LEFT);
textView.getPaint().setTextSize(42);
textView.setLineSpacing(0, 1.4f);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.bottomMargin = dpToPx(12);
params.leftMargin = dpToPx(10);
params.rightMargin = dpToPx(10);
textView.setLayoutParams(params);
addView(textView);
}
}

/**
* 动态添加图片
* @param imageUrl
*/
private void appendImageView(String imageUrl) {
ImageView imageView = new ImageView(context);
final int screenWidth = getDeviceScreenWidth();
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(screenWidth, (int) (screenWidth * 2.0 / 3));
params.bottomMargin = dpToPx(12);
imageView.setLayoutParams(params);
ImageLoader.getInstance().displayImage(imageUrl, imageView);
addView(imageView);
}

/**
* 清除多余的字符
* @param str
* @return
*/
private String clearNeedlessChars(String str) {
str = str.replaceAll("&amp;","&");
str = str.replaceAll("&quot;","\""); //"
str = str.replaceAll("&nbsp;&nbsp;","\t");// 替换跳格
str = str.replaceAll("&nbsp;"," ");// 替换空格
str = str.replaceAll("&lt;","<");
str = str.replaceAll("&gt;",">");
str = str.replaceAll("\r","");
str = str.replaceAll("\n","");
str = str.replaceAll("\t","");

return str;
}

/**
* 清除多余的尾部换行符 注意:replaceFirst不会替换字符串本身的内容
* @param content
* @return
*/
private String clearNewlineChar(String content) {
int startPos = 0;
int endPos = content.length() - 1;

// 清除文字首部多余的换行符
while (startPos <= endPos) {
if (content.charAt(startPos) == '\n' || content.charAt(startPos) == '\r') {
startPos++;
// 当所有内容都是换行符的情况
if (startPos > endPos) {
content = "";
endPos -= startPos;
break;
}
} else {
// 获取清除后的字符串,并重新设置尾部位置
content = content.substring(startPos);
endPos -= startPos;
break;
}
}
// 清除文字尾部多余的换行符
while (endPos > 0) {
if (content.charAt(endPos) == '\n' || content.charAt(endPos) == '\r') {
endPos--;
} else {
content = content.substring(0, endPos+1);
break;
}
}

return content;
}

/**
* 获取屏幕宽度
* @return
*/
public int getDeviceScreenWidth() {
DisplayMetrics dm = getResources().getDisplayMetrics();
int w = dm.widthPixels;
int h = dm.heightPixels;
return w > h ? h : w;
}

/**
* dp转px
* @param dp
* @return
*/
public int dpToPx(int dp) {
return (int) (getResources().getDisplayMetrics().density * ((float) dp)+0.5);
}
}

使用

在布局文件中定义之后,代码中直接调用setContent()方法设置内容即可。

1
2
mixedLayout = (MixedTextImageLayout) findViewById(R.id.mixed_layout);
mixedLayout.setContent(content);

效果

总结

其实图文混排有多种方式可以实现,最常用的可能就是直接用WebView加载了,简单易用且样式丰富,但是对移动端而言,交互相对较难。所以具体使用哪种方式,还是要根据需求进行取舍。当然,这种方式除了加载文字和图片之外,也可以加载其他布局(需要对代码进行扩展)。

,