Android—Bitmap解析與應(yīng)用

項目地址

https://github.com/ListerChen/BitmapProject

一起暮、Bitmap基本介紹

Bitmap也稱為位圖迹鹅,是圖片在內(nèi)存中的表現(xiàn)形式蜈出,任何圖片(JPEG, PNG, WEBP...)加載到內(nèi)存后都是一個Bitmap對象。Bitmap實際是像素點的集合,假設(shè)它的寬高為width和height郭蕉,那么該Bitmap就包含width*height個像素豌骏,它在內(nèi)存中占用的內(nèi)存就是(width*height*單個像素內(nèi)存)凭豪。

為了減小圖片在磁盤上所占的空間,將Bitmap保存到磁盤上時會進行壓縮位仁,圖片的文件格式實際代表的是不同的壓縮方式與壓縮率柑贞,而將磁盤上的文件加載到內(nèi)存中時就要進行解壓縮。

1.1 圖片格式介紹

常見的靜態(tài)圖片格式為JPEG聂抢、PNG和WEBP凌外,它們有著不同的壓縮方式,保存到本地后所占用的空間大小也不一樣涛浙。

  1. JPEG是一種有損壓縮格式康辑,以24位顏色壓縮存儲單個位圖,但是不支持透明度轿亮。使用JPEG進行壓縮時需要選擇適當(dāng)?shù)膲嚎s率疮薇,避免圖片質(zhì)量太差。
  2. PNG是一種無損壓縮格式我注,支持所有的顏色按咒,由于是無損壓縮,PNG一般用于APP圖標這類對線條或者清晰度有要求的圖片但骨。由于PNG所占空間較大励七,目前一般將PNG轉(zhuǎn)為WEBP使用。
  3. WEBP支持無損壓縮和有損壓縮奔缠,并且他的無損壓縮率優(yōu)于PNG掠抬,有損壓縮率優(yōu)于JPEG,同時它支持所有顏色校哎,并支持多幀動畫两波,唯一的缺點是壓縮效率不如JPEG和PNG。

1.2 Bitmap色深

Bitmap的本質(zhì)就是像素點的集合闷哆,它通過描述每個像素的ARGB信息來形成整張圖片腰奋,其中A表示透明度通道,RGB表示紅綠藍3種顏色通道抱怔,每個通道的值都在0-255之間劣坊,因此8bit可以完整地表示1個通道,那么4x8=32bit可以表示一個完整的像素屈留。但如果每個Bitmap都使用32bit來表示一個像素的話局冰,對內(nèi)存來說是一個較大的負擔(dān)括儒,因此對于質(zhì)量要求不高的Bitmap來說,可以采用較少的內(nèi)存去表示一個像素锐想。

色深指的是每一個像素點用多少bit來存儲ARGB值帮寻,色深越大,圖片的色彩越豐富赠摇,一般來說色深有8bit固逗、16bit、32bit等藕帜,Bitmap.Config中的色深配置如下烫罩。

ALPHA_8: 這種方案只存儲透明度通道,色深為8bit洽故,使用場景特殊贝攒,比如設(shè)置遮蓋效果等。
ARGB_8888: ARGB每個通道值采8bit來表示时甚,色深為32bit隘弊,每個像素點需要4byte的內(nèi)存來存儲數(shù)據(jù),圖片質(zhì)量高荒适,所占內(nèi)存大梨熙。
ARGB_4444: ARGB每個通道都是4位,色深為16bit刀诬,由于這種配置下的圖片質(zhì)量較差咽扇,Android官方已經(jīng)將其標為棄用。
RGB_565: 色深為16bit陕壹,RGB通道值分別占5质欲、6、5位糠馆,但是沒有存儲A通道值嘶伟,所以不支持透明度,可用于對清晰度要求不高的照片榨惠。
RGBA_F16: 色深為64bit奋早,該配置主要用于廣色域與HDR內(nèi)容盛霎。
HARDWARE: 這是一種特殊配置赠橙,該配置下,Bitmap會被存儲在顯存中愤炸,并且Bitmap是不可變的期揪。這種配置僅適用于,當(dāng)Bitmap唯一的操作就是將自己繪制在屏幕上時规个。

