图像处理工具类、Bitmap处理、理解ThumbnailUtils

在实际项目中,我们经常会遇到处理各种各样的图片问题。

比如:图片的旋转、缩放、图片格式转换、获取图片类型、验证图片大小、写入图片 等。
这里我们使用Java.awt.Graphics2D来实现常用图像处理的功能,形成我们的图像处理工具类。

Java代码  收藏代码

  1. package com.zhangsx.util.image;  
  2.   
  3. import java.util.Iterator;  
  4. import java.awt.Graphics2D;  
  5. import java.awt.RenderingHints;  
  6. import java.awt.image.BufferedImage;  
  7. import java.io.IOException;  
  8. import java.io.OutputStream;  
  9. import java.io.ByteArrayInputStream;  
  10. import java.io.ByteArrayOutputStream;  
  11. import javax.imageio.ImageIO;  
  12. import javax.imageio.ImageReader;  
  13. import javax.imageio.stream.ImageInputStream;  
  14.   
  15. /** 
  16.  * 图像处理工具类。 
  17.  *  
  18.  * @version 1.00 2010-1-15 
  19.  * @since 1.5 
  20.  * @author ZhangShixi 
  21.  */  
  22. public class ImageUtil {  
  23.   
  24.     /** 
  25.      * 旋转图像。 
  26.      * @param bufferedImage 图像。 
  27.      * @param degree 旋转角度。 
  28.      * @return 旋转后的图像。 
  29.      */  
  30.     public static BufferedImage rotateImage(  
  31.             final BufferedImage bufferedImage, final int degree) {  
  32.         int width = bufferedImage.getWidth();  
  33.         int height = bufferedImage.getHeight();  
  34.         int type = bufferedImage.getColorModel().getTransparency();  
  35.   
  36.         BufferedImage image = new BufferedImage(width, height, type);  
  37.         Graphics2D graphics2D = image.createGraphics();  
  38.         graphics2D.setRenderingHint(  
  39.                 RenderingHints.KEY_INTERPOLATION,  
  40.                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);  
  41.   
  42.         graphics2D.rotate(Math.toRadians(degree), width / 2, height / 2);  
  43.         graphics2D.drawImage(bufferedImage, 00null);  
  44.   
  45.         try {  
  46.             return image;  
  47.         } finally {  
  48.             if (graphics2D != null) {  
  49.                 graphics2D.dispose();  
  50.             }  
  51.         }  
  52.     }  
  53.   
  54.     /** 
  55.      * 将图像按照指定的比例缩放。 
  56.      * 比如需要将图像放大20%,那么调用时scale参数的值就为20;如果是缩小,则scale值为-20。 
  57.      * @param bufferedImage 图像。 
  58.      * @param scale 缩放比例。 
  59.      * @return 缩放后的图像。 
  60.      */  
  61.     public static BufferedImage resizeImageScale(  
  62.             final BufferedImage bufferedImage, final int scale) {  
  63.         if (scale == 0) {  
  64.             return bufferedImage;  
  65.         }  
  66.   
  67.         int width = bufferedImage.getWidth();  
  68.         int height = bufferedImage.getHeight();  
  69.   
  70.         int newWidth = 0;  
  71.         int newHeight = 0;  
  72.   
  73.         double nowScale = (double) Math.abs(scale) / 100;  
  74.         if (scale > 0) {  
  75.             newWidth = (int) (width * (1 + nowScale));  
  76.             newHeight = (int) (height * (1 + nowScale));  
  77.         } else if (scale < 0) {  
  78.             newWidth = (int) (width * (1 - nowScale));  
  79.             newHeight = (int) (height * (1 - nowScale));  
  80.         }  
  81.   
  82.         return resizeImage(bufferedImage, newWidth, newHeight);  
  83.     }  
  84.   
  85.     /** 
  86.      * 将图像缩放到指定的宽高大小。 
  87.      * @param bufferedImage 图像。 
  88.      * @param width 新的宽度。 
  89.      * @param height 新的高度。 
  90.      * @return 缩放后的图像。 
  91.      */  
  92.     public static BufferedImage resizeImage(  
  93.             final BufferedImage bufferedImage,  
  94.             final int width, final int height) {  
  95.         int type = bufferedImage.getColorModel().getTransparency();  
  96.         BufferedImage image = new BufferedImage(width, height, type);  
  97.   
  98.         Graphics2D graphics2D = image.createGraphics();  
  99.         graphics2D.setRenderingHint(  
  100.                 RenderingHints.KEY_INTERPOLATION,  
  101.                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);  
  102.   
  103.         graphics2D.drawImage(bufferedImage, 00, width, height, 00,  
  104.                 bufferedImage.getWidth(), bufferedImage.getHeight(), null);  
  105.   
  106.         try {  
  107.             return image;  
  108.         } finally {  
  109.             if (graphics2D != null) {  
  110.                 graphics2D.dispose();  
  111.             }  
  112.         }  
  113.     }  
  114.   
  115.     /** 
  116.      * 将图像水平翻转。 
  117.      * @param bufferedImage 图像。 
  118.      * @return 翻转后的图像。 
  119.      */  
  120.     public static BufferedImage flipImage(  
  121.             final BufferedImage bufferedImage) {  
  122.         int width = bufferedImage.getWidth();  
  123.         int height = bufferedImage.getHeight();  
  124.         int type = bufferedImage.getColorModel().getTransparency();  
  125.   
  126.         BufferedImage image = new BufferedImage(width, height, type);  
  127.         Graphics2D graphics2D = image.createGraphics();  
  128.         graphics2D.drawImage(bufferedImage, 00, width, height,  
  129.                 width, 00, height, null);  
  130.   
  131.         try {  
  132.             return image;  
  133.         } finally {  
  134.             if (graphics2D != null) {  
  135.                 graphics2D.dispose();  
  136.             }  
  137.         }  
  138.     }  
  139.   
  140.     /** 
  141.      * 获取图片的类型。如果是 gif、jpg、png、bmp 以外的类型则返回null。 
  142.      * @param imageBytes 图片字节数组。 
  143.      * @return 图片类型。 
  144.      * @throws java.io.IOException IO异常。 
  145.      */  
  146.     public static String getImageType(final byte[] imageBytes)  
  147.             throws IOException {  
  148.         ByteArrayInputStream input = new ByteArrayInputStream(imageBytes);  
  149.         ImageInputStream imageInput = ImageIO.createImageInputStream(input);  
  150.         Iterator iterator = ImageIO.getImageReaders(imageInput);  
  151.         String type = null;  
  152.         if (iterator.hasNext()) {  
  153.             ImageReader reader = iterator.next();  
  154.             type = reader.getFormatName().toUpperCase();  
  155.         }  
  156.   
  157.         try {  
  158.             return type;  
  159.         } finally {  
  160.             if (imageInput != null) {  
  161.                 imageInput.close();  
  162.             }  
  163.         }  
  164.     }  
  165.   
  166.     /** 
  167.      * 验证图片大小是否超出指定的尺寸。未超出指定大小返回true,超出指定大小则返回false。 
  168.      * @param imageBytes 图片字节数组。 
  169.      * @param width 图片宽度。 
  170.      * @param height 图片高度。 
  171.      * @return 验证结果。未超出指定大小返回true,超出指定大小则返回false。 
  172.      * @throws java.io.IOException IO异常。 
  173.      */  
  174.     public static boolean checkImageSize(  
  175.             final byte[] imageBytes, final int width, final int height)  
  176.             throws IOException {  
  177.         BufferedImage image = byteToImage(imageBytes);  
  178.         int sourceWidth = image.getWidth();  
  179.         int sourceHeight = image.getHeight();  
  180.         if (sourceWidth > width || sourceHeight > height) {  
  181.             return false;  
  182.         } else {  
  183.             return true;  
  184.         }  
  185.     }  
  186.   
  187.     /** 
  188.      * 将图像字节数组转化为BufferedImage图像实例。 
  189.      * @param imageBytes 图像字节数组。 
  190.      * @return BufferedImage图像实例。 
  191.      * @throws java.io.IOException IO异常。 
  192.      */  
  193.     public static BufferedImage byteToImage(  
  194.             final byte[] imageBytes) throws IOException {  
  195.         ByteArrayInputStream input = new ByteArrayInputStream(imageBytes);  
  196.         BufferedImage image = ImageIO.read(input);  
  197.   
  198.         try {  
  199.             return image;  
  200.         } finally {  
  201.             if (input != null) {  
  202.                 input.close();  
  203.             }  
  204.         }  
  205.     }  
  206.   
  207.     /** 
  208.      * 将BufferedImage持有的图像转化为指定图像格式的字节数组。 
  209.      * @param bufferedImage 图像。 
  210.      * @param formatName 图像格式名称。 
  211.      * @return 指定图像格式的字节数组。 
  212.      * @throws java.io.IOException IO异常。 
  213.      */  
  214.     public static byte[] imageToByte(  
  215.             final BufferedImage bufferedImage, final String formatName)  
  216.             throws IOException {  
  217.         ByteArrayOutputStream output = new ByteArrayOutputStream();  
  218.         ImageIO.write(bufferedImage, formatName, output);  
  219.   
  220.         try {  
  221.             return output.toByteArray();  
  222.         } finally {  
  223.             if (output != null) {  
  224.                 output.close();  
  225.             }  
  226.         }  
  227.     }  
  228.   
  229.     /** 
  230.      * 将图像以指定的格式进行输出,调用者需自行关闭输出流。 
  231.      * @param bufferedImage 图像。 
  232.      * @param output 输出流。 
  233.      * @param formatName 图片格式名称。 
  234.      * @throws java.io.IOException IO异常。 
  235.      */  
  236.     public static void writeImage(final BufferedImage bufferedImage,  
  237.             final OutputStream output, final String formatName)  
  238.             throws IOException {  
  239.         ImageIO.write(bufferedImage, formatName, output);  
  240.     }  
  241.   
  242.     /** 
  243.      * 将图像以指定的格式进行输出,调用者需自行关闭输出流。 
  244.      * @param imageBytes 图像的byte数组。 
  245.      * @param output 输出流。 
  246.      * @param formatName 图片格式名称。 
  247.      * @throws java.io.IOException IO异常。 
  248.      */  
  249.     public static void writeImage(final byte[] imageBytes,  
  250.             final OutputStream output, final String formatName)  
  251.             throws IOException {  
  252.         BufferedImage image = byteToImage(imageBytes);  
  253.         ImageIO.write(image, formatName, output);  
  254.     }  
  255. }  

 

 

 

