ThumbnailUtils的使用
原文出處:http://jayfeng.com/2016/03/16/%E7%90%86%E8%A7%A3ThumbnailUtils/
前言
 特別喜歡系統中一些小而精的工具類,有的時候分析一下別有一番味道。
 ThumbnailUtils是系統內置的一個生成縮略圖的工具類,只有512行代碼,網上有很多使用ThumbnailUtils的例子,剛好我個人正在整理Bitmap的相關資料,希望從中也能有所收獲。
幾個概念
像素規范
 系統中對縮略圖的像素定義了三種規范:
 
| 12345 | // frameworks/base/core/java/android/provider/MediaStoreSaver.java// Images.Thumbnailspublic static final int MINI_KIND = 1; // 512 x 384public static final int FULL_SCREEN_KIND = 2; // 未定義public static final int MICRO_KIND = 3; // 160 * 120 | 
 對于開發者,只支持MINI_KIND和MICRO_KIND兩種類型。為什么是這個像素呢?因為ThumbnailUtils中定義如下:
 
| 123456 | public class ThumbnailUtils { /* Maximum pixels size for created bitmap. */ private static final int MAX_NUM_PIXELS_THUMBNAIL = 512 * 384; private static final int MAX_NUM_PIXELS_MICRO_THUMBNAIL = 160 * 120; private static final int UNCONSTRAINED = -1;} | 
其中MAX_NUM_PIXELS_MICRO_THUMBNAIL的值之前是128?128,在4.2+版本上被調整為160120,原因很簡單,現在手機拍攝照片比例普遍是4:3,如果不是這個比例生成縮略圖的時候需要更多的計算。
尺寸規范
 系統中對MINI_KIND和MICRO_KIND兩種類型的圖片尺寸做了限制,強調一下,是“系統”。
 