我們可以根據(jù)對圖片質(zhì)量的要求創(chuàng)建不同色深的Bitmap凤薛,如果對顯示質(zhì)量有較高的要求可以使用ARGB_8888姓建,一般來說使用RGB_565即可,這也可以減少OOM的概率缤苫。

Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

1.2 通過采樣加載大圖

ImageView是顯示Bitmap的載體速兔,一般情況下ImageView的寬高會小于Bitmap,如果將一個完整的Bitmap加載到偏小的ImageView中會浪費內(nèi)存活玲。關(guān)于這個問題涣狗,Android官方提供了一個優(yōu)化方法。

該方法通過比較ImageView與Bitmap的大小并計算采樣率舒憾,最終將采樣后的小圖加載到內(nèi)存中镀钓。流程如下:在調(diào)用BitmapFactory.decodeXXX(res, resId, BitmapFactory.Options)解析圖片時,先將BitmapFactory.OptionsinJustDecodeBounds設(shè)為true镀迂,此時不會將圖片加載到內(nèi)存中丁溅,而是只得到Bitmap的寬高,隨后通過圖片寬高計算采樣率探遵。得到采樣率后再將inJustDecodeBounds設(shè)為false窟赏,再加載Bitmap時可以得到大圖的采樣圖。

public static Bitmap decodeSampledBitmapFromResource(
            Resources res, int resId, int reqWidth, int reqHeight) {
    final BitmapFactory.Options options = new BitmapFactory.Options();
    // 該屬性默認為false, 為true時不會將圖片加載到內(nèi)存中, 而是只計算寬高
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 計算采樣率
    options.inSampleSize = calculateInSampleSize(
            options, reqWidth, reqHeight);
    // 設(shè)置inJustDecodeBounds為false, 將圖片加載到內(nèi)存
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // 獲得Bitmap的寬高
    final int height = options.outHeight;
    final int width = options.outWidth;
    // 計算采樣率
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

如果使用Glide這類圖片加載框架將Bitmap加載到ImageView中箱季,框架會自動幫我們完成采樣的工作饰序。不過如果你需要獲取某個Bitmap的縮略圖,上述方法還是有用武之地的规哪。

二求豫、Bitmap相關(guān)應(yīng)用

2.1 圖片裁剪

圖片裁剪是圖片處理的基本功能,APP中的設(shè)置頭像功能就需要用到圖片裁剪诉稍,用戶可以通過一個預(yù)覽框在原圖上裁剪出特定區(qū)域的圖案蝠嘉。
系統(tǒng)本身提供了裁剪的功能,我們可以在A頁面通過以下代碼啟動一個裁剪圖片的Activity杯巨,裁剪結(jié)束在A頁面的onActivityResult(...)中得到裁剪后的圖片蚤告。

// uri為圖片的地址
public void startCropPicture(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1); // 裁剪框比例
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 300); // 輸出圖片大小
    intent.putExtra("outputY", 300);
    intent.putExtra("scale", true);
    intent.putExtra("return-data", true);
    startActivityForResult(intent, REQUEST_CODE);
}

圖片裁剪的原理很簡單,通過Bitmap.createBitmap(originBitmap, left, top, width, height)即可在originBitmap上裁出指定區(qū)域的Bitmap服爷,因此我們可以自己實現(xiàn)一個裁剪頭像的功能杜恰。需要注意的是,Bitmap.createBitmap(...)方法參數(shù)中傳入的坐標和寬高都是基于原始Bitmap的仍源,如果傳入的參數(shù)超出了原始Bitmap的寬高就會拋出異常心褐。

本文Demo實現(xiàn)了裁剪圖案的基礎(chǔ)功能,拖動裁剪框右下角可以修改框大小笼踩,拖動其他區(qū)域可以移動裁剪框逗爹,可以通過本文開頭的鏈接下載代碼。

除了Bitmap.createBitmap(originBitmap, left, top, width, height)裁剪出指定的Bitmap嚎于,也可以在繪制時直接指定Bitmap的繪制區(qū)域掘而,通過Canvas.drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)即可實現(xiàn)挟冠,其中src表示對原圖片的裁剪區(qū)域,dst表示對裁剪后的圖片繪制到View上的區(qū)域袍睡。

2.2 圖片拼接

