在开发APP软件中,boss突然提出想在软件中添加一个多张照片上传的功能,作为菜鸟的我,琢磨了两天,才弄出来,今天特地贴出来。本篇博客主要介绍的是将本地图片上传到服务器的方法技巧。主要技术点是:
一、运用第三方可以从相册中选取多张图片。
二、将图片进行压缩处理。
三、上传到阿里云OSS。
技术相对简单,主要是将这些技术结合起来的例子并不多,而且阿里云OSS的资料也不是很丰富,开发文档也是很简略,因此趁此机会将我的思路共享出来。
PS:请先配置好应用权限。另外,这是公司项目的一个功能,有些地方只好删除,可能看着不是很完整,有时间我再自己做个Demo共享一下。
一、图片选择
android机型几乎每个品牌都有自己的相册,因此为了兼容性,自定义一个图库是比较好的办法,而且这次开发要求选取多张图片,使用自定义的开来也是最佳方案,当然为了开发的简便性,我直接使用的第三方,如果需要自定义的,可以参考下鸿洋大神的博客。
Android 超高仿微信图片选择器 图片该这么加载
不过目前,支持多图选择的第三方控件也是挺多的,比如MultiImageSelector、MultipleImagePick、PhotoPicker、GalleryPick等等,我大致看了下,使用方法是大同小异的。这次开发中,我们使用的是GalleryPick,基本使用方法可以看看github的介绍。GalleryPick地址
这里几乎就没什么好介绍了,需要的注意的地方,YangcyYe也介绍的很详细。代码:
ImageConfig imageConfig
= new ImageConfig.Builder(
// GlideLoader 可用自己用的缓存库
new GlideLoader())
// 如果在 4.4 以上,则修改状态栏颜色 (默认黑色)
.steepToolBarColor(getResources().getColor(R.color.blue))
// 标题的背景颜色 (默认黑色)
.titleBgColor(getResources().getColor(R.color.blue))
// 提交按钮字体的颜色 (默认白色)
.titleSubmitTextColor(getResources().getColor(R.color.white))
// 标题颜色 (默认白色)
.titleTextColor(getResources().getColor(R.color.white))
// 开启多选 (默认为多选) (单选 为 singleSelect)
.mutiSelect()
.crop()
// 开启拍照功能 (默认关闭)
.showCamera()
// 多选时的最大数量 (默认 9 张)
.mutiSelectMaxSize(6)
// 已选择的图片路径
.pathList(path)
// 拍照后存放的图片路径(默认 /temp/picture)
.filePath("/ImageSelector/Pictures")
.requestCode(REQUEST_CODE)
.build();
ImageSelector.open(OrderDetailActivity.this, imageConfig); // 开启图片选择器
图片选择后是在onActivityResult函数的回调的,和自带的图库是一样的。
二、图片压缩
我们公司的项目主要是上传手机拍的照片,现在的手机像素都是极高的,一不小心就是OOM,更重要的是在天朝流量是“奢侈品”,让用户一不小心就是十几M的流量没了,这也是不行的,所以上传前必须对照片进行压缩优化。
网上相关介绍也是颇多,我直接贴代码,各位看看,应该是挺好理解的。
public class BitmapUtils {
//压缩图片尺寸
public static Bitmap compressBySize(String pathName, int targetWidth,
int targetHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;// 不去真的解析图片,只是获取图片的头部信息,包含宽高等;
Bitmap bitmap = BitmapFactory.decodeFile(pathName, options);
// 得到图片的宽度、高度;
float imgWidth = options.outWidth;
float imgHeight = options.outHeight;
// 分别计算图片宽度、高度与目标宽度、高度的比例;取大于等于该比例的最小整数;
int widthRatio = (int) Math.ceil(imgWidth / (float) targetWidth);
int heightRatio = (int) Math.ceil(imgHeight / (float) targetHeight);
options.inSampleSize = 1;
// 如果尺寸接近则不压缩,否则进行比例压缩
if (widthRatio > 1 || widthRatio > 1) {
if (widthRatio > heightRatio) {
options.inSampleSize = widthRatio;
} else {
options.inSampleSize = heightRatio;
}
}
//设置好缩放比例后,加载图片进内容;
options.inJustDecodeBounds = false; // 这里一定要设置false
bitmap = BitmapFactory.decodeFile(pathName, opts);
return bitmap;
}
}
一般而言,大小保持在720P左右即可,至少我是这么设置的。压缩的图片,可以通过适配器实现QQ控件图片上传时展现的效果。代码:
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 3);
recycler.setLayoutManager(gridLayoutManager);
adapter = new Adapter(this, path);
recycler.setAdapter(adapter);
其适配器的完整代码
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
private Context context;
private LayoutInflater mLayoutInflater;
private List<String> result;
private final static String TAG = "Adapter";
public Adapter(Context context, List<String> result) {
mLayoutInflater = LayoutInflater.from(context);
this.context = context;
this.result = result;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(mLayoutInflater.inflate(R.layout.image, null));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Glide.with(context)
.load(result.get(position))
.centerCrop()
.into(holder.image);
}
@Override
public int getItemCount() {
return result.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
ImageView image;
public ViewHolder(View itemView) {
super(itemView);
image = (ImageView) itemView.findViewById(R.id.image);
}
}
}
图片如此处理就可以了,但我们公司使用的是阿里云Oss,因此,我把处理过的文件又重新保存到sd里面去了。也把代码贴出来吧:
long time = System.currentTimeMillis();
String t = new SimpleDateFormat("yyyy_MM_dd").format(new Date(time));
String d = new SimpleDateFormat("HH_mm").format(new Date(time));
// 压缩图片保存的目录路径
String filePath = MyApplication.getTruename() + "/" + t + "/" + d;
// 压缩后图片保存的文件名
String fileName = "photo" + "(" + number + ")" + ".jpg";
File file = new File(getSDPath() + "/" + "myApp" + "/" + filePath);
File dir = new File(file, fileName);//将要保存图片的路径,android推荐这种写法,将目录名和文件名分开,不然容易报错。
try {
//压缩图片
Bitmap bitmap = BitmapUtils.compressBySize(srcPath, 1280, 768);
if (!file.exists()) {
file.mkdirs();
}
if (!dir.exists()) {
try {
dir.createNewFile();
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dir));
bitmap.compress(Bitmap.CompressFormat.JPEG, 30, bos);
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
getSdPath就是获取SD的路径,代码如下:
public static String getSDPath() {
File sdDir = null;
boolean sdCardExist = Environment.getExternalStorageState()
.equals(android.os.Environment.MEDIA_MOUNTED);//判断sd卡是否存在
if (sdCardExist) {
sdDir = Environment.getExternalStorageDirectory();//获取根目录
} else {
getBaseContext().getCacheDir().getAbsolutePath(); // 获取内置内存卡目录
}
return sdDir.toString();
}
这样基本就完成了图片处理的操作,现在可以开始进行上传操作了。
三、图片上传
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK && data != null) {
// 处理逻辑,requestcode是自己设置和传过来的,RESULT_OK是常量
path = data.getStringArrayListExtra(ImageSelectorActivity.EXTRA_RESULT);
// 如果多张图片,可以采取循环上传
for(String srcPath, path){
Log.i("MyTag", srcPath) ; // 可以看到每张原始图片的地址
// 图片处理和保存的逻辑
// 图片上传的逻辑,推荐不要放在循环中,多张图片容易造成线程过多
};
initOss(); // 配置OSS
// 第一张图片的上传
OssUpload(path.get(0));
}
}
OSS的配置,建议只实例化一次,即不要放在循环体中。
// ACCESS_ID,ACCESS_KEY是在阿里云申请的
OSSCredentialProvider credentialProvider = new OSSPlainTextAKSKCredentialProvider(ACCESS_ID, ACCESS_KEY);
ClientConfiguration conf = new ClientConfiguration();
conf.setConnectionTimeout(15 * 1000); // 连接超时,默认15秒
conf.setSocketTimeout(15 * 1000); // socket超时,默认15秒
conf.setMaxConcurrentRequest(8); // 最大并发请求数,默认5个
conf.setMaxErrorRetry(2); // 失败后最大重试次数,默认2次
// oss为全局变量,OSS_ENDPOINT是一个OSS区域地址
oss = new OSSClient(getApplicationContext(), OSS_ENDPOINT, credentialProvider, conf);
实例化后,就要将图片上传了,这个逻辑应该在onActivityResult这个方法中执行
public void ossUpload(String strPath){
// 判断图片是否全部上传
// 如果已经是最后一张图片上传成功,则跳出
number++;
if (number == path.size()) {
// 结束的处理逻辑,并退出该方法
return;
}
// 指定数据类型,没有指定会自动根据后缀名判断
ObjectMetadata objectMeta = new ObjectMetadata();
objectMeta.setContentType("image/jpeg");
// 构造上传请求
// 这里的objectKey其实就是服务器上的路径,即目录+文件名
//因为目录命名逻辑涉及公司信息,被我删去,造成不知道这个objectKey不知为何物,如下是我们公司的大致命名逻辑
//String objectKey = keyPath + "/" + carArr[times] + ".jpg";
PutObjectRequest put = new PutObjectRequest(BUCKET_NAME, objectKey,dir.getPath());
put.setMetadata(objectMeta);
try {
PutObjectResult putObjectResult = oss.putObject(put);
} catch (ClientException e) {
e.printStackTrace();
} catch (ServiceException e) {
e.printStackTrace();
}
// 异步上传时可以设置进度回调
put.setProgressCallback(new OSSProgressCallback<PutObjectRequest>() {
@Override
public void onProgress(PutObjectRequest request, long currentSize, long totalSize) {
// 在这里可以实现进度条展现功能
Log.d("PutObject", "currentSize: " + currentSize + " totalSize: " + totalSize);
}
});
OSSAsyncTask task = oss.asyncPutObject(put, new OSSCompletedCallback<PutObjectRequest, PutObjectResult>() {
@Override
public void onSuccess(PutObjectRequest request, PutObjectResult result) {
Log.d("PutObject", "UploadSuccess");
Log.d("ETag", result.getETag());
Log.d("RequestId", result.getRequestId());
// 这里进行递归单张图片上传,在外面判断是否进行跳出
if (number <= path.size() - 1) {
upOss(path.get(number));
}
}
@Override
public void onFailure(PutObjectRequest request, ClientException clientExcepion, ServiceException serviceException) {
// 请求异常
if (clientExcepion != null) {
// 本地异常如网络异常等
clientExcepion.printStackTrace();
}
if (serviceException != null) {
// 服务异常
Log.e("ErrorCode", serviceException.getErrorCode());
Log.e("RequestId", serviceException.getRequestId());
Log.e("HostId", serviceException.getHostId());
Log.e("RawMessage", serviceException.getRawMessage());
}
ToastUtils.showShort(mContext, "图片上传失败,请重新上传");
return;
}
});
}
至此,整个多图上传的过程就完成了,作为菜鸟的我,目前想出来的就是这个笨方法,希望各位大神有更好的方法,不吝赐教!
2016-10-25 PS:写文章时,只是想将个人想法拿出来探讨,没想到能有这么人评论与浏览,如今看来,这篇博客还存在许多问题,我计划在本周更新本篇博客,以及将demo贴出。