Android——Luban图片压缩算法学习

这个库单独使用感觉相当简单,作者封装的非常好,使用特方便

  • 源码地址以及使用教程:Luban

本篇使用的代码是在RxJava——基础学习(三),简单实践基础上,添加了图片的点击事件。最近没有再学习RxJava,因为RxJava正处于过渡时期,2.0版本要发布了,修改还蛮大的,就想等2.0发布后,再继续学习RxJava


1.简单使用 <p>

使用RecyclerView将图片展示出来

前两张图,是特意添加的两个比较大的,不同分辨率的图片,第3个图之后的就是手机截屏后的图,分辨率就是手机屏幕的分辨率

  • 第1个图5120 * 2880,大小为5.68M
  • 第2个图3840 * 2400,大小为1.08M
  • 第3个图1080 * 1920,大小为1.19M

前两个图,不做任何处理,直接使用ImageView展示,在我的坚果手机百分百OOM


点击每一个图片后,开启一个新的Activity来展示图片。在新的Activity中,使用Luban将图片进行压缩,得到压缩后的图片后,使用ImageView展示出来

代码:

private void showPicFileByLuban(@NonNull File file) {
    Luban.get(ShowPicActivity.this)
         .load(file)//目标图片
         .putGear(Luban.THIRD_GEAR)//压缩等级
         .setCompressListener(new OnCompressListener() {
            @Override
            public void onStart() {//开始压缩
            }

            @Override
            public void onSuccess(File file) {//压缩成功,拿到压缩的图片,在UI线程
                Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
                mToolBar.setSubtitle(bitmap.getWidth() + "*" + bitmap.getHeight() + "-->" + bitmap.getByteCount());
                    iv.setImageBitmap(bitmap);
                }

                @Override
                public void onError(Throwable e) {//压缩失败
                }
            })
        .launch();//开启压缩
}

代码很简单,压缩后的是一个File,根据需求,对这个File再做处理

点击图片后

注意不同分辨率的图片压缩后的宽高

这个库强大的地方在于针对不同的分辨率图片,压缩比例计算,控制图片文件的大小

整个Demo的代码上传到了GithubPicStore

使用很简单,则意味着源码做了大量的优化,设计巧妙,下面学习大神的代码


2. 尝试学习源码 <p>

根据使用过程用到的方法来进行学习源码,其中最重要就是关于压缩比例的计算,学习作者的封装的思路和技巧


2.1 get(Context context)方法 <p>

public static Luban get(Context context) {
    if (INSTANCE == null) INSTANCE = new Luban(Luban.getPhotoCacheDir(context));
    return INSTANCE;
}

这个方法用来创建Luban对象,Luban的构造方法是私有的并且需要一个File对象,在get()内,在new的时候,就调用了Luban.getPhotoCacheDir(context),这个方法是用来指定缓存目录的,缓存目录默认为:系统默认缓存文件夹下的luban_disk_cache文件夹

Luban.getPhotoCacheDir(context)内又调用了getPhotoCacheDir(Context context, String cacheName)方法

private static File getPhotoCacheDir(Context context, String cacheName) {
        File cacheDir = context.getCacheDir();
        if (cacheDir != null) {
            File result = new File(cacheDir, cacheName);
            if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) {//result文件夹不能创建,或者创建了却不是一个文件夹
                return null;
            }
            return result;
        }
        if (Log.isLoggable(TAG, Log.ERROR)) {
            Log.e(TAG, "default disk cache dir is null");
        }
        return null;
}

设置缓存目录的方法


2.2 load(File file)设置压缩目标图片 <p>

public Luban load(File file) {
    mFile = file;
    return this;
}

这个方法倒是比较容易理解,设置过目标图片文件后,又返回了Luban对象本身,这样就可以用方法链了


2.3 putGear(int gear)设置压缩等级 <p>

 public Luban putGear(int gear) {
    this.gear = gear;
    return this;
}

有两个压缩等级:1档3档,默认为3档,设置其他的档位是无效的


2.4 setComressListener()设置压缩进度监听<p>

 public Luban setCompressListener(OnCompressListener listener) {
    compressListener = listener;
    return this;
}

设置监听,OnCompressListener内部有3个方法

public interface OnCompressListener {
    //压缩开始前
    void onStart();
    //压缩成功后
    void onSuccess(File file);
    //压缩失败
    void onError(Throwable e);
}

三个方法都在UI线程,可以直接用来更新UI


