[TOC]
# 1. 前言
混合模式能夠將兩張圖片無縫結合,實現類似 Photoshop中的兩張圖片融合效果。通過 Paint 類中的 setXfermode(Xfermode xfermode)函數實現的,它 的參數 Xfermode 是一個空類,主要靠它的子類來實現不同的功能。主要子類有:AvoidXfermode、PixelXorXfermode、PorterDuffxfermode。
## 1.1 注意點
AvoidXfermode、PixelXorXfermode 是完全不 支持硬件加速的,PorterDuffXfermode 是部分不支持硬件加速。所以,在使用 Xfermode 時,為了保險需要做到:
* 禁用硬件加速,即:setLayerType(View.LAYER\_TYPE\_SOFTWARE, null);
* 使用離屏繪制;即:把繪制的核心代碼放在 canvas.save()和 canvas.restore()函數之間即可。
對于離屏繪制,也就是使用:
```java
//新建圖層
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas. ALL\_SAVE\_FLAG);
//核心繪制代碼
...
//還原圖層
canvas.restoreToCount(layerId);
```
這里以PorterDuffXfermode為案例來學習。
# 2. PorterDuffXfermode
繼承關系圖為:

簡單追蹤其源碼,可以看到僅提供了一個公開構造方法:
~~~
public class PorterDuffXfermode extends Xfermode {
public PorterDuffXfermode(PorterDuff.Mode mode) {
porterDuffMode = mode.nativeInt;
}
}
~~~
而其父類Xfermode也是異常簡單:
~~~
public class Xfermode {
static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
@UnsupportedAppUsage
int porterDuffMode = DEFAULT;
}
~~~
也就是其實重點在于[PorterDuff.Mode](https://developer.android.google.cn/reference/kotlin/android/graphics/PorterDuff.Mode)這個枚舉類型。這里直接查閱官方文檔,很有意思的是,PorterDuffXfermode混合模式的命名就來源于作者Thomas Porter和Tom Duff,在1984年他們發表的論文《Compositing Digital Images》(《數字合成圖像》)中研究了12中合成操作來控制源圖像和目標圖像的顏色混合結果,也被叫做Aplha通道合成模式。當然,在這個類中還提供了幾種混合模式,且不限于alpha通道。分類如下:

比如在論文中作者展示了一張圖片:

很難想象作者這個效果完成在1984年。確實讓人大為驚嘆。
為了便于理解其計算過程,這里按照文檔說明進行。標記為alpha通道輸出,為顏色值輸出。下面是枚舉類型中所有的模式:
## 2.1 ADD
兩個部分相加:

簡單來說就是對 SRC 與 DST 兩張圖片相交區域的飽和度進行相加。
## 2.2 CLEAR
直接歸零:

## 2.3 DARKEN
兩個圖片的重合區域有變暗的效果:

## 2.4 DST
丟棄源圖像,保留目標內:

* DST_ATOP;
* DST_IN;
* DST_OUT;
* DST_OVER;
## 2.5 LIGHTEN
有重合 區域才有變亮的效果:

## 2.6 MULTIPLY
兩部分的乘積,也就是是用源圖像的 Alpha 值乘以目標 圖像的 Alpha 值。由于源圖像的非相交區域所對應的目標圖像像素的 Alpha 值是 0,所以結果 像素的 Alpha 值仍是 0,源圖像的非相交區域在計算后是透明的。與 Photoshop 中的**正片疊底**效果是一致的。

## 2.7 SRC
在處理源圖像所在區域的相交問題時,全部以源圖像顯示:

### 2.7.1 SRC_ATOP;
### 2.7.2 SRC_IN
在這個公式中,目標值的透明度也是乘積形式計算,故而遇到空白像素的時候還是0:

故而我們可以用來做圖片固定圖形的裁切。比如下圖:

且由于SRC\_IN 模式是在相交時利用目標圖像的透明度來改變源圖像的透明度和飽和度的。故而,當目標圖像的透明度在 0~255 之間時,就會把源圖像的透明度和顏色值都變小。利用這 個特性,可以實現倒影效果。
### 2.7.3 SRC_OUT;
### 2.7.4 SRC_OVER;
## 2.8 XOR
丟棄二者重疊部分像素,繪制剩余像素:

## 2.9 OVERLAY
根據目標顏色值來屏蔽源或者目標

## 2.10 SCREEN
將源像素和目標像素相加,然后將源像素與目標像素相減。可以達到**濾色**的目的。

# 3. 案例
比如首先準備兩個圖像,兩個圖像有重疊:

也就是簡單的繪制矩形和平移、旋轉畫布:
~~~
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply {
// 繪制兩個矩形
drawRect(mRect, mPaint)
// 移動一下畫布
val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mPaint)
translate(100f, 0f)
rotate(45f, 300f, 300f)
// 繪制另一個矩形
drawRect(mRect, mOtherPaint)
// 恢復圖層
restoreToCount(saveLayerId)
}
}
~~~
下面開始測試,測試的時候,首先關閉硬件加速,即:
~~~
// 關閉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
~~~
但是,很不幸的是,由于這里的混合模式是兩個圖像之間的關系,而在上面的預備工作中我將之放置在了兩個圖層中,故而會導致在設置了畫筆的Xfermode之后達不到預想的效果的。因為兩個圖像之間的計算應該在同一個圖層中進行,事實上,經過了測試也是如此。故而這里修改為一個正方形和一個圓形的圖案,即:
~~~
private fun drawRect(canvas: Canvas){
canvas.drawRect(mRect, mRectPaint)
}
private fun drawCircle(canvas: Canvas){
canvas.drawCircle(300f, 300f, 100f, mCirclePaint)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply { // this == canvas
// 繪制目標
drawCircle(this)
// 設置混合模式
mRectPaint.setXfermode(mXfermodes[6])
// 繪制另一個矩形
drawRect(this)
// 清空混合模式
mRectPaint.setXfermode(null)
}
}
~~~
當然,在初始化方法中初始了畫筆和對應的模式:
~~~
private lateinit var mCirclePaint: Paint
private lateinit var mRectPaint: Paint
private lateinit var mRect: Rect
private lateinit var mXfermodes: List<PorterDuffXfermode>
private fun init() {
mRect = Rect(200, 200, 400, 400)
mRectPaint = Paint()
mRectPaint.isAntiAlias = true
mRectPaint.color = Color.RED
mRectPaint.isDither = true
mRectPaint.strokeWidth = 5f
mRectPaint.style = Paint.Style.FILL
mCirclePaint = Paint()
mCirclePaint.isAntiAlias = true
mCirclePaint.color = Color.BLUE
mCirclePaint.isDither = true
mCirclePaint.strokeWidth = 5f
mCirclePaint.style = Paint.Style.FILL
// 關閉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
// 初始化混合模式
mXfermodes = listOf<PorterDuffXfermode>(
PorterDuffXfermode(PorterDuff.Mode.ADD),
PorterDuffXfermode(PorterDuff.Mode.CLEAR),
PorterDuffXfermode(PorterDuff.Mode.DST),
PorterDuffXfermode(PorterDuff.Mode.DARKEN),
PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
PorterDuffXfermode(PorterDuff.Mode.SRC),
PorterDuffXfermode(PorterDuff.Mode.XOR),
PorterDuffXfermode(PorterDuff.Mode.SCREEN)
)
}
~~~
效果:

## 3.1 圓形圖片效果
因為混合模式主要是針對圖片的模式,故而也可以用來進行圖片的裁剪工作。比如這里我將一個圖片裁剪為圓形。這里需要注意的是,因為需要保留的是圖片內容,故而這里將加載的Bitmap圖片設置為源圖像,也就是先加載。對應代碼:
~~~
/**
* 繪制目標圖,也就是要顯示的部分的圖
* @param canvas 畫布
* @return 返回正方形圖像的寬度
*/
private fun drawSrcBitmap(canvas: Canvas): Int{
val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.logo)
canvas.drawBitmap(resBitmap, 200f, 200f, mRectPaint)
return resBitmap.width
}
/**
* 創建的遮罩也的是一個Bitmap對象
* @param width 正方形圖片的寬度
* @return 返回創建的遮罩的Bitmap實例
*/
private fun getMaskSrcBitmap(width: Int): Bitmap{
val center = width / 2f
val bm = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
val canvas = Canvas(bm)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = Color.BLACK
canvas.drawCircle(center, center, (width/2).toFloat(), paint)
return bm
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply { // this == canvas
val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint)
// 繪制目標
val bitmapWidth = drawSrcBitmap(this)
// 設置混合模式
mRectPaint.xfermode = mXfermodes[7] // PorterDuffXfermode(PorterDuff.Mode.DST_IN)
// 繪制圓
drawBitmap(getMaskSrcBitmap(bitmapWidth), 200f, 200f, mRectPaint)
// 清空混合模式
mRectPaint.xfermode = null
restoreToCount(saveLayerId)
}
}
~~~
效果:

這里需要注意的是,在進行圖片裁剪的時候,需要的的遮罩層也是一個Bitmap圖片,而不是直接繪制的圓形,比如下面的代碼就是不行的:
~~~
private fun getMaskSrcBitmap2(canvas: Canvas, width: Int){
val center = width / 2f
canvas.drawCircle(center, center, (width/2).toFloat(), mRectPaint)
}
~~~
如果遮罩層為上面的函數,然后將設置混合模式的一行代碼注釋掉,結果為:

如果加上設置混合模式,結果無圓形裁剪效果:

所以這里粗略得出結論:當繪制的為直接圖形的時候,源和目標保持一致,也就是可以使用兩個直接繪制的圖形;也可以使用兩個Bitmap圖像。
## 3.1 圖片倒影效果
這里還是使用MULTIPLY來實現,可以利用MULTIPLY中乘積的特性來進行計算一個虛化的漸變圖效果。步驟為:
* 生成一個漸變、半透明的畫布;
* 使用canvas來在畫布上繪制對應大小的bitmap;
* 使用畫筆的設置Xfermode來設置圖像混合模式;
比如:
~~~
class PorterDuffXfermodeDemo2 : View {
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init()
}
private lateinit var mRectPaint: Paint
private lateinit var mRect: Rect
private fun init() {
mRect = Rect(200, 200, 400, 400)
mRectPaint = Paint()
mRectPaint.isAntiAlias = true
mRectPaint.color = Color.RED
mRectPaint.isDither = true
mRectPaint.strokeWidth = 5f
mRectPaint.style = Paint.Style.FILL
// 關閉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
/**
* 創建一個漸變的半透明圖層,用作蒙版
* @param width 正方形蒙版寬高
* @return 繪制了這個半透明圖像的Bitmap對象
*/
private fun createMaskBitmap(width: Int): Bitmap{
val tempBitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888)
val canvas = Canvas(tempBitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = Color.BLUE
// 設置漸變,從底部到頂部
val x = width / 2
val linearGradient = LinearGradient(
x.toFloat(),
width.toFloat(),
x.toFloat(),
0f,
0x000000ff.toInt(),
0x880000ff.toInt(),
Shader.TileMode.CLAMP
)
paint.setShader(linearGradient)
canvas.drawRoundRect(0f, 0f, width.toFloat(), width.toFloat(), 8f, 8f, paint)
return tempBitmap
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply { // this == canvas
val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint)
// 繪制目標
// val bitmapWidth = drawSrcBitmap(this)
val bitmap = createMaskBitmap(500)
drawBitmap(bitmap, 100f, 100f, mRectPaint)
restoreToCount(saveLayerId)
}
}
}
~~~
就可以得到一個半透明的圖像,如下:

當然實際上這里我調整漸變顏色為白色,且調整了不透明度:
~~~
val linearGradient = LinearGradient(
x.toFloat(),
width.toFloat(),
x.toFloat(),
0f,
0x00ffffff.toInt(),
0x33ffffff.toInt(),
Shader.TileMode.CLAMP
)
~~~
然后,使用圖像的混合模式,指定為MULTIPLY,進行累乘像素:
~~~
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply { // this == canvas
val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint)
// 繪制目標
val bitmapWidth = drawSrcBitmap(this)
// 設置圖像混合模式
mRectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
// 創建圖層蒙版
val bitmap = createMaskBitmap(bitmapWidth)
// 繪制
drawBitmap(bitmap, 100f, 100f, mRectPaint)
// 清空圖像混合模式
mRectPaint.xfermode = null
restoreToCount(saveLayerId)
}
}
~~~
效果:

~~~
/**
* 繪制目標圖,也就是要顯示的部分的圖
* @param canvas 畫布
* @return 返回正方形圖像的寬度
*/
private fun drawSrcBitmap(canvas: Canvas): Int{
val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.logo)
canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint)
return resBitmap.width
}
~~~
然后需要做的就是加載原圖,然后下一這一部分的效果,對其即可。這里就可以使用新建圖層,然后移動畫布,并鏡像翻轉畫布來解決。比如:
~~~
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply { // this == canvas
// 因為這里兩個地方需要,所以加載圖片的放入了公共部分
// 繪制原圖
canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint)
// 新建圖層
val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint)
// 移動畫布
translate(0f, resBitmap.width.toFloat())
// 鏡像翻轉畫布
scale(1f, -1f, 100f + resBitmap.width / 2, 100f + resBitmap.width / 2)
// 繪制目標
canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint)
// 設置圖像混合模式
mRectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
// 創建圖層蒙版
val bitmap = createMaskBitmap(resBitmap.width)
// 繪制
drawBitmap(bitmap, 100f, 100f, mRectPaint)
// 清空圖像混合模式
mRectPaint.xfermode = null
restoreToCount(saveLayerId)
}
}
~~~

