【轮子】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 {
    /**
     * 通过降低取样点压缩图片,不推荐直接使用<br/>
     * 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定宽高压缩<br/>
     * 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则取2的幂数保证所有手机表现一致。<br/>
     * 需注意,由于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);
    }

    /**
     * 通过降低取样点压缩图片,不推荐直接使用<br/>
     * 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定大小压缩<br/>
     * 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则取2的幂数保证所有手机表现一致。<br/>
     * 需注意,由于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);
    }

    /**
     * 压缩图片<br/>
     * 通过设定压缩后的宽高的最大像素,将图片等比例缩小<br/>
     * 先通过降低取样点,将图片压缩到比目标宽高稍大一点,然后再通过Matrix将图片精确调整到目标大小<br/>
     * 压缩后图像使用RGB_565模式,即每个像素占位2字节,限定大小压缩<br/>
     * 若被压缩图片本身就小于限定大小,则不改变其大小,只更改图像颜色模式为RGB_565<br/>
     * 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则通过混合压缩的方式保证压缩的结果一致<br/>
     *
     * @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;
    }

    /**
     * 压缩图片<br/>
     * 通过设定压缩后的大小,将图片等比例缩小<br/>
     * 先通过降低取样点,将图片压缩到比目标宽高稍大一点,然后再通过Matrix将图片精确调整到目标大小<br/>
     * 压缩后图像使用RGB_565模式,即每个像素占位2字节<br/>
     * 若被压缩图片本身就小于限定大小,则不改变其大小,只更改图像颜色模式为RGB_565<br/>
     * 由于inSampleSize压缩比这个参数在不同手机表现不同,有的手机可以取任意整数,有的手机只能取2的幂数,则通过混合压缩的方式保证压缩的结果一致<br/>
     *
     * @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);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 摘要:对android 上图片压缩,其实总结起来基本可以分为两类压缩:尺寸压缩和质量压缩, 尺寸压缩其实也可以理解...
    男爵是只猫丶阅读 8,728评论 2 14
  • 目录介绍 01.如何计算Bitmap占用内存1.1 如何计算占用内存1.2 上面方法计算内存对吗1.3 一个像素占...
    杨充211阅读 3,946评论 1 9
  • 2021期待与你一起共事,点击查看岗位[https://www.jianshu.com/p/6f4d67fa406...
    闲庭阅读 16,582评论 0 75
  • 图片压缩就是为了避免我们内存溢出,所有要对一系列进行压缩二次采样等 1.什么是OOM?为什么会引起OOM? out...
    lay_wn阅读 953评论 0 1
  • 7.1 压缩图片 一、基础知识 1、图片的格式 jpg:最常见的图片格式。色彩还原度比较好,可以支持适当压缩后保持...
    AndroidMaster阅读 2,467评论 0 13