項目地址
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凌外,它們有著不同的壓縮方式,保存到本地后所占用的空間大小也不一樣涛浙。
- JPEG是一種有損壓縮格式康辑,以24位顏色壓縮存儲單個位圖,但是不支持透明度轿亮。使用JPEG進行壓縮時需要選擇適當(dāng)?shù)膲嚎s率疮薇,避免圖片質(zhì)量太差。
- PNG是一種無損壓縮格式我注,支持所有的顏色按咒,由于是無損壓縮,PNG一般用于APP圖標這類對線條或者清晰度有要求的圖片但骨。由于PNG所占空間較大励七,目前一般將PNG轉(zhuǎn)為WEBP使用。
- 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.Options
的inJustDecodeBounds
設(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張一樣的圖片逸寓,最終效果如下。
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也添加了進去毅该。
當(dāng)然你也可能見過這張圖博秫。
乍一看,這兩張圖中的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,示意圖如下束亏。
之前提到進行圖像混合時铃在,先繪制的內(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ǔ)形狀岔绸。
如果要將圖像切割為五角星這樣的圖案,就需要使用一張五角星的底圖橡伞,需要注意的是盒揉,底圖上五角星以外的部分應(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);
}
}
最終的效果如下挂脑。
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);
}
}
怎么樣橄仍,是不是覺得小姐姐看上去都溫柔了一些韧涨?
出了純色混合,也可以將兩張圖片進行合成侮繁,例如通過一張毛玻璃的底圖虑粥,可以為照片添加一定的模糊效果,底圖如下所示鼎天。
繪制時將底圖作為DST舀奶,將照片作為SRC暑竟,混合模式使用OVERLAY斋射,代碼與繪制切割五角星的大同小異,不再贅述但荤。最終得到如下的效果罗岖。
2.5.3 圖像alpha漸變
之前遇到一個很有意思的UI需求,給定一張底圖腹躁,要求繪制時圖像的透明度從上到下是漸變的1-0桑包,效果如下所示。因為background是白色纺非,所以透明部分透出來的是白色哑了。
這個其實也很簡單,在本地新建一個漸變的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é)省空間。
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)減小颊咬。