Bitmap处理

在日常开发中,可以说和Bitmap低头不见抬头见,基本上每个应用都会直接或间接的用到,而这里面又涉及到大量的相关知识。
所以这里把Bitmap的常用知识做个梳理,限于经验和能力,不做太深入的分析。

1. 区别decodeResource()和decodeFile()

这里的区别不是指方法名和参数的区别,而是对于解码后图片尺寸在处理上的区别:

decodeFile()用于读取SD卡上的图,得到的是图片的原始尺寸
decodeResource()用于读取Res、Raw等资源,得到的是图片的原始尺寸 * 缩放系数

可以看的出来,decodeResource()比decodeFile()多了一个缩放系数,缩放系数的计算依赖于屏幕密度,当然这个参数也是可以调整的:

// 通过BitmapFactory.Options的这几个参数可以调整缩放系数
public class BitmapFactory {
    public static class Options {
        public boolean inScaled;     // 默认true
        public int inDensity;        // 无dpi的文件夹下默认160
        public int inTargetDensity;  // 取决具体屏幕
    }
}
public class BitmapFactory {
    public static class Options {
        public boolean inScaled;     // 默认true
        public int inDensity;        // 无dpi的文件夹下默认160
        public int inTargetDensity;  // 取决具体屏幕
    }
}

