接上一篇:安卓选择图片上传功能
在这篇文章的基础上继续打造带进度的图片上传功能。过程蛮复杂,在本文中将贴出大量代码。嫌字多不看的出门左转。另外在上一篇文章中问我要demo的同学们,待后期我上传github后贴出地址。
前言
在查阅了大量资料以后,现在首先对于上传文件主要有两种方式。
- 把文件转换成字符串通过json格式上传【参考xutils3的post联网请求方式】
- 直接封装成一个File对象然后通过表单方式上传【参考okhttp上传文件方式】
首先第一种方式就是普通的联网请求,这种方式有人告诉我,如果是通过json格式去入参的话,无法拿到进度回调。因为json是拼接完成之后一次性全部发送。我不知道这种说法是否有依据来源。也就是说如果要实现进度,那么就要采用以文件形式上传,换而言之也就是流的概念。
一开始我采用的是HttpUrlConnection这种原始的联网工具,实际证明,这种方式只能拿到从堆内存中读到缓存中的进度,而不能拿到上传过程中的进度。在网上找寻许久,最后找到的方法综合起来,都是把okhttp的请求体进行封装来拿到进度。那么下面我们来看一下代码。
代码
其中最重要的文件是ProgressRequestBody
public class ProgressRequestBody extends RequestBody {
private RequestBody mRequestBody;//真正的mRequestBody
private OnProgressListener mProgressListener;//上传回调接口
/**
* 构造函数,赋值
*
* @param requestBody 待包装的请求体
* @param progressListener 回调接口
*/
public ProgressRequestBody(RequestBody requestBody, OnProgressListener progressListener) {
mRequestBody = requestBody;
mProgressListener = progressListener;
}
public interface OnProgressListener {
void OnProgress(long byteWrite, long contentLength);
}
/**
* 重写调用实际的响应体的contentType
*
* @return MediaType
*/
@Override
public MediaType contentType() {
return mRequestBody.contentType();
}
@Override
public long contentLength() {
try {
return mRequestBody.contentLength();//总字节长度
} catch (IOException e) {
return -1;
}
}
/**
* 重写进行写入
*
* @param sink BufferedSink
* @throws IOException 异常
*/
@Override
public void writeTo(BufferedSink sink) throws IOException {
CountingSink countingSink = new CountingSink(sink);
BufferedSink bufferedSink = Okio.buffer(countingSink);
mRequestBody.writeTo(bufferedSink);
bufferedSink.flush();
}
protected final class CountingSink extends ForwardingSink {
private long byteWrite;//当前写入字节数
public CountingSink(Sink delegate) {
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
byteWrite += byteCount;
mProgressListener.OnProgress(byteWrite, contentLength());
}
}
这个重写的类看起来还是挺复杂的,简单解释一下,这个类是把okhttp中的RequestBody进行了封装,并且在里面设计了一个接口,在ForwardingSink这个抽象类里把进度暴露出来,这样的解释不知道有没有明白,如果不明白我还是建议去学习接口回调,这个东西的重要性不必多说。
然后在外部再封装一层,封装一层的原因是为了后期在这里做一些其他需求的操作。
public class CountingRequestBody extends ProgressRequestBody {
/**
* 再封装一层
*
* @param requestBody 请求体
* @param progressListener 上传进度监听
*/
public CountingRequestBody(RequestBody requestBody, OnProgressListener progressListener) {
super(requestBody, progressListener);
}
}
然后使用它
public class OkHttpUploadFileUtils {
/**
* 上传文件
* 注意:这里的字段不能为null不然会报空指针
*
* @param file_path 文件路径
* @param po 上传实体
* @param callBack 外部回调
*/
public static void uploadFile(String file_path, FilePo po, final UploadCallBack callBack) {
File file = new File(file_path);
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("service", "--")
.addFormDataPart("bucketName", po.getBucketName())
.addFormDataPart("key", po.getKey())
.addFormDataPart("system", po.getSystem())
.addFormDataPart("file_name", po.getFile_name())
.addFormDataPart("file_size", file.length() + "")
.addFormDataPart("file_type", po.getFile_type())
.addFormDataPart("operation", po.getOperation())
.addFormDataPart("operator_id", po.getOperator_id())
.addFormDataPart("operator_name", po.getOperator_name())
.addFormDataPart("appkey", "--")
.addFormDataPart("language", "--")
.addFormDataPart("token", TpzIniUtils.getString("token_login", null))
.addFormDataPart(
"inputStream",
file.getName(),
RequestBody.create(MediaType.parse("application/octet-stream"), file)
)
.build();
CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody, new CountingRequestBody.OnProgressListener() {
@Override
public void OnProgress(long byteWrite, long contentLength) {
callBack.onUploading(byteWrite * 100 / contentLength);//暴露进度
}
});
Request request = new Request.Builder()
.url(UrlList.PREFIX_URL) //URL
.post(countingRequestBody)
.build();
execute(request, callBack);
}
//发起请求
private static void execute(Request request, UploadCallBack callBack) {
try {
Call call = BaseApplication.okHttpClient.newCall(request);
Response response = call.execute();//这里用的是同步回调
JsonResults<FileUploadedBean> jsonResult = JsonTools.convertFromJson(
response.body().string(),
new TypeToken<JsonResults<FileUploadedBean>>() {
});
if (jsonResult.getResult_data().isResult()) {
callBack.onSuccess(jsonResult.getResult_data().isResult(), jsonResult.getResult_data().getFile_id());
} else {
callBack.onFailed(null);//失败回调
}
} catch (Exception e) {
callBack.onFailed(e);//失败回调
}
callBack.onFinish();//结束回调
}
}
这里有一些我项目中的东西不便于暴露,我想大多数中级水平的开发者都能看得懂。我自己设计了一个接口用于暴露数据,如下:
public interface UploadCallBack {
void onSuccess(boolean result, String attach_id);
void onFailed(Exception e);
void onFinish();
void onUploading(float progress);
}
这里使用的是一个同步的联网操作,如果有朋友有比较好的异步上传的逻辑欢迎留言评论。
然后在外部文件中调用:
OkHttpUploadFileUtils.uploadFile(imgList.get(i).getImg_url(), filePo, new UploadCallBack() {
@Override
public void onSuccess(boolean result, String attach_id) {
imgList.get(finalI).setAttach_id(attach_id);
setImgUploaderProgress(imgList.get(finalI), 100, result);
}
@Override
public void onFailed(Exception e) {
setImgUploaderProgress(imgList.get(finalI), 100, false);
}
@Override
public void onFinish() {
}
@Override
public void onUploading(float progress) {
if (progress < 100) {
setImgUploaderProgress(imgList.get(finalI), (int) progress, true);
}
}
});
}
这个方法可能部分人看不明白,这是我针对自己的逻辑做的一些UI处理,在onUploading这个方法中可以看到,进度从里面暴露出来。
实际效果:
后记
我师傅不让我使用一些第三方的工具,他说不能只做个搬运工,复制粘贴。有些东西还是要知道一下原理。进行二次封装,也避免了项目中引入大量依赖。尽可能的在能力之内维持项目的精简高效。