【轮子】Android中的图片缩放

写在开头

本文只是介绍,通过安卓原生的方式将一张原始图片缩放到合适的大小,严格来说是缩放图片,而非压缩图片的技术
并且由于缩放后的图片占空间还是较大,并且算法耗时较长,所以对于我的使用场景(压缩上传图片)不是很好用,但是拿来做图片墙的话还行
若想压缩上传图片的话,还是推荐使用现成的库,下面先推荐两个图片压缩库,传送门:
AiYaCompressHelper
Luban

图片压缩

生产环境用户反映,上传身份证经常会失败,而且很费时间,检查代码发现前辈的代码写的浆糊一般,净出现一些w,ww,www的变量名,我知道他们都指代宽width,但原谅后辈脑子笨,实在记不住谁是谁,只能删掉重来。
压缩图片的思路,无非就是以下几步:

  1. 通过inSampleSize减少取样点,先将图片大概压缩一下
  2. 通过Matrix,对图片大小进行精确调整
  3. 改变编码格式,将图片转存为PNG或者JPG

减少采样点

减少采样点是BitmapFactory中提供的一个方法,主要是用到了inSampleSize参数。若inSampleSize为1时,采样后的图片就是原始大小的图片,若inSampleSize为2,则采样后的图片的宽高为原图的1/2,像素面积为原图的1/4,占空间也为1/4;若inSampleSize为4,则采样后的图片的宽高为原图的1/4,像素面积为原图的1/16,占空间也为1/16以此类推。
但是inSampleSize参数不能为浮点数,以及小于1的数,小于1时作为1来处理,即不能将图片放大,因为原理上不允许。
这里有一个特殊情况,官方文档中指出,inSampleSize参数取值应该为2的幂,即1、2、4、8等,若给的值不为2的幂,则会取一个比给的值小的最大的2的幂来代替。例如inSampleSize参数取值为10,则会用8来代替。但这个结论并非在所有系统上成立,因此此处应该严格控制,否则会得到意想不到的结果。

获取采样率的步骤遵循以下流程:

  1. BitmapFactory.Options.inJustDecodeBounds = true,若此时加载图片,只会加载图片的宽高信息
  2. 加载图片,然后从BitmapFactory.Options中取出宽高信息
  3. 根据目标大小计算采样率
  4. 可选步骤,BitmapFactory.Options.inPreferredConfig = Config.RGB_565,将图像设为565模式,此时图像深度为2字节。安卓中默认模式为RGB_8888,图像深度为4字节,但大部分情况不需要图像透明属性
  5. BitmapFactory.Options.inJustDecodeBounds = false,重新加载图片

需要注意的是,降低采样点加载图片耗时较长,一次处理大概需要200-400ms,大量图片请使用线程池。

Matrix变换

由于修改采样点无法将图片缩放到一个准确的大小,所以还需要Matrix做后续处理。因为图片在内存中存储就是一个个像素点,Matrix可以对每个像素点进行相应的变换,即可完成对图像的变换。
需要注意的是,为什么有了Matrix变换,还需要用更改采样点的方式做一次预处理?因为Matrix变换需要将图片加载进内存操作,而现在手机相机,拍一张图像大约16M像素点,若使用RGB_8888模式加载,一张图片大概需要60M的内存,虽然现在手机内存够大,但一个应用程序可用内存也就几百M,加载大量图片还是会OOM。所以应该先减少采样点加载图片,做好预处理之后再用Matrix微调。

顺便说一下Matrix,Matrix基本上就是一个用来操作图片的类,但是不止是缩放,还有其他功能,比如反转,位移,倾斜等

setTranslate(float dx,float dy):位移操作
setSkew(float kx,float ky):倾斜操作,kx、ky为X、Y方向上的比例
setSkew(float kx,float ky,float px,float py):倾斜操作,以px、py为轴心进行倾斜,kx、ky为X、Y方向上的倾斜比例
setRotate(float degrees):旋转操作,轴心为(0,0)
setRotate(float degrees,float px,float py):旋转操作,轴心为(px,py)
setScale(float sx,float sy):缩放操作,sx、sy为X、Y方向上的缩放比例。
setScale(float sx,float sy,float px,float py):缩放操作,以(px,py)为轴心进行缩放,sx、sy为X、Y方向上的缩放比例

不过这不是本文的重点,不再详述。

完整代码贴出:

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Base64;

import java.io.ByteArrayOutputStream;

/**
 * Created by ZhangXuan
 * 图像缩放工具类
 */