我们分具体情况来看,现在有一张720×720的图片:

inScaled属性

如果inScaled设置为false,则不进行缩放,解码后图片大小为720×720; 否则请往下看。

如果inScaled设置为true或者不设置,则根据inDensity和inTargetDensity计算缩放系数。

默认情况

把这张图片放到drawable目录下, 默认:

以720p的红米3为例子,缩放系数 = inTargetDensity(具体320 / inDensity(默认160)= 2 = density,解码后图片大小为1440×1440。
以1080p的MX4为例子,缩放系数 = inTargetDensity(具体480 / inDensity(默认160)= 3 = density, 解码后图片大小为2160×2160。

*dpi文件夹的影响

把图片放到drawable或者raw这样不带dpi的文件夹,会按照上面的算法计算。

如果放到xhdpi会怎样呢? 在MX4上,放到xhdpi,解码后图片大小为1080 x 1080。

因为放到有dpi的文件夹,会影响到inDensity的默认值,放到xhdpi为160 x 2 = 320; 所以缩放系数 = 480(屏幕) / 320 (xhdpi) = 1.5; 所以得到的图片大小为1080 x 1080。

手动设置缩放系数

如果你不想依赖于这个系统本身的density,你可以手动设置inDensity和inTargetDensity来控制缩放系数:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = 1;
options.inDensity = 160;
options.inTargetDensity = 160;
bitmap = BitmapFactory.decodeResource(getResources(),
        R.drawable.origin, options);
// MX4上,虽然density = 3
// 但是通过设置inTargetDensity / inDensity = 160 / 160 = 1
// 解码后图片大小为720x720
System.out.println("w:" + bitmap.getWidth()
        + ", h:" + bitmap.getHeight());new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = 1;
options.inDensity = 160;
options.inTargetDensity = 160;
bitmap = BitmapFactory.decodeResource(getResources(),
        R.drawable.origin, options);
// MX4上,虽然density = 3
// 但是通过设置inTargetDensity / inDensity = 160 / 160 = 1
// 解码后图片大小为720x720
System.out.println("w:" + bitmap.getWidth()
        + ", h:" + bitmap.getHeight());

2. recycle()方法

官方说法

首先,Android对Bitmap内存(像素数据)的分配区域在不同版本上是有区分的:

As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.

从3.0开始,Bitmap像素数据和Bitmap对象一起存放在Dalvik堆中,而在3.0之前,Bitmap像素数据存放在Native内存中。
所以,在3.0之前,Bitmap像素数据在Nativie内存的释放是不确定的,容易内存溢出而Crash,官方强烈建议调用recycle()(当然是在确定不需要的时候);而在3.0之后,则无此要求。

参考链接:Managing Bitmap Memory

一点讨论

3.0之后官方无recycle()建议,是不是就真的不需要recycle()了呢?

在医生的这篇文章:Bitmap.recycle引发的血案 最后指出:“在不兼容Android2.3的情况下,别在使用recycle方法来管理Bitmap了,那是GC的事!”。文章开头指出了原因在于recycle()方法的注释说明:

/**
 * ... This is an advanced call, and normally need not be called,
 * since the normal GC process will free up this memory when
 * there are no more references to this bitmap.
 */
public void recycle() {}
public void recycle() {}

事实上这个说法是不准确的,是不能作为recycle()方法不调用的依据的。

因为从commit history中看,这行注释早在08年初始化代码的就有了,但是早期的代码并没有因此不需要recycle()方法了。

图像处理工具类、Bitmap处理、理解ThumbnailUtils_第1张图片

如果3.0之后真的完全不需要主动recycle(),最新的AOSP源码应该有相应体现,我查了SystemUI和Gallery2的代码,并没有取缔Bitmap的recycle()方法。

所以,我个人认为,如果Bitmap真的不用了,recycle一下又有何妨?

PS:至于医生说的那个bug,显然是一种优化策略,APP开发中加个两个bitmap不相等的判断条件即可。

3. Bitmap到底占多大内存

这个已经有一篇bugly出品的绝好文章讲的很清楚:

Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?

4. inBitmap

BitmapFactory.Options.inBitmap是AndroiD3.0新增的一个属性,如果设置了这个属性则会重用这个Bitmap的内存从而提升性能。

但是这个重用是有条件的,在Android4.4之前只能重用相同大小的Bitmap,Android4.4+则只要比重用Bitmap小即可。

在官方网站有详细介绍,这里列举示例代码的两个方法了解一下:

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found,
            // set it as the value of inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use
        // if the byte size of the new bitmap is smaller than
        // the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height =
            targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height
            * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions,
    // the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
        && candidate.getHeight() == targetOptions.outHeight
        && targetOptions.inSampleSize == 1;
} static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found,
            // set it as the value of inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use
        // if the byte size of the new bitmap is smaller than
        // the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height =
            targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height
            * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions,
    // the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
        && candidate.getHeight() == targetOptions.outHeight
        && targetOptions.inSampleSize == 1;
}