2.5 launch()开启压缩方法 <p>

这个方法是Luban中的核心方法,内部使用了RxJava,这个方法内的重点是根据压缩档位来进行不同的操作

public Luban launch() {
        checkNotNull(mFile, "the image file cannot be null, please call .load() before this method!");//用来判断null

        if (compressListener != null) compressListener.onStart();

        if (gear == Luban.FIRST_GEAR)//1档
            Observable.just(mFile)
                    .map(new Func1<File, File>() {
                        @Override
                        public File call(File file) {
                            return firstCompress(file);
                        }
                    })
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .doOnError(new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            if (compressListener != null) compressListener.onError(throwable);
                        }
                    })
                    .onErrorResumeNext(Observable.<File>empty())
                    .filter(new Func1<File, Boolean>() {
                        @Override
                        public Boolean call(File file) {
                            return file != null;
                        }
                    })
                    .subscribe(new Action1<File>() {
                        @Override
                        public void call(File file) {
                            if (compressListener != null) compressListener.onSuccess(file);
                        }
                    });
        else if (gear == Luban.THIRD_GEAR)//3档
            Observable.just(mFile)
                    .map(new Func1<File, File>() {
                        @Override
                        public File call(File file) {
                            return thirdCompress(file);
                        }
                    })
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .doOnError(new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            if (compressListener != null) compressListener.onError(throwable);
                        }
                    })
                    .onErrorResumeNext(Observable.<File>empty())
                    .filter(new Func1<File, Boolean>() {
                        @Override
                        public Boolean call(File file) {
                            return file != null;
                        }
                    })
                    .subscribe(new Action1<File>() {
                        @Override
                        public void call(File file) {
                            if (compressListener != null) compressListener.onSuccess(file);
                        }
                    });

        return this;
    }

这个方法内使用了RxJava,开启一个独立的线程来进行压缩,即使图片很大,也不会阻塞UI线程


方法开始有一个判null的方法,这个方法单独封装在了一个辅助工具类内

public static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {
    if (reference == null) {//若null,就抛异常,并把异常提示信息显示出来
        throw new NullPointerException(String.valueOf(errorMessage));
    }
    return reference;
}

这个方法的重中之重是thirdCompress(file)firstCompress(file),两个方法看懂一个,另一个就比较容易理解了


2.6 thirdCompress(file)3档压缩 <p>

设计思路:

压缩算法思路

源码:

 private File thirdCompress(@NonNull File file) {
        String thumb = mCacheDir.getAbsolutePath() + "/" + System.currentTimeMillis();//压缩后图片缓存路径

        thumb = filename == null || filename.isEmpty() ? thumb : filename;//判null处理

        double size;//文件大小 单位为KB
        String filePath = file.getAbsolutePath();//文件的绝对路径

        int angle = getImageSpinAngle(filePath);//图片的角度,为了保持所有的图片都能够竖直显示在屏幕
        int width = getImageSize(filePath)[0];//图片的宽
        int height = getImageSize(filePath)[1];//图片的高
        int thumbW = width % 2 == 1 ? width + 1 : width;//临时宽,将宽变作偶数
        int thumbH = height % 2 == 1 ? height + 1 : height;//临时高,将高变作偶数

        width = thumbW > thumbH ? thumbH : thumbW;//将小的一边给width,最短边
        height = thumbW > thumbH ? thumbW : thumbH;//将大的一边给height,最长边

        double scale = ((double) width / height);//比例,图片短边除以长边为该图片比例

        if (scale <= 1 && scale > 0.5625) {//比例在[1,0.5625)间
            //判断最长边是否过界
            if (height < 1664) {//最长边小于1664px
                if (file.length() / 1024 < 150) return file;//如果文件的大小小于150KB

                size = (width * height) / Math.pow(1664, 2) * 150;//计算文件大小
                size = size < 60 ? 60 : size;//判断文件大小是否小于60KB
            } else if (height >= 1664 && height < 4990) {//最长边大于1664px小于4990px
                thumbW = width / 2;//最短边缩小2倍
                thumbH = height / 2;//最长边缩小2倍
                size = (thumbW * thumbH) / Math.pow(2495, 2) * 300;//计算文件大小
                size = size < 60 ? 60 : size;//判断文件大小是否小于60KB
            } else if (height >= 4990 && height < 10240) {//如果最长边大于4990px小于10240px
                thumbW = width / 4;//最短边缩小2倍
                thumbH = height / 4;//最长边缩小2倍
                size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;//计算文件大小
                size = size < 100 ? 100 : size;判断文件大小是否小于100KB
            } else {//最长边大于10240px
                int multiple = height / 1280 == 0 ? 1 : height / 1280;//最长边与1280相比的倍数
                thumbW = width / multiple;//最短边根据倍数压缩
                thumbH = height / multiple;//最长边根据倍数压缩
                size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;//计算文件大小
                size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
            }
        } else if (scale <= 0.5625 && scale > 0.5) {//比例在[0.5625,00.5)区间
            if (height < 1280 && file.length() / 1024 < 200) return file;//最长边小于1280px并且文件大小在200KB内,就返回

            int multiple = height / 1280 == 0 ? 1 : height / 1280;//倍数,最长边与1280相比
            thumbW = width / multiple;//最短边根据倍数压缩
            thumbH = height / multiple;//最长边根据倍数压缩
            size = (thumbW * thumbH) / (1440.0 * 2560.0) * 400;//计算文件大小
            size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
        } else {//比例小于0.5
            int multiple = (int) Math.ceil(height / (1280.0 / scale));//最长边乘以比例后与1280相比的结果向上取整
            thumbW = width / multiple;//最短边根据倍数压缩
            thumbH = height / multiple;//最长边根据倍数压缩
            size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;//计算文件大小
            size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
        }
        //根据计算结果来进行压缩图片
        return compress(filePath, thumb, thumbW, thumbH, angle, (long) size);
    }

