zoukankan      html  css  js  c++  java
  • Android图片处理的一些总结

      最近项目因为需要支持GIF,之前项目没有GIF的需求——用的是Picasso,本来打算在Picasso基础上加android-gif-drawable的,但是我们又用了PhotoView (对图片显示双击放大等功能),因为涉及到Drawable的一些处理,加上Picasso自身重新实现了Drawable,android-gif-drawable也新实现了Drawable,所以需要把二者的Drawable综合到一起,工作会很麻烦。为了偷懒,随决定换到Glide

      Glide和Picasso各有优点,都是很优秀的网络图片处理开源库,包括图片下载、缓存、展示等,使用起来很小白。个人觉得Glide更优些,对GIF的支持应该算是Glide的杀手锏,还可以对视频做处理获取缩略图。

    一.图片的压缩

      为了省流量,以及防止OOM,必须在图片上传的时候对图片进行压缩。减小图片大小,对于手机端来说无非是三种:一是裁剪大小,二是降低质量,三是图片的颜色编码。我们的策略也是:先裁剪大小,然后进行质量压缩,采用低编码。我们的目标是图片压缩到200kb以内。

      静态图片基本策略:微信以1280*720的尺寸为基准裁剪的。这个基准应该是几年前的时候,那个时候主流或偏上的手机屏幕的尺寸。目前基本都在1920*1080的或更高,所以我们采用1920*1080的基准,完全能达到要求了。分别用图片对应的高除以1920,图片的宽除以1080,得到scale,比较两个scale,哪个大用那个,然后对图片进行缩放处理。长图片另外处理(判断长图的依据:高宽比例或宽高比例大于等于4则认为是长图,长图不做裁剪只做质量压缩)。

      GIF图片压缩基本策略:抽帧后再拼凑的方式压缩。比如2帧取1帧,然后判断达到要求否,如果么有,就继续3帧抽1帧,一直做下去达到要求的大小为止(目前没有想到更好的办法压缩)...抽帧后拼凑的时候需要注意每帧的时间需要delay(原帧之间的时间)*抽帧的数量级(比如2帧取1帧,那么就是2)。缺点:可能导致效果不太理想,抽掉的帧太多导致动图动的不流畅。基本能满足大部分的GIF图片。

      图片的颜色编码:Bitmap.Config.RGB_565。

    判断长图:

        /**
         * 图片的宽高或高宽比例>=4则定为长图
         */
        public static boolean isLongImg(int imgWidth, int imgHeight) {
            if (imgWidth > 0 && imgHeight > 0) {
                int num = imgHeight > imgWidth ? imgHeight / imgWidth : imgWidth / imgHeight;
                if(DEBUG){
                    Log.i("PhotoView", "宽高或高宽比例>=4认为是长图: " + num);
                }
                if (num >= LONG_IMG_MINIMUM_RATIO) {
                    return true;
                }
            }
            return false;
        }

    计算图片的scale:

    public final static int MAX_HEIGHT = 1920;
      public final static int MAX_WIDTH = 1080;
      /**
         * 根据图片的宽高,以定义的MAX_WIDTH和MAX_HEIGHT做参照,计算图片需要缩放的倍数
         **/
        private static int calculateInSampleSize(BitmapFactory.Options options) {
            final int imageHeight = options.outHeight;
            final int imageWidth = options.outWidth;
    
            if(Constant.DEBUG) {
                Log.i(TAG, "==图片的原始width*height: " + imageWidth + " * " + imageHeight);
            }
            if (imageWidth <= MAX_WIDTH && imageHeight <= MAX_HEIGHT) {
                return 1;
            } else {
                double scale = imageWidth >= imageHeight ? imageWidth / MAX_WIDTH : imageHeight / MAX_HEIGHT;
                double log = Math.log(scale) / Math.log(2);
                double logCeil = Math.ceil(log);// 向上舍入
                return (int) Math.pow(2, logCeil);// 2的x数倍,因为图片的缩放处理是以2的整数倍进行的
            }
        }
    View Code

    图片的裁剪:

    private static ByteArrayOutputStream compressJpegImg(Bitmap bmp, String sourceImgPath, int maxSize){
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            bmp = BitmapFactory.decodeFile(sourceImgPath, options);
            int inSampleSize = 1;
            boolean bLongBigBitmap = isLongImg(options.outWidth, options.outHeight);
            if (!bLongBigBitmap) {
                //普通图片
                inSampleSize = calculateInSampleSize(options);
            }
    
            int quality = 95; // 默认值95,即对所有图片都默认压缩一次,不管原始图片大小,先压缩一次之后再对应处理
            if (inSampleSize > 1) {
                /**
                 * 对于普通图片压缩比大于2的,第一次的默认质量压缩做大些,防止OOM
                 * 经测试10MB的图片inSampleSize= 1, 即仅仅被80%的质量压缩后大概在1.x Mb
                 */
                quality = 81;
            }
    
            BitmapFactory.Options newOptions = new BitmapFactory.Options();
            newOptions.inSampleSize = inSampleSize;
            newOptions.inPreferredConfig = Bitmap.Config.RGB_565;
            bmp = BitmapFactory.decodeFile(sourceImgPath, newOptions);
            try {
                bmp = rotaingImageView(readPictureDegree(sourceImgPath), bmp);
            } catch (Throwable e) {
                System.gc();
                if (Constant.DEBUG) {
                    e.printStackTrace();
                }
                try {
                    bmp = rotaingImageView(readPictureDegree(sourceImgPath), bmp);
                } catch (Throwable e2) {
                    if (Constant.DEBUG) {
                        e2.printStackTrace();
                    }
                }
            }
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            bmp.compress(Bitmap.CompressFormat.JPEG, quality, os);
    
            if(Constant.DEBUG) {
                Log.i(TAG, "==缩放并压缩质量一次后图片大小: " + (os.toByteArray().length / 1024) + "KB, 压缩质量:" + quality + "%, 缩放倍数: " + inSampleSize);
            }
            if (bLongBigBitmap) {
                /** 长图压缩在1MB以内 */
                bmp = compressLongImg(bmp, os, quality);
            } else {
                /** 普通图片压缩在200Kb以内 */
                bmp = compressNormalImg(bmp, os, quality, maxSize);
            }
            return os;
        }
    View Code

    旋转矫正图片的角度:

    public static Bitmap rotaingImageView(int angle, Bitmap bitmap) {
            if(angle == 0){
                return bitmap;
            }
            // 旋转图片 动作
            Matrix matrix = new Matrix();
            matrix.postRotate(angle);
            if(Constant.DEBUG) {
                System.out.println("angle2=" + angle);
            }
            // 创建新的图片
            Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
            return resizedBitmap;
        }
    View Code

    获取当前图片旋转的角度:

    /**
         * 读取图片属性:旋转的角度
         * 
         * @param path
         *            图片绝对路径
         * @return degree旋转的角度
         */
        public static int readPictureDegree(String path) {
            int degree = 0;
            try {
                ExifInterface exifInterface = new ExifInterface(path);
                int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
                //Log.i("PhotoView", "=========orientation: " + orientation);
                switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    degree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    degree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    degree = 270;
                    break;
                default:
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return degree;
        }
    View Code

    长图片的质量压缩:

    /** 长图压缩在1MB以内 */
        private static Bitmap compressLongImg(Bitmap bmp, ByteArrayOutputStream os, int quality){
            if (os.toByteArray().length / 1024 > 5 * 1024) {
                quality = 60;
            }
            int i = 0;
            while (os.toByteArray().length > IMAGE_MAX_SIZE_1MB && i < 20) {
                i++;
                try {
                    os.reset();
                    quality = quality * 90 / 100;
                    if (quality <= 0) {
                        quality = 5;
                    }
                    // Log.i(TAG, "==长图压缩质量quality: " + quality + "%, 压缩次数: " + (i + 1));
                    bmp.compress(Bitmap.CompressFormat.JPEG, quality, os);
                } catch (Exception e) {
                }
            }
            if(Constant.DEBUG) {
                Log.i(TAG, "长图:" + os.toByteArray().length / 1024 + "Kb");    
            }
            return bmp;
        }
    View Code

    普通静态图片的质量压缩:

    /** 普通图片压缩在200Kb以内 */
        private static Bitmap compressNormalImg(Bitmap bmp, ByteArrayOutputStream os, int quality, int maxSize){
            int length = os.toByteArray().length / 1024;
            if (length >= 1000) {
                quality = 20;
            } else if (length >= 300) {
                quality -= (length - 200) / 20 * 0.8;
            }
    
            if (quality <= 0) {
                quality = 50;
            }
            int i = 0;
    
            while (os.toByteArray().length > maxSize && i < 20) {
                i++;
                try {
                    os.reset();
                    quality = quality * 91 / 100;
                    if (quality <= 0) {
                        quality = 5;
                    }
                    if(Constant.DEBUG) {
                        Log.i(TAG, "==普通图片压缩质量quality: " + quality + "%,  压缩次数: " + (i + 1));
                    }
                    bmp.compress(Bitmap.CompressFormat.JPEG, quality, os);
                } catch (Exception e) {
                }
            }
            if(Constant.DEBUG) {
                Log.i(TAG, "普通:" + os.toByteArray().length / 1024 + "Kb");
            }
            return bmp;
        }
    View Code

    GIF图片的压缩,其中用到的GifImageDecoder网上找的解析GIF的代码,也可以用Glide自带的GifDecoder,只是需要一个BitmapProvider对象来满足其代理模式。AnimatedGifEncoder来自Glide库:

    /**
         * 抽帧的方式
         * **/
        private static boolean compressGifImg(String sourceImgPath, File desFile) {
            File sourceFile = new File(sourceImgPath);
            if (sourceFile == null || !sourceFile.exists()) {
                return false;
            }
            if (sourceFile.length() < IMAGE_MAX_SIZE_1MB) {
                return FileUtils.copyFile(sourceImgPath, desFile.getAbsolutePath());
            } else {
                //Toast.makeText(BusOnlineApp.mApp.getApplicationContext(),"Gif图片太大需要压缩",Toast.LENGTH_SHORT).show();
            }
            GifImageDecoder gifImageDecoder = new GifImageDecoder();
            InputStream is = null;
            try {
                is = new FileInputStream(sourceFile);
            } catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            try {
                if(gifImageDecoder.read(is) != GifImageDecoder.STATUS_OK){
                    LogUtil.i(TAG, "Gif图片解析失败");
                    return false;
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    
            int step = 1;
            boolean status = false;
            int iCount = gifImageDecoder.getFrameCount();
            ArrayList<GifFrame> listFrams = new ArrayList<GifImageDecoder.GifFrame>();
            do {
                listFrams.clear();
                step++;
                for (int i = 0; i < iCount; i += step) {
                    listFrams.add(gifImageDecoder.getGifFrames().get(i));
                }
                status = makeGif(desFile, listFrams, step);
                if (status) {
                    if(Constant.DEBUG)
                        Log.i(TAG, "Gif图片压缩完成后: " + desFile.length() / 1024 + "KB");
                } else {
                    Log.i(TAG, "Gif图片合成失败");
                    break;
                }
            } while (desFile.length() > IMAGE_MAX_SIZE_1MB);
    
            gifImageDecoder.recycle();
            
            return status;
        }
    View Code

    GIF抽帧后拼凑:

    private static boolean makeGif(File saveFile, ArrayList<GifFrame> gifFrames, int step) {
            AnimatedGifEncoder gifEncoder = new AnimatedGifEncoder();
            if (!saveFile.exists())
                try {
                    saveFile.createNewFile();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }
         //为了矫正时间做出的调整
            if (step > 3) {
                step--;
            }
            OutputStream os;
            try {
                os = new FileOutputStream(saveFile);
                gifEncoder.start(os); 
                for (int i = 0; i < gifFrames.size(); i++) {
                    gifEncoder.addFrame(gifFrames.get(i).image);
                    gifEncoder.setDelay(gifFrames.get(i).delay * step);
                    gifEncoder.setRepeat(0);
                }
                return gifEncoder.finish();
            } catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return false;
        }
    View Code

    图片压缩处理完成。

    二.PhotoView长图预览大图的时候显示效果优化

      最终效果:类似新浪微博或微信朋友圈。宽度填充整个屏幕,高度可滑动;或宽度滑动,高度占据屏幕的2/3(手机全景图),具体根据显示的View来,我们需要的是填充整个屏幕,所以View是match_parent的。

      因为用的是PhotoView库,所以在PhotoViewAttacher.class中修改的源码函数private void updateBaseMatrix(Drawable d) ,思路:计算缩放比例,按照当前显示的View尺寸来计算的,代码如下:

    /**
         * Calculate Matrix for FIT_CENTER
         *
         * @param d- Drawable being displayed
         */
        private void updateBaseMatrix(Drawable d) {
            ImageView imageView = getImageView();
            if (null == imageView || null == d) {
                return;
            }
    
            final float viewWidth = getImageViewWidth(imageView);
            final float viewHeight = getImageViewHeight(imageView);
            final int drawableWidth = d.getIntrinsicWidth();
            final int drawableHeight = d.getIntrinsicHeight();
    
            mBaseMatrix.reset();
    
            if (isLongImg(drawableWidth, drawableHeight)) {
                final float widthScale = viewWidth / drawableWidth;
                float heightScale = 1f;
                if (drawableWidth > drawableHeight) {
                    // 长图类似全景图,高度只占photoview的1/2
                    heightScale = viewHeight / (drawableHeight * 2);
                } else {
                    heightScale = viewHeight / drawableHeight;
                }
                float scale = Math.max(widthScale, heightScale);
                mBaseMatrix.postScale(scale, scale);
                mBaseMatrix.postTranslate(0f, 0f);
            } else {
                if (mScaleType == ScaleType.CENTER) {
                    mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, (viewHeight - drawableHeight) / 2F);
                } else if (mScaleType == ScaleType.CENTER_CROP) {
                    final float widthScale = viewWidth / drawableWidth;
                    final float heightScale = viewHeight / drawableHeight;
                    float scale = Math.max(widthScale, heightScale);
                    mBaseMatrix.postScale(scale, scale);
                    mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F);
                } else if (mScaleType == ScaleType.CENTER_INSIDE) {
                    final float widthScale = viewWidth / drawableWidth;
                    final float heightScale = viewHeight / drawableHeight;
                    float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
                    mBaseMatrix.postScale(scale, scale);
                    mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F);
                } else {
                    RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
                    RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);
    
                    if ((int) mBaseRotation % 180 != 0) {
                        mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
                    }
    
                    switch (mScaleType) {
                    case FIT_CENTER:
                        mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
                        break;
    
                    case FIT_START:
                        mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
                        break;
    
                    case FIT_END:
                        mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
                        break;
    
                    case FIT_XY:
                        mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
                        break;
    
                    default:
                        break;
                    }
                }
            }
    
            resetMatrix();
        }
    View Code

    三.Glide的一些问题

      1.从缓存中获取图片作为占位图问题

      需求:在列表里面显示缩略的小图,当点击小图后显示大图,因为小图已经下载来了,那么在下载大图的时候用小图去占位显示,用户体验效果会好很多。

      但是Glide是每个size的都是单独缓存的,所以就存在这样的问题,无法用已经下载的小图去占位显示。因为Glide缓存id即存储在内存或本地文件中的文件名是根据图片信息:name(网络图片的url),decoder,encoder,transformation,size等等去用散列算法生成的一个key,所以据我了解就算有了网络图片的url,不知道图片的size等信息是无法拼凑出这个key,从缓存中单独拿出数据的,从而无法实现前面说的先显示缩略图来占位的效果。so,加以改造Glide的源码,在生成小图的缓存key的时候去掉一些信息,只留下name信息(对于要求不管缩略图还是原图的GIF都要动的,此方法不行,我们的效果:列表中显示小缩略图的时候不动就如jpeg,效果参加:新浪微博)。为什么不能全部去掉呢?因为全部去掉对于GIF图片就可能存在不动的情况,因为去掉后,缓存的数据中没有decoder,encoder,transformation等信息,导致可能无法识别成GIF的问题。所以区别对待:缓存小图的时候只用name,缓存原图的时候加上全部信息。具体代码在Glide的EngineKey.class中的函数public void updateDiskCacheKey(MessageDigest messageDigest),代码如下:

     @Override
        public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException {
            /**
             * 注释掉其他信息,为了方便获取文件名字,只依照url去生成
             * */
            if(bSimple){
                signature.updateDiskCacheKey(messageDigest);
                messageDigest.update(id.getBytes(STRING_CHARSET_NAME));
            }else {
                byte[] dimensions = ByteBuffer.allocate(8)
                        .putInt(width)
                        .putInt(height)
                        .array();
                signature.updateDiskCacheKey(messageDigest);
                messageDigest.update(id.getBytes(STRING_CHARSET_NAME));
                messageDigest.update(dimensions);
                messageDigest.update((cacheDecoder   != null ? cacheDecoder  .getId() : "").getBytes(STRING_CHARSET_NAME));
                messageDigest.update((decoder        != null ? decoder       .getId() : "").getBytes(STRING_CHARSET_NAME));
                messageDigest.update((transformation != null ? transformation.getId() : "").getBytes(STRING_CHARSET_NAME));
                messageDigest.update((encoder        != null ? encoder       .getId() : "").getBytes(STRING_CHARSET_NAME));
                // The Transcoder is not included in the disk cache key because its result is not cached.
                messageDigest.update((sourceEncoder  != null ? sourceEncoder .getId() : "").getBytes(STRING_CHARSET_NAME));
            }
        }
        private static boolean bSimple = true;
        public static void setCacheKeySimple(boolean b){
            bSimple = b;
        }
    View Code

    2.Glide加载长图问题

    因为显示图片的ImageView尺寸在绘制的时候只能是手机屏幕的尺寸,但是Glide的图片在加载或下载图片的时候的尺寸是从传进去的imageView来获取的!Glide.with(mContext).load(path).error(color).into(imageView)

    就算利用Glide.with(mContext).load(path).override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).into(imageView)也无法在源码中会判断imageView的尺寸做矫正的。源码如下:

    private int getViewHeightOrParam() {
                final LayoutParams layoutParams = view.getLayoutParams();
                if (isSizeValid(view.getHeight())) {
                    return view.getHeight();
                } else if (layoutParams != null) {
                    return getSizeForParam(layoutParams.height, true /*isHeight*/);
                } else {
                    return PENDING_SIZE;
                }
            }
    
            private int getViewWidthOrParam() {
                final LayoutParams layoutParams = view.getLayoutParams();
                if (isSizeValid(view.getWidth())) {
                    return view.getWidth();
                } else if (layoutParams != null) {
                    return getSizeForParam(layoutParams.width, false /*isHeight*/);
                } else {
                    return PENDING_SIZE;
                }
            }

    所以我们就需要改造下,目前比较笨的方法也如同处理缓存的方式一样,加入个flag判断,如下:

    private int getViewHeightOrParam() {
                if(USE_ORIGINAL_SIZE){
                    return Target.SIZE_ORIGINAL;
                }
                final LayoutParams layoutParams = view.getLayoutParams();
                if (isSizeValid(view.getHeight())) {
                    return view.getHeight();
                } else if (layoutParams != null) {
                    return getSizeForParam(layoutParams.height, true /*isHeight*/);
                } else {
                    return PENDING_SIZE;
                }
            }
    
            private int getViewWidthOrParam() {
                if(USE_ORIGINAL_SIZE){
                    return Target.SIZE_ORIGINAL;
                }
                final LayoutParams layoutParams = view.getLayoutParams();
                if (isSizeValid(view.getWidth())) {
                    return view.getWidth();
                } else if (layoutParams != null) {
                    return getSizeForParam(layoutParams.width, false /*isHeight*/);
                } else {
                    return PENDING_SIZE;
                }
            }
    public static boolean USE_ORIGINAL_SIZE = false;
    public
    static void useOriginalSize(boolean bOriginal){ USE_ORIGINAL_SIZE = bOriginal; }

    3.Glide在使用BaseAdapter时候setTag()问题

      因为在Glide中调用View的setTag(Object tag)会导致冲突,貌似是Glide中有使用此法,所以我们就调用另外一个imageView.setTag(int key, Object tag); ---对应getTag(int key), 但是要注意这个key,不能自定义int值,不然会报错:The key must be an application-specific resource id.  我们可以用view的id,比如:

    convertView.setTag(R.layout.comm_act_detail_layout, viewHolder);
    ...... viewHolder
    = (ViewHolder) convertView.getTag(R.layout.comm_act_detail_layout);

    4.GLide图片下载

    在子线程中调用下载

    public static Bitmap downloadPicByUrl(Context context, String picUrl){
            Bitmap bitmap=null;
            
            try {
                FutureTarget<Bitmap> futureTarget = Glide.with(context).load(picUrl).asBitmap().into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
                bitmap = futureTarget.get();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } 
            return bitmap;
        }

    或者用下面的方法得到File

    File file = Glide.with(context).load(path).downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get();
  • 相关阅读:
    JavaScript循环 — for、for/in、while、do/while
    Git
    js根据日期获取所在周
    nodejs安装 Later version of Node.js is already installed. Setup will now exit 及 node与npm版本不符
    sqlserver 2014 json
    根据官方数据制作中国省市区数据库
    kubernetes系列③:集群升级-实践(参照官方文档)
    kubernetes系列:服务外部访问集中管理组件-ingress-nginx
    kubernetes系列-部署篇:Kubernetes的包管理工具-helm
    kubernetes系列-部署篇:使用kubeadm初始化一个高可用的Kubernetes集群
  • 原文地址:https://www.cnblogs.com/solossl/p/5813441.html
Copyright © 2011-2022 走看看