第7章 圖片

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)存大小:2368
4224*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,將不受影響挽荡。
僅僅影響decodeResourcedecodeResourceStream方法倦零,此時若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ā)生改變,如果改變就不再給它設置圖片卖局。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末斧蜕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子砚偶,更是在濱河造成了極大的恐慌批销,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件染坯,死亡現(xiàn)場離奇詭異均芽,居然都是意外死亡,警方通過查閱死者的電腦和手機单鹿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進店門掀宋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仲锄,你說我怎么就攤上這事劲妙。” “怎么了昼窗?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵是趴,是天一觀的道長。 經(jīng)常有香客問我澄惊,道長唆途,這世上最難降的妖魔是什么富雅? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮肛搬,結(jié)果婚禮上没佑,老公的妹妹穿的比我還像新娘。我一直安慰自己温赔,他們只是感情好蛤奢,可當我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陶贼,像睡著了一般啤贩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拜秧,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天痹屹,我揣著相機與錄音,去河邊找鬼枉氮。 笑死志衍,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的聊替。 我是一名探鬼主播楼肪,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼惹悄!你這毒婦竟也來了春叫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤俘侠,失蹤者是張志新(化名)和其女友劉穎象缀,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爷速,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年霞怀,在試婚紗的時候發(fā)現(xiàn)自己被綠了惫东。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡毙石,死狀恐怖廉沮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情徐矩,我是刑警寧澤滞时,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站滤灯,受9級特大地震影響坪稽,放射性物質(zhì)發(fā)生泄漏曼玩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一窒百、第九天 我趴在偏房一處隱蔽的房頂上張望黍判。 院中可真熱鬧,春花似錦篙梢、人聲如沸顷帖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贬墩。三九已至,卻和暖如春妄呕,著一層夾襖步出監(jiān)牢的瞬間震糖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工趴腋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吊说,地道東北人。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓优炬,卻偏偏與公主長得像颁井,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蠢护,可洞房花燭夜當晚...
    茶點故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Glide筆記 一雅宾、簡介 在泰國舉行的谷歌開發(fā)者論壇上,谷歌為我們介紹了一個名叫Glide的圖片加載庫葵硕,作者是bu...
    AndroidMaster閱讀 3,870評論 0 27
  • 一眉抬、簡介 在泰國舉行的谷歌開發(fā)者論壇上,谷歌為我們介紹了一個名叫Glide的圖片加載庫懈凹,作者是bumptech蜀变。這...
    天天大保建閱讀 7,451評論 2 28
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,498評論 25 707
  • 今天發(fā)現(xiàn)我有很多朋友煮出來的湯不好喝,所以此刻我還是來簡單的說說介评,如何才能把湯煮得好喝库北。 其實煮湯是很簡單的,但是...
    12月32號閱讀 2,511評論 8 5
  • 問答 什么是html们陆,xhtml寒瓦,html5?html是超文本標記語言坪仇,是和瀏覽器溝通的語言杂腰。xhtml是可擴展超...
    17057任合宇閱讀 268評論 0 0