项目背景
最近在做 Android 项目时,需要实现一个类似微信朋友圈的'发说说'功能。核心需求包括:支持拍照、从图库选择图片、以及多张图片批量上传。市面上很多 App 都有类似功能,但具体实现细节往往各有不同。下面分享一下基于 Retrofit2.0 和 PhotoPicker 库的实现过程。
一、集成图片选择器
本项目使用了一个名为 photopicker 的开源库,它封装了图片选择和预览的功能,支持是否调用相机、选中后预览等操作。
1. 初始化 Intent
在 Activity 中简单配置 PhotoPickerIntent:
PhotoPickerIntent intent = new PhotoPickerIntent(MainActivity.this);
intent.setSelectModel(SelectModel.MULTI); // 多选模式
// intent.setShowCamera(true); // 是否显示拍照按钮
intent.setMaxTotal(6); // 最多选择照片数量,默认为 6
intent.setSelectedPaths(imagePaths); // 已选中的照片地址,用于回显选中状态
startActivityForResult(intent, REQUEST_CAMERA_CODE);
2. 处理返回结果
重写 onActivityResult 方法,根据请求码区分是选择了照片还是预览了照片:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
switch (requestCode) {
case REQUEST_CAMERA_CODE:
loadAdapter(data.getStringArrayListExtra(PhotoPickerActivity.EXTRA_RESULT));
break;
case REQUEST_PREVIEW_CODE:
loadAdapter(data.getStringArrayListExtra(PhotoPreviewActivity.EXTRA_RESULT));
break;
}
}
}
二、UI 适配:GridAdapter
在展示图片列表时,我们需要一个特殊的入口来触发选择器。这里采用在路径集合中默认添加一个特殊标识(如 "000000")的方式。
1. 适配器逻辑
当图片数量达到上限(例如 7 张,含占位符)时,移除最后一个占位符,保证 UI 整洁。
private class GridAdapter extends BaseAdapter {
private ArrayList<String> listUrls;
private LayoutInflater inflater;
public GridAdapter(ArrayList<String> listUrls) {
this.listUrls = listUrls;
if (listUrls.size() == 7) {
listUrls.remove(listUrls.size() - 1);
}
inflater = LayoutInflater.from(MainActivity.this);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
convertView = inflater.inflate(R.layout.item_image, parent, false);
holder.image = (ImageView) convertView.findViewById(R.id.imageView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
final String path = listUrls.get(position);
if (path.equals("000000")) {
// 显示添加图标
holder.image.setImageResource(R.mipmap.ic_launcher);
} else {
Glide.with(MainActivity.this)
.load(path)
.placeholder(R.mipmap.default_error)
.error(R.mipmap.default_error)
.centerCrop()
.crossFade()
.into(holder.image);
}
return convertView;
}
static class ViewHolder {
ImageView image;
}
}
三、上传管理:Retrofit2 实现
这是核心部分。我们定义了一个接口来处理单张或多张图片的上传。注意,Retrofit 的 @Multipart 注解配合 RequestBody 可以灵活处理文件流。
1. 接口定义
为了兼容不同的上传场景,提供了三种重载方法:
public interface FileUploadService {
/**
* 上传一张图片
*/
@Multipart
@POST("/upload")
Call<String> uploadImage(@Part("fileName") String description,
@Part("file\"; filename=\"image.png\"") RequestBody imgs);
/**
* 上传 6 张图片(固定参数)
*/
@Multipart
@POST("/upload")
Call<String> uploadImage(@Part("description") String description,
@Part("file\"; filename=\"image.png\"") RequestBody imgs1,
@Part("file\"; filename=\"image.png\"") RequestBody imgs2,
@Part("file\"; filename=\"image.png\"") RequestBody imgs3,
@Part("file\"; filename=\"image.png\"") RequestBody imgs4,
@Part("file\"; filename=\"image.png\"") RequestBody imgs5,
@Part("file\"; filename=\"image.png\"") RequestBody imgs6);
/**
* 简便写法:使用 Map 动态传入多个文件
*/
@Multipart
@POST("/upload")
Call<String> uploadImage(@Part("description") String description,
@PartMap Map<String, RequestBody> imgs);
}
2. 执行上传
实际调用时,建议使用 @PartMap 方式,这样代码更简洁,不需要为每个文件写单独参数。
public static void uploadMany(ArrayList<String> paths, String desc) {
Map<String, RequestBody> photos = new HashMap<>();
if (paths.size() > 0) {
for (int i = 0; i < paths.size(); i++) {
String filename = paths.get(i).substring(paths.get(i).lastIndexOf("/") + 1);
String key = "file\"; filename=" + filename;
photos.put(key, RequestBody.create(MediaType.parse("multipart/form-data"), new File(paths.get(i))));
}
}
Call<String> stringCall = apiManager.uploadImage(desc, photos);
stringCall.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
Log.d(TAG, "onResponse: " + response.body());
}
@Override
public void onFailure(Call<String> call, Throwable t) {
Log.e(TAG, "onFailure", t);
}
});
}
四、主界面逻辑整合
在 MainActivity 中,我们将上述组件串联起来。注意清理掉了一些过时的包引用和作者信息,保持代码纯净。
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CAMERA_CODE = 10;
private static final int REQUEST_PREVIEW_CODE = 20;
private ArrayList<String> imagePaths = new ArrayList<>();
private GridView gridView;
private GridAdapter gridAdapter;
private Button mButton;
private EditText textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gridView = findViewById(R.id.gridView);
mButton = findViewById(R.id.button);
textView = findViewById(R.id.et_context);
// 设置列数自适应
int cols = getResources().getDisplayMetrics().widthPixels / getResources().getDisplayMetrics().densityDpi;
cols = cols < 3 ? 3 : cols;
gridView.setNumColumns(cols);
// 点击事件:判断是添加图片还是预览
gridView.setOnItemClickListener((parent, view, position, id) -> {
String imgPath = (String) parent.getItemAtPosition(position);
if ("000000".equals(imgPath)) {
PhotoPickerIntent intent = new PhotoPickerIntent(MainActivity.this);
intent.setSelectModel(SelectModel.MULTI);
intent.setShowCamera(true);
intent.setMaxTotal(6);
intent.setSelectedPaths(imagePaths);
startActivityForResult(intent, REQUEST_CAMERA_CODE);
} else {
PhotoPreviewIntent intent = new PhotoPreviewIntent(MainActivity.this);
intent.setCurrentItem(position);
intent.setPhotoPaths(imagePaths);
startActivityForResult(intent, REQUEST_PREVIEW_CODE);
}
});
// 初始化列表,预留一个添加按钮位置
imagePaths.add("000000");
gridAdapter = new GridAdapter(imagePaths);
gridView.setAdapter(gridAdapter);
// 提交按钮
mButton.setOnClickListener(v -> {
String desc = textView.getText().toString().trim();
if (desc.isEmpty()) desc = "随便聊聊";
new Thread(() -> FileUploadManager.uploadMany(imagePaths, desc)).start();
});
}
// ... onActivityResult 和 loadAdapter 方法同上 ...
}
五、服务端接收示例
为了让整个流程跑通,后端也需要相应的处理逻辑。这里提供 SpringMVC 和 Struts2 两种常见框架的接收示例。
1. SpringMVC 接收
@RequestMapping("/upload")
public String addUser(@RequestParam("file") CommonsMultipartFile[] files, HttpServletRequest request) {
for (CommonsMultipartFile file : files) {
System.out.println("fileName---------->" + file.getOriginalFilename());
if (!file.isEmpty()) {
try {
FileOutputStream os = new FileOutputStream("f:/img/" + new Date().getTime() + ".jpg");
FileInputStream in = file.getInputStream();
byte[] buffer = new byte[1024];
int b;
while ((b = in.read(buffer)) != -1) {
os.write(buffer, 0, b);
}
os.flush();
os.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return "success";
}
2. Struts2 接收
@Controller
public class UploadFile extends ActionSupport {
private File[] file;
private String description;
public File[] getFile() { return file; }
public void setFile(File[] file) { this.file = file; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
@Action("/upload")
public String upload() {
System.out.println("上传的文件=" + Arrays.toString(file));
System.out.println("说说内容=" + description);
return SUCCESS;
}
}
六、效果说明
完成上述步骤后,应用即可实现以下功能:
- 点击网格中的加号图标,唤起系统相册或相机。
- 支持多选图片,并在界面上实时预览缩略图。
- 输入文字描述后点击发送,数据通过 Retrofit 异步上传至服务器。
- 服务端可正常接收文件流并保存。