thumbWwidth有区别,width是最短边,而thumbW是压缩目标的宽的大小

拿到计算的结果后,调用了compress()方法

注意:比例是短边除以长边


compress()方法代码:

private File compress(String largeImagePath, String thumbFilePath, int width, int height, int angle, long size) {
       //根据最终计算的宽高来压缩图片
       Bitmap thbBitmap = compress(largeImagePath, width, height);
      
    //根据拿到的图片角度,使用`Matrix`旋转图片
    //有的手机照片会存在旋转90°的情况
    thbBitmap = rotatingImage(angle, thbBitmap);
     //保存图片在缓存文件中
    return saveImage(thumbFilePath, thbBitmap, size);
}

compress(largeImagePath, width, height)就是Bitmap的二次采样,将Bitmap的宽高压缩到目标大小


saveImage()代码:

    /**
     * 保存图片到指定路径
     * Save image with specified size
     *
     * @param filePath the image file save path 储存路径
     * @param bitmap   the image what be save   目标图片
     * @param size     the file size of image   期望大小
     */
    private File saveImage(String filePath, Bitmap bitmap, long size) {
        checkNotNull(bitmap, TAG + "bitmap cannot be null");//判`null`

        File result = new File(filePath.substring(0, filePath.lastIndexOf("/")));

        if (!result.exists() && !result.mkdirs()) return null;

        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        int options = 100;
        bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);//进行质量压缩,是图片文件的大小达到计算目标的大小

        while (stream.toByteArray().length / 1024 > size && options > 6) {//若图片文件的大小大于目标大小,并且质量压缩率大于6
            stream.reset();
            options -= 6;
            bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);
        }

        try {
            FileOutputStream fos = new FileOutputStream(filePath);
            fos.write(stream.toByteArray());
            fos.flush();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return new File(filePath);
    }

代码是看完了,可有细节并不明白,比如,比例0.5625,文件大小60KB,100KB,质量压缩率6,这些怎么得来的并不晓得。

不过,主要是想学习作者封装的思路和设计,细节随着经验增长,再思考了


3.最后 <p>

代码也算是看了一遍,大体是懂了。不晓得作者郑梓斌Curzibn这位大神,看到我这种水平的分析他的代码,会不会觉得把他的一些好的设计给曲解了,我哪里考虑的不对,请留言指出啊 :)

以后要多读别人的代码,多向大神们学习 ,本篇博客完整代码

本人很菜,有错误请指出

共勉 :)

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,073评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,327评论 0 17
  • 淡绿色冒着热气的水杯 沉默地在角落立着 天花板落下白色的雨 布满四周 而它仍是立着 不点头 不弯腰 笔直地立着 仿...
    晨风和昏雨阅读 166评论 0 1
  • 01 早上八点整,通往校外的路上没有人经过。撑着伞慢慢悠悠地走着,耳边剩下雨触碰伞免得声音,鞋底亲吻大地的响动。偶...
    年岁Y阅读 377评论 0 0