當然,這里鏡像的效果好壞其實就取決于生成得Mask的圖像的好壞。所以也可以直接使用PS來生成一個無色到白色的漸變,可以降低不透明度設置為50%之類的,然后進行正片疊底。完整代碼:
~~~
class PorterDuffXfermodeDemo2 : View {
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init()
}
private lateinit var mRectPaint: Paint
private lateinit var mRect: Rect
private val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.logo)
private fun init() {
mRect = Rect(200, 200, 400, 400)
mRectPaint = Paint()
mRectPaint.isAntiAlias = true
mRectPaint.color = Color.RED
mRectPaint.isDither = true
mRectPaint.strokeWidth = 5f
mRectPaint.style = Paint.Style.FILL
// 關閉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
/**
* 加載背景圖層
* @param canvas 畫布
* @return 返回正方形圖像的寬度
*/
private fun loadBackgroundBitmap(canvas: Canvas){
val resBitmap = BitmapFactory.decodeResource(resources, R.drawable.bg)
canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint)
}
/**
* 創建一個漸變的半透明圖層,用作蒙版
* @param width 正方形蒙版寬高
* @return 繪制了這個半透明圖像的Bitmap對象
*/
private fun createMaskBitmap(width: Int): Bitmap{
val tempBitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888)
val canvas = Canvas(tempBitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = Color.BLUE
// 設置漸變,從底部到頂部
val x = width / 2
val linearGradient = LinearGradient(
x.toFloat(),
width.toFloat(),
x.toFloat(),
0f,
0x44ffffff.toInt(),
0x00ffffff.toInt(),
Shader.TileMode.CLAMP
)
paint.setShader(linearGradient)
canvas.drawRoundRect(0f, 0f, width.toFloat(), width.toFloat(), 8f, 8f, paint)
return tempBitmap
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply { // this == canvas
// 因為這里兩個地方需要,所以加載圖片的放入了公共部分
// 繪制原圖
canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint)
// 新建圖層
val saveLayerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), mRectPaint)
// 移動畫布
translate(0f, resBitmap.width.toFloat())
// 鏡像翻轉畫布
scale(1f, -1f, 100f + resBitmap.width / 2, 100f + resBitmap.width / 2)
// 繪制目標
canvas.drawBitmap(resBitmap, 100f, 100f, mRectPaint)
// 設置圖像混合模式
mRectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
// 創建圖層蒙版
val bitmap = createMaskBitmap(resBitmap.width)
// 繪制
drawBitmap(bitmap, 100f, 100f, mRectPaint)
// 清空圖像混合模式
mRectPaint.xfermode = null
restoreToCount(saveLayerId)
}
}
}
~~~
- 介紹
- UI
- MaterialButton
- MaterialButtonToggleGroup
- 字體相關設置
- Material Design
- Toolbar
- 下拉刷新
- 可折疊式標題欄
- 懸浮按鈕
- 滑動菜單DrawerLayout
- NavigationView
- 可交互提示
- CoordinatorLayout
- 卡片式布局
- 搜索框SearchView
- 自定義View
- 簡單封裝單選
- RecyclerView
- xml設置點擊樣式
- adb
- 連接真機
- 小技巧
- 通過字符串ID獲取資源
- 自定義View組件
- 使用系統控件重新組合
- 旋轉菜單
- 輪播圖
- 下拉輸入框
- 自定義VIew
- 圖片組合的開關按鈕
- 自定義ViewPager
- 聯系人快速索引案例
- 使用ListView定義側滑菜單
- 下拉粘黏效果
- 滑動沖突
- 滑動沖突之非同向沖突
- onMeasure
- 繪制字體
- 設置畫筆Paint
- 貝賽爾曲線
- Invalidate和PostInvalidate
- super.onTouchEvent(event)?
- setShadowLayer與陰影效果
- Shader
- ImageView的scaleType屬性
- 漸變
- LinearGradient
- 圖像混合模式
- PorterDuffXfermode
- 橡皮擦效果
- Matrix
- 離屏繪制
- Canvas和圖層
- Canvas簡介
- Canvas中常用操作總結
- Shape
- 圓角屬性
- Android常見動畫
- Android動畫簡介
- View動畫
- 自定義View動畫
- View動畫的特殊使用場景
- LayoutAnimation
- Activity的切換轉場效果
- 屬性動畫
- 幀動畫
- 屬性動畫監聽
- 插值器和估值器
- 工具
- dp和px的轉換
- 獲取屏幕寬高
- JNI
- javah命令
- C和Java相互調用
- WebView
- Android Studio快捷鍵
- Bitmap和Drawable圖像
- Bitmap簡要介紹
- 圖片縮放和裁剪效果
- 創建指定顏色的Bitmap圖像
- Gradle本地倉庫
- Gradle小技巧
- RxJava+Okhttp+Retrofit構建網絡模塊
- 服務器相關配置
- node環境配置
- 3D特效