参考链接:

Managing Bitmap Memory
Bitmap对象的复用

5. LRU缓存算法

LRU,Least Recently Used,Discards the least recently used items first。

在最近使用的数据中,丢弃使用最少的数据。与之相反的还有一个MRU,丢弃使用最多的数据。
这就是著名的局部性原理。

实现思路

1.新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。

图像处理工具类、Bitmap处理、理解ThumbnailUtils_第2张图片

LruCache

在Android3.1和support v4中均提供了Lru算法的实现类LruCache。

内部使用LinkedHashMap实现。

DiskLruCache

LruCache的所有对象和数据都是在内存中(或者说LinkedHashMap中),而DiskLruCache是磁盘缓存,不过它的实现要稍微复杂一点。

使用DiskLruCache后就不用担心文件或者图片太多占用过多磁盘空间,它能把那些不常用的图片自动清理掉。

DiskLruCache系统中并没有正式提供,需要另外下载: DiskLruCache

6. 计算inSampleSize

使用Bitmap节省内存最重要的技巧就是加载合适大小的Bitmap,因为以现在相机像素,很多照片都巨无霸的大,这些大图直接加载到内存,最容易OOM。

加载合适的Bitmap需要先读取Bitmap的原始大小,按缩小了合适的倍数的大小进行加载。

