package android.graphics;
import android.compat.annotation.UnsupportedAppUsage;
public class PorterDuff {
public enum Mode {
CLEAR (0),
SRC (1),
DST (2),
SRC_OVER (3),
DST_OVER (4),
SRC_IN (5),
DST_IN (6),
SRC_OUT (7),
DST_OUT (8),
SRC_ATOP (9),
DST_ATOP (10),
XOR (11),
DARKEN (16),
LIGHTEN (17),
MULTIPLY (13),
SCREEN (14),
ADD (12),
OVERLAY (15);
Mode(int nativeInt) {
this.nativeInt = nativeInt;
}
@UnsupportedAppUsage
public final int nativeInt;
}
public static int modeToInt(Mode mode) {
return mode.nativeInt;
}
public static Mode intToMode(int val) {
switch (val) {
default:
case 0: return Mode.CLEAR;
case 1: return Mode.SRC;
case 2: return Mode.DST;
case 3: return Mode.SRC_OVER;
case 4: return Mode.DST_OVER;
case 5: return Mode.SRC_IN;
case 6: return Mode.DST_IN;
case 7: return Mode.SRC_OUT;
case 8: return Mode.DST_OUT;
case 9: return Mode.SRC_ATOP;
case 10: return Mode.DST_ATOP;
case 11: return Mode.XOR;
case 16: return Mode.DARKEN;
case 17: return Mode.LIGHTEN;
case 13: return Mode.MULTIPLY;
case 14: return Mode.SCREEN;
case 12: return Mode.ADD;
case 15: return Mode.OVERLAY;
}
}
}
PorterDuff.Mode (Added in API level 1)在API level 1时就有了,父类的名称PorterDuff 是对托马斯-波特(Thomas Porter)和汤姆-达夫(Tom Duff)在 1984 年发表的题为《数字图像合成》(Compositing Digital Images)的开创性论文中所做工作的致敬。在这篇论文中,作者描述了 12 个合成运算符,这些运算符控制着如何计算源(要渲染的图形对象)与目标(渲染目标的内容)合成后产生的颜色。
由于波特和达夫的研究只关注源和目标的 alpha 通道效果,因此原论文中描述的 12 个操作符在这里称为 alpha 合成模式(alpha compositing modes)。
为方便起见,该类还提供了几种混合模式(blending modes),它们同样定义了源和目标的合成结果,但不受限于 alpha 通道。波特和达夫并没有定义这些混合模式,但为了方便起见,还是将其纳入了本类。
下面介绍的所有示例图都使用相同的源图像和目标图像:
以下代码片段显示了生成每个图表的绘制操作顺序(Java代码):
Paint paint = new Paint();
canvas.drawBitmap(destinationImage, 0, 0, paint);
PorterDuff.Mode mode = // choose a mode
paint.setXfermode(new PorterDuffXfermode(mode));
canvas.drawBitmap(sourceImage, 0, 0, paint);
12种Alpha合成模式(Alpha compositing modes)
|
|
|
|
|
|
|
|
|
|
|
|
源码中共有18种模式,而上述示例只有17种,没有ADD模式,此处单独列出
Lighten、Screen、Overlay、ADD这几种模式很相似,注意区分。
Adds the source pixels to the destination pixels and saturates the result.
将源像素添加到目标像素,并使结果饱和。
Destination pixels covered by the source are cleared to 0.
被源像素覆盖的目标像素会被清零。
Retains the smallest component of the source and destination pixels.
保留源像素和目标像素的最小分量。
The source pixels are discarded, leaving the destination intact.
源像素被丢弃,目标像素保持不变
Discards the destination pixels that are not covered by source pixels.
丢弃未被源像素覆盖的目标像素。
Keeps the destination pixels that cover source pixels, discards the remaining source and destination pixels.
保留覆盖源像素的目标像素,丢弃剩余的源像素和目标像素。
Keeps the destination pixels that are not covered by source pixels.
保留未被源像素覆盖的目标像素。
The source pixels are drawn behind the destination pixels.
源像素绘制在目标像素的后面。
Retains the largest component of the source and destination pixel.
保留源像素和目标像素的最大分量。
Multiplies the source and destination pixels.
将源像素和目标像素相乘。
Multiplies or screens the source and destination depending on the destination color.
根据目标颜色对源颜色和目标颜色进行倍增或筛选。
Adds the source and destination pixels, then subtracts the source pixels multiplied by the destination.
添加源像素和目标像素,然后减去源像素乘以目标像素的积。
The source pixels replace the destination pixels.
源像素替换目标像素。
Discards the source pixels that do not cover destination pixels. Draws remaining source pixels over destination pixels.
丢弃未覆盖目标像素的源像素。在目标像素上绘制剩余的源像素。
Keeps the source pixels that cover the destination pixels, discards the remaining source and destination pixels.
保留覆盖目标像素的源像素,丢弃剩余的源像素和目标像素。
Keeps the source pixels that do not cover destination pixels. Discards source pixels that cover destination pixels. Discards all destination pixels.
保留未覆盖目标像素的源像素。丢弃覆盖目标像素的源像素。丢弃所有目标像素。
The source pixels are drawn over the destination pixels.
在目标像素上绘制源像素。
Discards the source and destination pixels where source pixels cover destination pixels. Draws remaining source pixels.
当源像素覆盖目标像素时,丢弃源像素和目标像素。绘制剩余的源像素。
如果应用程序运行在API 14以上版本,而恰好又要使用那些不支持硬件加速的函数,就需要禁用硬件加速。
Android提供了不同的硬件加速方法,分Application、Activity、Window、View等4个层级。
1、在AndroidManifest.xml文件中为application标签添加属性,即可为整个应用程序开启或关闭硬件加速。
<application android:hardwareAccelerated="false" />
2、在AndroidManifest.xml文件的activity标签下使用hardAccelerated属性开启或关闭硬件加速。
<activity android:hardwareAccelerated="false" />
3、在Window层级上使用如下代码开启硬件加速(该级别不支持关闭功能,Kotlin代码)
window.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
(1) 设置此标志必须在setContentView之前。
(2) 如果在AndroidManifest.xml中开启了硬件加速,那么不能使用此标志禁用硬件加速。
(3) 一般是在AndroidManifest.xml中的application/activity明确指定关闭硬件加速的情况下,使用这个flag开启该window的硬件加速。
(4) 如果在AndroidManifest.xml中的application/activity上将android:hardwareAccelerated属性设置为true,则系统将自动设置此标志。
4、在View层级上可直接在代码中使用如下方式关闭硬件加速(该级别不支持开启功能)
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
或在布局文件中使用android:layerType="software"来关闭硬件加速。
<com.example.customview.xfermode.XfermodeView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layerType="software" />
该方法并不是关闭硬件加速,而是强制使用软件加速,如果硬件加速是关闭的,那么就算设置了LAYER_TYPE_HARDWARE也使用软件加速。
1、重写onDraw()、dispatchDraw()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
}
2、使用Bitmap构建
val mCanvas = Canvas(mBmp)
或
val mCanvas = Canvas()
mCanvas.setBitmap(mBmp)
若使用Bitmap构建了一个Canvas,则使用该Canvas绘制的图像都会保存在这个Bitmap上,而不会画在View上。
如果想画在View上,必须使用onDraw(canvas: Canvas)中的参数canvas再画一遍Bitmap,如再调用一遍canvas.drawBitmap(xxx)。
3、使用SurfaceView(Java代码)
SurfaceHolder surfaceHolder = getHolder();
Canvas canvas = surfaceHolder.lockCanvas();
// 绘图操作
surfaceHolder.unlockCanvasAndPost(canvas);
override fun onDraw(canvas: Canvas) {
val count = canvas.saveLayer(xxx)
canvas.drawOval(xxx)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(xxx)
paint.xfermode = null
if (count != null) {
canvas.restoreToCount(count)
}
}
代码中在调用saveLayer(xxx)时,会生成一块全新的透明画布(Bitmap),后续的所有绘图操作都在该画布上进行,该画布的大小就是指定的所要保存区域的大小。
新建画布,应用Xfermode的合成示意图:
中间的透明画布是调用saveLayer(xxx)自动生成的。而每次调用canvas.drawXXX()时会自动生成一个透明图层来专门绘制要画的图形,且每次生成的图层都会叠加到最近的画布上。在应用Xfermode算法后,在叠加到最近的调用saveLayer(xxx)生成的画布上时会进行计算。在新建画布上绘制完成后,再整体覆盖到原始的系统画布上显示出来。
若不调用saveLayer(xxx)就不会新建透明画布了,所有的绘图操作直接在原始的系统画布上进行。
图层(Layer)、画布(Bitmap)和Canvas的概念及相互关系:
1、图层(Layer)
每次调用canvas.drawXXX()都会生成一个透明图层专门来绘制该图形。
2、画布(Bitmap)
每块画布都是一个Bitmap,所有的图像都是画在该Bitmap上的,原始的系统画布也是在系统源码中创建了一块Bitmap。
画布有两种,一是View的原始画布,控件的背景就是画在原始画布上。二是人造画布,即通过saveLayer(xxx)、new Canvas(bitmap)等方法人为新建的画布。尤其是调用savaLayer(xxx)后生成的画布,之后的绘制都会在该新画布上进行,只有在调用restore()、restoreToCount()等方法后才会返回原始画布上绘制。
3、Canvas
Canvas只是画布的表现形式,所有的绘制都是通过Canvas来实现的。Canvas只能通过Bitmap生成,即new Canvas(bitmap),且无论是原始的系统画布还是人造画布,所有的东西都是通过Canvas画到Bitmap上的。因此可以把Canvas理解成绘图的工具,利用它封装的绘图函数来绘图,而绘制内容都会画到Bitmap上。故使用Canvas.clipXXX()系列函数将画布裁剪就是把它对应的Bitmap进行裁剪,结果就是之后再用Canvas绘图的区域会变小。
在介绍Xfermode各种模式时,每个图例中的灰白色背景表示绘制样式的透明背景图层,这点很重要。因为我们看到的视觉效果实际上是把各种包含图层在内的绘制操作叠加在一起的最终效果。
Xfermode可以把各种绘制进行“合成”,应用Xfermode时一定要留意是否要操作这个背景图层,否则各种合成模式的效果可能会有偏差。
PorterDuffXfermode是部分不支持硬件加速的,为保险起见,建议先禁用硬件加速。
请结合代码中的注释阅读该步骤:
1、调用Canvs.saveLayer()把绘制区域拉到单独的离屏缓冲⾥(此时dst和src图像带有透明背景图层)
2、绘制dst图像
3、调佣Paint.setXfermode()设置Xfermode
4、绘制src图像
5、调用Paint.setXfermode(null)清空Xfermode
6、调用Canvas.restoreToCount()回退到指定的Canvas
在设置Xfermode前画出的图像叫做目标图像,即给谁应用Xfermode;在设置Xfermode后画出的图像叫做源图像,即拿什么应用Xfermode。若颠倒绘制顺序,效果就反了。
工具类Extensions.kt
package com.example.customview.func
import android.content.res.Resources
import android.util.TypedValue
val Float.px
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
示例类XfermodeView.kt(有详细的注释)
package com.example.customview.xfermode
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import com.example.customview.func.px
/**
* @author Administrator
* @date 2023/5/18
*/
/**
* 定义要使用的Xfermode模式
*/
private val XFERMODE = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
class XfermodeView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
/**
* 限定区域大小(下文中用于给View的canvas创建透明画布)
*/
private val bounds = RectF(150f.px, 50f.px, 300f.px, 200f.px)
/**
* 创建两个宽高相等的Bitmap
*/
private val dstCircleBmp =
Bitmap.createBitmap(150f.px.toInt(), 150f.px.toInt(), Bitmap.Config.ARGB_8888)
private val srcSquareBmp =
Bitmap.createBitmap(150f.px.toInt(), 150f.px.toInt(), Bitmap.Config.ARGB_8888)
init {
val myCa = Canvas()
myCa.setBitmap(dstCircleBmp)
// 禁用硬件加速
setLayerType(View.LAYER_TYPE_NONE, null)
// 新建一块透明画布,后续内容将画到dstCircleBmp
val mCanvas = Canvas(dstCircleBmp)
paint.color = Color.parseColor("#D81B60")
// 会新建透明图层,在dstCircleBmp上画圆,限定圆所在的矩形范围
mCanvas.drawOval(50f.px, 0f.px, 150f.px, 100f.px, paint)
paint.color = Color.parseColor("#2196F3")
// 复用透明画布(mCanvas.hashCode()值没变),后续内容将画到srcSquareBmp
mCanvas.setBitmap(srcSquareBmp)
// 会新建透明图层,在srcSquareBmp上画方并限定范围
mCanvas.drawRect(0f.px, 50f.px, 100f.px, 150f.px, paint)
// 此时,init执行完,所有图像不可见
}
override fun onDraw(canvas: Canvas) {
// 设置系统画布背景色
// canvas.drawColor(Color.GREEN)
/*
新建透明画布(离屏Bitmap-离屏缓冲),把绘制区域拉到单独的离屏缓冲⾥。
若不调用该方法,最后一步叠加显示画布、图层、图像等信息时会把系统背景色计算在内,效果容易出错。
调用saveLayer()会生成一个表示当前保存的画布(主要是画布的配置信息)的栈层索引(该索引从0开始)
*/
val count = canvas.saveLayer(bounds, null)
/*
会新建透明图层,把dst图像画到透明画布上,并限制左、上边界。
然后所有透明画布、透明图层、图像叠加并覆盖到系统画布上显示,代码执行到此图像已可见
*/
canvas.drawBitmap(dstCircleBmp, 150f.px, 50f.px, paint)
// 设置Xfermode
paint.xfermode = XFERMODE
/*
会新建透明图层,把src图像画到透明画布上,并限制左、上边界。
因为设置了Xfermode,会计算与上一个canvas.drawXXX()生成的图像的叠加方式,
然后再覆盖到系统画布上显示,此时图像也可见
*/
canvas.drawBitmap(srcSquareBmp, 150f.px, 50f.px, paint)
// 清空Xfermode(若不执行此行代码,图像显示模式也是正确的)
paint.xfermode = null
/*
restoreToCount()会一直退栈到指定索引的画布为止,该画布就成为栈的最上层,
可作为最新的画布供后续使用(若不执行此行代码,图像显示模式也是正确的)
*/
count.let { canvas.restoreToCount(it) }
}
}
直接在布局文件中引用XfermodeView,代码较简单就不贴了。
默认你已了解View的坐标系。
绘制分析:
运行效果:
参考文献
[1] Android Developers官网
[2] 启舰.Android自定义控件开发入门与实战[M].北京:电子工业出版社,2018