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

as11051105 7年前
   <p>这个库单独使用感觉相当简单,作者封装的非常好,使用特方便</p>    <p>本篇使用的代码是在 RxJava——基础学习(三),简单实践 基础上,添加了图片的点击事件。最近没有再学习 RxJava ,因为 RxJava 正处于过渡时期, 2.0 版本要发布了,修改还蛮大的,就想等 2.0 发布后,再继续学习 RxJava</p>    <h2><strong>1.简单使用</strong></h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e6a1878c10500b099ed23921ddfbd2f0.png"></p>    <p style="text-align:center">使用RecyclerView将图片展示出来</p>    <p>前两张图,是特意添加的两个比较大的,不同分辨率的图片,第3个图之后的就是手机截屏后的图,分辨率就是手机屏幕的分辨率</p>    <ul>     <li> <p>第1个图 5120 * 2880 ,大小为 5.68M</p> </li>     <li> <p>第2个图 3840 * 2400 ,大小为 1.08M</p> </li>     <li> <p>第3个图 1080 * 1920 ,大小为 1.19M</p> </li>    </ul>    <p>前两个图,不做任何处理,直接使用 ImageView 展示,在我的坚果手机百分百 OOM</p>    <p>点击每一个图片后,开启一个新的 Activity 来展示图片。在新的 Activity 中,使用 Luban 将图片进行压缩,得到压缩后的图片后,使用 ImageView 展示出来</p>    <p>代码:</p>    <pre>  <code class="language-java">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();//开启压缩  }</code></pre>    <p>代码很简单,压缩后的是一个 File ,根据需求,对这个 File 再做处理</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/87841c98d7b97786a4211595b83a315c.png"></p>    <p style="text-align:center">点击图片后</p>    <p>注意不同分辨率的图片压缩后的宽高</p>    <p>这个库强大的地方在于针对不同的分辨率图片,压缩比例计算,控制图片文件的大小</p>    <p>整个 Demo 的代码上传到了 Github : PicStore</p>    <p>使用很简单,则意味着源码做了大量的优化,设计巧妙,下面学习大神的代码</p>    <h2><strong>2. 尝试学习源码</strong></h2>    <p>根据使用过程用到的方法来进行学习源码,其中最重要就是关于压缩比例的计算,学习作者的封装的思路和技巧</p>    <h3><strong>2.1 get(Context context)方法</strong></h3>    <pre>  <code class="language-java">public static Luban get(Context context) {      if (INSTANCE == null) INSTANCE = new Luban(Luban.getPhotoCacheDir(context));      return INSTANCE;  }</code></pre>    <p>这个方法用来创建 Luban 对象, Luban 的构造方法是私有的并且需要一个 File 对象,在 get() 内,在 new 的时候,就调用了 Luban.getPhotoCacheDir(context) ,这个方法是用来指定缓存目录的,缓存目录默认为:系统默认缓存文件夹下的 luban_disk_cache 文件夹</p>    <p>在 Luban.getPhotoCacheDir(context) 内又调用了 getPhotoCacheDir(Context context, String cacheName) 方法</p>    <pre>  <code class="language-java">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;  }</code></pre>    <p>设置缓存目录的方法</p>    <h3><strong>2.2 load(File file)设置压缩目标图片</strong></h3>    <pre>  <code class="language-java">public Luban load(File file) {      mFile = file;      return this;  }</code></pre>    <p>这个方法倒是比较容易理解,设置过目标图片文件后,又返回了 Luban 对象本身,这样就可以用方法链了</p>    <h3><strong>2.3 putGear(int gear)设置压缩等级</strong></h3>    <pre>  <code class="language-java">public Luban putGear(int gear) {      this.gear = gear;      return this;  }</code></pre>    <p>有两个压缩等级: 1档 和 3档 ,默认为 3档 ,设置其他的档位是无效的</p>    <h3><strong>2.4 setComressListener()设置压缩进度监听</strong></h3>    <pre>  <code class="language-java">public Luban setCompressListener(OnCompressListener listener) {      compressListener = listener;      return this;  }</code></pre>    <p>设置监听, OnCompressListener 内部有3个方法</p>    <pre>  <code class="language-java">public interface OnCompressListener {      //压缩开始前      void onStart();      //压缩成功后      void onSuccess(File file);      //压缩失败      void onError(Throwable e);  }</code></pre>    <p>三个方法都在 UI 线程,可以直接用来更新 UI</p>    <h3><strong>2.5 launch()开启压缩方法</strong></h3>    <p>这个方法是 Luban 中的核心方法,内部使用了 RxJava ,这个方法内的重点是根据压缩档位来进行不同的操作</p>    <pre>  <code class="language-java">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;      }</code></pre>    <p>这个方法内使用了 RxJava ,开启一个独立的线程来进行压缩,即使图片很大,也不会阻塞 UI 线程</p>    <p>方法开始有一个判 null 的方法,这个方法单独封装在了一个辅助工具类内</p>    <pre>  <code class="language-java">public static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {      if (reference == null) {//若null,就抛异常,并把异常提示信息显示出来          throw new NullPointerException(String.valueOf(errorMessage));      }      return reference;  }</code></pre>    <p>这个方法的重中之重是 thirdCompress(file) 和 firstCompress(file) ,两个方法看懂一个,另一个就比较容易理解了</p>    <h3><strong>2.6 thirdCompress(file)3档压缩</strong></h3>    <p>设计思路:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cb8b7a807bfbc3350f48b659b08f3383.png"></p>    <p style="text-align:center">压缩算法思路</p>    <p>源码:</p>    <pre>  <code class="language-java">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);      }</code></pre>    <p>thumbW 和 width 有区别, width 是最短边,而 thumbW 是压缩目标的宽的大小</p>    <p>拿到计算的结果后,调用了 compress() 方法</p>    <p>注意:比例是短边除以长边</p>    <p>compress()方法代码:</p>    <pre>  <code class="language-java">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);  }</code></pre>    <p>compress(largeImagePath, width, height) 就是 Bitmap 的二次采样,将 Bitmap 的宽高压缩到目标大小</p>    <p>saveImage()代码:</p>    <pre>  <code class="language-java">/**       * 保存图片到指定路径       * 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);      }</code></pre>    <p>代码是看完了,可有细节并不明白,比如,比例 0.5625 ,文件大小 60KB,100KB ,质量压缩率 6 ,这些怎么得来的并不晓得。</p>    <p>不过,主要是想学习作者封装的思路和设计,细节随着经验增长,再思考了</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/2bc57932468d</p>    <p> </p>