7.1 壓縮圖片
一、基礎(chǔ)知識
1、圖片的格式
- jpg:最常見的圖片格式。色彩還原度比較好降盹,可以支持適當壓縮后保持比較好的色彩度与柑。使用jpg格式,可以使生成的圖片大小比較小而不會使圖片看起來很模糊(失真)蓄坏。如果你圖片顏色很多的价捧,建議使用。
- jpeg:與jpg格式相似涡戳,經(jīng)常在拷相片的時候看到结蟋,但我們制作圖片的時候一般是保存jpg,文件相對jpg較大渔彰,因為里面存儲了很多相機里的拍攝參數(shù)嵌屎,像色相推正、飽和度、對比度等宝惰,可用于直接打印和印刷(相對于jpg要好)
- png:在網(wǎng)頁中用的較多的一種格式植榕,支持透明
格式類型 | 支持壓縮 | 支持透明 | 支持動畫 | 非矢量 |
---|---|---|---|---|
jpg | 是 | 否 | 否 | 是 |
jpeg | 是 | 否 | 否 | 是 |
png | 否 | 是 | 否 | 是 |
2、px尼夺、dp和dpi
- px (pixels)像素:就是屏幕上實際的像素點單位尊残。
- dip或dp (device independent pixels):設備獨立像素,與設備屏幕有關(guān)淤堵。
- dpi(dot per inch):屏幕像素密度寝衫,每英寸多少像素。
換算公式:
dip = px * (dpi / 160)
DisplayMetrics#density = dpi / 160
DisplayMetrics#densityDpi = dpi
3拐邪、各種寬度之間的區(qū)別
ImageView#getWidth()
顯示的ImageView控件的寬度慰毅。
ImageView#getMeasureWidth()
顯示的ImageView控件的測量寬度,在布局之前計算出來的庙睡。
ImageView#getMinimumWidth()
顯示的ImageView控件的最小寬度事富,是XML參數(shù)定義里的minWidth。
Drawable#getIntrinsicWidth()
decode進來之后乘陪,沒有進行縮放的值统台,若為BitmapDrawable則
getIntrinsicWidth() = ((BitmapDrawable)d).getBitmap().getWidth()
,但是decode的時候啡邑,可能會根據(jù)圖片所在的文件夾和設備屏幕dpi進行縮放贱勃,因此不一定等于原始圖片的寬度。
注意:若切換橫豎屏谤逼,組件的寬高會互換贵扰,但是圖片本身的固有寬高不會變
//獲取ImageView顯示的圖片在設備上的真實尺寸,注意調(diào)用的時機流部,一定要在layout完成之后
void getImgDisplaySize() {
Drawable imgDrawable = imageView.getDrawable();
if (imgDrawable != null) {
//獲得ImageView中Image的真實寬高戚绕,等價于getIntrinsicWidth()
int dw = mCurrentImage.getDrawable().getBounds().width();
int dh = mCurrentImage.getDrawable().getBounds().height();
//獲得ImageView中Image的變換矩陣
Matrix m = mCurrentImage.getImageMatrix();
float[] values = new float[10];
m.getValues(values);
//Image在繪制過程中的變換矩陣,從中獲得x和y方向的縮放系數(shù)枝冀,比如設置scaleType為centerCrop也會導致縮放
float sx = values[0];
float sy = values[4];
//計算Image在屏幕上實際繪制的寬高
realImgShowWidth = (int) (dw * sx);
realImgShowHeight = (int) (dh * sy);
}
}
ResourceId -> Uri
public static final String ANDROID_RESOURCE = "android.resource://";
public static final String FOREWARD_SLASH = "/";
private static Uri resourceIdToUri(Context context, int resourceId) {
return Uri.parse(ANDROID_RESOURCE + context.getPackageName() + FOREWARD_SLASH + resourceId);
}
Uri -> InputStream
ContentResolver#openInputStream(Uri uri) InputStream
4舞丛、ScaleType
該屬性用以表示顯示圖片的方式,默認值是FIT_CENTER
參考:圖片說明Andorid中ImageView的不同屬性ScaleType的區(qū)別
- CENTER:圖片按原來的大小居中顯示
- CENTER_CROP:等比例縮放果漾,使得圖片長(寬)大于等于ImageView的 長(寬)球切,一定會充滿ImageView
- CENTER_INSIDE:圖片居中顯示;若圖片比較大绒障,按比例縮小使得長(寬)小于等于ImageView的 長(寬);若圖片較小吨凑,直接居中顯示。
- FIT_CENTER FIT_START FIT_END:大圖等比例縮小户辱,使整幅圖能夠居中顯示在ImageView中鸵钝,小圖等比例放大糙臼,同樣
要整體居中顯示在ImageView中,顯示居中/左上/右下 - FIT_XY:不按比例縮放圖片蒋伦,把圖片塞滿整個View
- MATRIX:由Matrix來決定弓摘,配合方法:
ImageView#setImageMatrix (Matrix matrix)
二、圖片的基本認識
1痕届、圖片的存在形式
- 文件形式(即以二進制形式存在于硬盤上)
獲取大小(Byte):File.length() - 流的形式(即以二進制形式存在于內(nèi)存中)
獲取大小(Byte):new FileInputStream(File).available()
和文件形式獲得的大小應該是一樣的 - Bitmap形式
獲取大小(Byte):Bitmap.getByteCount() 不準確
2韧献、BitmapFactory.Options
用于解碼Bitmap時的各種參數(shù)控制
2.1、inPreferredConfig
設置色彩模式研叫。默認值是ARGB_8888锤窑,一個像素點占用4 bytes空間;一般對透明度不做要求的話嚷炉,一般采用RGB_565(5+6+5=16)模式渊啰,一個像素點占用2 bytes。
bitmap占用內(nèi)存大小=圖片長度(px)*圖片寬度(px)*單位像素占用的字節(jié)數(shù)
例如:若一張圖片加載之后的寬高23684224申屹,采用默認的色彩模式ARGB_8888绘证,
則占用內(nèi)存大小:23684224*4/1024/1024=38.15625MB哗讥∪履牵看到bitmap占用這么大,所以用完調(diào)用Bitmap.recycle()是個好習慣(推薦)杆煞,不調(diào)用也沒關(guān)系魏宽,因為GC進程會自動回收。
注意:如果inPreferredConfig不為null决乎,解碼器會嘗試使用此參數(shù)指定的顏色模式來對圖片進行解碼队询,如果inPreferredConfig為null或者在解碼時無法滿足此參數(shù)指定的顏色模式,解碼器會自動根據(jù)原始圖片的特征以及當前設備的屏幕位深构诚,選取合適的顏色模式來解碼蚌斩,例如,如果圖片中包含透明度范嘱,那么對該圖片解碼時使
用的配置就需要支持透明度送膳,默認會使用ARGB_8888來解碼。
2.2彤侍、inJustDecodeBounds
若為true,那么在解碼的時候?qū)⒉粫祷豣itmap逆趋,只會返回這個bitmap的尺寸盏阶。這個屬性的目的:如果你只想知道一個bitmap的尺寸,但又不想將其加載到內(nèi)存時使用闻书。這是一個非常有用的屬性名斟。
2.3脑慧、inSampleSize
這個值是一個int,當它小于1的時候砰盐,將會被當做1處理闷袒,如果大于1,那么就會按照比例(1 /inSampleSize)縮小bitmap的寬和高岩梳、降低分辨率囊骤,大于1時這個值將會被處置為2的倍數(shù)。例如冀值,width=100也物,height=100,inSampleSize=2列疗,那么就會將bitmap處理為滑蚯,width=50,height=50抵栈,寬高降為1 / 2告材,像素數(shù)降為1 / 4。
2.4古劲、inScaled斥赋、inDensity和inTargetDensity
inScaled:設置這個Bitmap是否可以被縮放,默認值是true绢慢,表示可以被縮放灿渴。
inDensity:
若圖片放在drawable文件夾中,inDensity屬性會根據(jù)drawable文件夾的分辨率來賦值胰舆,drawable文件夾(不指定分辨率,即文件夾名后不跟分辨率),則默認的inDensity就是160骚露,對應關(guān)系如下:
ldpi -----> 120
mdpi -----> 160
hdpi -----> 240
xhdpi -----> 320
xxhdpi -----> 480
xxxhdpi -----> 640
inTartgetDensity:
會根據(jù)屏幕的像素密度來賦值锄贷,就是DisplayMetrics#densityDpi
輸出圖片的寬高 = 原圖片的寬高 / inSampleSize * (inTargetDensity / inDensity)
注意:還與inScaled有關(guān)默勾。若inJustDecodeBounds=true,將不受影響挽荡。
僅僅影響decodeResource
和decodeResourceStream
方法倦零,此時若inTargetDensity = 0误续,則設置為DisplayMetrics#densityDpi。若inDensity = 0扫茅,將被設置為上面表格的值
也就是說bitmap占用內(nèi)存的大小蹋嵌,還與設備和所在的文件夾有關(guān),因為寬高可能會進行縮放葫隙。
三栽烂、圖片壓縮
問:我們?yōu)槭裁匆獕嚎s圖片呢?
答:一,避免占用內(nèi)存過多腺办。二焰手,可能要上傳圖片,如果圖片太大怀喉,浪費流量书妻。(有時候需要上傳原圖除外)
1、避免內(nèi)存過多的壓縮方法
歸根結(jié)底躬拢,圖片是要顯示在界面組件上的躲履,所以還是要用到bitmap,從上面可得出Bitmap的在內(nèi)存中的大小只和圖片尺寸和色彩模式有關(guān)估灿,那么要想改變Bitmap在內(nèi)存中的大小崇呵,要么改變尺寸,要么改變色彩模式馅袁。
2域慷、避免上傳浪費流量的壓縮方法
改變圖片尺寸,改變色彩模式汗销,改變圖片質(zhì)量都行犹褒。正常情況下,先改變圖片尺寸和色彩模式弛针,再改變圖片質(zhì)量叠骑。
注意:如果是Bitmap#compress(CompressFormat.PNG, quality, baos),這樣的png格式削茁,quality就沒有作用了宙枷,bytes.length不會變化,因為png圖片是無損的茧跋,不能進行壓縮慰丛。
CompressFormat還有一個屬性是,CompressFormat.WEBP格式瘾杭,該格式是google自己推出來一個圖片格式诅病。
3、改變圖片質(zhì)量的壓縮方法
它是在保持像素的前提下改變圖片的位深及透明度等粥烁,來達到壓縮圖片的目的
/**
* 根據(jù)bitmap壓縮圖片質(zhì)量
* @param bitmap 未壓縮的bitmap
* @return 壓縮后的bitmap
*/
public static Bitmap cQuality(Bitmap bitmap){
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
int beginRate = 100;
//第一個參數(shù) :圖片格式 贤笆,第二個參數(shù): 圖片質(zhì)量,100為最高讨阻,0為最差 芥永,第三個參數(shù):保存壓縮后的數(shù)據(jù)的流
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bOut);
while(bOut.size()/1024/1024>100){ //如果壓縮后大于100Kb,則提高壓縮率钝吮,重新壓縮
beginRate -=10;
bOut.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, beginRate, bOut);
}
ByteArrayInputStream bInt = new ByteArrayInputStream(bOut.toByteArray());
Bitmap newBitmap = BitmapFactory.decodeStream(bInt);
if(newBitmap!=null){
return newBitmap;
}else{
return bitmap;
}
}
4埋涧、改變圖片大小的壓縮算法
4.1贴唇、采樣率法
public static boolean getCacheImage(String filePath,String cachePath){
OutputStream out = null;
BitmapFactory.Options option = new BitmapFactory.Options();
option.inJustDecodeBounds = true; //設置為true,只讀尺寸信息飞袋,不加載像素信息到內(nèi)存
Bitmap bitmap = BitmapFactory.decodeFile(filePath, option); //此時bitmap為空
option.inJustDecodeBounds = false;
int bWidth = option.outWidth;
int bHeight= option.outHeight;
int toWidth = 400;
int toHeight = 800;
int be = 1; //be = 1代表不縮放
if(bWidth/toWidth>bHeight/toHeight&&bWidth>toWidth){
be = (int)bWidth/toWidth;
}else if(bWidth/toWidth<bHeight/toHeight&&bHeight>toHeight){
be = (int)bHeight/toHeight;
}
option.inSampleSize = be; //設置縮放比例
bitmap = BitmapFactory.decodeFile(filePath, option);
try {
out = new FileOutputStream(new File(cachePath));
} catch (IOException e) {
e.printStackTrace();
}
return bitmap.compress(CompressFormat.JPEG, 100, out);
}
4.2、縮放法壓縮(martix)
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),
bit.getHeight(), matrix, true);
4.3链患、RGB_565法
BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inPreferredConfig = Bitmap.Config.RGB_565;
bm = BitmapFactory.decodeFile(Environment
.getExternalStorageDirectory().getAbsolutePath()
+ "/DCIM/Camera/test.jpg", options2);
4.4巧鸭、createScaledBitmap(固定寬高,內(nèi)部就是martix壓縮法)
bm = Bitmap.createScaledBitmap(bit, 150, 150, true);
其實說白了麻捻,Bitmap壓縮都是圍繞這個來做文章:Bitmap所占用的內(nèi)存 = 圖片長度 x 圖片寬度 x 一個像素點占用的字節(jié)數(shù)纲仍。3個參數(shù),任意減少一個的值贸毕,就達到了壓縮的效果郑叠。
5、質(zhì)量和大小結(jié)合壓縮
正常情況下我們應該把兩者相結(jié)合的明棍,所以有了下面的算法(在項目中直接用乡革,清晰度在手機上沒問題)
public static File scal(Uri fileUri){
String path = fileUri.getPath();
File outputFile = new File(path);
long fileSize = outputFile.length();
final long fileMaxSize = 200 * 1024;
if (fileSize >= fileMaxSize) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
int height = options.outHeight;
int width = options.outWidth;
double scale = Math.sqrt((float) fileSize / fileMaxSize);
options.outHeight = (int) (height / scale);
options.outWidth = (int) (width / scale);
options.inSampleSize = (int) (scale + 0.5);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
outputFile = new File(PhotoUtil.createImageFile().getPath());
FileOutputStream fos = null;
try {
fos = new FileOutputStream(outputFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos);
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
Log.d("", "sss ok " + outputFile.length());
if (!bitmap.isRecycled()) {
bitmap.recycle();
}else{
File tempFile = outputFile;
outputFile = new File(PhotoUtil.createImageFile().getPath());
PhotoUtil.copyFileUsingFileChannels(tempFile, outputFile);
}
}
return outputFile;
}
public static Uri createImageFile(){
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
File image = null;
try {
image = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
} catch (IOException e) {
e.printStackTrace();
}
// Save a file: path for use with ACTION_VIEW intents
return Uri.fromFile(image);
}
public static void copyFileUsingFileChannels(File source, File dest){
FileChannel inputChannel = null;
FileChannel outputChannel = null;
try {
try {
inputChannel = new FileInputStream(source).getChannel();
outputChannel = new FileOutputStream(dest).getChannel();
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
} catch (IOException e) {
e.printStackTrace();
}
} finally {
try {
inputChannel.close();
outputChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、高清加載長圖或大圖方案 —— 局部加載
在Android開發(fā)中摊腋,加載圖片是很常見的情況沸版,我們一般選擇傳統(tǒng)的加載圖片框架如universalimageloader,picasso兴蒸,fresco等视粮。但是加載巨圖怎么解決,就是一個圖片很大橙凳,比如清明上河圖蕾殴,世界地圖等,一個屏幕顯示不完岛啸,又不能縮小钓觉,壓縮,該怎么解決值戳?
android早就給我們解決方案 —— BitmapRegionDecoder议谷。這個類就是用來顯示指定區(qū)域的圖像,當原始圖像大堕虹,你只需要部分圖像時卧晓,BitmapRegionDecoder特別有用
1、使用
最主要的就是BitmapRegionDecoder#newInstance
方法獲取一個對象赴捞,然后通過這個對象去調(diào)用decodeRegion(Rect rect, BitmapFactory.Options options)
得到Bitmap逼裆,最后就可以
顯示在屏幕上了∩庹考慮到用戶可以觸摸移動圖像胜宇,我們用手勢控制器GestureDetector來控制圖片顯示的區(qū)域耀怜。
2、方法
BitmapRegionDecoder.newInstance(InputStream is, boolean isShareable) BitmapRegionDecoder
decodeRegion(Rect rect, BitmapFactory.Options options) Bitmap
3桐愉、實例
public class LargeImageView extends View implements GestureDetector.OnGestureListener {
private final String TAG = this.getClass().getSimpleName();
private BitmapRegionDecoder mDecoder;
//繪制的區(qū)域
private volatile Rect mRect = new Rect();
private int mScaledTouchSlop;
// 分別記錄上次滑動的坐標
private int mLastX = 0;
private int mLastY = 0;
//圖片的寬度和高度
private int mImageWidth, mImageHeight;
//手勢控制器
private GestureDetector mGestureDetector;
private BitmapFactory.Options options;
public LargeImageView(Context context) {
super(context);
init(context, null);
}
public LargeImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public LargeImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
//設置顯示圖片的參數(shù)财破,如果對圖片質(zhì)量有要求,就選擇ARGB_8888模式
options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
mScaledTouchSlop = ViewConfiguration.get(getContext())
.getScaledTouchSlop();
Log.d(TAG, "sts:" + mScaledTouchSlop);
//初始化手勢控制器
mGestureDetector = new GestureDetector(context, this);
//獲取圖片的寬高
InputStream is = null;
try {
is = context.getResources().getAssets().open("timg.jpg");
//初始化BitmapRegionDecode从诲,并用它來顯示圖片
mDecoder = BitmapRegionDecoder
.newInstance(is, false);
BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
// 設置為true則只獲取圖片的寬高等信息左痢,不加載進內(nèi)存
tmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, tmpOptions);
mImageWidth = tmpOptions.outWidth;
mImageHeight = tmpOptions.outHeight;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//把觸摸事件交給手勢控制器處理
return mGestureDetector.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent e) {
mLastX = (int) e.getRawX();
mLastY = (int) e.getRawY();
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
int x = (int) e2.getRawX();
int y = (int) e2.getRawY();
move(x, y);
return true;
}
/**
* 移動的時候更新圖片顯示的區(qū)域
*
* @param x
* @param y
*/
private void move(int x, int y) {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
//如果圖片寬度大于屏幕寬度
if (mImageWidth > getWidth()) {
//移動rect區(qū)域
mRect.offset(-deltaX, 0);
//檢查是否到達圖片最右端
if (mRect.right > mImageWidth) {
mRect.right = mImageWidth;
mRect.left = mImageWidth - getWidth();
}
//檢查左端
if (mRect.left < 0) {
mRect.left = 0;
mRect.right = getWidth();
}
invalidate();
}
//如果圖片高度大于屏幕高度
if (mImageHeight > getHeight()) {
mRect.offset(0, -deltaY);
//是否到達最底部
if (mRect.bottom > mImageHeight) {
mRect.bottom = mImageHeight;
mRect.top = mImageHeight - getHeight();
}
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = getHeight();
}
//重繪
invalidate();
}
mLastX = x;
mLastY = y;
}
@Override
public void onLongPress(MotionEvent e) {
mLastX = (int) e.getRawX();
mLastY = (int) e.getRawY();
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
int x = (int) e2.getRawX();
int y = (int) e2.getRawY();
move(x, y);
return true;
}
@Override
protected void onDraw(Canvas canvas) {
//顯示圖片
Bitmap bm = mDecoder.decodeRegion(mRect, options);
canvas.drawBitmap(bm, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
//默認顯示圖片的中心區(qū)域,開發(fā)者可自行選擇
mRect.left = imageWidth / 2 - width / 2;
mRect.top = imageHeight / 2 - height / 2;
mRect.right = mRect.left + width;
mRect.bottom = mRect.top + height;
}
}
五系洛、BitmapShader —— 實現(xiàn)圓形俊性、圓角圖片
1、相關(guān)方法
BitmapShader繼承自Shader描扯,在給Paint設置了Shader之后定页,Paint就類似于PS里面的筆刷,刷出來的是設置的Bitmap绽诚。
涉及的相關(guān)方法:
BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)
Shader#setLocalMatrix(@Nullable Matrix localM)
Paint#setShader(Shader shader)
2典徊、TileMode的取值
CLAMP 拉伸
REPEAT 重復
MIRROR 鏡像
如果大家給電腦屏幕設置屏保的時候,如果圖片太小恩够,可以選擇重復宫峦、拉伸、鏡像玫鸟;
重復:就是橫向导绷、縱向不斷重復這個bitmap
鏡像:橫向不斷翻轉(zhuǎn)重復,縱向不斷翻轉(zhuǎn)重復屎飘;
拉伸:這個和電腦屏保的模式應該有些不同妥曲,這個拉伸的是圖片最后的那一個像素;橫向的最后一個橫行像素钦购,不斷的重復檐盟,縱項的那一列像素,不斷的重復押桃;
現(xiàn)在大概明白了葵萎,BitmapShader通過設置給mPaint,然后用這個mPaint繪圖時唱凯,就會根據(jù)你設置的TileMode羡忘,對繪制區(qū)域進行著色。
這里需要注意一點:就是BitmapShader是從你的畫布的左上角開始繪制的磕昼,不在view的右下角繪制個正方形卷雕,它不會在你正方形的左上角開始。
7.2 Glide講解
一票从、簡介
在泰國舉行的谷歌開發(fā)者論壇上漫雕,谷歌為我們介紹了一個名叫Glide的圖片加載庫滨嘱,作者是bumptech。這個庫被廣泛的運用在Google的開源項目中浸间,包括2014年Google I/O大會上發(fā)布的官方App太雨。
Glide是一款由Bump Technologies開發(fā)的圖片加載框架,使得我們可以在Android平臺上以極度簡單的方式加載和展示圖片魁蒜。Glide默認使用HttpUrlConnection進行網(wǎng)絡請求躺彬,為了讓App保持一致的網(wǎng)絡請求形式,可以讓Glide使用我們指定的網(wǎng)絡請求形式請求網(wǎng)絡資源梅惯。
二、依賴
1.jar包
Github地址:https://github.com/bumptech/glide/releases
2.Gradle
dependencies {
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.android.support:support-v4:23.3.0'
}
三仿野、權(quán)限
<uses-permission android:name="android.permission.INTERNET" />
四铣减、混淆
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
五、使用
-
Glide.with(context).load(imageUrl).into(imageView);
//從URL中加載 -
Glide.with(context).load(R.mipmap.ic_launcher).into(imageView);
//從Res資源中加載 -
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Running.jpg");
Glide.with(context).load(file).into(imageView);
//從文件加載 -
Uri uri = resourceIdToUri(context, R.mipmap.ic_launcher);
Glide.with(context).load(uri).into(imageView);
//從Uri加載
Glide.with()方法用于創(chuàng)建一個加載圖片的實例脚作。with()方法可以接收Context葫哗、Activity、Fragment或者FragmentActivity類型的參數(shù)球涛。特別需要注意的是with()方法中傳入的實例會決定Glide加載圖片的生命周期劣针,如果傳入的是Activity、Fragment或者FragmentActivity的實例亿扁,那么當其被銷毀時圖片加載也會停止捺典,如果傳入的是ApplicationContext時只有當應用程序被殺掉的時候圖片加載才會停止。
使用Glide加載圖片不用擔心內(nèi)存浪費从祝,甚至是內(nèi)存溢出的問題襟己。因為Glide不會直接將圖片的完整尺寸全部加載到內(nèi)存中,而是用多少加載多少牍陌。Glide會自動判斷ImageView的大小擎浴,然后只將這么大的圖片像素加載到內(nèi)存當中,幫助我們節(jié)省內(nèi)存開支毒涧。
下面一個小的工具函數(shù)可以將資源id轉(zhuǎn)換為一個Uri:
public static final String ANDROID_RESOURCE = "android.resource://";
public static final String FOREWARD_SLASH = "/";
private static Uri resourceIdToUri(Context context, int resourceId) {
return Uri.parse(ANDROID_RESOURCE + context.getPackageName() + FOREWARD_SLASH + resourceId);
}
六贮预、方法
1、指定圖片格式
如果調(diào)用了.asBitmap()
方法契讲,則.load()中的參數(shù)指向的可以是一個靜態(tài)圖片也可以是GIF圖片仿吞,如果是一張GIF圖片,則加載之后只會展示GIF圖片的第一幀捡偏。
如果調(diào)用的.asGif()
方法茫藏,則.load()方法中的參數(shù)指向的必須是一個GIF圖片,如果是一張靜態(tài)圖片霹琼,則圖片加載完成之后展示的只是出錯占位符(如果沒有設置出錯占位符务傲,則什么也不展示)凉当。
//顯示靜態(tài)圖片(若加載的是gif圖那么就會顯示第一幀的圖片)
.asBitmap()
//顯示動態(tài)圖片(若加載的是靜態(tài)圖會加載失敗)
.asGif()
2售葡、指定占位圖顯示
//加載時顯示的圖片
.placeholder(R.drawable.image_load)
//加載失敗時顯示的圖片
.error(R.drawable.image_error)
3看杭、設置緩存
Android應用中一個較好的圖片處理框架,會最小化網(wǎng)絡請求的消耗挟伙。Glide也是一樣楼雹,默認使用內(nèi)存和磁盤緩存來避免不必要的網(wǎng)絡請求。然而尖阔,如果你的圖片變化的非持澹快,你需要禁止一些緩存介却。
比如你請求一個1000x1000像素的圖片谴供,你的ImageView是500x500像素,Glide會保存兩個版本的圖片到緩存里齿坷。
//禁止內(nèi)存緩存桂肌,但仍然會緩存到磁盤
.skipMemoryCache(true)
//禁止磁盤緩存(Glide默認緩存策略是:DiskCacheStrategy.RESULT)
.diskCacheStrategy(DiskCacheStrategy.NONE)
//緩存參數(shù)
//ALL:緩存源資源和轉(zhuǎn)換后的資源(即緩存所有版本圖像,默認行為)
//NONE:不作任何磁盤緩存永淌,然而默認的它將仍然使用內(nèi)存緩存
//SOURCE:僅緩存源資源(原來的全分辨率的圖像)崎场,上面例子里的1000x1000像素的圖片
//RESULT:緩存轉(zhuǎn)換后的資源(最終的圖像,即降低分辨率后的或者是轉(zhuǎn)換后的)
如果你有一個圖片你需要經(jīng)常處理它遂蛀,會生成各種不同的版本的圖片谭跨,緩存它的原始的分辨率圖片才有意義。我們
使用DiskCacheStrategy.SOURCE
去告訴Glide只緩存原始版本:
Glide.with(context).load("url").diskCacheStrategy(DiskCacheStrategy.SOURCE).into(imageView);
4李滴、設置加載尺寸
Glide在緩存和內(nèi)存里自動限制圖片的大小去適配ImageView的尺寸饺蚊。用Glide時,如果圖片不需要自動適配ImageView悬嗓,調(diào)用override(horizontalSize, verticalSize)污呼,
它會在將圖片顯示在ImageView之前調(diào)整圖片的大小。
//加載圖片為100*100像素的尺寸
.override(100, 100)
5包竹、設置圖片縮放
如果調(diào)用了.centerCrop()
方法燕酷,則顯示圖片的時候短的一邊填充容器,長的一邊跟隨縮放周瞎;
如果調(diào)用了.fitCenter()
方法苗缩,則顯示圖片的時候長的一邊填充容器,短的一邊跟隨縮放声诸;
這兩個方法可以都調(diào)用酱讶,如果都調(diào)用,則最終顯示的效果是后調(diào)用的方法展示的效果彼乌。
//它是一個裁剪技術(shù)泻肯,即縮放圖像讓它填充到ImageView界限內(nèi)并且裁剪額外的部分渊迁,ImageView可能會完全填充,但圖像可能不會完整顯示
.centerCrop()
//它是一個裁剪技術(shù)灶挟,即縮放圖像讓圖像都測量出來等于或小于ImageView的邊界范圍琉朽,該圖像將會完全顯示,但可能不會填滿整個ImageView
.fitCenter()
Glide
.with(context)
.load(UsageExampleListViewAdapter.eatFoodyImages[0])
.override(600, 200) // resizes the image to these dimensions (in pixel)
.centerCrop() // this cropping technique scales the image so that it fills the requested bounds and then crops the extra.
.into(imageViewResizeCenterCrop);
6稚铣、設置資源加載優(yōu)先級
假設你正在創(chuàng)建一個信息展示界面箱叁,包含頂部的一個主要照片,還有底部的2個并不重要的小圖惕医。對于用戶體驗耕漱,我們最好先加載主角照片,然后再加載底部不緊急的圖片抬伺。Glide里的.priority()方法和Priority的枚舉變量支持你的想法螟够。
Priority枚舉變量,以遞增方式列出:
- Priority.LOW
- Priority.NORMAL
- Priority.HIGH
- Priority.IMMEDIATE
你應當明白優(yōu)先級并不是非常嚴格的沛简。Glide會將它們作為一個指導來最優(yōu)化處理請求。但并不意味著所有的圖片都能夠按請求的順序加載斥废。
.priority(Priority.HIGH)
7椒楣、設置圓角或圓形圖片
//圓角圖片
.transform(new GlideRoundTransform(this))
//圓形圖片
.transform(new GlideCircleTransform(this))
8、設置縮略圖
縮略圖的優(yōu)點
縮略圖不同于前面提到的占位圖牡肉。占位圖應當是跟app綁定在一起的資源捧灰。縮略圖是一個動態(tài)的占位圖统锤,可以從網(wǎng)絡加載毛俏。縮略圖也會被先加載饲窿,直到實際圖片請求加載完畢煌寇。如果因為某些原因,縮略圖獲得的時間晚于原始圖片逾雄,它并不會替代原始圖片阀溶,而是簡單地被忽略掉。
提示:另外一個非常棒的平滑圖片顯示的方法鸦泳,通過加載圖片主色調(diào)的占位圖银锻。
Glide提供了2個不同的方法產(chǎn)生縮略圖。
第一種:簡單的縮略圖
通過在加載的時候指定一個小的分辨率做鹰,產(chǎn)生一個縮略圖击纬。這個方法在ListView和詳細視圖的組合中非常有用。如果你已經(jīng)在ListView中用到了250x250像素的圖片钾麸,那么在在詳細視圖中會需要一個更大分辨率的圖片更振。然而從用戶的角度炕桨,我們已經(jīng)看見了一個小版本的圖片,為什么需要好幾秒殃饿,同樣的圖片(高分辨率的)才能被再次加載出來呢谋作?
在這種情況下,從顯示250x250像素版本的圖片平滑過渡到詳細視圖里查看大圖更有意義乎芳。Glide里的.thumbnail()方法讓這個變?yōu)榭赡茏裱痢_@里,.thumbnal()的參數(shù)是一個(0,1)之間浮點數(shù):
Glide.with(context).load("url")
.thumbnail(0.1f)//系數(shù)需在(0,1)之間奈惑,這樣會先加載縮略圖然后加載全圖
.into(imageView);
這里傳遞一個0.1f作為參數(shù)吭净,Glide會加載原始圖片大小的10%的圖片。如果原始圖片有1000x1000像素肴甸,縮略圖的分辨率為100x100像素导坟。由于圖片將會比ImageView小,你需要確弊に冢縮放類型是否正確役电。
注意到你所有的請求設置都會影響到你的縮略圖。例如庶柿,如果你使用了一個變換讓你的圖片變?yōu)榛叶葓D村怪,縮略圖也同樣將會是灰度圖。
第二種:高級縮略圖請求(原圖與縮略圖完全不同 )
.thumbnail()傳入一個浮點類型的參數(shù)浮庐,非常簡單有效甚负,但并不是總是有意義。如果縮略圖需要從網(wǎng)絡加載同樣全分辨率圖片审残,可能根本都不快梭域。這樣,Glide提供了另一個方法去加載和顯示縮略圖:傳遞一個新的Glide請求作為參數(shù)搅轿。
private void loadImageThumbnailRequest() {
// setup Glide request without the into() method
DrawableRequestBuilder<String> thumbnailRequest = Glide
.with( context )
.load( eatFoodyImages[2] );
// pass the request as a a parameter to the thumbnail request
Glide
.with( context )
.load( UsageExampleGifAndVideos.gifUrl )
.thumbnail( thumbnailRequest )
.into( imageView3 );
}
區(qū)別在于第一個縮略圖請求是完全獨立于第二個原始請求的病涨。縮略圖可以來自不同資源或者圖片URL璧坟,你可以在它上面應用不同的變換没宾。
9、設置動畫
加載圖片時所展示的動畫沸柔,可以是Animator類型的屬性動畫循衰,也可以是int類型的動畫資源。這個動畫只在第一次加載的時候會展示褐澎,以后都會從緩存中獲取圖片会钝,因此也就不會展示動畫了(圖片的改變時才會有用)。
//設置加載動畫
.animate(R.anim.alpha_in)
//實現(xiàn)ViewPropertyAnimation.Animator接口
.animate(ViewPropertyAnimation.Animator animator)
//淡入淡出動畫,也是默認動畫迁酸,動畫默認的持續(xù)時間是300毫秒先鱼。類似:.crossFade(int duration)
.crossFade()
//移除所有動畫
.dontAnimate()
ViewPropertyAnimation.Animator animationObject = new ViewPropertyAnimation.Animator() {
@Override
public void animate(View view) {
// if it's a custom view class, cast it here
// then find subviews and do the animations
// here, we just use the entire view for the fade animation
view.setAlpha( 0f );
ObjectAnimator fadeAnim = ObjectAnimator.ofFloat( view, "alpha", 0f, 1f );
fadeAnim.setDuration( 2500 );
fadeAnim.start();
}
};
10、加載本地視頻(相當于一張縮略圖)
//只能加載本地視頻(顯示的只是視頻的第一幀圖像奸鬓,相當于一張縮略圖焙畔,不能播
//放視頻),網(wǎng)絡視頻無法加載串远。如果你想要從網(wǎng)絡URL播放視頻宏多,參考VideoView
String files = Environment.getExternalStorageDirectory().getAbsolutePath() + "/glide.avi";
Glide.with(this).load(files).into(view);
11、定制view中使用SimpleTarget和ViewTarget
Glide中的回調(diào):Target
假設我們并沒有ImageView作為圖片加載的目標澡罚。我們只需要Bitmap本身伸但。Glide提供了一個用Target獲取Bitmap資源的方法。Target只是用來回調(diào)留搔,它會在所有的加載和處理完畢時返回想要的結(jié)果更胖。
Glide提供了多種多樣有各自明確目的Target。先從SimpleTarget介紹隔显。
SimpleTarget
private SimpleTarget target = new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
// do something with the bitmap
// for demonstration purposes, let's just set it to an ImageView
imageView1.setImageBitmap( bitmap );
}
};
Glide
.with(context) // could be an issue!
.load(eatFoodyImages[0])
.asBitmap()
.into(target);
使用Target注意事項:
- 第一個是SimpleTarget對象的定義却妨。java/Android可以允許你在.into()內(nèi)匿名定義,但這會顯著增加在Glide處理完
圖片請求前Android垃圾回收清理匿名target對象的可能性括眠。最終彪标,會導致圖片被加載了,但是回調(diào)永遠不會被調(diào)用哺窄。
所以捐下,請確保將你的回調(diào)定義為一個字段對象账锹,防止被萬惡的Android垃圾回收給清理掉萌业。 - 第二個關(guān)鍵部分是Glide的.with( context )。這個問題實際上是Glide一個特性問題:當你傳遞了一個context奸柬,例如
當前app的activity生年,當activity停止后,Glide會自動停止當前的請求廓奕。這種整合到app生命周期內(nèi)是非常有用的抱婉,但也
是很難處理的。如果你的target是獨立于app的生命周期桌粉。這里的解決方案是使用application的context:.with(context.getApplicationContext())蒸绩。當app自己停止運行的時候,Glide會只取消掉圖片的請求铃肯。
特定大小的Target
另外一個潛在問題是Target沒有一個明確的大小患亿。如果你傳遞一個ImageView作為.into()的參數(shù),Glide會使用ImageView的
大小來限制圖片的大小。例如如果要加載的圖片是1000x1000像素步藕,但是ImageView的尺寸只有250x250像素惦界,Glide會降低圖片到小尺寸,以節(jié)省處理時間和內(nèi)存咙冗。顯然沾歪,由于target沒有具體大小,這對target并不起效雾消。但是灾搏,如果你有個期望的具體大小,你可以增強回調(diào)仪或。如果你知道圖片應當為多大确镊,那么在你的回調(diào)定義里應當指明,以節(jié)省內(nèi)存:
private SimpleTarget target2 = new SimpleTarget<Bitmap>(250, 250) {
@Override
public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
imageView2.setImageBitmap(bitmap);
}
};
ViewTarget
有很多原因?qū)е挛覀儾荒苤苯邮褂肐mageView范删。前面已經(jīng)介紹了如何獲取Bitmap±儆颍現(xiàn)在,我們將更深入學習到旦。假設你有個自定義的View旨巷。由于沒有已知的方法在哪里設置圖片,Glide并不支持加載圖片到定制的View內(nèi)添忘。然而用ViewTarget會讓這個更簡單采呐。
讓我們看一個簡單的定制View,它繼承于FrameLayout搁骑,內(nèi)部使用了一個ImageView:
public class FutureStudioView extends FrameLayout {
...
public void setImage(Drawable drawable) {
iv = (ImageView) findViewById(R.id.custom_view_image);
iv.setImageDrawable(drawable);
}
}
由于我們定制的View并不是繼承自ImageView斧吐,這里不能使用常規(guī)的.into()方法。因此仲器,我們只能創(chuàng)建一個ViewTarget煤率,用來傳遞給.into()方法:
void loadImageViewTarget() {
FutureStudioView customView = (FutureStudioView) findViewById(R.id.custom_view);
viewTarget = new ViewTarget<FutureStudioView, GlideDrawable>(customView) {
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
this.view.setImage(resource.getCurrent());
}
};
Glide.with(context.getApplicationContext()) // safer!
.load(eatFoodyImages[2])
.into(viewTarget);
}
在target的回調(diào)方法中,我們在定制view上使用我們創(chuàng)建的setImage(Drawable drawable)方法設置圖片乏冀。同時蝶糯,確保你注意到我們已經(jīng)在ViewTarget的構(gòu)造方法里傳遞了我們的定制view:new ViewTarget<FutureStudioView, GlideDrawable>(customView)。
12辆沦、設置監(jiān)聽請求接口
首先昼捍,創(chuàng)建一個listener作為一個字段對象,避免被垃圾回收:
private RequestListener<String, GlideDrawable> requestListener
= new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model,
Target<GlideDrawable> target, boolean isFirstResource) {
// todo log exception
// important to return false so the error placeholder can be placed
//加載異常
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, String model,
Target<GlideDrawable> target, boolean isFromMemoryCache,
boolean isFirstResource) {
//加載成功
//view.setImageDrawable(resource);
return false;
}
};
在onException方法中肢扯,你可以抓取問題妒茬,并決定你需要做什么,比如記錄日志蔚晨。如果Glide應當處理這個后果乍钻,比如顯示一個出錯占位圖,在onException方法中返回false是很重要的。
Glide
.with( context )
.load(UsageExampleListViewAdapter.eatFoodyImages[0])
.listener( requestListener )
.error( R.drawable.cupcake )
.into( imageViewPlaceholder );
只有在listener的onException方法里返回false团赁,R.drawable.cupcake才會顯示出來育拨。
13、設置取消或恢復請求
以下兩個方法是為了保證用戶界面的滑動流暢而設計的欢摄。當在ListView中加載圖片的時候熬丧,如果用戶滑動ListView的時候繼續(xù)加載圖片,就很有可能造成滑動不流暢怀挠、卡頓的現(xiàn)象析蝴,這是由于Activity需要同時處理滑動事件以及Glide加載圖片。Glide為我們提供了這兩個方法绿淋,讓我們可以在ListView等滑動控件滑動的過程中控制Glide停止加載或繼續(xù)加載闷畸,可以有效的保證界面操作的流暢。
//當列表在滑動的時候可以調(diào)用pauseRequests()取消請求
Glide.with(context).pauseRequests();
//當列表滑動停止時可以調(diào)用resumeRequests()恢復請求
Glide.with(context).resumeRequests();
// ListView滑動時觸發(fā)的事件
lv.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
// 當ListView處于滑動狀態(tài)時吞滞,停止加載圖片佑菩,保證操作界面流暢
Glide.with(MainActivity.this).pauseRequests();
break;
case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
// 當ListView處于靜止狀態(tài)時,繼續(xù)加載圖片
Glide.with(MainActivity.this).resumeRequests();
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
14裁赠、獲取緩存大小
new GetDiskCacheSizeTask(textView).execute(new File(getCacheDir(), DiskCache.Factory.DEFAULT_DISK_CACHE_DIR));
private class GetDiskCacheSizeTask extends AsyncTask<File, Long, Long> {
private final TextView resultView;
public GetDiskCacheSizeTask(TextView resultView) {
this.resultView = resultView;
}
@Override
protected void onPreExecute() {
resultView.setText("Calculating...");
}
@Override
protected void onProgressUpdate(Long... values) {
super.onProgressUpdate(values);
}
@Override
protected Long doInBackground(File... dirs) {
try {
long totalSize = 0;
for (File dir : dirs) {
publishProgress(totalSize);
totalSize += calculateSize(dir);
}
return totalSize;
} catch (RuntimeException ex) {
final String message = String.format("Cannot get size of %s: %s", Arrays.toString(dirs), ex);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
resultView.setText("error");
Toast.makeText(resultView.getContext(), message, Toast.LENGTH_LONG).show();
}
});
}
return 0L;
}
@Override
protected void onPostExecute(Long size) {
String sizeText = android.text.format.Formatter.formatFileSize(resultView.getContext(), size);
resultView.setText(sizeText);
}
private long calculateSize(File dir) {
if (dir == null) return 0;
if (!dir.isDirectory()) return dir.length();
long result = 0;
File[] children = dir.listFiles();
if (children != null)
for (File child : children)
result += calculateSize(child);
return result;
}
}
15殿漠、清除內(nèi)存緩存
//可以在UI主線程中進行
Glide.get(this).clearMemory();
16、清除磁盤緩存
//需要在子線程中執(zhí)行
Glide.get(this).clearDiskCache();
17佩捞、圖片裁剪绞幌、模糊、濾鏡等處理
變換
在圖片顯示出之前可以對圖片進行變換處理一忱。例如莲蜘,如果你的app需要顯示一張灰度圖,但只能獲取到一個原始全色彩的版本帘营,你可以使用一個變換去將圖片從有明艷色彩的版本轉(zhuǎn)換成慘淡的黑白版票渠。不要誤會我們,變換不僅限于顏色仪吧。你可以改變圖片的很多屬性:大小庄新、邊框鞠眉、色彩薯鼠、像素點,等等械蹋!在之前介紹用Glide調(diào)整圖片大小時出皇,已經(jīng)介紹了自帶的兩個
變換fitCenter和 centerCrop。這兩個方案都有一個顯著的特征哗戈,他們有他們自己的Glide轉(zhuǎn)換方法郊艘,所以,這篇文章不再介紹了。
實現(xiàn)自己的變換
為了實現(xiàn)你自己自定義的變換纱注,你需要創(chuàng)建一個新的類去實現(xiàn)變換接口畏浆。這個方法需要實現(xiàn)的內(nèi)容還是相當復雜的,你需要深入探索Glide的內(nèi)部結(jié)構(gòu)才能讓其工作好狞贱。如果你只是想要常規(guī)的圖片(不包括Gif和視頻)變換刻获,我們建議只要處理抽象的BitmapTransformation類。它簡化了相當多的實現(xiàn)瞎嬉,能覆蓋95%的使用范圍蝎毡。
所以,讓我們先看一下BitmapTransformation實現(xiàn)的一個例子氧枣。用Renderscript去模糊圖片沐兵。我們可以用之前用過的代碼去實現(xiàn)一個Glide變換。我們的框架必須繼承BitmapTransformation類:
public class BlurTransformation extends BitmapTransformation {
public BlurTransformation(Context context) {
super( context );
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return null; // todo
}
@Override
public String getId() {
return null; // todo
}
}
現(xiàn)在便监,我們用前面文章的代碼扎谎,借助Renderscript來實現(xiàn)圖片的模糊處理。
public class BlurTransformation extends BitmapTransformation {
private RenderScript rs;
public BlurTransformation(Context context) {
super( context );
rs = RenderScript.create( context );
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
Bitmap blurredBitmap = toTransform.copy( Bitmap.Config.ARGB_8888, true );
// Allocate memory for Renderscript to work with
Allocation input = Allocation.createFromBitmap(
rs,
blurredBitmap,
Allocation.MipmapControl.MIPMAP_FULL,
Allocation.USAGE_SHARED
);
Allocation output = Allocation.createTyped(rs, input.getType());
// Load up an instance of the specific script that we want to use.
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setInput(input);
// Set the blur radius
script.setRadius(10);
// Start the ScriptIntrinisicBlur
script.forEach(output);
// Copy the output to the blurred bitmap
output.copyTo(blurredBitmap);
toTransform.recycle();
return blurredBitmap;
}
@Override
public String getId() {
return "blur";
}
}
getId()方法為這個變換描述了一個獨有的識別烧董。Glide使用那個關(guān)鍵字作為緩存系統(tǒng)的一部分簿透。防止出現(xiàn)異常問題,確保其唯一解藻。
應用一個簡單的變換
Glide有兩個不同的方式進行變換老充。第一個是傳遞一個你的類的實例作為.transform()的參數(shù)。不管是圖片還是gif螟左,都可以進行變換啡浊。另一個則是使用.bitmapTransform(),
它只接受bitmap的變換胶背。由于我們的實現(xiàn)都是基于bitmap巷嚣,我們可以使用第一個:
Glide.with(context)
.load(eatFoodyImages[0])
.transform(new BlurTransformation(context))
//.bitmapTransform(new BlurTransformation(context)) // this would work too!
.into(imageView1);
這足夠讓Glide從網(wǎng)絡下載的圖片自動實現(xiàn)模糊算法。非常有用钳吟!
實現(xiàn)多重變換
通常廷粒,Glide的流接口(fluent interface)允許方法被連接在一起,然而變換并不是這樣的红且。確保你只調(diào)用.transform()或者.bitmapTransform()一次坝茎,不然,之前的設置將會被覆蓋暇番!然而嗤放,你可以通過傳遞多個轉(zhuǎn)換對象當作參數(shù)到.transform()(或者.bitmapTransform())中來進行多重變換:
Glide.with(context)
.load(eatFoodyImages[1])
.transform(new GreyscaleTransformation(context), new BlurTransformation(context))
.into(imageView2);
在這段代碼中,我們先對圖片進行了灰度變換壁酬,然后模糊處理次酌。Glide會為你自動進行兩個轉(zhuǎn)換恨课。牛逼吧!
提示:當你使用變換的時候岳服,你不能使用.centerCrop()或者.fitCenter()剂公。
Glide的變換集合
如果你已經(jīng)對你的app里要用什么變換有了想法,在花點時間看看下面的庫吧:Glide-transformations(https://github.com/wasabeef/glide-transformations)吊宋。它提供了許多變換的集合诬留。值得去看一下你的idea是否已經(jīng)被實現(xiàn)了。
這個庫有2個不同版本贫母。擴展庫包括更多的變換文兑,并且是用手機的GPU進行計算。需要一個額外的依賴腺劣,所以這兩個版本的設置還有點不一樣绿贞。你應當看看支持的變換的列表,再決定你需要用哪個版本橘原。
Glide變換的設置
設置是很簡單的籍铁!對于基本版,你可以在你的build.gradle里加一行:
dependencies {
compile 'jp.wasabeef:glide-transformations:2.0.0'
}
如果你想要使用GPU變換:
repositories {
jcenter()
mavenCentral()
}
dependencies {
compile 'jp.wasabeef:glide-transformations:2.0.0'
compile 'jp.co.cyberagent.android.gpuimage:gpuimage-library:1.3.0'
}
Glide變換的使用
在你同步了Android Studio的builde.gradle文件后趾断,你已經(jīng)可以進行使用變換集合了拒名。使用方式與使用自定義變換一樣。假如我們要用glide變換集合去模糊圖片:
Glide
.with( context )
.load( eatFoodyImages[2] )
.bitmapTransform( new jp.wasabeef.glide.transformations.BlurTransformation( context, 25 ) )
.into( imageView3 );
你也可以像上面一樣應用一組變換芋酌。一個單獨的變換或者一組變換增显,.bitmapTransform()都可以接受!
示例:圓角處理
Glide.with(mContext)
.load(R.drawable.image_example)
.bitmapTransform(new RoundedCornersTransformation(mContext, 30, 0, RoundedCornersTransformation.CornerType.BOTTOM))
.into(imageView);
可實現(xiàn)Transformation接口脐帝,進行更靈活的圖片處理同云,如進行簡單地圓角處理。
public class RoundedCornersTransformation implements Transformation<Bitmap> {
private BitmapPool mBitmapPool;
private int mRadius;
public RoundedCornersTransformation(Context context, int mRadius) {
this(Glide.get(context).getBitmapPool(), mRadius);
}
public RoundedCornersTransformation(BitmapPool mBitmapPool, int mRadius) {
this.mBitmapPool = mBitmapPool;
this.mRadius = mRadius;
}
@Override
public Resource<Bitmap> transform(Resource<Bitmap> resource, int outWidth, int outHeight) {
//從其包裝類中拿出Bitmap
Bitmap source = resource.get();
int width = source.getWidth();
int height = source.getHeight();
Bitmap result = mBitmapPool.get(width, height, Bitmap.Config.ARGB_8888);
if (result == null) {
result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(result);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
canvas.drawRoundRect(new RectF(0, 0, width, height), mRadius, mRadius, paint);
//返回包裝成Resource的最終Bitmap
return BitmapResource.obtain(result, mBitmapPool);
}
@Override
public String getId() {
return "com.wiggins.glide.widget.GlideCircleTransform(radius=" + mRadius + ")";
}
}
自定義圖片處理時為了避免創(chuàng)建大量Bitmap以及減少GC堵腹,可以考慮重用Bitmap炸站,這就需要使用BitmapPool,例如從Bitmap池中取一個Bitmap疚顷,用這個Bitmap生成一個Canvas,然后在這個Canvas上畫初始的Bitmap并使用Matrix旱易、Paint或者Shader處理這張圖片。為了有效并正確重用Bitmap需要遵循以下三條準則:
- 永遠不要把transform()傳給你的原始resource或原始Bitmap給recycle()了腿堤,更不要放回BitmapPool阀坏,因為這些都自動完成了。值得注意的是释液,任何從BitmapPool取出的用于自定義圖片變換的輔助Bitmap全释,如果不經(jīng)過transform()方法返回装处,就必須主動放回BitmapPool或者調(diào)用recycle()回收误债。
- 如果你從BitmapPool拿出多個Bitmap或不使用你從BitmapPool拿出的一個Bitmap浸船,一定要返回extras給BitmapPool。
- 如果你的圖片處理沒有替換原始resource(例如由于一張圖片已經(jīng)匹配了你想要的尺寸寝蹈,你需要提前返回)李命,transform()方法就返回原始resource或原始Bitmap。例如:
private static class MyTransformation extends BitmapTransformation {
public MyTransformation(Context context) {
super(context);
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
Bitmap result = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888);
// 如果BitmapPool中找不到符合該條件的Bitmap箫老,get()方法會返回null封字,就需要我們自己創(chuàng)建Bitmap了
if (result == null) {
// 如果想讓Bitmap支持透明度,就需要使用ARGB_8888
result = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.ARGB_8888);
}
//創(chuàng)建最終Bitmap的Canvas.
Canvas canvas = new Canvas(result);
Paint paint = new Paint();
paint.setAlpha(128);
// 將原始Bitmap處理后畫到最終Bitmap中
canvas.drawBitmap(toTransform, 0, 0, paint);
// 由于我們的圖片處理替換了原始Bitmap耍鬓,就return我們新的Bitmap就行阔籽。
// Glide會自動幫我們回收原始Bitmap。
return result;
}
@Override
public String getId() {
// Return some id that uniquely identifies your transformation.
return "com.wiggins.glide.MyTransformation";
}
}
七牲蜀、GlideModule使用
GlideModule是一個接口笆制,全局改變Glide行為的一種方式,通過全局GlideModule配置Glide(GlideModule#applyOptions
)涣达,用GlideBuilder設置選項在辆,用Glide注冊ModelLoader(GlideModule#registerComponents
)等。你需要創(chuàng)建Glide的實例度苔,來訪問GlideBuilder匆篓。可以通過創(chuàng)建一個公共的類寇窑,實現(xiàn)GlideModule的接口來定制Glide鸦概。所有的GlideModule實現(xiàn)類必須是public的,并且只擁有一個空的構(gòu)造器甩骏,以便在Glide延遲初始化時完残,可以通過反射將它們實例化。
1横漏、自定義一個GlideModule
public class MyGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// Apply options to the builder here.
}
@Override
public void registerComponents(Context context, Glide glide) {
// register ModelLoaders here.
}
}
2谨设、AndroidManifest.xml注冊
<meta-data
android:name="com.wiggins.glide.MyGlideModule"
android:value="GlideModule" />//value是固定的
3、混淆處理
-keepnames class com.wiggins.glide.MyGlideModule
# or more generally
#-keep public class * implements com.bumptech.glide.module.GlideModule
4缎浇、多個GlideModule沖突問題
GlideModule不能指定調(diào)用順序扎拣,所以應該避免不同的GlideModule之間有沖突的選項設置,可以考慮將所有的設置都放到一個GlideModule里面素跺,或者排除掉某個manifest文件的某個Module二蓝。
<meta-data
android:name="com.wiggins.glide.MyGlideModule"
tools:node="remove" />
5、更改Glide配置
已經(jīng)知道如何使用Glide module去自定義Glide≈秆幔現(xiàn)在我們看一下接口的第一個方法:applyOptions(Context context, GlideBuilder builder)
刊愚。這個方法將GlideBuilder的對象當作參數(shù),并且是void返回類型踩验,所以你在這個方法里能調(diào)用GlideBuilder可以用的方法鸥诽。
.setMemoryCache(MemoryCache memoryCache)
.setBitmapPool(BitmapPool bitmapPool)
.setDiskCache(DiskCache.Factory diskCacheFactory)
.setDiskCacheService(ExecutorService service)
.setResizeService(ExecutorService service)
.setDecodeFormat(DecodeFormat decodeFormat)
顯而易見商玫,GlideBuilder對象可以讓你訪問到Glide的核心部分。使用文中的方法牡借,你可以改變磁盤緩存拳昌、內(nèi)存緩存等等。
5.1 設置Glide內(nèi)存緩存大小
MemoryCache用來把resources緩存在內(nèi)存里钠龙,以便能馬上能拿出來顯示炬藤。默認情況下Glide使用LruResourceCache,我們可以通過它的構(gòu)造器設置最大緩存內(nèi)存大小碴里。
//獲取系統(tǒng)分配給應用的總內(nèi)存大小
int maxMemory = (int) Runtime.getRuntime().maxMemory();
//設置圖片內(nèi)存緩存占用八分之一
int memoryCacheSize = maxMemory / 8;
//設置內(nèi)存緩存大小
builder.setMemoryCache(new LruResourceCache(memoryCacheSize));
獲取默認的使用內(nèi)存
//MemoryCache和BitmapPool的默認大小由MemorySizeCalculator類決定沈矿,MemorySizeCalculator會根據(jù)給定屏幕大小可用內(nèi)存算出合適的緩存大小,
這也是推薦的緩存大小咬腋,我們可以根據(jù)這個推薦大小做出調(diào)整
MemorySizeCalculator calculator = new MemorySizeCalculator(context);
int defaultMemoryCacheSize = calculator.getMemoryCacheSize();
int defaultBitmapPoolSize = calculator.getBitmapPoolSize();
5.2 設置Glide磁盤緩存大小
//方式一
//指定的是數(shù)據(jù)的緩存地址
File cacheDir = context.getExternalCacheDir();
//最多可以緩存多少字節(jié)的數(shù)據(jù)
int diskCacheSize = 1024 * 1024 * 30;
//設置磁盤緩存大小
builder.setDiskCache(new DiskLruCacheFactory(cacheDir.getPath(), "glide", diskCacheSize));
//方式二
//存放在data/data/xxxx/cache/
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "glide", diskCacheSize));
//方式三
//存放在外置文件
builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, "glide", diskCacheSize));
5.3 設置圖片解碼格式
默認格式RGB_565相對于ARGB_8888的4字節(jié)/像素可以節(jié)省一半的內(nèi)存细睡,但是圖片質(zhì)量就沒那么高了,而且不支持透明度帝火。
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
5.4 設置BitmapPool緩存內(nèi)存大小
Bitmap池用來允許不同尺寸的Bitmap被重用溜徙,這可以顯著地減少因為圖片解碼像素數(shù)組分配內(nèi)存而引發(fā)的垃圾回收。默認情況下Glide使用LruBitmapPool作為Bitmap池犀填,LruBitmapPool采用Lru算法保存最近使用的尺寸的Bitmap蠢壹,我們可以通過它的構(gòu)造器設置最大緩存內(nèi)存大小。
builder.setBitmapPool(new LruBitmapPool(memoryCacheSize));
5.5 設置用來檢索cache中沒有的Resource的ExecutorService
//為了使縮略圖請求正確工作九巡,實現(xiàn)類必須把請求根據(jù)Priority優(yōu)先級排好序
builder.setDiskCacheService(ExecutorService service);
builder.setResizeService(ExecutorService service);
6图贸、集成網(wǎng)絡框架
Glide包含一些小的、可選的集成庫冕广,目前Glide集成庫當中包含了訪問網(wǎng)絡操作的Volley和OkHttp疏日,也可以通過Glide的ModelLoader接口自己寫網(wǎng)絡請求。
6.1 將OkHttp集成到Glide當中
a)添加依賴
dependencies {
//OkHttp 2.x
compile 'com.github.bumptech.glide:okhttp-integration:1.4.0@aar'
compile 'com.squareup.okhttp:okhttp:2.7.5'
//OkHttp 3.x
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
compile 'com.squareup.okhttp3:okhttp:3.2.0'
}
結(jié)尾的@aar可以將庫中的AndroidManifest.xml文件一起導出撒汉,Gradle自動合并必要的GlideModule到AndroidManifest.xml沟优,然后使用所集成的網(wǎng)絡連接,所以不用再
將以下文本添加到項目的AndroidManifest.xml文件中:
<meta-data
android:name="com.bumptech.glide.integration.okhttp.OkHttpGlideModule"
android:value="GlideModule" />
b)創(chuàng)建OkHttp集成庫的GlideModule
<meta-data
android:name="com.wiggins.glide.okhttp.OkHttpGlideModule"
android:value="GlideModule" />
c)混淆配置
-keep class com.wiggins.glide.okhttp.OkHttpGlideModule
#or
-keep public class * implements com.bumptech.glide.module.GlideModule
注意:
a.OkHttp 2.x和OkHttp 3.x需使用不同的集成庫睬辐;
b.Gradle會自動將OkHttpGlideModule合并到應用的manifest文件中挠阁;
c.如果你沒有對所有的GlideModule配置混淆規(guī)則(即沒有使用-keep public class * implements com.bumptech.glide.module.GlideModule
),則需要把OkHttp的GlideModule進行混淆配置:-keep class com.wiggins.glide.okhttp.OkHttpGlideModule
溯饵。
6.2 將Volley集成到Glide當中
a)添加依賴
dependencies {
compile 'com.github.bumptech.glide:volley-integration:1.4.0@aar'
compile 'com.mcxiaoke.volley:library:1.0.8'
}
b)創(chuàng)建Volley集成庫的GlideModule
<meta-data
android:name="com.wiggins.glide.volley.VolleyGlideModule"
android:value="GlideModule" />
c)混淆配置
-keep class com.wiggins.glide.volley.VolleyGlideModule
#or
-keep public class * implements com.bumptech.glide.module.GlideModule
7侵俗、替換Glide組件、使用ModelLoader自定義數(shù)據(jù)源
7.1丰刊、替換Glide組件
替換Glide組件功能需要在自定義模塊的registerComponents()方法中加入具體的替換邏輯隘谣。相比于更改Glide配置,替換Glide組件這個功能的難度就明顯大了不少啄巧。Glide中的組件非常繁多寻歧,也非常復雜掌栅,但其實大多數(shù)情況下并不需要我們?nèi)プ鍪裁刺鎿Q。不過熄求,有一個組件卻有著比較大的替換需求渣玲,那就是Glide的HTTP通訊組件逗概。
替換Glide組件功能需要在自定義模塊的GlideModule#registerComponents(Context context, Glide glide)
方法中加入具體的替換邏輯什黑,需要在方法中調(diào)用Glide#register(Class<T> modelClass, Class<Y> resourceClass, ModelLoaderFactory<T, Y> factory)
死嗦,其中modelClass表示 數(shù)據(jù)模型的類型,一般為GlideUrl
,Glide.with(context).load("url")底層就是將轉(zhuǎn)化為了GlideUrl搬瑰;resourceClass表示URL所指向的資源的類型,一般為InputStream拗慨。
默認情況下陷寝,Glide使用的是基于原生HttpURLConnection進行訂制的HTTP通訊組件,但是現(xiàn)在大多數(shù)的Android開發(fā)者都更喜歡使用OkHttp星掰,因此將Glide中的HTTP通訊組件修改成OkHttp的這個需求比較常見多望,那么今天我們也會以這個功能來作為例子進行講解。
Model:數(shù)據(jù)模型氢烘,一般為URL字符串
Resource:URL所指向的網(wǎng)絡資源
它主要和三個接口有關(guān):
ModelLoader:數(shù)據(jù)模型Loader怀偷,將任意復雜的數(shù)據(jù)模型轉(zhuǎn)化為具體的可被DataFetcher使用的數(shù)據(jù)類型。需要返回一個從url拉取數(shù)據(jù)的DataFetcher播玖,泛型類型為上面指定的類型椎工。
public interface ModelLoader<T, Y> {
DataFetcher<Y> getResourceFetcher(T var1, int var2, int var3);
}
ModelLoaderFactory:ModelLoader的工廠,build方法返回ModelLoader蜀踏。
public interface ModelLoaderFactory<T, Y> {
ModelLoader<T, Y> build(Context var1, GenericLoaderFactory var2);
void teardown();
}
DataFetcher:獲取 由model表示的resource要解碼的數(shù)據(jù)
public interface DataFetcher<T> {
T loadData(Priority var1) throws Exception; //重要方法维蒙,返回給glide的數(shù)據(jù)
void cleanup();
String getId();
void cancel();
}
默認地,Glide內(nèi)部使用標準的HTTPUrlConnection去下載圖片果覆。Glide也提供兩個集成庫颅痊。這三個方法優(yōu)點是在安全設置上都是相當嚴格的。唯一的不足之處是當你從一個使用HTTPS局待,還是self-signed的服務器下載圖片時八千,Glide并不會下載或者顯示圖片,因為self-signed認證會被認為存在安全問題燎猛。
首先創(chuàng)建一個跳過SSL認證的OkHttpClient
public class UnsafeOkHttpClient {
public static OkHttpClient getUnsafeOkHttpClient() {
try {
// Create a trust manager that does not validate certificate chains
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
//return null;//刪除這行恋捆,多謝下面評論的幾位小伙伴指出空指針問題,并提供解決方案重绷。
}
}
};
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setSslSocketFactory(sslSocketFactory);
okHttpClient.setProtocols(Arrays.asList(Protocol.HTTP_1_1));
okHttpClient.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
return okHttpClient;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
創(chuàng)建的OkHttpClient關(guān)閉了所有的SSL認證檢查沸停。
集成到 Glide
Glide的OkHTTP集成庫做的都是一樣的工作,所以我們可以跟隨他們的步驟昭卓。首先愤钾,我們需要在GlideModule里聲明我們的定制瘟滨。你應該想到,我們需要在registerComponents()方法里做適配能颁。我們可以調(diào)用.register()方法去交換Glide基礎(chǔ)構(gòu)成杂瘸。Glide使用一個ModelLoader去鏈接到數(shù)據(jù)模型創(chuàng)建一個具體的數(shù)據(jù)類型。我們的例子中伙菊,我們需要創(chuàng)建一個ModelLoader败玉,它連接到一個URL,通過GlideUrl類響應并轉(zhuǎn)化為輸入流镜硕。Glide需要能夠創(chuàng)建我們的新ModelLoader的實例运翼,所以我們在.register()方法中傳入一個工廠:
public class UnsafeOkHttpGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder glideBuilder) {
}
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
}
}
public class OkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {
@Override
public DataFetcher<InputStream> getResourceFetcher(GlideUrl glideUrl, int i, int i1) {
return new OkHttpStreamFetcher(client, glideUrl);
}
private final OkHttpClient client;
public OkHttpUrlLoader(OkHttpClient client) {
this.client = client;
}
/**
* The default factory for {@link OkHttpUrlLoader}s.
*/
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private static volatile OkHttpClient internalClient;
private OkHttpClient client;
private static OkHttpClient getInternalClient() {
if (internalClient == null) {
synchronized (Factory.class) {
if (internalClient == null) {
internalClient = UnsafeOkHttpClient.getUnsafeOkHttpClient();
}
}
}
return internalClient;
}
/**
* Constructor for a new Factory that runs requests using a static singleton client.
*/
public Factory() {
this(getInternalClient());
}
/**
* Constructor for a new Factory that runs requests using given client.
*/
public Factory(OkHttpClient client) {
this.client = client;
}
@Override
public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
return new OkHttpUrlLoader(client);
}
@Override
public void teardown() {
// Do nothing, this instance doesn't own the client.
}
}
}
public class OkHttpStreamFetcher implements DataFetcher<InputStream> {
private final OkHttpClient client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
public OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) {
this.client = client;
this.url = url;
}
@Override
public InputStream loadData(Priority priority) throws Exception {
Request.Builder requestBuilder = new Request.Builder()
.url(url.toStringUrl());
for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
String key = headerEntry.getKey();
requestBuilder.addHeader(key, headerEntry.getValue());
}
Request request = requestBuilder.build();
Response response = client.newCall(request).execute();
responseBody = response.body();
if (!response.isSuccessful()) {
throw new IOException("Request failed with code: " + response.code());
}
long contentLength = responseBody.contentLength();
stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
return stream;
}
@Override
public void cleanup() {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// Ignored
}
}
if (responseBody != null) {
responseBody.close();
}
}
@Override
public String getId() {
return url.getCacheKey();
}
@Override
public void cancel() {
}
}
7.2、使用ModelLoader自定義數(shù)據(jù)源
如果需要根據(jù)不同的要求請求不同尺寸不同質(zhì)量的圖片兴枯,這時我們就可以使用自定義數(shù)據(jù)源血淌。
a)定義處理URL接口
public interface IDataModel {
String buildDataModelUrl(int width, int height);
}
b)實現(xiàn)不同的處理URL接口
public class JpgDataModel implements IDataModel {
private String dataModelUrl;
public JpgDataModel(String dataModelUrl) {
this.dataModelUrl = dataModelUrl;
}
@Override
public String buildDataModelUrl(int width, int height) {
//http://78re52.com1.z0.glb.clouddn.com/resource/gogopher.jpg?imageView2/1/w/200/h/200/format/jpg
return String.format("%s?imageView2/1/w/%d/h/%d/format/jpg", dataModelUrl, width, height);
}
}
c)實現(xiàn)ModelLoader
public class MyDataLoader extends BaseGlideUrlLoader<IDataModel> {
public MyDataLoader(Context context) {
super(context);
}
public MyDataLoader(ModelLoader<GlideUrl, InputStream> urlLoader) {
super(urlLoader, null);
}
@Override
protected String getUrl(IDataModel model, int width, int height) {
return model.buildDataModelUrl(width, height);
}
public static class Factory implements ModelLoaderFactory<IDataModel, InputStream> {
@Override
public ModelLoader<IDataModel, InputStream> build(Context context, GenericLoaderFactory factories) {
return new MyDataLoader(factories.buildModelLoader(GlideUrl.class, InputStream.class));
}
@Override
public void teardown() {
}
}
}
d)根據(jù)不同的要求采用不同的策略加載圖片
//加載jpg圖片
Glide.with(this).using(new MyDataLoader(this)).load(new JpgDataModel(imageUrl)).into(imageView);
e)如何跳過.using()
public class MyGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
}
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(IDataModel.class, InputStream.class, new MyDataLoader.Factory());
}
}
//加載jpg圖片
Glide.with(this).load(new JpgDataModel(imageUrl)).into(imageView);
八、特點
使用簡單财剖;
可配置度及自適應程度高悠夯;
支持常見圖片格式如jpg、png躺坟、gif沦补、webp等;
支持多種數(shù)據(jù)源如網(wǎng)絡瞳氓、本地策彤、資源、Uri等匣摘;
高效緩存策略(支持Memory和Disk圖片緩存店诗,默認Bitmap格式采用RGB_565內(nèi)存使用至少減少一半);
生命周期集成(根據(jù)Context/Activity/Fragment/FragmentActivity生命周期自動管理請求)音榜;
高效處理Bitmap(使用BitmapPool使Bitmap復用庞瘸,主動調(diào)用recycle回收需要回收的Bitmap,減小系統(tǒng)回收壓力)赠叼。
九擦囊、優(yōu)點
多樣化媒體加載,支持Gif嘴办、WebP瞬场、Video及縮略圖以等類型;
生命周期集成涧郊,提供多種方式與生命周期綁定贯被,可以更好的讓加載圖片請求的生命周期動態(tài)管理起來;
高效的緩存策略
- 支持Memory和Disk圖片緩存;
- 緩存相應大小的圖片尺寸(Picasso只會緩存原始尺寸圖片彤灶,而Glide會根據(jù)你ImageView的大小來緩存相應大小的圖片尺寸看幼,因此Glide會比Picasso加載的速度要快);
- 內(nèi)存開銷谢仙隆(Picasso默認的是ARGB_8888格式诵姜,而Glide默認的Bitmap格式是RGB_565格式,這個內(nèi)存開銷大約可以減小一半)搏熄。
十棚唆、缺點
使用方法復雜:由于Glide功能強大,所以使用的方法非常多搬卒,其源碼也相對的復雜瑟俭;
包較大:Glide(v3.7.0)的大小約465kb翎卓。
十一契邀、使用場景
需要更多的內(nèi)容表現(xiàn)形式(如Gif、縮略圖等)失暴;
更高的性能要求(緩存坯门、加載速度等)。
十二逗扒、特別說明
1.ImageView的setTag問題
問題描述:如果使用Glide的into(imageView)為ImageView設置圖片的同時使用ImageView的setTag(final Object tag)方法古戴,將會導致java.lang.IllegalArgumentException: You must not call setTag() on a view Glide is targeting異常。因為Glide的ViewTarget中通過view.setTag(tag)和view.getTag()標記請求的矩肩,由于Android 4.0之前Tag存儲在靜態(tài)map里现恼,如果Glide使用setTag(int key, final Object tag)方法標記請求則可能會導致內(nèi)存泄露,所以Glide默認使用view.setTag(tag)標記請求黍檩,你就不能重復調(diào)用了叉袍。
解決辦法:如果你需要為ImageView設置Tag,必須使用setTag(int key, final Object tag)及getTag(int key)方法刽酱,其中key必須是合法的資源id以確保key
的唯一性喳逛,典型做法就是在資源文件中聲明type="id"的item資源。
2.placeholder()導致的圖片變形問題
問題描述:使用.placeholder()方法在某些情況下會導致圖片顯示的時候出現(xiàn)圖片變形的情況棵里。這是因為Glide默認開啟的crossFade動畫導致的TransitionDrawable繪制異常润文,具體描述可以查看https://github.com/bumptech/glide/issues/363。根本原因就是你的placeholder圖片和你要加載顯示的圖片寬高比不一樣殿怜,而Android的
TransitionDrawable無法很好地處理不同寬高比的過渡問題典蝌,這是Android也是Glide的Bug。
解決辦法:使用.dontAnimate()方法禁用過渡動畫头谜,或者使用animate()方法自己寫動畫骏掀,再或者自己修復TransitionDrawable的問題。
3.ImageView的資源回收問題
問題描述:默認情況下Glide會根據(jù)with()使用的Activity或Fragment的生命周期自動調(diào)整資源請求以及資源回收。但是如果有很占內(nèi)存的Fragment或Activity不銷毀而僅僅是隱藏視圖砖织,那么這些圖片資源就沒辦法及時回收款侵,即使是GC的時候。
解決辦法:可以考慮使用WeakReference侧纯,如:
final WeakReference<ImageView> imageViewWeakReference = new WeakReference<>(imageView);
ImageView target = imageViewWeakReference.get();
if (target != null) {
Glide.with(context).load(uri).into(target);
}
4.由于Bitmap復用導致的在某些設備上圖片錯亂的問題
問題描述: Glide默認使用BitmapPool的方式對應用中用到的Bitmap進行復用新锈,以減少頻繁的內(nèi)存申請和內(nèi)存回收,而且默認使用的Bitmap模式為RGB565以減少內(nèi)存開銷眶熬。但在某些設備上(通常在Galaxy系列5.X設備上很容易復現(xiàn))某些情況下會出現(xiàn)圖片加載錯亂的問題妹笆,具體詳見https://github.com/bumptech/glide/issues/601。原因初步確定是OpenGL紋理渲染異常娜氏。
解決辦法:GlideModule使用PREFER_ARGB_8888(Glide4.X已經(jīng)默認使用該模式了)拳缠,雖然內(nèi)存占用比RGB565更多一點,但可以更好地處理有透明度Bitmap的復用問
題贸弥,或者禁用Bitmap復用setBitmapPool(new BitmapPoolAdapter())來修復這個問題(不推薦這種處理方式)窟坐。
5.異步線程完成后加載圖片的崩潰問題
問題描述:通常情況下異步線程會被約束在Activity生命周期內(nèi),所以異步線程完成后使用Glide加載圖片是沒有問題的绵疲。但如果你的異步線程在Activity銷毀時沒
有取消掉哲鸳,那么異步線程完成后Glide就無法為一個已銷毀的Activity加載圖片資源,拋出的異常如下(在with()方法中就進行判斷并拋出異常):
java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity
at com.bumptech.glide.manager.RequestManagerRetriever.assertNotDestroyed(RequestManagerRetriever.java:134)
at com.bumptech.glide.manager.RequestManagerRetriever.get(RequestManagerRetriever.java:102)
at com.bumptech.glide.Glide.with(Glide.java:653)
at com.frank.glidedemo.TestActivity.onGetDataCompleted(TestActivity.java:23)
at com.frank.glidedemo.TestActivity.access$000(TestActivity.java:10)
at com.frank.glidedemo.TestActivity$BackgroundTask.onPostExecute(TestActivity.java:46)
at com.frank.glidedemo.TestActivity$BackgroundTask.onPostExecute(TestActivity.java:28)
at android.os.AsyncTask.finish(AsyncTask.java:632)
at android.os.AsyncTask.access$600(AsyncTask.java:177)
at android.os.AsyncTask$InternalHandler.handleMessage(AsyncTask.java:645)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:157)
at android.app.ActivityThread.main(ActivityThread.java:5356)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1265)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1081)
at dalvik.system.NativeStart.main(Native Method)
解決辦法:正確管理BackgroundThreads(異步線程)盔憨,當Activity停止或銷毀時徙菠,停止所有相關(guān)的異步線程及后續(xù)的UI操作,或者加載前使用isFinishing()
或isDestroyed()進行限制(不建議這種處理方式)郁岩。
7.3 Android中的緩存策略
當程序第一次從網(wǎng)絡上加載圖片后婿奔,將其緩存在存儲設備中,下次使用這張圖片的時候就不用再從網(wǎng)絡從獲取了问慎。很多時候為了提高應用的用戶體驗萍摊,往往還會把圖片在內(nèi)存中再緩存一份,因為從內(nèi)存中加載圖片比存儲設備中快蝴乔。一般情況會把圖片存一份到內(nèi)存中记餐,一份到存儲設備中,如果內(nèi)存中沒找到就去存儲設備中找薇正,還沒有找到就從網(wǎng)絡上下載片酝。
緩存策略包含緩存的添加、獲取和刪除操作挖腰。不管是內(nèi)存還是存儲設備雕沿,緩存大小都是有限制的。如何刪除舊的緩存并添加新的緩存猴仑,就對應緩存算法审轮。
目前常用的一種緩存算法是LRU(Least Recently Used)肥哎,最近最少使用算法。核心思想: 當緩存存滿時疾渣, 會優(yōu)先淘汰那些近期最少使用的緩存對象篡诽。采用LRU算法的緩存有兩種: LruCache和DiskLruCache。LruCache用于實現(xiàn)內(nèi)存緩存, DiskLruCache則充當了存儲設備緩存榴捡。
1杈女、LruCache
LruCache是一個泛型類, 它內(nèi)部采用了一個LinkedHashMap以強引用的方式存儲外界的緩存對象, 其提供了get和put方法來完成緩存的獲取和添加的操作吊圾。當緩存滿了時达椰,LruCache會移除較早使用的緩存對象, 然后在添加新的緩存對象项乒。LruCache是線程安全的啰劲。
強引用: 直接的對象引用
軟引用: 當一個對象只有軟引用存在時, 系統(tǒng)內(nèi)存不足時此對象會被gc回收
弱引用: 當一個對象只有弱引用存在時, 對象會隨下一次gc時被回收
LruCache 典型初始化過程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
只需要提供緩存的總?cè)萘看笮?一般為進程可用內(nèi)存的1/8)并重寫 sizeOf 方法即可。sizeOf
方法作用是計算緩存對象的大小檀何。這里大小的單位需要和總?cè)萘康膯挝唬ㄟ@里是kb)一致蝇裤,因此除以1024。一些特殊情況下埃碱,需要重寫LruCache的entryRemoved方法猖辫,LruCache移除舊緩存時會調(diào)用entryRemoved
方法酥泞,因此可以在entryRemoved中完成一些資源回收工作(如果需要的話)砚殿。
還有獲取和添加方法,都比較簡單:
get(K key) V
remove(K key) V
從Android 3.1開始芝囤,LruCache成為Android源碼的一部分似炎。
2、DiskLruCache
DiskLruCache用于實現(xiàn)磁盤緩存悯姊,DiskLruCache得到了Android官方文檔推薦羡藐,但它不屬于Android SDK的一部分。
2.1悯许、DiskLruCache的創(chuàng)建
DiskLruCache并不能通過構(gòu)造方法來創(chuàng)建, 他提供了open()方法用于創(chuàng)建自身, 如下所示:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
- File directory:表示磁盤緩存在文件系統(tǒng)中的存儲路徑仆嗦。可以選擇SD卡上的緩存目錄, 具體是指/sdcard/Andriod/data/package_name/cache目錄先壕,也可以選擇data目錄下. 或者其他地方瘩扼。 這里給出的建議:如果應用卸載后就希望刪除緩存文件的話,那么就選擇SD卡上的緩存目錄垃僚, 如果希望保留緩存數(shù)據(jù)那就應該選擇SD卡上的其他目錄集绰。
- appVersion: 表示應用的版本號,一般設為1即可谆棺。當版本號發(fā)生改變的時候DiskLruCache會清空之前所有的緩存文件栽燕, 在實際開發(fā)中這個實用性不大。
- valueCount: 一般設為1。
- maxSize: 表示緩存的總大小碍岔。當緩存大小超出這個設定值后浴讯,會清除一些緩存而保證總大小不大于這個設定值。
//初始化DiskLruCache蔼啦,包括一些參數(shù)的設置
public void initDiskLruCache() {
//配置固定參數(shù)
// 緩存空間大小
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
//下載圖片時的緩存大小
private static final long IO_BUFFER_SIZE = 1024 * 8;
// 緩存空間索引兰珍,用于Editor和Snapshot,設置成0表示Entry下面的第一個文件
private static final int DISK_CACHE_INDEX = 0;
//設置緩存目錄
File diskLruCache = getDiskCacheDir(mContext, "bitmap");
if (!diskLruCache.exists())
diskLruCache.mkdirs();
//創(chuàng)建DiskLruCache對象询吴,當然是在空間足夠的情況下
if (getUsableSpace(diskLruCache) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskLruCache,
getAppVersion(mContext), 1, DISK_CACHE_SIZE);
mIsDiskLruCache = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
//上面的初始化過程總共用了3個方法
//設置緩存目錄
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
// 獲取可用的存儲大小
@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD)
return path.getUsableSpace();
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
//獲取應用版本號掠河,注意不同的版本號會清空緩存
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
2.2、DiskLruCache的緩存添加
DiskLruCache的緩存添加的操作是通過Editor完成的, Editor表示一個緩存對象的編輯對象.
如果還是緩存圖片為例子, 每一張圖片都通過圖片的url為key, 這里由于url可能會有特殊字符所以采用url的md5值作為key. 根據(jù)這個key就可以通過edit()來獲取Editor對象, 如果這個緩存對象正在被編輯, 那么edit()就會返回null. 即DiskLruCache不允許同時編輯一個緩存對象.
當用.edit(key)獲得了Editor對象之后. 通過editor.newOutputStream(0)就可以得到一個文件輸出流. 由于之前open()方法設置了一個節(jié)點只能有一個數(shù)據(jù). 所以在獲得輸出流的時候傳入常量0即可.
有了文件輸出流, 可以當網(wǎng)絡下載圖片時, 圖片就可以通過這個文件輸出流寫入到文件系統(tǒng)上.最后猛计,要通過Editor中commit()來提交寫操作, 如果下載中發(fā)生異常, 那么使用Editor中abort()來回退整個操作.
2.3唠摹、DiskLruCache的緩存查找
和緩存的添加過程類似, 緩存查找過程也需要將url轉(zhuǎn)換成key, 然后通過DiskLruCache#get()方法可以得到一個Snapshot對象, 接著在通過Snapshot對象即可得到緩存的文件輸入流, 有了文件輸入流, 自然就可以得到Bitmap對象. 為了避免加載圖片出現(xiàn)OOM所以采用壓縮的方式. 在前面對BitmapFactory.Options的使用說明了. 但是這中方法對FileInputStream的縮放存在問題. 原因是FileInputStream是一種有序的文件流, 而兩次decodeStream調(diào)用會影響文件的位置屬性, 這樣在第二次decodeStream的時候得到的會是null. 針對這一個問題, 可以通過文件流來得到它所對應的文件描述符, 然后通過BitmapFactory.decodeFileDescription()來加載一張縮放后的圖片.
/**
* 磁盤緩存的讀取
* @param url
* @param reqWidth
* @param reqHeight
* @return
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException
{
if(Looper.myLooper() == Looper.getMainLooper())
Log.w(TAG, "it's not recommented load bitmap from UI Thread");
if(mDiskLruCache == null)
return null;
Bitmap bitmap = null;
String key = hashKeyForDisk(url);
Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null)
{
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fd = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
if(bitmap != null)
addBitmapToMemoryCache(key, bitmap);
}
return bitmap;
}
3、ImageLoader的實現(xiàn)
一個好的ImageLoader應該具備以下幾點:
- 圖片的壓縮
- 網(wǎng)絡拉取
- 內(nèi)存緩存
- 磁盤緩存
- 圖片的同步加載
- 圖片的異步加載
異步加載過程:
- bindBitmap先嘗試從內(nèi)存緩存讀取圖片奉瘤,如果沒有會在線程池中調(diào)用loadBitmap方法勾拉。獲取成功將圖片封裝為LoadResult對象通過mMainHandler向UI線程發(fā)送消息。選擇線程池和Handler來提供并發(fā)能力和異步能力盗温。
- 為了解決View復用導致的列表錯位問題藕赞,在給ImageView設置圖片之前都會檢查它的url有沒有發(fā)生改變,如果改變就不再給它設置圖片卖局。