在Android开发中经常会遇到需要上传图片的场景,虽然现在有很多很好的第三方框架可供使用,而且傻瓜集成,配置简单。但是有些时候总感觉第三方框架过于臃肿,用着总感觉没有自己写的来得踏实。所以让我们也任性一回,自己写一个图片上传吧。
准备工作
图片上传听起来没有多少事要做,但是小细节还是很繁杂的,更有很多藏在背后的知识。一整套下来,收获还是会很大的。那么具体会有那些步骤呢,别急,容我捋捋:
- 获得图片源(拍摄或选取)
- 编辑(获取基本信息,压缩,调整大小)
- 编码
- 上传
看吧,虽然看起来很简单的任务,认真下来,会发现有很多的知识会牵扯进来,所以废话不多说,让我们开始吧!
正式开始
获取图片源
一般我们会有两种方式来获得图片源——拍照、从系统选取。先说拍照,拍照对于国内开发者而言简直就是噩梦般的存在,由于某些厂商所谓的定制,在适配中会有各种各样莫名其妙的Bug存在,但由于篇幅有限,今天仅仅展示最常规的操作。
通常,假如需要拍照的话,我们需要在Manifest文件中声明我们需要使用相机硬件,所以需要假如特性声明
<uses-feature android:name="android.hardware.camera" />
接下来就是真正的编码工作了,一般我们有两种途径来获得拍摄结果,假如对图片的要求不是太高,我们可以选择直接获取缩略图。系统拍摄完图片后会将结果编码成一个较小的Bitmap,放在Intent对象的extra里,作为key为data的值,返回给调用者缩略图。这时候我们拿到的直接就是Bitmap对象,就可以直接进入下一步了。但这种情况一般不常见,因为缩略图的使用情况很少,一般会用来做icon。尽管简单,但是我们还是来看看怎样做吧
// 获取图片,仅仅需要缩略图
static final int TAKE_PHOTO=1;
//调用拍照
Intent intent =new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if(null != intent.resolveActivity(getPackagerManager())){
startActivityForResult(intent,TAKE_PHOTO);
}
@Override
// 拍摄结束
protected void onActivityResult(int requestCode,int resultCode,Intent data){
if(RESULT_OK==resultCode && TAKE_PHOTO==requestCode){
Bundle extras=data.getExtras();
Bitmap bitmap=(Bitmap)extras.get("data");
...
}
}
但是假如我们需要高清大图呢,为此我们付出的代价是需要码更多的代码。
高清大图需要我们在调用相机拍摄前,提供一个Uri来提供图片的保存位置,名称等信息。既然涉及到保存,那肯定少不了权限声明:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
当然,假如你不想用户拍摄的图片公开,并且应用运行在Android 4.4及以上的平台的话,你不需要这个权限,但是你只能将图片保存在getExternalFilesDir()返回的目录下。
所以通常这个权限还是需要的。
通常,我们会从File对象获得Uri,所以接下来我们需要创建File对象。这里需要注意一个小问题,为了避免文件名冲突造成不必要的问题,我们通常将File对象的名称中加入时间戳,并且放在外部存储的图片目录中。
// 生成保存图片的File对象
private File createFile() throws IOException{
//文件名
String fileName="IMAG_"+new SimpleDataFormat("yyyyMMdd_HHmmss").format(new Date());
//文件目录
File fileDir=Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
// 创建(文件名,文件扩展名,所属目录)
File target=new File(fileName,".jpg",fileDir);
return target;
}
// 拍摄
private void takePhoto(){
Intent takePhoto = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (null!=takePhoto .resolveActivity(getPackageManager())) {
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {
//异常处理
...
}
if (photoFile != null) {
takePhoto.putExtra(MediaStore.EXTRA_OUTPUT,Uri.fromFile(photoFile));
startActivityForResult(takePhoto , TAKE_PHOTO);
}
}
}
这样,你就可以拿到高清大图了。但是假如你需要提供给用户一种选取图片的功能呢?同样借助Intent,但是问题是通过Intent选取的图片是以Uri的方式返回的,我们希望统一风格,更希望后续操作更方便,那么怎样将其转换成绝对路径呢,这是个问题。但问题总会有解决办法的。既然我们已经通过选取动作知道了图片的Uri,那么反推一下,Android系统中,Uri用得最多的地方是哪呢?当然是四大组件之一的ContentProvider了。由此,我们是不是就可以查特定Uri的信息了,哈哈,问题解决了吗。图样图森破,这里有个深坑,4.4及以上的系统的Uri格式和之前的是不一样,哈哈,傻眼了吧,网上各种方法试过没用,多说无益,用代码说明吧
public static final int PICK_PHOTO=2;
// 通过Uri获取图片的真正路径
public static String getRealFilePath(final Context context, final Uri uri) {
String filePath = "";
Cursor cursor = null;
String[] column = {MediaStore.Images.Media.DATA};
if (Build.VERSION.SDK_INT >= 19) {
String wholeID = DocumentsContract.getDocumentId(uri);
String id = wholeID.split(":")[1];
String sel = MediaStore.Images.Media._ID + "=?";
cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
column, sel, new String[]{id}, null);
} else{
CursorLoader cursorLoader = new CursorLoader(
context, uri, column, null, null, null);
cursor = cursorLoader.loadInBackground();
}
if (null != cursor && cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndex(column[0]);
filePath = cursor.getString(columnIndex);
}
cursor.close();
return filePath;
}
// 开始选取图片
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
if (null != intent.resolveActivity(getPackageManager())) {
startActivityForResult(Intent.createChooser(intent, "选择图片"), CODE_PICK_PHOTO);
} else {
Toast.makeText(SaveDataActivity.this, "没有应用可供使用,请确保安装了相册应用", Toast.LENGTH_SHORT).show();
}
至此,我们已经拿到了,图片的路径,接下来我们就可以进行下一步的工作了。
编辑
因为现在的手机拍摄出的图片一般都很大,需要进行一定的压缩,不然不仅会消耗手机内存,造成内存溢出,同时在上传图片时可能会造成图片上传失败等问题。另外有些手机拍摄出的图片可能会带有旋转角度,因此对图片进行一些常规的编辑是很有必要的。
首先,我们需要先对图片进行压缩,便于显示和上传。这就需要用到解码图片,高效显示高清大图的技能了。一般我们都是在有限的空间里显示图片的,所以我们需要根据实际需要的展示的空间来动态计算图片的缩放比例,根据谷歌的train课堂,我们知道可以用如下方法计算实际的缩放比例。
/*计算图片缩放比例*/
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
有了缩放比例,我们就可以开心地进行真正的压缩操作了,这里我把图片的宽高设置为比较小的400*800.
/*根据路径获取图片*/
public static Bitmap getSmallBitmap(String filePath) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
options.inSampleSize = calculateInSampleSize(options, 480, 800);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filePath, options);
}
接下来让我们来修正旋转的图片。在这里就需要用到一个获取图片旋转角度的类——ExifInterface。该类提供了一系列读取和修改JPEG图片Exif信息的方法和常量,有了它我们可以很方便的获取到图片的旋转角度。该类有一个很常用的方法
public int getAttributeInt (String tag, int defaultValue)
tag是指代的属性名,defaultValue是获取不到值后返回的默认值。而指代图片旋转角度的tag是TAG_ORIENTATION,因此,我们可以用下面的方法获取到指定绝对路径下的图片的旋转角度。
// 获取图片旋转角度
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
那么怎样旋转图片能呢?很简单,使用Matrix。其实大家可以记住一点,一遇到图片,必定少不了Matrix,因为Matrix和图片有太多剪不断理还乱的关系。很多高级操作都要用到Matrix。Matrix有一个设置图片旋转的方法,
public static Bitmap rotationPicture(Bitmap bitmap,int rotation){
// 创建操作图片用的matrix对象
Matrix matrix = new Matrix();
// 旋转图片动作
if (rate == 270) {
matrix.postRotate(270);
} else if (rate == 90) {
matrix.postRotate(90);
} else {
matrix.postRotate(-rate);
}
// 创建新的图片
return Bitmap.createBitmap(bitmap, 0, 0,
bitmap.getWidth(), bitmap.getHeight(), matrix,
true);
}
好了,基本编辑进行得差不多了,进入下一步。
编码
有些朋友可能不太清楚,上传图片时最好需要编一下码。通常情况下,会将图片转换为Base64编码。因为图片一般比较大,而Base64编码可用于在HTTP环境下传递较长的标识信息。Base64是一种二进制转文本的编码方式,使用64个 ASCII码字来替换原始字符
所以可以将二进制信息转换成文本信息,便于传输。
而在Android中,由于使用Java作为编程语言,Java的Sdk已经写好了Base64的编码,解码方法,我们可以直接用了,不再需要手动写,具体使用如下。
/*base64编码图片*/
public static String bitmapToString(Bitmap bitmap) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 40, baos);
byte[] b = baos.toByteArray();
return Base64.encodeToString(b, Base64.DEFAULT);
}
这里又进行了一次压缩,压缩率为40%。
上传
终于进行到激动人心的一步了,我选择使用Retrofit框架作为上传工具。虽然Retrofit可以直接上传文件,但是我就是要自己写,你打我啊。开玩笑,我不想说的是,由于服务器端要求上传Base64编码的图片,和一系列标识信息,所以宝宝心里苦啊,但是宝宝还是要说。其实Retrofit上传图片,网上一大堆的教程,但是都用不上,这可愁死我了。肿么办,还是自己动手丰衣足食。
定义接口走起
public interface UploadData {
@FormUrlEncoded
@POST("saveproject")
Call<ResponseBody> upload(@FieldMap Map<String, Object> fields);
}
上传走起
public static boolean update(String param1, String param2, String param3, int type, String imgData) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.build();
Map<String, Object> params = new HashMap<>();
params.put("param1", param1);
params.put("param2", param2);
params.put("param3", param3);
params.put("type", new Integer(type));
if (null != data) {
params.put("datas", data);
}
UploadData uploadData = retrofit.create(UploadData.class);
Call<ResponseBody> call = uploadData.upload(params);
try {
ResponseBody body = call.execute().body();
if (null == body) {
return false;
}
JSONArray array = new JSONArray(body.string());
return array.getJSONObject(0).getInt("flag") == 1;
} catch (IOException e) {
e.printStackTrace();
return false;
} catch (JSONException e) {
return false;
}
}
ok,打完收工,虽然简单粗暴,但是管用啊,今天先写到这里,要吃饭去了。