Android 处理大图问题

作者: 猪_队友 | 来源:发表于2018-07-01 16:51 被阅读210次

    背景

    无论在现实开发中,还是面试中,这个问题都会经常遇到。

    具体情况可以分为两种

    • 图片的大小很大,但是需要在android中显示的区域却没有图片真正大小那么大。

    比如一个高清图片作为头像,图片的大小是1M,10241024。但是在手机里只需要显示8080的大小。

    比如著名的清明上河图(30000*926)如果只是要显示缩略图,就不必加载原图

    • 图片的大小很大,需要在Android中可以显示原图大小。

    比如要查看高清头像的原图

    比如著名的清明上河图(30000*926)如果显示原图这样的大图加载到内存中占用106M的大小,很显然系统是接受不了的。

    针对这两种不同的需求我们采用不同的策略。

    图片的大小很大,但是需要在android中显示的区域却没有图片真正大小那么大。

    首先针对要缩略图这类的需求
    压缩图片方式有很多种:

    • 质量压缩
    • 尺寸压缩
    • 采样率压缩
    • libjpeg库来进行压缩

    一、质量压缩用于本地想服务器上传图片,或者保存到本地的时候

    /**
         * 质量压缩
         * 设置bitmap options属性,降低图片的质量,像素不会减少
         * 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
         * 设置options 属性0-100,来实现压缩
         *
         * @param bmp
         * @param file
         */
        public static void qualityCompress(Bitmap bmp, File file) {
            // 0-100 100为不压缩
            int quality = 20;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 把压缩后的数据存放到baos中
            bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
            try {
                FileOutputStream fos = new FileOutputStream(file);
                fos.write(baos.toByteArray());
                fos.flush();
                fos.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    二、尺寸压真生实现像素的减少 多用于缓存缩略图

    需要考虑将大图片读入内存后的释放事宜。

    /**
         * 尺寸压缩(通过缩放图片像素来减少图片占用内存大小)
         *
         * @param bmp
         * @param file
         */
    
        public static void sizeCompress(Bitmap bmp, File file) {
            // 尺寸压缩倍数,值越大,图片尺寸越小
            int ratio = 8;
            // 压缩Bitmap到对应尺寸
            Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
            Canvas canvas = new Canvas(result);
            Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
            canvas.drawBitmap(bmp, null, rect, null);
    
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 把压缩后的数据存放到baos中
            result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
            try {
                FileOutputStream fos = new FileOutputStream(file);
                fos.write(baos.toByteArray());
                fos.flush();
                fos.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    三、采样率压缩

    设置图片的采样率,降低图片像素
    不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜。不过因为采样率是整数,有时候采样率会不合适。2太大,3太小。
    不同的压缩方法,看情景去应用

    这就需要BitmapFactory. Options里的两个参数

    • inJustDecodeBounds
      设置为true就可以让解析方法禁止为bitmap分配内存,就是只会得到里面的数据信息,而不会把bitmap加载到内存里面
    • inSampleSize
      采样率,这个就是图像数字化的时候,单位时间采样本数,白话就是图片的某一点需要样本去表示。比如我们本来需要16个,采样率变成2 ,那么就变成4个了。
      >1 说明高清变普清
      <1 就是普清变高清
      更加官方的解释请自行百度,我就是比喻一下。

    这样我们就可以把图片压缩成我们想要的结果了。

    public static int calculateInSampleSize(BitmapFactory.Options options,
            int reqWidth, int reqHeight) {
        // 源图片的高度和宽度
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            // 计算出实际宽高和目标宽高的比率
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
            // 一定都会大于等于目标的宽和高。
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    
    
    
    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
            int reqWidth, int reqHeight) {
        // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        // 调用上面定义的方法计算inSampleSize值
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 使用获取到的inSampleSize值再次解析图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    
      img.setImageBitmap(
                    decodeSampledBitmapFromResource(getResources(), R.drawable.qmsht, 100, 100));
    

    一般来说这样就完了,但是我们的图片如果来自于IO流里面数据呢,这样做就有问题了。

      private void loadSLImg() {
            InputStream inputStream = null;
            try {
                inputStream = getAssets().open("qmsht.jpg");
            } catch (IOException e) {
                e.printStackTrace();
            }
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(inputStream, null, options);
    
            options.inSampleSize = calculateInSampleSize(options, 800, 100);
        
            options.inJustDecodeBounds = false;
    
            Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
            img.setImageBitmap(bitmap);
    
    
        }
    

    结果运行我们发现,并没有出现图片,这是为什么呢?
    因为我们之前

    options.inJustDecodeBounds = true;
    

    已经生效,所以

    Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
    

    bitmap 为null,并不会有位图解析出来。
    你可能会说 后来不是 设置 false了吗?

    options.inJustDecodeBounds = false;
    

    但是inputStream流文件已经被设置options.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(inputStream, null, options);方法之后这个流文件某些属性已经被更改。因为源码在Native,所以我们并不是具体情节,所以我们可以做一些测试看看结果。

    options.inJustDecodeBounds = false;

      InputStream inputStream = null;
            try {
                inputStream = getAssets().open("qmsht.jpg");
                Log.e("inputStream", "原size:" + inputStream.available());
            } catch (IOException e) {
                e.printStackTrace();
            }
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = false;
            Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
            Log.e("inputStream", "处理后size:" + inputStream.available());
            Log.e("inputStream", "bitmap 是否为null:" + (bitmap==null)+" getWidth  "+bitmap.getWidth());
    
    E/inputStream: 原size:8063397
    E/inputStream: 处理后size:0
    E/inputStream: bitmap 是否为null:false getWidth  30000
    

    说明inputStream被消耗掉了
    options.inJustDecodeBounds = true;

     InputStream inputStream = null;
            try {
                inputStream = getAssets().open("qmsht.jpg");
                Log.e("inputStream", "原size:" + inputStream.available());
            } catch (IOException e) {
                e.printStackTrace();
            }
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
            Log.e("inputStream", "处理后size:" + inputStream.available());
            Log.e("inputStream", "bitmap 是否为null:" + (bitmap==null));
    
    E/inputStream: 原size:8063397
    E/inputStream: 处理后size:8057973
    E/inputStream: bitmap 是否为null:true
    

    从结果不难看出,所以我们的策略就是,一个流分析,计算出inSampleSize
    流一个流来加载图片。
    至于根据path来在家图片就没有那么麻烦了
    Bitmap bitmap = BitmapFactory.decodeFile(path, options);
    你懂的其实他也是两次。

    图片的大小很大,需要在Android中可以显示原图大小。

    这一类就也很好办,那我就一部分一部分来显示吧,局部显示总不会超吧,如果局部显示也会超,那么先压缩再局部显示。
    android 中有这么一个类 BitmapRegionDecoder。看名称就是区域解析。然后通过手势来移动不同的区域,动态的解析不懂得区域,达到看图无缝连接。

      private void loadBigImg() {
            if (inputStream == null) {
                try {
                    inputStream = getAssets().open("qmsht.jpg");
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
                //获得图片的宽、高
                BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
                tmpOptions.inJustDecodeBounds = true;
                BitmapFactory.decodeStream(inputStream, null, tmpOptions);
                width = tmpOptions.outWidth;
                height = tmpOptions.outHeight;
    
    
                try {
                    bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                options = new BitmapFactory.Options();
                options.inPreferredConfig = Bitmap.Config.RGB_565;
            }
            Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(old_x, 0, old_x + width / 40, height), options);
            img.setImageBitmap(bitmap);
        }
    
    

    很简单吧,然后可以通过手势,来改变Rect的四个坐标,我们就可以随意看大图了。

    @Override
        public boolean onTouchEvent(MotionEvent event) {
    
            switch (event.getAction()) {
    
                case MotionEvent.ACTION_DOWN:
                    dow_x = (int) event.getX();
                    break;
                case MotionEvent.ACTION_MOVE:
    
                    old_x += (int) (dow_x - event.getX());
                    loadBigImg();
    
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
    
            return true;
        }
    

    结尾

    这些都是原理和简单操作,要解决实际问题还需要更细致的处理。共勉~

    相关文章

      网友评论

      • 码无止境:而且此文也要详细说明下,这些压缩跟图片加载到内存的关系,这个很容易让人误解的,保存到本地的图片大小跟加载内存的大小没任何关系,图片加载到内存的大小,只跟图片分辨率(长宽)以及图片色值编码格式,如果是放本地的话,跟设备密度也有关。
        猪_队友:@码无止境 这个上一篇有说,不过没把压缩图片和内存关系说明,感谢道友指正,抽时间把这块详细写一下
      • 码无止境:质量压缩要说明一下,只能是有损压缩的格式图片才行,像png这种无损压缩的图片格式,你质量压缩,quality 这个参数就没有作用了,会被忽略。
        猪_队友:@码无止境 压缩我一笔带过了,下一篇可以把这块详细说明下。。感谢 道友

      本文标题:Android 处理大图问题

      本文链接:https://www.haomeiwen.com/subject/sgycuftx.html