极简构造图文混排消息流

消息流形式的图文混排,最常见的就是微博类应用了,一般包括几个部分:文字和若干张图片;点击条目查看详情;点击某一张图片,滑动查看所有大图;查看原图等。看上去好像东西也不少,其实实现起来极其简单,今天就来记录一下其具体的实现方式。

##图文列表

文字部分没什么好讲的,就是一个 TextView ,直接设置文字即可。重点是图片,图片数量是不确定的,一行一般显示三四个,显示多行,这里基本所有人都能想到用 GridView ,RecyclerView/ListView 里嵌套 GridView ,但是众所周知,这样做其实是不符合 Android 开发规范的,类似的还有 ScrollView 嵌套 ListView 等,虽然网上有很多教程该怎么测量高度,怎么禁止 GridView 或者 ListView 滑动,即使能够解决卡顿问题,可它依旧是不符合规范的。尤其是今年已经是 2015 年,一定不要再用 ListView 取嵌套 GridView 了。

我们可以这样做,因为图片数量不确定,那动态添加图片就可以了。这时,流式布局就应运而生了,具体怎么打造一个流式布局,可以看慕课网的视频打造Android流式布局和热门标签,讲的非常好,这里我们可以直接拿来用。使用 RecyclerView ,在 adapter 里使用 FlowLayout 添加图片即可。

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
if (!TextUtils.isEmpty(images)) {
String url;
// int width = dp2px(70);
String[] imgs = images.split(";");
holder.flImages.setVisibility(View.VISIBLE);
holder.flImages.removeAllViews();
for (int i = 0; i < imgs.length; i++) {
SimpleDraweeView view = (SimpleDraweeView) inflater.inflate(R.layout.images, holder.flImages, false);
url = imgs[i];
Log.i("url", url);
view.setImageURI(Uri.parse(url));//Fresco加载图片
/*ImageView view = new ImageView(context);
ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(width, width);
layoutParams.setMargins(0, 30, 20, 0);
view.setLayoutParams(layoutParams);
Picasso.with(context).load(imgs[i]).skipMemoryCache().resize(width, width).centerInside().into(view);//Picasso加载图片
Bitmap bitmap = getHttpBitmap(imgs[i]);//直接获取流加载图片
view.setImageBitmap(bitmap);*/

final int imgPosition = i;
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(context, ImageZoomActivity.class);
intent.putExtra("IMAGES", item);
intent.putExtra("CURRENT_IMG_POSITION", imgPosition);
context.startActivity(intent);
}
});
holder.flImages.addView(view);
}
} else {
holder.flImages.setVisibility(View.GONE);
}

如果使用 Fresco 加载图片,需要初始化 Fresco.initialize(this); 这样,就可以列表展示图文了。当然,一定不能忘记加上联网权限,我发现一个非常诡异的问题,可能是 Android Studio 的 bug 吧。最新的 AS 在没添加需要的权限的时候,使用一些框架,居然没提示,从昨天下午到今天上午加载图片,换了 Picasso,Fresco,ImageLoader,统统不行,最后实在没法,自己手动处理成流再转成 Bitmap 加载,才发现居然是网络权限没加。而之前换了那么多框架,居然一个都没报错,日志也没任何提示。

##ViewPager 展示大图
这里要用到 PhotoView 这个开源库,可以双击放大,初始化 PageAdapter 的时候,添加跟图片地址数量相等的 PhotoView 到 ViewPager 即可。

1
2
3
4
5
6
7
8
9
public ImagePageAdapter(List<String> imageList) {
this.imageList = imageList;
int size = imageList.size();
for (int i = 0; i != size; i++) {
final PhotoView iv = new PhotoView(ImageZoomActivity.this);
iv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
mViews.add(iv);
}
}

