先附上裁剪篇和選取篇的鏈接耙旦,結(jié)合本文食用風(fēng)味更佳~
選取篇域滥;裁剪篇
壓縮目標(biāo)
在講壓縮之前先要明確我們的目標(biāo)
- 對圖片進(jìn)行處理昆汹,使其滿足我們對圖片分辨率的要求坟奥;
- 盡可能減小圖片文件的大小树瞭,來節(jié)省上傳時間和用戶流量拇厢;
- 避免oom;
壓縮圖片相關(guān)函數(shù)
明確目標(biāo)之后首先我們來編寫我們可能需要用到的函數(shù)
讀取圖片
從file或者uri中讀取bitmap的這一步晒喷,我們要對圖片進(jìn)行第一次的處理孝偎。生成bitmap可以使用這個方法BitmapFactory.decodeStream()
,由于目標(biāo)圖片可能分辨率很大,如果這里不進(jìn)行處理很容易造成oom厨埋。
這里我們可以利用兩個方式來降低bitmap所占用的內(nèi)存邪媳。
inSampleSize
利用BitmapFactory.Options
中的inSampleSize
屬性可以減小圖片的分辨率。若inSampleSize=x
得到的bitmap屬性就是原始分辨率的1/x荡陷。inSampleSize值只能為2的倍數(shù)雨效。也就是說當(dāng)inSampleSize
的值為2,4废赞,6徽龟,8的時候才有用。這種方式并不能精確的得到我們想要的分辨率唉地,但是作為初步的壓縮還是非常合適的据悔。
那么計(jì)算inSampleSize
的值可以先獲取原始圖片的大小,再根據(jù)我們自己的目標(biāo)大小來進(jìn)行初步壓縮耘沼。要獲取原始圖片大小极颓,我們可以利用BitmapFactory.Options
的inJustDecodeBounds
屬性。
inPreferredConfig
利用BitmapFactory.Options
中的inPreferredConfig
屬性可以改變圖片的默認(rèn)模式群嗤,bitmap有如下四種模式
模式 | 組成 | 占用內(nèi)存 |
---|---|---|
ALPHA_8 | Alpha由8位組成 | 一個像素占用1個字節(jié) |
ARGB_4444 | 4個4位組成即16位 | 一個像素占用2個字節(jié) |
ARGB_8888 | 4個8位組成即32位 | 一個像素占用4個字節(jié) |
RGB_565 | R為5位菠隆,G為6位,B為5位共16位 | 一個像素占用2個字節(jié) |
Android默認(rèn)的圖片模式為ARGB_8888狂秘,但是如果我們的圖片不需要太高的質(zhì)量并且沒有透明通道骇径。我們完全可以使用RGB_565這種模式。
完整代碼
/**
* @param
* @param uri
* @param targetWidth 限制寬度
* @param targetHeight 限制高度
* @return
* @throws Exception
*/
public static Bitmap getBitmapFromUri(Context context, Uri uri, float targetWidth, float targetHeight) throws Exception {
Bitmap bitmap = null;
InputStream input = context.getContentResolver().openInputStream(uri);
BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
onlyBoundsOptions.inJustDecodeBounds = true;
onlyBoundsOptions.inDither = true;
onlyBoundsOptions.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
if (input != null) {
input.close();
}
//獲取原始圖片大小
int originalWidth = onlyBoundsOptions.outWidth;
int originalHeight = onlyBoundsOptions.outHeight;
if ((originalWidth == -1) || (originalHeight == -1))
return null;
float widthRatio = originalWidth / targetWidth;
float heightRatio = originalHeight / targetHeight;
//計(jì)算壓縮值
float ratio = widthRatio > heightRatio ? widthRatio : heightRatio;
if (ratio < 1)
ratio = 1;
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = (int) ratio;
bitmapOptions.inDither = true;
bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
input = context.getContentResolver().openInputStream(uri);
//實(shí)際獲取圖片
bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
if (input != null) {
input.close();
}
return bitmap;
}
處理bitmap
bitmap的處理比較簡單者春,我們可以使用Android系統(tǒng)為我們提供的函數(shù)extractThumbnail(Bitmap source, int width, int height)
破衔,這個函數(shù)的內(nèi)部實(shí)現(xiàn)很有意思,有空大家可以先看看钱烟。而如果我們的壓縮要保證圖片的等比例處理晰筛,需要合理的去計(jì)算新的width和height。計(jì)算方法如下
float widthRadio = (float) bitmap.getWidth() /(float) maxWidth;
float heightRadio = (float) bitmap.getHeight() / (float)maxHeight;
float radio = widthRadio > heightRadio ? widthRadio : heightRadio;
if (radio > 1) {
bitmap = ThumbnailUtils.extractThumbnail(bitmap, (int) (bitmap.getWidth() / radio), (int) (bitmap.getHeight() / radio));
}
保存bitmap并壓縮文件大小
得到了合適分辨率的bitmap拴袭,我們接下來就需要對圖片的大小進(jìn)行壓縮和保存了读第。接下來問題就來了,一張圖片應(yīng)該占用多大的空間呢稻扬?我的辦法就是引入一個參數(shù)表明1像素占用的大小來處理圖片。壓縮圖片大小我們可以使用bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
注意只有jpeg格式的圖片才能被這個函數(shù)壓縮羊瘩!第二個參數(shù)表明圖片的壓縮質(zhì)量泰佳,100表示不壓縮盼砍。我們無法估算這個參數(shù)對圖片最終大小的影響,所以我們只能采用循環(huán)的方式來處理我們的圖片逝她。
/**
* @param image
* @param outputStream
* @param limitSize 單位byte 由單位像素占用大小計(jì)算得出
* @throws IOException
*/
public static void compressImage(Bitmap image, OutputStream outputStream, float limitSize) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
int options = 100;
//ignoreSize 以下的圖片不進(jìn)行壓縮浇坐,發(fā)現(xiàn)小圖的的壓縮效率不高而且質(zhì)量損毀的十分嚴(yán)重。
while (baos.toByteArray().length > limitSize&&baos.toByteArray().length> ignoreSize) {
baos.reset();
image.compress(Bitmap.CompressFormat.JPEG, options, baos);
//每次減少的量黔宛,可以進(jìn)行調(diào)整近刘。由于compress這個函數(shù)占用時間很長所以我們應(yīng)當(dāng)盡量減少循環(huán)次數(shù)
options -= 15;
Log.i("lzc","currentSize"+(baos.toByteArray().length/1024));
}
image.compress(Bitmap.CompressFormat.JPEG, options, outputStream);
baos.close();
outputStream.close();
}
壓縮
編寫完成壓縮相關(guān)函數(shù),接下來我們就要考慮這些函數(shù)的調(diào)用方式了臀晃。很明顯這些操作都是耗時操作觉渴,不能放在主線程中執(zhí)行。而且我們有壓縮多張圖片的需求徽惋,考慮到內(nèi)存問題案淋,我們應(yīng)該使用service單獨(dú)開進(jìn)程來對圖片壓縮。
另外险绘,多張圖片的壓縮是順序踢京,還是并發(fā)執(zhí)行的問題值得我們考慮。順序執(zhí)行可以減少內(nèi)存占用而并發(fā)執(zhí)行可以減少壓縮時間宦棺。
我選擇了并發(fā)執(zhí)行瓣距,畢竟壓縮之后還要緊接上傳,不宜讓用戶等待過久代咸。我們來建立我們的service開啟線程池來處理壓縮圖片流程蹈丸。
創(chuàng)建service時建立線程池
@Override
public void onCreate() {
super.onCreate();
fileHashtable.clear();
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
9, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
每次startServcie向線程池增加一個事件
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
executorService.execute(new PressRunnable(intent));
return super.onStartCommand(intent, flags, startId);
}
處理事件和壓縮
//壓縮圖片線程
private class PressRunnable implements Runnable {
private Intent intent;
public PressRunnable(Intent intent) {
this.intent = intent;
}
@Override
public void run() {
onHandleIntent(intent);
}
}
//處理intent得到參數(shù)
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_FOO.equals(action)) {
final Uri uri = intent.getParcelableExtra(EXTRA_PARAM1);
final ChoicePhotoManager.Option option = (ChoicePhotoManager.Option) intent.getSerializableExtra(EXTRA_PARAM2);
realCount = intent.getIntExtra(EXTRA_PARAM3, 1);
int position = intent.getIntExtra(EXTRA_PARAM4, 0);
handleActionFoo(uri, option, position);
}
}
}
//壓縮圖片
private void handleActionFoo(Uri uri, ChoicePhotoManager.Option option, int position) {
File file = new File(FileUntil.UriToFile(uri, this));
if (!file.exists())
return;
//創(chuàng)建新文件
File newFile = FileUntil.createTempFile(FileName + UUID.randomUUID() + ".jpg");
//壓縮圖片并保存到新文件
FileUntil.compressImg(this, file, newFile, option.pressRadio, option.maxWidth, option.maxHeight);
//讀取原始圖片的旋轉(zhuǎn)信息,并給以現(xiàn)有圖片
FileUntil.setFilePictureDegree(newFile, FileUntil.readPictureDegree(file.getPath()));
fileHashtable.put(position, newFile);
synchronized (PressImgService.class) {
count++;
if (realCount == count) {
callFinish();
}
}
}
//壓縮完畢關(guān)閉servcie 回傳壓縮后圖片的uri
private void callFinish() {
Intent intent = new Intent();
intent.setAction(callbackReceiver);
Uri[] uris = new Uri[fileHashtable.keySet().size()];
for (Map.Entry<Integer, File> integerFileEntry : fileHashtable.entrySet()) {
uris[integerFileEntry.getKey()] = Uri.fromFile(integerFileEntry.getValue());
}
for (int i = 0; i < realCount; i++) {
Log.i("lzc", "position---asd" + i);
}
intent.putExtra("data", uris);
sendBroadcast(intent);
fileHashtable.clear();
count = 0;
stopSelf();
}
注意
上面的代碼很長侣背,但是要注意的只有兩點(diǎn)白华。
- 要注意讀取之前文件的旋轉(zhuǎn)信息,并賦值給新的文件贩耐。這樣弧腥,新的圖片才能得到正確的旋轉(zhuǎn)角度。
用到的函數(shù)如下潮太。
//讀取文件的旋轉(zhuǎn)信息
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;
}
//為文件設(shè)置旋轉(zhuǎn)信息
public static void setFilePictureDegree(File file, int degree) {
try {
ExifInterface exifInterface = new ExifInterface(file.getPath());
int orientation = ExifInterface.ORIENTATION_NORMAL;
switch (degree) {
case 90:
orientation = ExifInterface.ORIENTATION_ROTATE_90;
break;
case 180:
orientation = ExifInterface.ORIENTATION_ROTATE_180;
break;
case 270:
orientation = ExifInterface.ORIENTATION_ROTATE_270;
break;
}
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation + "");
exifInterface.saveAttributes();
} catch (IOException e) {
e.printStackTrace();
}
}
2.由于是多線程并發(fā)管搪,所以我們需要對幾個關(guān)鍵模塊加上同步鎖。第一個是多張圖片完成壓縮記的計(jì)數(shù)
synchronized (PressImgService.class) {
count++;
if (realCount == count) {
callFinish();
}
第二個地方是我們圖片讀取的地方铡买,否則會產(chǎn)生多張圖拼接到一起的問題更鲁。
synchronized (PressImgService.class) {
bitmap = getBitmapFromUri(context, Uri.fromFile(file), maxWidth, maxHeight);
}
這樣我們細(xì)數(shù)圖片上傳功能用到的知識點(diǎn)的三篇文章就全部講完了。
撒花奇钞,完結(jié)澡为!
細(xì)數(shù)圖片上傳功能用到的知識點(diǎn)(圖片選取&拍照篇)
細(xì)數(shù)圖片上傳功能用到的知識點(diǎn)(裁剪篇)
細(xì)數(shù)圖片上傳功能用到的知識點(diǎn)(圖片壓縮篇)