那么,这个缩小的倍数的计算就是inSampleSize的计算。

// 根据maxWidth, maxHeight计算最合适的inSampleSize
public static int $sampleSize(BitmapFactory.Options options,
        int maxWidth, int maxHeight) {
    // raw height and width of image
    int rawWidth = options.outWidth;
    int rawHeight = options.outHeight;

    // calculate best sample size
    int inSampleSize = 0;
    if (rawHeight > maxHeight || rawWidth > maxWidth) {
        float ratioWidth = (float) rawWidth / maxWidth;
        float ratioHeight = (float) rawHeight / maxHeight;
        inSampleSize = (int) Math.min(ratioHeight, ratioWidth);
    }
    inSampleSize = Math.max(1, inSampleSize);

    return inSampleSize;
}
public static int $sampleSize(BitmapFactory.Options options,
        int maxWidth, int maxHeight) {
    // raw height and width of image
    int rawWidth = options.outWidth;
    int rawHeight = options.outHeight;

    // calculate best sample size
    int inSampleSize = 0;
    if (rawHeight > maxHeight || rawWidth > maxWidth) {
        float ratioWidth = (float) rawWidth / maxWidth;
        float ratioHeight = (float) rawHeight / maxHeight;
        inSampleSize = (int) Math.min(ratioHeight, ratioWidth);
    }
    inSampleSize = Math.max(1, inSampleSize);

    return inSampleSize;
}

关于inSampleSize需要注意,它只能是2的次方,否则它会取最接近2的次方的值。

7. 缩略图

为了节省内存,需要先设置BitmapFactory.Options的inJustDecodeBounds为true,这样的Bitmap可以借助decodeFile方法把高和宽存放到Bitmap.Options中,但是内存占用为空(不会真正的加载图片)。

有了具备高宽信息的Options,结合上面的inSampleSize算法算出缩小的倍数,我们就能加载本地大图的某个合适大小的缩略图了

/**
 * 获取缩略图
 * 支持自动旋转
 * 某些型号的手机相机图片是反的,可以根据exif信息实现自动纠正
 * @return
 */
public static Bitmap $thumbnail(String path,
        int maxWidth, int maxHeight, boolean autoRotate) {

    int angle = 0;
    if (autoRotate) {
        angle = ImageLess.$exifRotateAngle(path);
    }

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    // 获取这个图片的宽和高信息到options中, 此时返回bm为空
    Bitmap bitmap = BitmapFactory.decodeFile(path, options);
    options.inJustDecodeBounds = false;
    // 计算缩放比
    int sampleSize = $sampleSize(options, maxWidth, maxHeight);
    options.inSampleSize = sampleSize;
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inPurgeable = true;
    options.inInputShareable = true;

    if (bitmap != null && !bitmap.isRecycled()) {
        bitmap.recycle();
    }
    bitmap = BitmapFactory.decodeFile(path, options);

    if (autoRotate && angle != 0) {
        bitmap = $rotate(bitmap, angle);
    }

    return bitmap;
}
public static Bitmap $thumbnail(String path,
        int maxWidth, int maxHeight, boolean autoRotate) {

    int angle = 0;
    if (autoRotate) {
        angle = ImageLess.$exifRotateAngle(path);
    }

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    // 获取这个图片的宽和高信息到options中, 此时返回bm为空
    Bitmap bitmap = BitmapFactory.decodeFile(path, options);
    options.inJustDecodeBounds = false;
    // 计算缩放比
    int sampleSize = $sampleSize(options, maxWidth, maxHeight);
    options.inSampleSize = sampleSize;
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inPurgeable = true;
    options.inInputShareable = true;

    if (bitmap != null && !bitmap.isRecycled()) {
        bitmap.recycle();
    }
    bitmap = BitmapFactory.decodeFile(path, options);

    if (autoRotate && angle != 0) {
        bitmap = $rotate(bitmap, angle);
    }

    return bitmap;
}