圖片拼接常見于分享時拼接所有圖片知染,達到"一圖看盡所有"的效果。代碼示例如下斑胜,為了方便演示持舆,示例中將4張一樣的正方形圖片splitBitmap拼接到了一起。

private Bitmap getJointBitmap(Bitmap splitBitmap) {
    int width = splitBitmap.getWidth();
    int height = splitBitmap.getHeight();
    Bitmap bitmap = Bitmap.createBitmap(
                width * 2, height * 2, Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawBitmap(splitBitmap, 0, 0, null);
    canvas.drawBitmap(splitBitmap, width, 0, null);
    canvas.drawBitmap(splitBitmap, 0, height, null);
    canvas.drawBitmap(splitBitmap, width, height, null);
    return bitmap;
}

拼接圖片前先創(chuàng)建了一個寬高都為splitBitmap寬高2倍的Bitmap作為容器伪窖,然后通過Canvas在容器的四個位置繪制4張一樣的圖片逸寓,最終效果如下。

圖片拼接.jpg

2.3 矩陣變換

Bitmap是像素點的集合覆山,我們可以通過矩陣運算改變每個像素點的位置竹伸,達到圖形變換的效果。Android中可以通過Matrix類來進行變換簇宽,Matrix本身是一個3x3的矩陣勋篓,可以通過Matrix m = new Matrix()新建一個單位矩陣,原始矩陣的值如下所示魏割。

[1 0 0]
[0 1 0]
[0 0 1]

Matrix中各個位置的變換信息如下所示譬嚣,scale表示縮放,skew表示錯切钞它,trans表示平移拜银,persp等表示透視參數(shù)。Bitmap中的每個像素點可以使用一個3x1的矩陣表示遭垛,其中x表示當(dāng)前像素點的橫坐標尼桶,y表示縱坐標。用該矩陣左乘Bitmap中的所有像素后锯仪,就能得到變換后的圖像泵督。

[scaleX  skewX   transX]     [x]     [scaleX * x + skewX * y + transX]
[skewY   scaleY  transY]  x  [y]  =  [skewY * x + scaleY * y + transY]
[persp0  persp1  persp2]     [1]     [persp0 * x + persp1 * y + persp2]

簡單來說,Matrix是一個容器庶喜,保存了用戶期望的矩陣變換信息小腊。在將Matrix應(yīng)用于Bitmap之前,我們可以對其進行各種操作久窟,將變換信息保存進去秩冈。矩陣運算可以實現(xiàn)平移、旋轉(zhuǎn)瘸羡、縮放漩仙、錯切,因此Matrix也為提供了類似方法犹赖。

setTranslate(float dx,float dy): 控制 Matrix 進行位移队他。
setSkew(float kx,float ky): 控制 Matrix 進行傾斜,kx峻村、ky為X麸折、Y方向上的比例。
setSkew(float kx,float ky,float px,float py): 控制 Matrix 以 px, py 為軸心進行傾斜粘昨,kx垢啼、ky為X、Y方向上的傾斜比例
setRotate(float degrees): 控制 Matrix 進行 depress 角度的旋轉(zhuǎn)张肾,軸心為(0,0)
setRotate(float degrees,float px,float py): 控制 Matrix 進行 depress 角度的旋轉(zhuǎn)芭析,軸心為(px,py)
setScale(float sx,float sy): 設(shè)置 Matrix 進行縮放,sx, sy 為 X, Y方向上的縮放比例吞瞪。
setScale(float sx,float sy,float px,float py): 設(shè)置 Matrix 以(px,py)為軸心進行縮放馁启,sx、sy 為 X芍秆、Y方向上的縮放比例

很多時候矩陣變換并不是單一的平移惯疙、旋轉(zhuǎn)或縮放,這些變換經(jīng)常結(jié)合在一起使用妖啥,此時setXXX()方法無法滿足要求霉颠。因此Matrix提供了preXXX()postXXX()方法來組合多個矩陣操作,多個矩陣操作之間通過乘法運算荆虱。由于矩陣的乘法是不滿足交換律的蒿偎,因此進行矩陣運算時需要注意乘法的順序。

在使用preXXX()postXXX()時怀读,我們可以將矩陣變換的所有計算看成一個乘法列表酥郭,調(diào)用preXXX()方法時就是向列表頭部添加操作,調(diào)用postXXX()時就是向列表尾部添加操作愿吹。例如以下代碼中矩陣乘法的執(zhí)行順序就是2->1->3->4不从。需要注意的是,setXXX()方法會重置Matrix的變換犁跪,如果對下方執(zhí)行完4個運算的Matrix調(diào)用setTranslate()方法椿息,那么該Matrix就只有平移效果了。

Matrix matrix = new Matrix();
matrix.preScale(...); // 1
matrix.preTranslate(...); // 2
matrix.postTranslate(...); // 3
matrix.postRotate(...); // 4

2.4 顏色變換

顏色變換主要通過ColorFilter進行坷衍,通過Paint.setColorFilter(ColorFilter filter)可以設(shè)置顏色過濾器寝优,該過濾器會對每一個像素的顏色進行過濾,得到最終的圖像枫耳。ColorFilter有3個子類乏矾,這里主要介紹ColorMatrixColorFilter。

2.4.1 ColorMatrixColorFilter

該顏色過濾器通過矩陣進行色彩變換,先來介紹一下色彩矩陣钻心,Android中的色彩是以ARGB的形式存儲的凄硼,我們可以通過ColorMatrix修改顏色的值,ColorMatrix定義了一個4x5的float矩陣捷沸,矩陣的4行分別表示在RGBA上的向量摊沉,其范圍值在0f-2f之間,如果為1就是原效果痒给。每一行的第5列表示偏移量说墨,就是指在當(dāng)前通道上增大或減小多少。

ColorMatrix colorMatrix = new ColorMatrix(new float[]{
        1, 0, 0, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
});

ColorMatrix與顏色之間的運算如下所示苍柏,其實就是矩陣運算尼斧,與上一節(jié)的Matrix類似。

[a, b, c, d, e]     [R]     [a*R + b*G + c*B + d*A + e]
[f, g, h, i, j]     [G]     [f*R + g*G + h*B + i*A + j]
[k, l, m, n, o]  x  [B]  =  [k*R + l*G + m*B + n*A + o]
[p, q, r, s, t]     [A]     [p*R + q*G + r*B + s*A + t]
                    [1]

有了ColorMatrix试吁,就可以對Bitmap上所有的顏色進行修改棺棵,例如調(diào)整每個通道的值,將初始值1改為0.5潘悼,就可以將Bitmap變暗律秃。不過我不太了解色彩,也沒在項目中實際使用過治唤,感興趣的朋友可以看參考1棒动。

2.5 圖像混合

圖像混合是指對兩張原始圖像(我們稱為DST和SRC)的內(nèi)容按某種規(guī)則合成,從而形成一張包含DST和SRC特點的新圖像宾添。例如DST為圓形圖像船惨,SRC為照片,可以將它們合成為圓形照片缕陕。

Android通過PorterDuffXfermode實現(xiàn)圖像混合粱锐,它實際上是通過公式對兩張圖像在Canvas上的所有像素進行ARGB運算,最終在每個像素點得到新的ARGB值扛邑。需要注意的是怜浅,在onDraw(Canvas)方法中進行圖像混合時,先繪制的圖像為DST蔬崩,后繪制的圖像為SRC恶座,因此需要注意圖像的繪制順序。

PorterDuffXfermode一共提供了18種混合模式沥阳,它們的計算公式如下跨琳,Sa表示SRC的ALPHA通道,Sc表示SRC的顏色桐罕;Da表示DST的ALPHA通道脉让,Dc表示DST的顏色桂敛。以CLEAR為例,該模式會清除SRC區(qū)域的所有內(nèi)容溅潜。

合成模式 公式
CLEAR [0, 0]
SRC [Sa, Sc]
DST [Da, Dc]
SRC_OVER [Sa + (1 - Sa)Da, Rc = Sc + (1 - Sa)Dc]
DST_OVER [Sa + (1 - Sa)Da, Rc = Dc + (1 - Da)Sc]
SRC_IN [Sa * Da, Sc * Da]
DST_IN [Sa * Da, Sa * Dc]
SRC_OUT [Sa * (1 - Da), Sc * (1 - Da)]
DST_OUT [Da * (1 - Sa), Dc * (1 - Sa)]
SRC_ATOP [Da, Sc * Da + (1 - Sa) * Dc]
DST_ATOP [Sa, Sa * Dc + Sc * (1 - Da)]
XOR [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
DARKEN [Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + min(Sc, Dc)]
LIGHTEN [Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + max(Sc, Dc)]
MULTIPLY [Sa * Da, Sc * Dc]
SCREEN [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc]
ADD Saturate(S + D)

如果你使用過PorterDuffXfermode术唬,你可能見過下面這張圖,Android官方的樣例就是這個效果伟恶,不過官方只提供了16種混合模式的樣例碴开,我在Demo中把ADD和OVERLAY也添加了進去毅该。

圖像合成1.jpg

當(dāng)然你也可能見過這張圖博秫。

圖像合成2.jpg

乍一看,這兩張圖中的DST和SRC原始圖像都是一樣的眶掌,但是為什么使用了同樣的混合模式后挡育,顯示的結(jié)果不同呢?

關(guān)鍵就在于DST和SRC的大小朴爬,第一張圖中的DST和SRC都是Bitmap即寒,它們的大小與Canvas相等,只是在Bitmap的某個區(qū)域繪制了圓和矩形召噩。Demo代碼如下母赵,可以看到makeDst()makeSrc()中創(chuàng)建的Bitmap與整個View(或者說Canvas)是相等的。這也解釋了為什么第一張圖的CLEAR模式下的結(jié)果是空白的具滴,因為SRC的大小是整個View的大小蹈垢,CLEAR模式表示清除SRC區(qū)域的內(nèi)容派任,最終將整個View的內(nèi)容清除了。

public class XFerModeView extends View {

    private Paint mPaint;
    private PorterDuffXfermode mPorterDuffXfermode;
    private int mWidth;
    private int mHeight;

    // 省略構(gòu)造方法......

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mWidth != w || mHeight != h) {
            mWidth = w;
            mHeight = h;
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackground(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        drawCompositionInFullSize(canvas);
        canvas.restoreToCount(sc);
    }

    private void drawBackground(Canvas canvas) {
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.BLACK);
        canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
    }

    private void drawCompositionInFullSize(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        Bitmap dst = makeDst();
        Bitmap src = makeSrc();
        // 繪制DST
        canvas.drawBitmap(dst, 0, 0, mPaint);
        // 設(shè)置圖像混合模式
        mPaint.setXfermode(mPorterDuffXfermode);
        // 繪制SRC
        canvas.drawBitmap(src, 0, 0, mPaint);
        // 清除圖像混合模式
        mPaint.setXfermode(null);
    }

    private Bitmap makeDst() {
        Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        mPaint.setColor(0xFFFFCC44);
        c.drawOval(10, 10, mWidth * 3f / 4, mHeight * 3f / 4, mPaint);
        return bm;
    }

    private Bitmap makeSrc() {
        Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        mPaint.setColor(0xFF66AAFF);
        c.drawRect(mWidth * 1f / 3, mHeight * 1f / 3,
                mWidth * 19f / 20, mHeight * 19f / 20, mPaint);
        return bm;
    }

在第二張圖中,繪制SRC和DST時創(chuàng)建的圖像大小就是圓或矩形的大小触创,最終的結(jié)果也與第一張圖有所不同,修改后的代碼如下描融。還是以CLEAR模式為例睹晒,此時清除的就只是矩形區(qū)域SRC的圖像,可以看到DST中與SRC相交的部分被清除了显拳。

public class XFerModeView extends View {

    // 省略重復(fù)代碼......

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackground(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        drawCompositionInSelfSize(canvas);
        canvas.restoreToCount(sc);
    }

    /**
     * 混合圖像的大小只有可見區(qū)域大小
     */
    private void drawCompositionInSelfSize(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(0xFFFFCC44);
        canvas.drawOval(10, 10, mWidth * 3f / 4, mHeight * 3f / 4, mPaint);
        mPaint.setXfermode(mPorterDuffXfermode);
        mPaint.setColor(0xFF66AAFF);
        canvas.drawRect(mWidth * 1f / 3, mHeight * 1f / 3,
                mWidth * 19f / 20, mHeight * 19f / 20, mPaint);
        mPaint.setXfermode(null);
    }
}

使用圖像混合時還有一個要注意的地方:上述代碼在onDraw(Canvas)方法中繪制混合圖像時會先調(diào)用int sc = canvas.saveLayer(...)生成一個新的圖層(Layer)棚愤,sc表示圖層的編號,隨后在新Layer上繪制DST和SRC杂数,繪制完后將該Layer添加到Canvas上宛畦。那么這里為什么需要新的Layer來繪制DST和SRC,而不是直接在Canvas上繪制呢耍休?

Layer可以理解為畫布Canvas的一個層級刃永,默認情況下Canvas只有一個Layer,所有的繪制都在同一圖層上羊精。當(dāng)需要繪制多層圖像時斯够,可以通過canvas.saveLayer(...)生成新的Layer囚玫,在新Layer上繪制的內(nèi)容是獨立的,不會影響到其他Layer的內(nèi)容读规,調(diào)用canvas. restoreToCount(int sc)時將該Layer覆蓋到Canvas現(xiàn)有的圖像上抓督。Canvas通過棧的形式管理Layer,示意圖如下束亏。

Layer.png

之前提到進行圖像混合時铃在,先繪制的內(nèi)容是DST,后繪制的是SRC碍遍。如果不新建Layer的話定铜,在繪制SRC時,Canvas上的所有內(nèi)容都會被當(dāng)作DST怕敬,所以背景等內(nèi)容也會參與圖像混合揣炕,很容易得到錯誤的效果。

以上就是圖像混合的基本介紹东跪,圖像混合的應(yīng)用場景比較廣泛畸陡,這里介紹幾種常見的場景。

2.5.1 圖像切割

圖像切割用于將圖像切割成特定的形狀虽填《」В可以是常見形狀如圓形或圓角矩形,也可以切割為五角星這樣的非常規(guī)形狀斋日,使用這一類非常規(guī)形狀時需要該形狀的底圖牲览。

將圖像裁剪為圓角矩形時比較簡單,在onDraw(Canvas)中新建圖層桑驱,繪制圓角矩形作為DST竭恬,再繪制原圖作為SRC即可,此時圖像混合模式應(yīng)設(shè)置為SRC_IN熬的,代碼如下痊硕,decodeSampledBitmapFromResource(...)就是1.2節(jié)的大圖采樣。

public class RoundCornerView extends View {

    private Paint mPaint;
    private PorterDuffXfermode mFerMode;
    private Bitmap mBitmap;
    private Rect mBitmapRect;
    private int mWidth;
    private int mHeight;

    // 省略構(gòu)造函數(shù)...

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mFerMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mWidth != w || mHeight != h) {
            mWidth = w;
            mHeight = h;
            mBitmap = BitmapUtils.decodeSampledBitmapFromResource(
                    getContext().getResources(), R.drawable.compress_test, mWidth, mHeight);
            mBitmapRect = new Rect(0, 0, mWidth, mHeight);
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        canvas.drawRoundRect(0, 0, mWidth, mHeight, 50, 50, mPaint);
        mPaint.setXfermode(mFerMode);
        canvas.drawBitmap(mBitmap, null, mBitmapRect, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(sc);
    }

    ......
}

最終效果如下押框,同理可以將圖像切割為圓形等基礎(chǔ)形狀岔绸。

切割為圓角矩形.jpg

如果要將圖像切割為五角星這樣的圖案,就需要使用一張五角星的底圖橡伞,需要注意的是盒揉,底圖上五角星以外的部分應(yīng)該是透明的,否則切割出來還是原來的形狀兑徘。其代碼與切割為圓角矩形大同小異刚盈,只需要將繪制圓角矩形的部分換成繪制五角星即可。

public class StarPicView extends View {

    private Paint mPaint;
    private PorterDuffXfermode mMode;
    private Bitmap mBgBitmap;
    private Bitmap mBitmap;
    private int mWidth, mHeight;
    private Rect mDrawRect;

    // 省略構(gòu)造函數(shù)......

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mWidth != w || mHeight != h) {
            mWidth = w;
            mHeight = h;
            mBgBitmap = BitmapUtils.decodeSampledBitmapFromResource(
                    getContext().getResources(), R.drawable.star4, mWidth, mHeight);
            mBitmap = BitmapUtils.decodeSampledBitmapFromResource(
                    getContext().getResources(), R.drawable.icon3, mWidth, mHeight);
            mDrawRect = new Rect(0, 0, mWidth, mHeight);
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        canvas.drawBitmap(mBgBitmap, null, mDrawRect, mPaint);
        mPaint.setXfermode(mMode);
        canvas.drawBitmap(mBitmap, null, mDrawRect, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(sc);
    }
}

最終的效果如下挂脑。

切割為五角星.jpg
2.5.2 色彩合成

色彩合成可以為圖片添加新的效果藕漱,當(dāng)使用純色與照片混合時欲侮,可以改變圖片整體的色調(diào)。例如黃色可以讓圖片具有泛黃的懷舊效果肋联,紅色可以讓圖片更溫暖威蕉。以下代碼通過SCREEN混合模式將半透明的紅色與圖片混合。

public class ColorComposeView extends View {

    ......

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mMode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
    }

    ......

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        canvas.drawColor(0x44FF0000);
        mPaint.setXfermode(mMode);
        canvas.drawBitmap(mBitmap, null, mRect, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(sc);
    }
}

怎么樣橄仍,是不是覺得小姐姐看上去都溫柔了一些韧涨?

色彩混合.jpeg

出了純色混合,也可以將兩張圖片進行合成侮繁,例如通過一張毛玻璃的底圖虑粥,可以為照片添加一定的模糊效果,底圖如下所示鼎天。

毛玻璃底圖.jpeg

繪制時將底圖作為DST舀奶,將照片作為SRC暑竟,混合模式使用OVERLAY斋射,代碼與繪制切割五角星的大同小異,不再贅述但荤。最終得到如下的效果罗岖。

圖像合成之模糊效果.jpg
2.5.3 圖像alpha漸變

之前遇到一個很有意思的UI需求,給定一張底圖腹躁,要求繪制時圖像的透明度從上到下是漸變的1-0桑包,效果如下所示。因為background是白色纺非,所以透明部分透出來的是白色哑了。

圖像漸變.jpg

這個其實也很簡單,在本地新建一個漸變的drawable烧颖,然后通過XOR模式進行混合弱左,具體代碼可參考文章開頭代碼。

三炕淮、圖片壓縮

3.1 質(zhì)量壓縮

質(zhì)量壓縮減小的是圖片在磁盤上的體積大小拆火,通過Bitmap.compress(CompressFormat, quality, outputStream)將Bitmap保存到本地時,可以選擇對應(yīng)的文件格式以及質(zhì)量標準涂圆,文件格式包含JPEG们镜、PNG和WEBP三種,質(zhì)量的取值為0-100润歉,0代表最差質(zhì)量模狭,100代表最高質(zhì)量。WEBP格式將會在API30被棄用踩衩,取而代之的是WEBP_LOSSLESS和WEBP_LOSSY嚼鹉,用于更清晰地描述是無損壓縮還是有損壓縮邪意。

將Bitmap保存為60質(zhì)量標準的jpeg的代碼如下。

private void qualityCompressJPG() {
    OutputStream os = getOutputStreamByName("jpgFile60.jpeg");
    if (os != null) {
        mOriginBitmap.compress(Bitmap.CompressFormat.JPEG, 60, os);
    }
}

private OutputStream getOutputStreamByName(String fileName) {
    BufferedOutputStream bos = null;
    File dir = new File(FILE_DIR);
    boolean dirExist = true;
    if (!dir.exists()) {
        dirExist = dir.mkdirs();
    }
    if (dirExist) {
        File file = new File(dir, fileName);
        if (file.exists()) {
            file.delete();
        }
        try {
            boolean fileExist;
            fileExist = file.createNewFile();
            if (fileExist) {
                bos = new BufferedOutputStream(new FileOutputStream(file));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return bos;
}

三種圖片格式中反砌,PNG為無損壓縮雾鬼,因此當(dāng)文件格式選擇PNG時Bitmap.compress(CompressFormat, quality, outputStream)方法會無視quality參數(shù)。下圖為保存同一張圖片時宴树,選擇不同的格式與不同質(zhì)量的對比策菜。可以發(fā)現(xiàn)WEBP格式所占用的空間是比較理想的酒贬,如果APP的體積比較大又憨,可以考慮把資源文件轉(zhuǎn)化為WEBP來節(jié)省空間。

質(zhì)量壓縮.jpeg

3.2 尺寸壓縮

尺寸壓縮就是指壓縮原始Bitmap的寬高锭吨,通過減小像素個數(shù)來減小Bitmap占用的空間蠢莺,這種壓縮方式下,不管Bitmap所占的內(nèi)存零如,還是圖片保存到磁盤上所占的空間都會有顯著的減小躏将。

尺寸壓縮可以通過Bitmap.createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)來創(chuàng)建壓縮后的Bitmap,filter參數(shù)可以簡單地理解為考蕾,如果為true的話就會消耗更長的時間獲得更好的圖片質(zhì)量祸憋,false則相反。

private void sizeCompress1(int scale) {
    int width = mOriginBitmap.getWidth() / scale;
    int height = mOriginBitmap.getHeight() / scale;
    Bitmap b = Bitmap.createScaledBitmap(
                    mOriginBitmap, width, height, false);
    OutputStream os = getOutputStreamByName("sizeCompress1.webp");
    if (os != null) {
        b.compress(Bitmap.CompressFormat.WEBP, 100, os);
    }
}

通過Bitmap b = Bitmap.createScaledBitmap(src, w, h, filter)來創(chuàng)建Bitmap時肖卧,它的Config是基于原始Bitmap的蚯窥,如果原始Bitmap的Config是ARGB_8888而壓縮后又不需要這么高的清晰度,那么可以選擇新建RGB_565的Bitmap塞帐,并通過Canvas將壓縮后的圖片繪制上去拦赠。

private void sizeCompress2(int scale) {
    int width = mOriginBitmap.getWidth() / scale;
    int height = mOriginBitmap.getHeight() / scale;
    Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(b);
    Rect rect = new Rect(0, 0, width, height);
    canvas.drawBitmap(mOriginBitmap, null, rect, null);
    OutputStream os = getOutputStreamByName("sizeCompress2_565.webp");
    if (os != null) {
        b.compress(Bitmap.CompressFormat.WEBP, 100, os);
    }
}

如果第2種方式選擇ARGB_8888,創(chuàng)建出來的Bitmap其所占的內(nèi)存(通過Bitmap.getByteCount()計算)以及保存到磁盤上的大小都與第1種方式相同葵姥。如果選擇RGB_565荷鼠,相比于第1種方式,Bitmap所占的內(nèi)存會縮小一半牌里,保存到磁盤上后的體積也會適當(dāng)減小颊咬。

參考&推薦閱讀

  1. Android自定義控件其實很簡單
  2. Android動態(tài)模糊實現(xiàn)的研究
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市牡辽,隨后出現(xiàn)的幾起案子喳篇,更是在濱河造成了極大的恐慌,老刑警劉巖态辛,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件麸澜,死亡現(xiàn)場離奇詭異,居然都是意外死亡奏黑,警方通過查閱死者的電腦和手機炊邦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門编矾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人馁害,你說我怎么就攤上這事窄俏。” “怎么了碘菜?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵凹蜈,是天一觀的道長。 經(jīng)常有香客問我忍啸,道長仰坦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任计雌,我火速辦了婚禮悄晃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凿滤。我一直安慰自己妈橄,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布鸭巴。 她就那樣靜靜地躺著眷细,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鹃祖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天普舆,我揣著相機與錄音恬口,去河邊找鬼。 笑死沼侣,一個胖子當(dāng)著我的面吹牛祖能,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛾洛,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼养铸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了轧膘?” 一聲冷哼從身側(cè)響起钞螟,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谎碍,沒想到半個月后鳞滨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡蟆淀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年拯啦,在試婚紗的時候發(fā)現(xiàn)自己被綠了澡匪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡褒链,死狀恐怖唁情,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情甫匹,我是刑警寧澤荠瘪,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站赛惩,受9級特大地震影響哀墓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜喷兼,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一篮绰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧季惯,春花似錦吠各、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至藕筋,卻和暖如春纵散,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隐圾。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工伍掀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人暇藏。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓蜜笤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親盐碱。 傳聞我的和親對象是個殘疾皇子把兔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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