先附上裁剪篇和選取篇的鏈接,結合本文食用風味更佳~
選取篇;裁剪篇
壓縮目標
在講壓縮之前先要明確我們的目標
- 對圖片進行處理,使其滿足我們對圖片分辨率的要求;
- 盡可能減小圖片文件的大小,來節(jié)省上傳時間和用戶流量;
- 避免oom;
壓縮圖片相關函數
明確目標之后首先我們來編寫我們可能需要用到的函數
讀取圖片
從file或者uri中讀取bitmap的這一步,我們要對圖片進行第一次的處理。生成bitmap可以使用這個方法BitmapFactory.decodeStream()
,由于目標圖片可能分辨率很大,如果這里不進行處理很容易造成oom。
這里我們可以利用兩個方式來降低bitmap所占用的內存。
inSampleSize
利用BitmapFactory.Options
中的inSampleSize
屬性可以減小圖片的分辨率。若inSampleSize=x
得到的bitmap屬性就是原始分辨率的1/x。inSampleSize值只能為2的倍數。也就是說當inSampleSize
的值為2,4,6,8的時候才有用。這種方式并不能精確的得到我們想要的分辨率,但是作為初步的壓縮還是非常合適的。
那么計算inSampleSize
的值可以先獲取原始圖片的大小,再根據我們自己的目標大小來進行初步壓縮。要獲取原始圖片大小,我們可以利用BitmapFactory.Options
的inJustDecodeBounds
屬性。
inPreferredConfig
利用BitmapFactory.Options
中的inPreferredConfig
屬性可以改變圖片的默認模式,bitmap有如下四種模式
模式 | 組成 | 占用內存 |
---|---|---|
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默認的圖片模式為ARGB_8888,但是如果我們的圖片不需要太高的質量并且沒有透明通道。我們完全可以使用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;
//計算壓縮值
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);
//實際獲取圖片
bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
if (input != null) {
input.close();
}
return bitmap;
}
處理bitmap
bitmap的處理比較簡單,我們可以使用Android系統(tǒng)為我們提供的函數extractThumbnail(Bitmap source, int width, int height)
,這個函數的內部實現很有意思,有空大家可以先看看。而如果我們的壓縮要保證圖片的等比例處理,需要合理的去計算新的width和height。計算方法如下
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,我們接下來就需要對圖片的大小進行壓縮和保存了。接下來問題就來了,一張圖片應該占用多大的空間呢?我的辦法就是引入一個參數表明1像素占用的大小來處理圖片。壓縮圖片大小我們可以使用bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
注意只有jpeg格式的圖片才能被這個函數壓縮!第二個參數表明圖片的壓縮質量,100表示不壓縮。我們無法估算這個參數對圖片最終大小的影響,所以我們只能采用循環(huán)的方式來處理我們的圖片。
/**
* @param image
* @param outputStream
* @param limitSize 單位byte 由單位像素占用大小計算得出
* @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 以下的圖片不進行壓縮,發(fā)現小圖的的壓縮效率不高而且質量損毀的十分嚴重。
while (baos.toByteArray().length > limitSize&&baos.toByteArray().length> ignoreSize) {
baos.reset();
image.compress(Bitmap.CompressFormat.JPEG, options, baos);
//每次減少的量,可以進行調整。由于compress這個函數占用時間很長所以我們應當盡量減少循環(huán)次數
options -= 15;
Log.i("lzc","currentSize"+(baos.toByteArray().length/1024));
}
image.compress(Bitmap.CompressFormat.JPEG, options, outputStream);
baos.close();
outputStream.close();
}
壓縮
編寫完成壓縮相關函數,接下來我們就要考慮這些函數的調用方式了。很明顯這些操作都是耗時操作,不能放在主線程中執(zhí)行。而且我們有壓縮多張圖片的需求,考慮到內存問題,我們應該使用service單獨開進程來對圖片壓縮。
另外,多張圖片的壓縮是順序,還是并發(fā)執(zhí)行的問題值得我們考慮。順序執(zhí)行可以減少內存占用而并發(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得到參數
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);
//讀取原始圖片的旋轉信息,并給以現有圖片
FileUntil.setFilePictureDegree(newFile, FileUntil.readPictureDegree(file.getPath()));
fileHashtable.put(position, newFile);
synchronized (PressImgService.class) {
count++;
if (realCount == count) {
callFinish();
}
}
}
//壓縮完畢關閉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();
}
注意
上面的代碼很長,但是要注意的只有兩點。
- 要注意讀取之前文件的旋轉信息,并賦值給新的文件。這樣,新的圖片才能得到正確的旋轉角度。
用到的函數如下。
//讀取文件的旋轉信息
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;
}
//為文件設置旋轉信息
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ā),所以我們需要對幾個關鍵模塊加上同步鎖。第一個是多張圖片完成壓縮記的計數
synchronized (PressImgService.class) {
count++;
if (realCount == count) {
callFinish();
}
第二個地方是我們圖片讀取的地方,否則會產生多張圖拼接到一起的問題。
synchronized (PressImgService.class) {
bitmap = getBitmapFromUri(context, Uri.fromFile(file), maxWidth, maxHeight);
}
這樣我們細數圖片上傳功能用到的知識點的三篇文章就全部講完了。
撒花,完結!
細數圖片上傳功能用到的知識點(圖片選取&拍照篇)
細數圖片上傳功能用到的知識點(裁剪篇)
細數圖片上傳功能用到的知識點(圖片壓縮篇)