系统内置了一个ThumbnailUtils也能生成缩略图,细节上不一样但原理是相同的。

8. Matrix变形

学过线性代数或者图像处理的同学们一定深知Matrix的强大,很多常见的图像变换一个Matrix就能搞定,甚至更复杂的也是如此。

// Matrix matrix = new Matrix();
// 每一种变化都包括set,pre,post三种,分别为设置、矩阵先乘、矩阵后乘。
平移:matrix.setTranslate()
缩放:matrix.setScale()
旋转:matrix.setRotate()
斜切:matrix.setSkew()

下面我举两个例子说明一下。

旋转

借助Matrix的postRotate方法旋转一定角度。

Matrix matrix = new Matrix();
// angle为旋转的角度
matrix.postRotate(angle);
Bitmap rotatedBitmap = Bitmap.createBitmap(originBitmap,
        0,
        0,
        originBitmap.getWidth(),
        originBitmap.getHeight(),
        matrix,
        true);new Matrix();
// angle为旋转的角度
matrix.postRotate(angle);
Bitmap rotatedBitmap = Bitmap.createBitmap(originBitmap,
        0,
        0,
        originBitmap.getWidth(),
        originBitmap.getHeight(),
        matrix,
        true);

缩放

借助Matrix的postScale方法旋转一定角度。

Matrix matrix = new Matrix();
// scaleX,scaleY分别为为水平和垂直方向上缩放的比例
matrix.postScale(scaleX, scaleY);
Bitmap scaledBitmap = Bitmap.createBitmap(originBitmap,
        0,
        0,
        originBitmap.getWidth(),
        originBitmap.getHeight(),
        matrix,
        true);new Matrix();
// scaleX,scaleY分别为为水平和垂直方向上缩放的比例
matrix.postScale(scaleX, scaleY);
Bitmap scaledBitmap = Bitmap.createBitmap(originBitmap,
        0,
        0,
        originBitmap.getWidth(),
        originBitmap.getHeight(),
        matrix,
        true);

Bitmap本身也带了一个缩放方法,不过是把bitmap缩放到目标大小,原理也是用Matrix,我们封装一下:

// 水平和宽度缩放到指定大小,注意,这种情况下图片很容易变形
Bitmap scaledBitmap = Bitmap.createScaledBitmap(originBitmap,
        dstWidth,
        dstHeight,
        true);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(originBitmap,
        dstWidth,
        dstHeight,
        true);

通过组合可以实现更多效果。

9. 裁剪

图片的裁剪的应用场景还是很多的:头像剪切,照片裁剪,圆角,圆形等等。

矩形

矩阵形状的裁剪比较简单,直接用createBitmap方法即可:

Canvas canvas = new Canvas(originBitmap);
draw(canvas);
// 确定裁剪的位置和裁剪的大小
Bitmap clipBitmap = Bitmap.createBitmap(originBitmap,
        left, top,
        clipWidth, clipHeight); canvas = new Canvas(originBitmap);
draw(canvas);
// 确定裁剪的位置和裁剪的大小
Bitmap clipBitmap = Bitmap.createBitmap(originBitmap,
        left, top,
        clipWidth, clipHeight);

圆角

对于圆角我们需要借助Xfermode和PorterDuffXfermode,把圆角矩阵套在原Bitmap上取交集得到圆角Bitmap。

// 准备画笔
Paint paint = new Paint();
paint.setAntiAlias(true);

// 准备裁剪的矩阵
Rect rect = new Rect(0, 0,
        originBitmap.getWidth(), originBitmap.getHeight());
RectF rectF = new RectF(new Rect(0, 0,
        originBitmap.getWidth(), originBitmap.getHeight()));

