Android圖片處理的一些總結

  最近項目由於須要支持GIF,以前項目沒有GIF的需求——用的是Picasso,原本打算在Picasso基礎上加android-gif-drawable的,可是咱們又用了PhotoView (對圖片顯示雙擊放大等功能),由於涉及到Drawable的一些處理,加上Picasso自身從新實現了Drawable,android-gif-drawable也新實現了Drawable,因此須要把兩者的Drawable綜合到一塊兒,工做會很麻煩。爲了偷懶,隨決定換到Glideandroid

  Glide和Picasso各有優勢,都是很優秀的網絡圖片處理開源庫,包括圖片下載、緩存、展現等,使用起來很小白。我的以爲Glide更優些,對GIF的支持應該算是Glide的殺手鐗,還能夠對視頻作處理獲取縮略圖。git

 

一.圖片的壓縮github

  爲了省流量,以及防止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。網絡

判斷長圖:app

    /**
     * 圖片的寬高或高寬比例>=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:ide

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();
相關文章
相關標籤/搜索