public class ImageScalingUtil {
    /**
     * 通过降低取样点压缩图片,不推荐直接使用
* 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定宽高压缩
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则取2的幂数保证所有手机表现一致。
* 需注意,由于inSampleSize的特性,若限定宽为1000x1000,实际图片宽为1010x600,则该图片会被压缩为505x300,图片会较小 * * @param imgPath 原图片路径 * @param reqWidth 最大宽度 * @param reqHeight 最大高度 * @return 压缩后的bitmap */ public static Bitmap reducingBitmapSampleFromPath(String imgPath, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;// 读取大小不读取内容 options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度 BitmapFactory.decodeFile(imgPath, options);// options读取图片 double outWidth = options.outWidth; double outHeight = options.outHeight;// 获取到当前图片宽高 int inSampleSize = 1; /* 先计算原图片宽高比ratio=width/height,再计算限定的范围的宽高比比reqRatio, 若reqRatio > ratio,则说明限定的范围更加细长,则以高为标准计算inSampleSize 否则,则说明限定范围更加粗矮,则以宽为计算标准 */ double ratio = outWidth / outHeight; double reqRatio = reqWidth / reqHeight; if (reqRatio > ratio) while (outHeight / inSampleSize > reqHeight) inSampleSize *= 2; else while (outWidth / inSampleSize > reqWidth) inSampleSize *= 2; options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imgPath, options); } /** * 通过降低取样点压缩图片,不推荐直接使用
* 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定大小压缩
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则取2的幂数保证所有手机表现一致。
* 需注意,由于inSampleSize的特性,若限定大小为500k,而原图为501k,则压缩后的图片为125.25k,图片会较小 * * @param imgPath 原图片路径 * @param reqSize 目标文件大小,单位为kb * @return 压缩后的bitmap */ public static Bitmap reducingBitmapSampleFromPath(String imgPath, int reqSize) { long area = reqSize * 1024 / 2;// 每个像素占2字节,将需求大小转为像素面积 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;// 读取大小不读取内容 options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度 BitmapFactory.decodeFile(imgPath, options);// options读取图片 double outWidth = options.outWidth; double outHeight = options.outHeight;// 获取到当前图片宽高 int inSampleSize = 1; while ((outHeight / inSampleSize) * (outWidth / inSampleSize) > area) inSampleSize *= 2; options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imgPath, options); } /** * 压缩图片
* 通过设定压缩后的宽高的最大像素,将图片等比例缩小
* 先通过降低取样点,将图片压缩到比目标宽高稍大一点,然后再通过Matrix将图片精确调整到目标大小
* 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定大小压缩
* 若被压缩图片本身就小于限定大小,则不改变其大小,只更改图像颜色模式为RGB_565
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则通过混合压缩的方式保证压缩的结果一致
* * @param imgPath 原图片路径 * @param reqWidth 最大宽度 * @param reqHeight 最大高度 * @return 压缩后的bitmap */ public static Bitmap compressBitmapFromPath(String imgPath, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;// 读取大小不读取内容 options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度 BitmapFactory.decodeFile(imgPath, options);// options读取图片 double outWidth = options.outWidth; double outHeight = options.outHeight;// 获取到当前图片宽高 int inSampleSize = 1; /* 先计算原图片宽高比ratio=width/height,再计算限定的范围的宽高比比reqRatio, 若reqRatio > ratio,则说明限定的范围更加细长,则以高为标准计算inSampleSize 否则,则说明限定范围更加粗矮,则以宽为计算标准 */ double ratio = outWidth / outHeight; double reqRatio = reqWidth / reqHeight; if (reqRatio > ratio) while (outHeight / inSampleSize > reqHeight) inSampleSize *= 2; else while (outWidth / inSampleSize > reqWidth) inSampleSize *= 2; options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; if (1 == inSampleSize) { // inSampleSize == 1,就说明原图比要求的尺寸小或者相等,那么不用继续压缩,直接返回。 return BitmapFactory.decodeFile(imgPath, options); } /* 否则的话,先将图片通过减少采样点的方式,以一个比限定范围稍大的尺寸读入内存, 防止因为图片太大而OOM,以及太大的图片加载时间过长 然后继续进行压缩的步骤 */ options.inSampleSize = inSampleSize / 2; Bitmap baseBitmap = BitmapFactory.decodeFile(imgPath, options); /* 使用之前计算过的宽高比, 若reqRatio > ratio,则说明限定的范围更加细长,则以高为标准计算压缩比 否则,则说明限定范围更加粗矮,则以宽为计算标准 */ float compressRatio = 1; if (reqRatio > ratio) compressRatio = reqHeight * 1.0f / baseBitmap.getHeight(); else compressRatio = reqWidth * 1.0f / baseBitmap.getWidth(); Bitmap afterBitmap = Bitmap.createBitmap( (int) (baseBitmap.getWidth() * compressRatio), (int) (baseBitmap.getHeight() * compressRatio), baseBitmap.getConfig()); Canvas canvas = new Canvas(afterBitmap); // 初始化Matrix对象 Matrix matrix = new Matrix(); // 根据传入的参数设置缩放比例 matrix.setScale(compressRatio, compressRatio); Paint paint = new Paint(); // 消除锯齿 paint.setAntiAlias(true); // 根据缩放比例,把图片draw到Canvas上 canvas.drawBitmap(baseBitmap, matrix, paint); return afterBitmap; } /** * 压缩图片
* 通过设定压缩后的大小,将图片等比例缩小
* 先通过降低取样点,将图片压缩到比目标宽高稍大一点,然后再通过Matrix将图片精确调整到目标大小
* 压缩后图像使用RGB_565模式,即每个像素占位2字节
* 若被压缩图片本身就小于限定大小,则不改变其大小,只更改图像颜色模式为RGB_565
* 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则通过混合压缩的方式保证压缩的结果一致
* * @param imgPath 原图片路径 * @param reqSize 压缩后文件大小,单位为kb * @return 压缩后的bitmap */ public static Bitmap compressBitmapFromPath(String imgPath, int reqSize) { long area = reqSize * 1024 / 2;// 每个像素占2字节,将需求大小转为像素面积 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;// 读取大小不读取内容 options.inPreferredConfig = Config.RGB_565;// 设置图片每个像素占2字节,没有透明度 BitmapFactory.decodeFile(imgPath, options);// options读取图片 double outWidth = options.outWidth; double outHeight = options.outHeight;// 获取到当前图片宽高 int inSampleSize = 1; while ((outHeight / inSampleSize) * (outWidth / inSampleSize) > area) inSampleSize *= 2; options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; if (1 == inSampleSize) { // inSampleSize == 1,就说明原图比要求的尺寸小或者相等,那么不用继续压缩,直接返回。 return BitmapFactory.decodeFile(imgPath, options); } /* 否则的话,先将图片通过减少采样点的方式,以一个比限定范围稍大的尺寸读入内存, 防止因为图片太大而OOM,以及太大的图片加载时间过长 然后继续进行压缩的步骤 */ options.inSampleSize = inSampleSize / 2; Bitmap baseBitmap = BitmapFactory.decodeFile(imgPath, options); /* 目标大小的面积与现在图片大小的面积的比的平方根,就是缩放比 java Math.sqrt() 函数不能开小数,而且先计算除法,再计算开放,再对结果求反误差很大,所以做两次开方计算 */ float compressRatio = 1; compressRatio = (float) (Math.sqrt(area) / Math.sqrt(baseBitmap.getWidth() * baseBitmap.getHeight())); Bitmap afterBitmap = Bitmap.createBitmap( (int) (baseBitmap.getWidth() * compressRatio), (int) (baseBitmap.getHeight() * compressRatio), baseBitmap.getConfig()); Canvas canvas = new Canvas(afterBitmap); // 初始化Matrix对象 Matrix matrix = new Matrix(); // 根据传入的参数设置缩放比例 matrix.setScale(compressRatio, compressRatio); Paint paint = new Paint(); // 消除锯齿 paint.setAntiAlias(true); // 根据缩放比例,把图片draw到Canvas上 canvas.drawBitmap(baseBitmap, matrix, paint); return afterBitmap; } /** * 将一张图片 以PNG的格式 转换成 base64 编码 * * @param bitmap * @return */ public static String savePNGAndToBase64(Bitmap bitmap) { ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); byte[] pngByte = baos.toByteArray();// 转为byte数组 return Base64.encodeToString(pngByte, Base64.DEFAULT); } /** * 将一张图片 以JPEG的格式 转换成 base64 编码 * * @param bitmap * @return */ public static String saveJPEGAndToBase64(Bitmap bitmap) { ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] pngByte = baos.toByteArray();// 转为byte数组 return Base64.encodeToString(pngByte, Base64.DEFAULT); } }

你可能感兴趣的:(【轮子】Android中的图片缩放)