Bitmap roundBitmap = Bitmap.createBitmap(originBitmap.getWidth(),
        originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(roundBitmap);
// 圆角矩阵,radius为圆角大小
canvas.drawRoundRect(rectF, radius, radius, paint);

// 关键代码,关于Xfermode和SRC_IN请自行查阅
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(originBitmap, rect, rect, paint);
Paint paint = new Paint();
paint.setAntiAlias(true);

// 准备裁剪的矩阵
Rect rect = new Rect(0, 0,
        originBitmap.getWidth(), originBitmap.getHeight());
RectF rectF = new RectF(new Rect(0, 0,
        originBitmap.getWidth(), originBitmap.getHeight()));

Bitmap roundBitmap = Bitmap.createBitmap(originBitmap.getWidth(),
        originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(roundBitmap);
// 圆角矩阵,radius为圆角大小
canvas.drawRoundRect(rectF, radius, radius, paint);

// 关键代码,关于Xfermode和SRC_IN请自行查阅
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(originBitmap, rect, rect, paint);

圆形

和上面的圆角裁剪原理相同,不过画的是圆形套在上面。

为了从中间裁剪出圆形,我们需要计算绘制原始Bitmap的left和top值。

int min = originBitmap.getWidth() > originBitmap.getHeight() ?
originBitmap.getHeight() : originBitmap.getWidth();
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap circleBitmap = Bitmap.createBitmap(min, min,
    Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(circleBitmap);
// 圆形
canvas.drawCircle(min / 2, min / 2, min / 2, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

// 居中显示
int left = - (originBitmap.getWidth() - min) / 2;
int top = - (originBitmap.getHeight() - min) / 2;
canvas.drawBitmap(originBitmap, left, top, paint);min = originBitmap.getWidth() > originBitmap.getHeight() ?
originBitmap.getHeight() : originBitmap.getWidth();
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap circleBitmap = Bitmap.createBitmap(min, min,
    Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(circleBitmap);
// 圆形
canvas.drawCircle(min / 2, min / 2, min / 2, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

// 居中显示
int left = - (originBitmap.getWidth() - min) / 2;
int top = - (originBitmap.getHeight() - min) / 2;
canvas.drawBitmap(originBitmap, left, top, paint);

从圆角、圆形的处理上我们应该能看的出来绘制任意多边形都是可以的。

10. 保存Bitmap

很多图片应用都支持裁剪功能,滤镜功能等等,最终还是需要把处理后的Bitmap保存到本地,不然就是再强大的功能也是白忙活了。

public static String $save(Bitmap bitmap,
        Bitmap.CompressFormat format, int quality, File destFile) {
    try {
        FileOutputStream out = new FileOutputStream(destFile);
        if (bitmap.compress(format, quality, out)) {
            out.flush();
            out.close();
        }

        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }

        return destFile.getAbsolutePath();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
} static String $save(Bitmap bitmap,
        Bitmap.CompressFormat format, int quality, File destFile) {
    try {
        FileOutputStream out = new FileOutputStream(destFile);
        if (bitmap.compress(format, quality, out)) {
            out.flush();
            out.close();
        }

        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }

        return destFile.getAbsolutePath();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

如果想更稳定或者更简单的保存到SDCard的包名路径下,可以再封装一下:

// 保存到本地,默认路径/mnt/sdcard/[package]/save/,用随机UUID命名文件
public static String $save(Bitmap bitmap,
        Bitmap.CompressFormat format, int quality, Context context) {
    if (!Environment.getExternalStorageState()
            .equals(Environment.MEDIA_MOUNTED)) {
        return null;
    }

    File dir = new File(Environment.getExternalStorageDirectory()
            + "/" + context.getPackageName() + "/save/");
    if (!dir.exists()) {
        dir.mkdirs();
    }
    File destFile = new File(dir, UUID.randomUUID().toString());
    return $save(bitmap, format, quality, destFile);
}
public static String $save(Bitmap bitmap,
        Bitmap.CompressFormat format, int quality, Context context) {
    if (!Environment.getExternalStorageState()
            .equals(Environment.MEDIA_MOUNTED)) {
        return null;
    }

    File dir = new File(Environment.getExternalStorageDirectory()
            + "/" + context.getPackageName() + "/save/");
    if (!dir.exists()) {
        dir.mkdirs();
    }
    File destFile = new File(dir, UUID.randomUUID().toString());
    return $save(bitmap, format, quality, destFile);
}

11. 巨图加载

巨图加载,当然不能使用常规方法,必OOM。

原理比较简单,系统中有一个类BitmapRegionDecoder:

public static BitmapRegionDecoder newInstance(byte[] data, int offset,
        int length, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(
        FileDescriptor fd, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(InputStream is,
        boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(String pathName,
        boolean isShareable) throws IOException {
} static BitmapRegionDecoder newInstance(byte[] data, int offset,
        int length, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(
        FileDescriptor fd, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(InputStream is,
        boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(String pathName,
        boolean isShareable) throws IOException {
}

可以按区域加载:

public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {
} Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {
}

微博的大图浏览也是通过这个BitmapRegionDecoder实现的,具体可自行查阅。

12. 颜色矩阵ColorMatrix

图像处理其实是一门很深奥的学科,所幸Android提供了颜色矩阵ColorMatrix类,可实现很多简单的特效,以灰阶效果为例子:

Bitmap grayBitmap = Bitmap.createBitmap(originBitmap.getWidth(),
        originBitmap.getHeight(), Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(grayBitmap);
Paint paint = new Paint();
ColorMatrix colorMatrix = new ColorMatrix();
// 设置饱和度为0,实现了灰阶效果
colorMatrix.setSaturation(0);
ColorMatrixColorFilter colorMatrixColorFilter =
        new ColorMatrixColorFilter(colorMatrix);
paint.setColorFilter(colorMatrixColorFilter);
canvas.drawBitmap(originBitmap, 0, 0, paint);new Canvas(grayBitmap);
Paint paint = new Paint();
ColorMatrix colorMatrix = new ColorMatrix();
// 设置饱和度为0,实现了灰阶效果
colorMatrix.setSaturation(0);
ColorMatrixColorFilter colorMatrixColorFilter =
        new ColorMatrixColorFilter(colorMatrix);
paint.setColorFilter(colorMatrixColorFilter);
canvas.drawBitmap(originBitmap, 0, 0, paint);

除了饱和度,我们还能调整对比度,色相变化等等。

13. ThumbnailUtils剖析

ThumbnailUtils是系统提供的一个专门生成缩略图的方法,我专门写了一篇文章分析,内容较多,请移步:理解ThumbnailUtils

14. 小结

既然与Bitmap经常打交道,那就把它都理清楚弄明白,这是很有必要的。

难免会有遗漏,欢迎留言,我会酌情补充。

 

 

 

 

 

理解ThumbnailUtils

前言

特别喜欢系统中一些小而精的工具类,有的时候分析一下别有一番味道。
ThumbnailUtils是系统内置的一个生成缩略图的工具类,只有512行代码,网上有很多使用ThumbnailUtils的例子,刚好我个人正在整理Bitmap的相关资料,希望从中也能有所收获。

几个概念

像素规范

系统中对缩略图的像素定义了三种规范:

 

1

2

3

4

5

 

// frameworks/base/core/java/android/provider/MediaStoreSaver.java

// Images.Thumbnails

public static final int MINI_KIND = 1; // 512 x 384

public static final int FULL_SCREEN_KIND = 2; // 未定义

public static final int MICRO_KIND = 3; // 160 * 120

 

对于开发者,只支持MINI_KIND和MICRO_KIND两种类型。为什么是这个像素呢?因为ThumbnailUtils中定义如下:

 

1

2

3

4

5

6

 

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两种类型的图片尺寸做了限制,强调一下,是“系统”。

 

1

2

 

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的两点,在大家工作中很有可能会碰到:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

 

/**

* 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的缩略图生成算法中为了从中间截图最合适的部分,包含了裁剪的逻辑。主要分两步:

  1. 先缩放:按照填满的思想缩放到目标大小
  2. 再裁剪:从中间裁剪目标大小的区域
 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

 

/**

* 把原始图片转化为目标大小的图片,从中间截图

* 注意:这里我把放大的一个逻辑处理删除了,那段逻辑永远不会执行

*/

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对外提供了如下接口生成缩略图:

 

1

2

3

 

// options主要用于是否回收原始Bitmap

public 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的正方形小图

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

 

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))

具体实现

同时支持不指定限制,也做了一个默认值处理,实现如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

 

// 计算缩放比例

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的规范,再封装一下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

 

// 规范化上面的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对应的缩略图比较,哪个大取哪个。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

 

/**

* 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的正方形。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

 

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 thumbnail

bitmap = extractThumbnail(bitmap,

TARGET_SIZE_MICRO_THUMBNAIL,

TARGET_SIZE_MICRO_THUMBNAIL, OPTIONS_RECYCLE_INPUT);

}

return bitmap;

}

 

这里你可能注意到了,如果从EXIF的代码中获取本身文件缩略图用的是decodeFile(),而后面非JPG图片获取缩略图用decodeFileDescriptor(),为什么呢?
不知道,也许是开发者“Ray Chen”忘记了,只改了一部分,另外一部分为了稳定性也没改。
据网上资料看,decodeFileDescriptor()比decodeFile()更省内存,没有论证,仅供参考。

未使用的无用代码

在ThumbnailUtils有一些私有方法,但是自己又没有去调用,暂且把这些方法定位无用代码吧:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

 

/**

* 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源码

 

你可能感兴趣的:(JavaSE)