| 12 | public static final int TARGET_SIZE_MINI_THUMBNAIL = 320;public static final int TARGET_SIZE_MICRO_THUMBNAIL = 96; | 
 當然這兩個字段是@hide的,是專門系統用的。
 如果圖片縮略圖,MINI_KIND則等比例縮到360,MICRO_KIND則縮放為96 x 96的正方形(實現方法參考下面的#最合適的縮略圖)
 如果視頻縮略圖,MINI_KIND則等比例縮到512(這個512是寫死在代碼里的magic number),MICRO_KIND則縮放為96 x 96的正方形(實現方法參考下面的#最合適的縮略圖)
Exif格式
Exif是一種圖像文件格式,它的數據存儲與JPEG格式是完全相同的。實際上Exif格式就是在JPEG格式頭部插入了數碼照片的信息,包括拍攝時的光圈、快門、白平衡、ISO、焦距、日期時間等各種和拍攝條件以及相機品牌、型號、色彩編碼、拍攝時錄制的聲音以及GPS全球定位系統數據、縮略圖等。
 具體元信息,可參考f/b/media/java/android/media/ExifInterface.java
 這里我特別指出ExifInterface的兩點,在大家工作中很有可能會碰到:
 
| 1234567891011121314 | /*** This is a class for reading and writing Exif tags in a JPEG file.*/public class ExifInterface { // 1. 方向,也就是旋轉角度 public static final String TAG_ORIENTATION = "Orientation"; // 2. 從Exif中獲取縮略圖, 如果沒有則返回null public byte[] getThumbnail() { synchronized (sLock) { return getThumbnailNative(mFilename);}}} | 
最合適的縮略圖
等比例縮放只需要按Bitmap.createBitmap即可,但是Thumbnail的縮略圖生成算法中為了從中間截圖最合適的部分,包含了裁剪的邏輯。主要分兩步:
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 | /*** 把原始圖片轉化為目標大小的圖片,從中間截圖* 注意:這里我把放大的一個邏輯處理刪除了,那段邏輯永遠不會執行*/private static Bitmap transform(Matrix scaler,Bitmap source, int targetWidth, int targetHeight, int options) { // 是否回收原始Bitmap boolean recycle = (options & OPTIONS_RECYCLE_INPUT) != 0; // 計算是按寬度還是高度計算縮放比例 // 這里通過高寬比計算縮放的方法,可以用填滿的思維去想象一下 float bitmapWidthF = source.getWidth(); float bitmapHeightF = source.getHeight(); float bitmapAspect = bitmapWidthF / bitmapHeightF; float viewAspect = (float) targetWidth / targetHeight; if (bitmapAspect > viewAspect) { float scale = targetHeight / bitmapHeightF; if (scale < .9F || scale > 1F) {scaler.setScale(scale, scale);} else {scaler = null;}} else { float scale = targetWidth / bitmapWidthF; if (scale < .9F || scale > 1F) {scaler.setScale(scale, scale);} else {scaler = null;}} // 調用Bitmap.createBitmap方法按上面算出的縮放比例等比例縮小Bitmap b1; if (scaler != null) { // this is used for minithumb and crop, so we want to filter here.b1 = Bitmap.createBitmap(source, 0, 0,source.getWidth(), source.getHeight(), scaler, true);} else {b1 = source;} if (recycle && b1 != source) {source.recycle();} // 從中間裁剪最合適部分 int dx1 = Math.max(0, b1.getWidth() - targetWidth); int dy1 = Math.max(0, b1.getHeight() - targetHeight);Bitmap b2 = Bitmap.createBitmap(b1,dx1 / 2,dy1 / 2,targetWidth,targetHeight); if (b2 != b1) { if (recycle || b1 != source) {b1.recycle();}} return b2;} | 
 基于上面的算法,ThumbnailUtils對外提供了如下接口生成縮略圖:
 
| 123 | // options主要用于是否回收原始Bitmappublic static Bitmap extractThumbnail(Bitmap source, int width, int height, int options)public static Bitmap extractThumbnail(Bitmap source, int width, int height) | 
視頻縮略圖
 使用MediaMetadataRetriever讀取視頻第一幀Bitmap,然后據此再生成縮略圖。
 如果kind為Thumbnails.MINI_KIND,就等比例生成最大寬或者高為512的小圖。
 如果king為Thumbnails.MICRO_KIND,就使用上面講的最合適的縮略圖算法,生成96 x 96的正方形小圖
 
| 123456789101112131415161718192021222324252627282930313233343536373839 | public static Bitmap createVideoThumbnail(String filePath, int kind) {Bitmap bitmap = null;MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try {retriever.setDataSource(filePath);bitmap = retriever.getFrameAtTime(-1);} catch (IllegalArgumentException ex) { // Assume this is a corrupt video file} catch (RuntimeException ex) { // Assume this is a corrupt video file.} finally { try {retriever.release();} catch (RuntimeException ex) { // Ignore failures while cleaning up.}} if (bitmap == null) return null; if (kind == Images.Thumbnails.MINI_KIND) { // Scale down the bitmap if it's too large. int width = bitmap.getWidth(); int height = bitmap.getHeight(); int max = Math.max(width, height); if (max > 512) { float scale = 512f / max; int w = Math.round(scale * width); int h = Math.round(scale * height);bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);}} else if (kind == Images.Thumbnails.MICRO_KIND) {bitmap = extractThumbnail(bitmap,TARGET_SIZE_MICRO_THUMBNAIL,TARGET_SIZE_MICRO_THUMBNAIL,OPTIONS_RECYCLE_INPUT);} return bitmap;} | 
內部方法
ThumbnailUtils其實對外的方法就上面三個演示的三個方法,除此之外,內部還有兩部分,一部分是生成圖片文件的縮略圖,另外一部分就是未使用的無用代碼。
計算SampleSize
系統中新加入一張圖,就要生成縮略圖了,最重要的就是計算SampleSize了,ThumbnailUtils提供了兩種算法:
按目標最小邊(minSideLength)
定義最小邊的縮放比例
 (int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength))
按目標像素(maxNumOfPixels)
定義像素的縮放比例
 (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels))
具體實現
 同時支持不指定限制,也做了一個默認值處理,實現如下:
 
| 1234567891011121314151617181920212223242526 | // 計算縮放比例private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { double w = options.outWidth; double h = options.outHeight; int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :(int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :(int) Math.min(Math.floor(w / minSideLength),Math.floor(h / minSideLength)); if (upperBound < lowerBound) { // return the larger one when there is no overlapping zone. return lowerBound;} if ((maxNumOfPixels == UNCONSTRAINED) &&(minSideLength == UNCONSTRAINED)) { return 1;} else if (minSideLength == UNCONSTRAINED) { return lowerBound;} else { return upperBound;}} | 
 但是上面的縮放比例不是標準的2的次放,不符合BitmapFactory的規范,再封裝一下:
 
| 123456789101112131415161718192021 | // 規范化上面的sampleSize為2的次方或者8的倍數// 據說這是BitmapFactory的要求,可以避免OOM?注釋里說的。private static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { int initialSize = computeInitialSampleSize(options, minSideLength,maxNumOfPixels); int roundedSize; if (initialSize <= 8) { // 如果小于8,轉化為2的次方(通過位移來轉化,可以借鑒一下)roundedSize = 1; while (roundedSize < initialSize) {roundedSize <<= 1;}} else { // 如果大于8,轉化為8的倍數roundedSize = (initialSize + 7) / 8 * 8;} return roundedSize;} | 
從EXIF中選取縮略圖
 只支持JPG中讀取EXIF信息。
 這里不是說EXIF有縮略圖就用這個縮略圖,而是會先用高寬算出文件本身的TargetSize對應的縮略圖,和EXIF中縮放到TargetSize對應的縮略圖比較,哪個大取哪個。
 
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 | /*** Creates a bitmap by either downsampling from the thumbnail in EXIF or the full image.* The functions returns a SizedThumbnailBitmap,* which contains a downsampled bitmap and the thumbnail data in EXIF if exists.*/private static void createThumbnailFromEXIF(String filePath, int targetSize, int maxPixels, SizedThumbnailBitmap sizedThumbBitmap) { if (filePath == null) return;ExifInterface exif = null; byte [] thumbData = null; try {exif = new ExifInterface(filePath);thumbData = exif.getThumbnail();} catch (IOException ex) {Log.w(TAG, ex);}BitmapFactory.Options fullOptions = new BitmapFactory.Options();BitmapFactory.Options exifOptions = new BitmapFactory.Options(); int exifThumbWidth = 0; int fullThumbWidth = 0; // Compute exifThumbWidth. if (thumbData != null) {exifOptions.inJustDecodeBounds = true;BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, exifOptions);exifOptions.inSampleSize = computeSampleSize(exifOptions, targetSize, maxPixels);exifThumbWidth = exifOptions.outWidth / exifOptions.inSampleSize;} // Compute fullThumbWidth.fullOptions.inJustDecodeBounds = true;BitmapFactory.decodeFile(filePath, fullOptions);fullOptions.inSampleSize = computeSampleSize(fullOptions, targetSize, maxPixels);fullThumbWidth = fullOptions.outWidth / fullOptions.inSampleSize; // Choose the larger thumbnail as the returning sizedThumbBitmap. if (thumbData != null && exifThumbWidth >= fullThumbWidth) { int width = exifOptions.outWidth; int height = exifOptions.outHeight;exifOptions.inJustDecodeBounds = false;sizedThumbBitmap.mBitmap = BitmapFactory.decodeByteArray(thumbData, 0,thumbData.length, exifOptions); if (sizedThumbBitmap.mBitmap != null) {sizedThumbBitmap.mThumbnailData = thumbData;sizedThumbBitmap.mThumbnailWidth = width;sizedThumbBitmap.mThumbnailHeight = height;}} else {fullOptions.inJustDecodeBounds = false;sizedThumbBitmap.mBitmap = BitmapFactory.decodeFile(filePath, fullOptions);}} | 
圖片文件縮略圖
 如果是MINI_KIND,尺寸最小邊縮放到320左右,像素縮放到512 x 387。否則就是MICRO_KIND,尺寸最大邊縮放到96,像素所放到160 x 120。
 如果圖片是JPG,參考上面的方法從EXIF中選取縮略圖。否則,用decodeFileDescriptor()老老實實等比例生成縮略圖。
 最終成功后,如果是MICRO_KIND,還要裁剪為96 x 96的正方形。
 
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 | public static Bitmap createImageThumbnail(String filePath, int kind) {boolean wantMini = (kind == Images.Thumbnails.MINI_KIND);int targetSize = wantMini? TARGET_SIZE_MINI_THUMBNAIL: TARGET_SIZE_MICRO_THUMBNAIL;int maxPixels = wantMini? MAX_NUM_PIXELS_THUMBNAIL: MAX_NUM_PIXELS_MICRO_THUMBNAIL;SizedThumbnailBitmap sizedThumbnailBitmap = new SizedThumbnailBitmap();Bitmap bitmap = null;MediaFileType fileType = MediaFile.getFileType(filePath);if (fileType != null && fileType.fileType == MediaFile.FILE_TYPE_JPEG) {createThumbnailFromEXIF(filePath, targetSize, maxPixels, sizedThumbnailBitmap);bitmap = sizedThumbnailBitmap.mBitmap;}if (bitmap == null) {FileInputStream stream = null;try {stream = new FileInputStream(filePath);FileDescriptor fd = stream.getFD();BitmapFactory.Options options = new BitmapFactory.Options();options.inSampleSize = 1;options.inJustDecodeBounds = true;BitmapFactory.decodeFileDescriptor(fd, null, options);if (options.mCancel || options.outWidth == -1|| options.outHeight == -1) {return null;}options.inSampleSize = computeSampleSize(options, targetSize, maxPixels);options.inJustDecodeBounds = false;options.inDither = false;options.inPreferredConfig = Bitmap.Config.ARGB_8888;bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);} catch (IOException ex) {Log.e(TAG, "", ex);} catch (OutOfMemoryError oom) {Log.e(TAG, "Unable to decode file " + filePath + ". OutOfMemoryError.", oom);} finally {try {if (stream != null) {stream.close();}} catch (IOException ex) {Log.e(TAG, "", ex);}}}if (kind == Images.Thumbnails.MICRO_KIND) {// now we make it a "square thumbnail" for MICRO_KIND thumbnailbitmap = extractThumbnail(bitmap,TARGET_SIZE_MICRO_THUMBNAIL,TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT);}return bitmap;} | 
 這里你可能注意到了,如果從EXIF的代碼中獲取本身文件縮略圖用的是decodeFile(),而后面非JPG圖片獲取縮略圖用decodeFileDescriptor(),為什么呢?
 不知道,也許是開發者“Ray Chen”忘記了,只改了一部分,另外一部分為了穩定性也沒改。
 據網上資料看,decodeFileDescriptor()比decodeFile()更省內存,沒有論證,僅供參考。
未使用的無用代碼
 在ThumbnailUtils有一些私有方法,但是自己又沒有去調用,暫且把這些方法定位無用代碼吧:
 
| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 | /*** Make a bitmap from a given Uri, minimal side length, and maximum number of pixels.* The image data will be read from specified pfd if it's not null, otherwise* a new input stream will be created using specified ContentResolver.** Clients are allowed to pass their own BitmapFactory.Options used for bitmap decoding. A* new BitmapFactory.Options will be created if options is null.*/private static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,Uri uri, ContentResolver cr, ParcelFileDescriptor pfd,BitmapFactory.Options options) {Bitmap b = null;try {if (pfd == null) pfd = makeInputStream(uri, cr);if (pfd == null) return null;if (options == null) options = new BitmapFactory.Options();FileDescriptor fd = pfd.getFileDescriptor();options.inSampleSize = 1;options.inJustDecodeBounds = true;BitmapFactory.decodeFileDescriptor(fd, null, options);if (options.mCancel || options.outWidth == -1|| options.outHeight == -1) {return null;}options.inSampleSize = computeSampleSize(options, minSideLength, maxNumOfPixels);options.inJustDecodeBounds = false;options.inDither = false;options.inPreferredConfig = Bitmap.Config.ARGB_8888;b = BitmapFactory.decodeFileDescriptor(fd, null, options);} catch (OutOfMemoryError ex) {Log.e(TAG, "Got oom exception ", ex);return null;} finally {closeSilently(pfd);}return b;}private static void closeSilently(ParcelFileDescriptor c) {if (c == null) return;try {c.close();} catch (Throwable t) {// do nothing}}private static ParcelFileDescriptor makeInputStream(Uri uri, ContentResolver cr) {try {return cr.openFileDescriptor(uri, "r");} catch (IOException ex) {return null;}} | 
小結
 通過學習ThumbnailUtils生成縮略圖的方方面面,結合自己的經驗實踐,從此生成縮略圖無憂。
 零零散散寫的有點亂,但基本上能運行到的每行代碼都覆蓋到了,對于理解ThumbnailUtils這個類來說,應該夠了。
附錄
ThumbnailUtils.java源碼
總結
以上是生活随笔為你收集整理的ThumbnailUtils的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Android 4.4 中 WebVie
- 下一篇: Android自定义控件系列
