最近项目由于须要支持GIF,以前项目没有GIF的需求——用的是Picasso,原本打算在Picasso基础上加android-gif-drawable的,可是咱们又用了PhotoView (对图片显示双击放大等功能),由于涉及到Drawable的一些处理,加上Picasso自身从新实现了Drawable,android-gif-drawable也新实现了Drawable,因此须要把两者的Drawable综合到一块儿,工做会很麻烦。为了偷懒,随决定换到Glide。android
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的整数倍进行的 } }
图片的裁剪:函数
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; }
旋转矫正图片的角度:
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; }
获取当前图片旋转的角度:
/** * 读取图片属性:旋转的角度 * * @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; }
长图片的质量压缩:
/** 长图压缩在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; }
普通静态图片的质量压缩:
/** 普通图片压缩在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; }
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; }
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; }
图片压缩处理完成。
二.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(); }
三.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; }
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();