然后就是加载大图片,重写 instantiateItem 方法即可。

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
public Object instantiateItem(View arg0, int arg1) {
final PhotoView photoView = mViews.get(arg1);
photoView.setMaximumScale(2.5f);
photoView.setMinimumScale(1f);
photoView.setMediumScale(1.5f);
photoView.setScale(1.0f, true);

final String image = imageList.get(arg1);
if (!TextUtils.isEmpty(image)) {

if (mDialog == null) {
mDialog = CustomProgressDialog.createDialog(ImageZoomActivity.this, "");
mDialog.show();
}
Log.e("imageUrl", image);
Picasso.with(ImageZoomActivity.this).load(image)
.transform(new BitmapTransform(MAX_WIDTH, MAX_HEIGHT))
.resize(size, size)
.centerInside()
.into(photoView, new Callback() {
@Override
public void onSuccess() {
if (mDialog != null) {
mDialog.dismiss();
}
}

@Override
public void onError() {
if (mDialog != null) {
mDialog.dismiss();
}
}
});
}
((ViewPager) arg0).addView(photoView);

return photoView;
}

接着重写完 PagerAdapter 其他必须要重写的方法,就完成了大图片的加载。这里为什么不是加载原图?当然是为了避免 OOM ,ViewPager 的预加载不仅会很慢,而且还很卡,甚至会 OOM ,因此在这里图片上加一个点击查看原图的按钮,在新的界面加载原图。

##加载原图
这里就没什么好讲的了,同样使用 Picasso ,只不过传入的参数不一样。

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
if (!TextUtils.isEmpty(imageUrl)) {
photoView.setMaximumScale(2.5f);
photoView.setMinimumScale(1f);
photoView.setMediumScale(1.5f);
photoView.setScale(1.0f, true);

if (mDialog == null) {
mDialog = CustomProgressDialog.createDialog(ImageOrginActivity.this, "");
mDialog.show();
}
Log.e("imageUrl", imageUrl);
Picasso.with(ImageOrginActivity.this).load(imageUrl).into(photoView, new Callback() {
@Override
public void onSuccess() {
if (mDialog != null) {
mDialog.dismiss();
}
}

@Override
public void onError() {
if (mDialog != null) {
mDialog.dismiss();
}
}
});

}

再增加一个轻触退出浏览的方法:

1
2
3
4
5
6
photoView.setOnViewTapListener(new PhotoViewAttacher.OnViewTapListener() {
@Override
public void onViewTap(View view, float x, float y) {
finish();
}
});

然后是长按保存到本地文件夹:

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
photoView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
final String[] strings = new String[]{"保存到手机"};
AlertDialog.Builder builder = new AlertDialog.Builder(ImageOrginActivity.this);
builder.setItems(strings, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
photoView.buildDrawingCache();
Bitmap bitmap = photoView.getDrawingCache();
savePhoto(bitmap, getCurrentTime() + ".jpg");
Toast.makeText(ImageOrginActivity.this, "图片保存成功", Toast.LENGTH_SHORT).show();
}
}).show();

return false;
}
});


private String savePhoto(Bitmap bmp, String fileName) {

File appDir = new File(Environment.getExternalStorageDirectory(), "photolistdemo"); //创建文件夹
if (!appDir.exists()) {
appDir.mkdir();
}
File file = new File(appDir, fileName);
try {
FileOutputStream fos = new FileOutputStream(file);
bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.flush();
fos.close();
MediaScannerConnection.scanFile(ImageOrginActivity.this,
new String[]{file.toString()}, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
Log.i("ExternalStorage", "Scanned " + path + ":");
Log.i("ExternalStorage", "-> uri=" + uri);
}
});

} catch (Exception e) {
e.printStackTrace();
}
return appDir.toString();
}


private String getCurrentTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
return sdf.format(new Date());
}

至此,一个相对完善,实现起来也极其简单的图文信息流就完成了。具体的代码放在这里PhotoListDemo ,使用了 Picasso PhotoView Fresco 这三个库,感谢作者。