内容来自:
不知不觉,已经来到了 xfermode 这部分了。非常感谢原博主提供的一系列非常优秀的教程,剖析、细节方面讲的非常好,这边对着源博客进行练习,做记录(内容几乎照搬源博客
),不然不使用,就特别容易忘,特别是前端,变化是非常快的;现在rn很火,混合开发也很流行,但我觉得还是先补补native基础,这些东西不用就忘;
典型如: xfermode,一个应用场景就是 蒙版图,通过获取页面上具体的控件位置,结合xfermode实现掏空,呈现完美蒙版页面;(还记得当年我们可是拿图当做蒙版的,😝);
好,开始!
GPU硬件加速
什么是硬件加速
GPU英文全称Graphic Processing Unit
,中文翻译为“图形处理器”。与CPU不同,GPU是专门为处理图形任务而产生的芯片。 为了专门处理多媒体的计算、存储任务,GPU就应运而生了,GPU中自带处理器和存储器,以用来专门计算和存储多媒体任务。
对于Android来讲,在API 11之前是没有GPU的概念的,在API 11之后,在程序集中加入了对GPU加速的支持,在API 14之后,硬件加速是默认开启的!我们可以显式地强制图像计算时使用GPU而不使用CPU.
在CPU绘制和GPU绘制时,在流程上区别的:
在基于软件
的绘制模型下,CPU
主导绘图,视图按照两个步骤绘制:
- 让View层次结构失效
- 绘制View层次结构
在基于硬件加速
的绘制模式下,GPU
主导绘图,绘制按照三个步骤绘制:
- 让View层次结构失效
- 记录、更新显示列表;
- 绘制显示列表
GPU加速时,流程中多了一项“记录、更新显示列表”,表示在第一步View层次结构失效后,将这些View的绘制函数作为绘制指令记录在一个显示列表中,然后再读取列表中的绘制指令并调用openGL相关函数完成实际绘制;
所以在GPU加速时,实际是使用OpenGL的函数来完成绘制的。
GPU硬件加速缺点
GPU加速硬件提高了Android系统显示和刷新的速度,但也有以下缺点:
- 兼容性问题:由于是将绘制函数转换成OpenGL命令来绘制,定然会存在OpenGL并不能完全支持原始绘制函数的问题,所以这就会造成在打开GPU加速时,效果会失效的问题。
- 内存消耗问题:由于需要OpenGL的指令,所以需要把系统中的OpenGL相关的包加载到内存中来,所以单纯OpenGL API调用就会占用8MB,而实际上会占用更多内存;
- 电量消耗问题:多使用了一个部件,当然会更耗电……
下图显示了一些特殊函数硬件加速开始支持的平台等级:(红叉表示任何平台都不支持,不在列表中的默认在API 11就开始支持)
http://developer.android.com/guide/topics/graphics/hardware-accel.html
关闭GPU硬件加速
现在的APP,很多都是从Android 4.0开始支持的,即API 必然大于14了,就就是默认开启了硬件加速,如果遇到了不支持硬件加速的函数时,就考虑到要关闭硬件加速了;
针对不同类型的(如上图),Android给我们提供了不同的禁用方法:
硬件加速分全局(Application)、Activity、Window、View 四个层级 ;
- 全局
在AndroidManifest.xml文件为application标签添加如下的属性即可为整个应用程序开启/关闭硬件加速:
<application android:hardwareAccelerated="true" ...>
- Activity
在Activity 标签下使用 hardwareAccelerated 属性开启或关闭硬件加速:
<activity android:hardwareAccelerated="false" />
- Window
在Window 层级使用如下代码开启硬件加速:(Window层级不支持关闭硬件加速)
getWindow().setFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
- View
View 级别如下关闭硬件加速:(view 层级上不支持开启硬件加速)
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
或使用android:layerType=”software”
关闭硬件加速<LinearLayout android:orientation="vertical" android:layerType="software" >
Xfermode
混合图形模式;读原博客时,这里有 AvoidXfermode
,因为 AvoidXfermode 不支持硬件加速,在android API > 16 时,此类过时了,这里就不记录了;具体参考原博客吧;
其他一些子类,在我的源码中,都找不到了,就不记录;
我们主要看一下 其子类PorterDuffXfermode
的使用;
PorterDuffXfermode有些函数也不支持硬件加速,在涉及到使用不支持硬件加速的函数时,我们需要在View层禁用掉硬件加速;
Xfermode使用流程:
- 禁用硬件加速(根据使用的函数来确定是否禁):
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
- 启用离屏绘制(图层)
//新建图层
int layerID = canvas.saveLayer(0,0,width,height,mPaint,Canvas.ALL_SAVE_FLAG);
//TODO 核心绘制代码
//还原图层
canvas.restoreToCount(layerID);
Android在绘图时会先检查该画笔Paint对象有没有设置Xfermode,如果没有设置Xfermode,那么直接将绘制的图形覆盖Canvas对应位置原有的像素;如果设置了Xfermode,那么会按照Xfermode具体的规则来更新Canvas中对应位置的像素颜色。
Xfermode 之 PorterDuffXfermode
构造函数:
public PorterDuffXfermode(PorterDuff.Mode mode)
看到了熟悉的PorterDuff.Mode,在setColorFilter时已经用过它,(模式真多18个)
更多参考原博客:
https://blog.csdn.net/harvic880925/article/details/51264653
在这里涉及到2个比较重要的概念,目标图 DST与源图 SRC;
示例代码(来自原博客):
private val wid = 400
private val hei = 400
private lateinit var dstBmp: Bitmap
private lateinit var srcBmp: Bitmap
private var paint: Paint
init {
paint = Paint(Paint.ANTI_ALIAS_FLAG)
dstBmp = makeDst(wid, hei)
srcBmp = makeSrc(wid, hei)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 根据使用绘制流程类,这里没有使用相关函数,不需要关闭硬件加速
val layerID = canvas.saveLayer(0f, 0f, width * 2.toFloat(), height * 2.toFloat(), paint)
// 1. 先画目标图像(圆)
canvas.drawBitmap(dstBmp, 0f, 0f, paint)
// 2.设置xfermode
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
// 3.画原图(方),最后在源图像上生成结果图并更新到目标图像上
canvas.drawBitmap(srcBmp, wid / 2.toFloat(), hei / 2.toFloat(), paint)
paint.xfermode = null
canvas.restoreToCount(layerID)
}
fun makeSrc(w: Int, h: Int): Bitmap {
val bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val c = Canvas(bm)
val p = Paint(Paint.ANTI_ALIAS_FLAG)
p.color = -0x995501
c.drawRect(0f, 0f, w.toFloat(), h.toFloat(), p)
return bm
}
fun makeDst(w: Int, h: Int): Bitmap {
val bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val c = Canvas(bm)
val p = Paint(Paint.ANTI_ALIAS_FLAG)
p.color = -0x33bc
c.drawOval(RectF(0f, 0f, w.toFloat(), h.toFloat()), p)
return bm
}
对比setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.XXXX));中的混合过程
在PorterDuffColorFilter中的混合过程与这里的setXfermode()设置混合模式的计算方式和效果是完全相同的,只是在PorterDuffColorFilter中只能使用纯色彩
,而且是完全覆盖在图片上方;而setXfermode()则不同,它只会在目标图像和源图像交合
的位置起作用,而且源图像不一定是纯色的。
需要关注的2点
计算效果图像时,是以源图像所在区域为计算目标的,把计算后的源图像更新到对应区域内。
- 区域一是源图像和目标图像的
相交
区域,由于在这个区域源图像和目标图像像素都不是空白像素,所以可以明显看出颜色的计算效果。 - 在区域二中,源图像所在区域的目标图像是空白像素,所以这块区域所表示的意义就是,当某一方区域是空白像素时,此时的计算结果。
总而言之:我们在下面的各个模式计算时,只需要关注图示中的区域一和区域二;其中区域一表示当源图像和目标图像像素都不是空白像素时的计算结果,而区域二则表示当某一方区域是空白像素时,此时的计算结果。
18种模式
18种模式说明
颜色叠加相关模式(6个)
涉及到的几个模式有6个(上图的第3,4列):
- Mode.ADD(饱和度相加)
计算公式是Saturate(S + D);ADD模式简单来说就是对SRC与DST两张图片相交区域
的饱和度
进行相加; - Mode.DARKEN(变暗)
- Mode.LIGHTEN(变亮)
两个图像重合的区域才会有颜色值变化,所以只有重合区域才有变亮的效果,源图像非重合的区域,由于对应区域的目标图像是空白像素,所以直接显示源图像。
Mode.MULTIPLY(正片叠底)
计算公式:[Sa * Da, Sc * Dc],在计算alpha值时的公式是Sa * Da,是用源图像的alpha值乘以目标图像的alpha值;由于源图像的非相交区域
所对应的目标图像像素的alpha是0
,所以结果像素的alpha值仍是0,所以源图像的非相交区域在计算后是透明的。Mode.OVERLAY(叠加)
Mode.SCREEN(滤色)
SRC相关模式(5个)
在遇到当图像相交时,需要显示源图像时,就需要从SRC相关的模式考虑了:
Mode.SRC
在处理源图像所在区域的相交问题时,全部以源图像显示Mode.SRC_IN
计算公式为:[Sa * Da, Sc * Da]
在这个公式中结果值的透明度和颜色值都是由Sa,Sc分别乘以目标图像的Da来计算的。所以当目标图像为空白像素时,计算结果也将会为空白像素。-
Mode.SRC_OUT
计算公式为:[Sa * (1 - Da), Sc * (1 - Da)]
计算结果的透明度=Sa * (1 - Da);也就是说当目标图像图像完全透明时,计算结果将是透明的;
源图像与目标图像的相交部分由于目标图像的透明度为100%,所以相交部分的计算结果为空白像素
(1-100%=0)。在目标图像为空白像素时,完全以源图像显示。以目标图像的透明度的补值来调节源图像的透明度和色彩饱和度。即当目标图像为空白像素时,就完全显示源图像,当目标图像的透明度为100%时,交合区域为空像素.
Mode.SRC_OVER
计算公式为:[Sa + (1 - Sa)Da, Rc = Sc + (1 - Sa)Dc] ;
在目标图像的顶部绘制源图像。从公式中也可以看出目标图像的透明度为Sa + (1 - Sa)*Da;即在源图像的透明度基础上增加一部分目标图像的透明度。增加的透明度是源图像透明度的补量;目标图像的色彩值的计算方式同理,所以当源图像透明度为100%时,就原样显示源图像;-
Mode.SRC_ATOP
计算公式为:[Da, Sc * Da + (1 - Sa) * Dc] ;对比SRC_IN
SRC_IN: [Sa * Da, Sc * Da]
SRC_ATOP:[Da, Sc * Da + (1 - Sa) * Dc]
先看透明度:在SRC_IN中是Sa * Da,在SRC_ATOP是Da
SRC_IN是源图像透明度乘以目标图像的透明度做为结果透明度,而SRC_ATOP是直接使用目标图像的透明度做为结果透明度
再看颜色值:
SRC_IN的颜色值为 Sc * Da,SRC_ATOP的颜色值为Sc * Da + (1 - Sa) * Dc;SRC_ATOP在SRC_IN的基础上还增加了(1 - Sa) * Dc;区别:
1. 当透明度只有100%和0%时,SRC_ATOP是SRC_IN是通用的
2. 当透明度不只有100%和0%时,SRC_ATOP相比SRC_IN源图像的饱和度会增加,即会显得更亮!
SRC模式相关应用示例
示例1:SRC_IN实现圆角图片
目标为圆角矩形,然后将原图放入进去,圆角图片就出来了;
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val srcBmp = BitmapFactory.decodeResource(resources, R.mipmap.juntuan)
val bmpWidth = srcBmp.width
val bmpHeight = srcBmp.height
// .1目标图为圆角矩形
val dstBmp = makeDest(bmpWidth, bmpHeight)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
val layerID = canvas.saveLayer(0f, 0f, bmpWidth.toFloat(), bmpHeight.toFloat(), paint)
canvas.drawBitmap(dstBmp, 0f, 0f, paint)
// 2.设置xfermode
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) // OVERLAY
canvas.drawBitmap(srcBmp, 0f, 0f, paint)
paint.xfermode = null
canvas.restoreToCount(layerID)
}
// dst 为圆角
fun makeDest(w: Int, h: Int): Bitmap {
val bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val c = Canvas(bm)
val p = Paint(Paint.ANTI_ALIAS_FLAG)
p.color = Color.GRAY
c.drawRoundRect(RectF(0f, 0f, w.toFloat(), h.toFloat()), 40f, 40f, p)
return bm
}
注意:其实这样实现圆角图片,是有问题的,问题在于重绘
示例2:SRC_IN实现倒影
在相交时利用目标图像的透明度来改变源图像的透明度和饱和度;利用这个特性,结合matrix形成倒影;
参考原博客,没有那个透明图,(。•ˇ‸ˇ•。);
- 我们这,用
drawable
构建一个渐变图形 (上面半透明-渐变到完全透明):
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 渐变色 -->
<gradient
android:angle="270"
android:endColor="#00ffffff"
android:startColor="#80ffffff"
android:type="linear" />
</shape>
- 生成目标图dstBm:
val dstDrawable = resources.getDrawable(R.drawable.drawable_liner_gradient_white)
val dstBm = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888)
val c = Canvas(dstBm)
dstDrawable.setBounds(0, 0, bmpWidth, bmpHeight / 1.5f.toInt())
dstDrawable.draw(c)
- 旋转原图180度,通过模式 SRC_IN,draw 到 dst 上, 整体如下(这里的原图,不是指的SRC):
val srcBmp = BitmapFactory.decodeResource(resources, R.mipmap.juntuan)
val bmpWidth = srcBmp.width
val bmpHeight = srcBmp.height
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
// 1.画出原图,注意不是 SRC
canvas.drawBitmap(srcBmp, 0f, 0f, paint)
// 有渐变度的drawable
val dstDrawable = resources.getDrawable(R.drawable.drawable_liner_gradient_white)
val dstBm = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888)
val c = Canvas(dstBm)
dstDrawable.setBounds(0, 0, bmpWidth, bmpHeight / 2f.toInt())
dstDrawable.draw(c)
// 倒立原图,形成 SRC
val matrix = Matrix()
matrix.setScale(1f, -1f)
val revertBmp = Bitmap.createBitmap(srcBmp, 0, 0, srcBmp.width, srcBmp.height, matrix, true)
// 2.再画出倒影,倒影在原图的下面(向下translate原图的高度)
canvas.translate(0f, bmpHeight.toFloat())
val layerID2 = canvas.saveLayer(0f, 0f, bmpWidth.toFloat(), bmpHeight.toFloat(), paint)
canvas.drawBitmap(dstBm,0f,0f, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(revertBmp, 0f,0f,paint)
paint.xfermode = null
canvas.restoreToCount(layerID2)
canvas.restore()
示例三:Mode.SRC_OUT 橡皮擦效果
把手指轨迹做为目标图像,在与源图像计算时,有手指轨迹的地方就变为空白像素了,形成橡皮檫。
注意:这里是在 DST 上做得文章 因为:Sa * (1 - Da)
;
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 10f
color = Color.BLACK
}
val srcBmp = BitmapFactory.decodeResource(resources, R.mipmap.sanjing)
var dstBmp = Bitmap.createBitmap(srcBmp.width, srcBmp.height, Bitmap.Config.ARGB_8888)
val path = Path()
var preX = 0f
var preY = 0f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// create src, draw path to srcBmp, 手势画到 dst 上
val c = Canvas(dstBmp)
c.drawPath(path, paint)
val layerID2 = canvas.saveLayer(0f, 0f, srcBmp.width.toFloat(), srcBmp.height.toFloat(), paint)
canvas.drawBitmap(dstBmp, 0f, 0f, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OUT)
canvas.drawBitmap(srcBmp, 0f, 0f, paint)
paint.xfermode = null
canvas.restoreToCount(layerID2)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(event.x, event.y)
preX = event.x
preY = event.y
return true
}
MotionEvent.ACTION_MOVE -> { // 平滑过渡
val endX = (preX + event.x) / 2
val endY = (preY + event.y) / 2
path.quadTo(preX, preY, endX, endY)
preX = event.x
preY = event.y
}
}
postInvalidate()
return super.onTouchEvent(event)
}
类似刮刮卡效果,请参考原博客;
DST相关模式(5个)
在DST相关的模式中,在处理相交区域时,优先以目标图像显示为主。
Mode.DST
计算公式为:[Da, Dc]
在处理源图像所在区域的相交问题时,正好与Mode.SRC相反,全部以目标图像显示 ;Mode.DST_IN
计算公式为:[Da * Sa,Dc * Sa]
Mode.SRC_IN计算公式为[Sa * Da, Sc * Da] 正好与SRC_IN相反,Mode.DST_IN是在相交时利用源图像的透明度来改变目标图像的透明度和饱和度。当源图像透明度为0时,目标图像就完全不显示。Mode.DST_OUT
计算公式为:[Da * (1 - Sa), Dc * (1 - Sa)]
Mode.SRC_OUT是利用目标图像的透明度的补值来改变源图像的透明度和饱和度。而Mode.DST_OUT反过来,是通过源图像的透明度补值来改变目标图像的透明度和饱和度。
简单来说,在Mode.DST_OUT模式下,就是相交区域显示的是目标图像,目标图像的透明度和饱和度与源图像的透明度相反,当源图像透明底是100%时,则相交区域为空值。当源图像透明度为0时,则完全显示目标图像。非相交区域完全显示目标图像。Mode.DST_OVER
计算公式为:[Sa + (1 - Sa)Da, Rc = Dc + (1 - Da)Sc]
对比Mode.SRC_OVERMode.DST_ATOP
计算公式为:[Sa, Sa * Dc + Sc * (1 - Da)]
对比Mode.SRC_ATOP
DST相关模式是完全可以使用SRC对应的模式来实现的,只不过需要将目标图像和源图像对调一下即可。
在SRC模式中,是以显示源图像为主,通过目标图像的透明度来调节计算结果的透明度和饱和度,而在DST模式中,是以显示目标图像为主,通过源图像的透明度来调节计算结果的透明度和饱和度。
示例一:Mode.DST_IN 区域内波浪效果
DST目标图像为波纹图;目标图片,除了文字部分为白色,其他均为透明像素;
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL_AND_STROKE
strokeWidth = 2f
color = Color.BLUE
}
val srcBmp = BitmapFactory.decodeResource(resources, R.mipmap.havric)
val dstBmp = Bitmap.createBitmap(srcBmp.width, srcBmp.height, Bitmap.Config.ARGB_8888)
val waveWidth = srcBmp.width * 2.0f
val originY = srcBmp.height / 2.0f - 10f
var dx = 0f
val path = Path()
val anim = ValueAnimator.ofFloat(0f, waveWidth).apply {
interpolator = LinearInterpolator()
duration = 2000
repeatMode = RESTART
repeatCount = ValueAnimator.INFINITE
addUpdateListener {
dx = it.animatedValue as Float
postInvalidate()
}
}
init {
anim.start()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(Color.BLACK)
canvas.save()
createWave()
// 生成dst 并清空
val c = Canvas(dstBmp)
c.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
c.drawPath(path, paint)
// === 画原图
canvas.drawBitmap(srcBmp, 0f, 0f, paint)
val layerID2 = canvas.saveLayer(0f, 0f, srcBmp.width.toFloat(), srcBmp.height.toFloat(), paint)
canvas.drawBitmap(dstBmp, 0f, 0f, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
canvas.drawBitmap(srcBmp, 0f, 0f, paint)
paint.xfermode = null
canvas.restoreToCount(layerID2)
canvas.restore()
}
// 创建波浪
private fun createWave() {
path.apply {
path.reset()
// path的起始位置向左移一个波长
path.moveTo(-waveWidth + dx, originY)
val halfWaveWidth = waveWidth / 2
var i = -halfWaveWidth
while (i <= srcBmp.width + halfWaveWidth) {
rQuadTo(halfWaveWidth / 2, -50f, halfWaveWidth, 0f)
rQuadTo(halfWaveWidth / 2, 50f, halfWaveWidth, 0f)
i += halfWaveWidth
}
lineTo(srcBmp.width * 1.0f, srcBmp.height * 1.0f)
lineTo(0f, srcBmp.height.toFloat())
close()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
anim?.let {
it.cancel()
}
}
其他模式
- Mode.CLEAR
计算公式:[0, 0]
计算结果直接就是[0,0]即空像素。也就是说,源图像所在区域都会变成空像素;
如上面的:val c = Canvas(dstBmp) c.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
- Mode.XOR
计算公式:[Sa + Da - Sa*Da,Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)]
效果图:像异或
,从计算公式上看:
公式中透明度部分:Sa + Da - Sa*Da,就是将目标图像和源图像的透明度相加,然后减去它们的乘积,所以计算结果的透明度会增大(即比目标图像和源图像都大,当其中一个图像的透明度为1时,那结果图像的透明度肯定是1)
然后再看颜色值部分:Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc);表示源图像和目标图像分别以自己的透明度的补值乘以对方的颜色值,然后相加得到结果。最后再加上Sc, Dc中的最小值。
如何应用
- 首先,目标图像和源图像混合,需不需要生成颜色的叠加特效,如果需要叠加特效则从颜色叠加相关模式中选择;也就是那6种;
- 当不需要特效,而需要根据某一张图像的透明像素来裁剪时,就需要使用SRC相关模式或DST相关模式了。由于SRC相关模式与DST相关模式是相通的,唯一不同的是决定当前哪个是目标图像和源图像;
- 当需要清空图像时,使用Mode.CLEAR;