《Android自定义控件入门到精通》文章索引 ☞ https://blog.youkuaiyun.com/Jhone_csdn/article/details/118146683
《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code
Xfermode
Xfermode在Android高版本中只保留了一个实现类PorterDuffXfermode,在Android24及以下低版本中,你还可以看到另外两个实现类AvoidXfermode,PixelXorXfermode,高版本已移除了,这里就不展开讲了,其中AvoidXfermode可以用于做选区和选区填充,非常强大,但是直接移除且并没有提供替代方案也是挺无奈的。
PorterDuffXfermode(PorterDuff.Mode mode)
- PorterDuff.Mode : 混合模式
PorterDuff.Mode
图形混合模式,其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响,而我们PorterDuffXfermode的名字也来源于这俩人的人名组合PorterDuff。
在讲ComposeShader的时候,我们已经初步了解过了PorterDuff.Mode
例如,使用Xfermode实现Ps中的正片叠底混合模式效果:
素材(需要一张桃花图和一张绿蓝渐变图):
在混合概念中,有SRC(源)和DST(目标)两个概念,怎么去理解呢?
- DST:如上面桃花图,在Ps中的下层图层中,它是SRC(源)作用的目标
- SRC:如上面绿蓝渐变图,在Ps中的上层图层中,它是用来混合的源
记住这句话就行了,谁先画在Canvas上,谁就是DST
@Override
protected void onDraw(Canvas canvas) {
//加载桃花图片作为dst
Bitmap dst = BitmapFactory.decodeResource(getResources(), R.mipmap.flower);
//先将目标dst画到图层上
canvas.drawBitmap(dst,null,new RectF(0,0,dst.getWidth(),dst.getHeight()),mPaint);
//设置Xfermode混合模式
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
//加载渐变颜色图片作为src
Bitmap src = BitmapFactory.decodeResource(getResources(), R.mipmap.green_blue);
//再将源图片src画到图层上
canvas.drawBitmap(src,null,new RectF(0,0,src.getWidth(),src.getHeight()),mPaint);
}
效果:
可以看到PorterDuff.Mode.MULTIPLY跟Ps中的正片叠底效果是一样的
我们用官方的提供的Demo效果来演示下所有的PorterDuff.Mode效果
public class TestView extends View {
private static final int W = 80;
private static final int H = 80;
private static final int ROW_MAX = 5;
private Bitmap mSrcB;
private Bitmap mDstB;
private Shader mBG;
private static final Xfermode[] sModes = {
new PorterDuffXfermode(PorterDuff.Mode.SRC),
new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
new PorterDuffXfermode(PorterDuff.Mode.DST),
new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
new PorterDuffXfermode(PorterDuff.Mode.SCREEN),
new PorterDuffXfermode(PorterDuff.Mode.ADD),
new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
new PorterDuffXfermode(PorterDuff.Mode.XOR),
new PorterDuffXfermode(PorterDuff.Mode.OVERLAY)
};
private static final String[] sLabels = {
"SRC", "SRC_IN", "SRC_OUT", "SRC_ATOP","SRC_OVER",
"DST", "DST_IN", "DST_OUT", "DST_ATOP","DST_OVER",
"DARKEN", "LIGHTEN", "MULTIPLY", "SCREEN","ADD",
"CLEAR", "XOR", "OVERLAY",
};
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mSrcB = makeSrc(W, H);
mDstB = makeDst(W, H);
//创建黑白小块bitmap,用来绘制透明区域的网格示意
Bitmap bm = Bitmap.createBitmap(new int[]{0xFFFFFFFF, 0xFFCCCCCC,
0xFFCCCCCC, 0xFFFFFFFF}, 2, 2,
Bitmap.Config.RGB_565);
//通过Shader实现重复填充背景
mBG = new BitmapShader(bm,
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT);
Matrix m = new Matrix();
m.setScale(6, 6);
mBG.setLocalMatrix(m);
}
private Bitmap makeDst(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFFFFCC44);
c.drawOval(new RectF(0, 0, w * 3 / 4, h * 3 / 4), p);
return bm;
}
private Bitmap makeSrc(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFF66AAFF);
c.drawRect(w / 3, h / 3, w * 19 / 20, h * 19 / 20, p);
return bm;
}
@Override
protected void onDraw(Canvas canvas) {
//关闭硬件加速,否则部分mode无效果
setLayerType(LAYER_TYPE_SOFTWARE,null);
canvas.drawColor(Color.WHITE);
Paint labelP = new Paint(Paint.ANTI_ALIAS_FLAG);
labelP.setTextAlign(Paint.Align.CENTER);
Paint paint = new Paint();
canvas.translate(15, 35);
int x = 0;
int y = 0;
for (int i = 0; i < sModes.length; i++) {
paint.setStyle(Paint.Style.STROKE);
paint.setShader(null);
//绘制黑色边框
canvas.drawRect(x - 0.5f, y - 0.5f,
x + W + 0.5f, y + H + 0.5f, paint);
paint.setStyle(Paint.Style.FILL);
paint.setShader(mBG);
//填充透明度背景
canvas.drawRect(x, y, x + W, y + H, paint);
//保存画布状态
int sc = canvas.saveLayer(x, y, x + W, y + H, null,Canvas.ALL_SAVE_FLAG );
//平移画布
canvas.translate(x, y);
canvas.drawBitmap(mDstB, 0, 0, paint);
paint.setXfermode(sModes[i]);
canvas.drawBitmap(mSrcB, 0, 0, paint);
paint.setXfermode(null);
//将平移画布后绘制的内容恢复到保存的画布状态位置
canvas.restoreToCount(sc);
canvas.drawText(sLabels[i],
x + W / 2, y+H+20 - labelP.getTextSize() / 2, labelP);
x += W + 10;
if ((i % ROW_MAX) == ROW_MAX - 1) {
x = 0;
y += H + 50;
}
}
}
}
第一排:SRC相关
- SRC:只显示源图像
- SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】
- SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤
- SRC_ATOP:在源图像和目标图像相交的地方绘制【源图像】,在不相交的地方绘制【目标图像】,相交处的效果受到源图像和目标图像alpha的影响
- SRC_OVER:将源图像放在目标图像上方
第二排:DST相关
- DST:只显示目标图像
- DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响
- DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤
- DST_ATOP:在源图像和目标图像相交的地方绘制【目标图像】,在不相交的地方绘制【源图像】,相交处的效果受到源图像和目标图像alpha的影响
- DST_OVER:将目标图像放在源图像上方
第三排:混合效果相关
- DARKEN:变暗,较深的颜色覆盖较浅的颜色,若两者深浅程度相同则混合
- LIGHTEN:变亮,与DARKEN相反,DARKEN和LIGHTEN生成的图像结果与Android对颜色值深浅的定义有关
- MULTIPLY:正片叠底,源图像素颜色值乘以目标图像素颜色值除以255得到混合后图像像素颜色值
- SCREEN:滤色,色调均和,保留两个图层中较白的部分,较暗的部分被遮盖
- ADD:饱和相加,对图像饱和度进行相加
第四排:归类到第三排(混合效果相关)(囧)
- CLEAR:清除图像
- XOR:在源图像和目标图像相交的地方之外绘制它们,在相交的地方受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制
- OVERLAY:叠加
每个模式都对应着它自己的算法,比如同个坐标上src 和 dst 两个像素点,颜色值和透明度分别进行加减乘除运算,得到新的像素颜色值,具体算法见下表:
- S :SRC的饱和度
- D :DST的饱和度
- Sa :SRC的Alpha,同理有Da
- Sc :SRC的Color值,同理有Dc
可以看到,除了ADD、CLEAR,其它都与透明度有关,例如SRC与SRC的透明度有关,SRC_IN与SRC和DST的透明度都有关,一般都是透明即不过滤,这里要注意一下SRC和DST图层透明区域的影响,这个很重要,这个在Demo中去体会吧。
深入了解请戳:https://developer.android.com/reference/android/graphics/PorterDuff.Mode.html
理解了PorterDuff.Mode就能实现一些好玩的效果
示例一:轨迹截图
功能描述:使用手势轨迹,在一张图片上随意截取想要的部分
在path这篇中,我们已经讲过自由绘制路径的效果了,不记得的可以回看一下。
根据理解,我们这个功能应该是使用DST_IN模式,保留与SRC相交部分的内容,全部代码:
public class TestView extends View {
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private Path mPath;
private Paint mPaint;
private Bitmap mBitmap;
private int w,h;
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#ff0000"));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPath = new Path();
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.paint_49);
w=mBitmap.getWidth();
h=mBitmap.getHeight();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
if (userXfermode) {
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(makeSrc(),0,0, mPaint);
mPaint.setXfermode(null);
}else{
canvas.drawPath(mPath, mPaint);
}
}
//将mPath画在透明图层上,注意透明图层的大小应该和DST图层大小一致
private Bitmap makeSrc() {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFFFF0000);
p.setStyle(Paint.Style.FILL);
c.drawPath(mPath, p);
return bm;
}
private float startX, startY;
private boolean userXfermode;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
userXfermode = false;
mPath.reset();
startX = event.getX();
startY = event.getY();
mPath.moveTo(startX, startY);
break;
case MotionEvent.ACTION_MOVE:
mPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
startX = event.getX();
startY = event.getY();
invalidate();
break;
case MotionEvent.ACTION_UP:
userXfermode = true;
invalidate();
break;
}
return super.onTouchEvent(event);
}
}
这个例子就是应用了SRC透明部分内容对应的DST内容会被清除,并保留SRC有内容的部分,类似Ps中的蒙版
示例二:绘画板
上个例子中,我们用DST_IN实现保留于SRC相交的内容,相反的,DST_OUT就可以实现清除与SRC相交的内容了。
绘画板有两个基础功能,绘图和擦除
xml: 自定义View(TestView)作为画板,RadioGroup中两个RadioButton用来切换当前是绘图还是橡皮擦功能
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#222222"
android:orientation="vertical">
<cn.code.code.wiget.TestView
android:id="@+id/testView"
android:layout_width="match_parent"
android:layout_height="200dp" />
<RadioGroup
android:id="@+id/radioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/paintBtn"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="笔"
android:button="@null"
android:textColor="@color/draw_color"
android:background="#ffffff"
android:paddingTop="4dp"
android:checked="true"
android:gravity="center"
android:paddingBottom="4dp"
android:textSize="14sp"
/>
<RadioButton
android:id="@+id/clearBtn"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="橡皮擦"
android:button="@null"
android:textColor="@color/draw_color"
android:background="#ffffff"
android:gravity="center"
android:paddingTop="4dp"
android:layout_marginLeft="1dp"
android:paddingBottom="4dp"
android:textSize="14sp"
/>
</RadioGroup>
</LinearLayout>
Activity:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
RadioGroup radioGroup=findViewById(R.id.radioGroup);
final TestView testView=findViewById(R.id.testView);
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId){
case R.id.paintBtn:
testView.setCurrentStatus(0);
break;
case R.id.clearBtn:
testView.setCurrentStatus(1);
break;
}
}
});
}
TestView
public class TestView extends View {
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private Path srcPath, dstPath;
private Paint mPaint;
private int width, height;
private Bitmap mBitmap;
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(0xffff0000);
srcPath = new Path();
dstPath = new Path();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//演示效果,宽高都是精确值
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
}
@Override
protected void onDraw(Canvas canvas) {
//绘制白底
canvas.drawColor(0xffffffff);
//用mBitmap实例化新画布,这样新的画布上就能有历史内容
Canvas bitmapCanvas = new Canvas(mBitmap);
//绘制笔的轨迹作为dst
bitmapCanvas.drawBitmap(makeDst(), 0, 0, mPaint);
//设置为DST_OUT,与SRC相交的部分将清空
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
//绘制橡皮擦的轨迹作为SRC
bitmapCanvas.drawBitmap(makeSrc(), 0, 0, mPaint);
mPaint.setXfermode(null);
//将最新的内容更新到画布上
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
}
public Bitmap makeDst() {
Bitmap bm = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xff000000);
p.setStyle(Paint.Style.STROKE);
p.setStrokeCap(Paint.Cap.ROUND);
p.setStrokeWidth(5);
c.drawPath(dstPath, p);
return bm;
}
public Bitmap makeSrc() {
Bitmap bm = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xffff0000);
p.setStyle(Paint.Style.STROKE);
p.setStrokeCap(Paint.Cap.ROUND);
p.setStrokeWidth(30);
c.drawPath(srcPath, p);
return bm;
}
private float startX, startY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//每次按下都要重置轨迹,以免在已有内容区域重复绘制
dstPath.reset();
srcPath.reset();
startX = event.getX();
startY = event.getY();
if (currentStatus == 0) {
dstPath.moveTo(startX, startY);
} else {
srcPath.moveTo(startX, startY);
}
break;
case MotionEvent.ACTION_MOVE:
if (currentStatus == 0) {
dstPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
} else {
srcPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
}
startX = event.getX();
startY = event.getY();
invalidate();
break;
}
return true;
}
private int currentStatus;//0:绘图 1:橡皮擦
public void setCurrentStatus(int status) {
currentStatus = status;
}
}
示例三:图片上色动画(进度演示?)
在剪映APP导出视频的界面,会有一个视频转换进度的动画效果,我们用Xfermode就可以实现
功能描述:我们用一个去色后的图片作底,给它上色并实现动画效果
public class TestView extends View {
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private Paint mPaint;
private Bitmap mBitmap;
private int w,h;
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#ff0000"));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.paint_49);
w=mBitmap.getWidth();
h=mBitmap.getHeight();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
canvas.drawBitmap(makeSrc(),0,0, mPaint);
mPaint.setXfermode(null);
}
//在一个白底图层上画红绿渐变色,这里用白底而非透明或者黑底图层了,跟Ps模板一样,黑遮挡不要的内容,白显示需要的内容
private Bitmap makeSrc() {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565);
Canvas c = new Canvas(bm);
c.drawColor(0xffffffff);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
LinearGradient linearGradient=new LinearGradient(0,0,w,0,0xffff0000,0xff00ff00, Shader.TileMode.CLAMP);
p.setShader(linearGradient);
c.drawRect(0,0,currentX,h, p);
return bm;
}
private int currentX;
//在Activity中调用这个方法开始动画
public void startAnimator(){
ValueAnimator animator=ValueAnimator.ofInt(0,w);
animator.setDuration(4000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentX= (int) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
}
然后添加点击事件,启用动画
findViewById(R.id.testView).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TestView testView= (TestView) v;
testView.startAnimator();
}
});
Xfermode的脏区及解决办法
啥叫脏区?
我们通过一个小例子来解释下,比如,我们要实现下面这种刮刮卡效果
示例四:刮刮卡效果
//小知识:每次canvas.drawXXX(),都相当于新建了一个图层,并在新的图层上绘制,参考Ps
@Override
protected void onDraw(Canvas canvas) {
//绘制一个白底背景
canvas.drawColor(Color.WHITE);
//绘制文字图层
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText("你在找什么?",mDstB.getWidth()/2,mDstB.getHeight()/2,mPaint);
//绘制美女图片图层
canvas.drawBitmap(mDstB,0,0,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
//绘制手势轨迹图层
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,mPaint);
mPaint.setXfermode(null);
}
按照我们的预期,应该是可以实现刮刮卡效果的
但是:
美女图片擦除了可以理解,我们的白色底和文字呢?也一起被擦除了?(这个黄底是父View的底色)
这就是Xfermode的脏区了(我并不想擦除文字和白底这两个图层)
那我们可不可以在当前画布之上在加一个画布,在新的画布上实现刮刮卡的效果,这样就不会影响到文字层的画布了
(后面讲Canvas的时候会细讲)
int saveLayerId = canvas.saveLayer(left, top, right, bottom, mPaint);
......
canvas.restoreToCount(saveLayerId);
所以加上这两行代码,就可以实现刮刮卡效果了,全部代码:
public class TestView extends View {
private Bitmap mDstB;
private Paint mPaint;
private Path mPath;
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(20);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(30);
mPath=new Path();
mDstB = BitmapFactory.decodeResource(getResources(), R.mipmap.paint_48);
//关闭硬件加速,否则部分mode无效果
setLayerType(LAYER_TYPE_SOFTWARE,null);
}
//小知识:每次canvas.drawXXX(),都相当于新建了一个图层,并在新的图层上绘制,参考Ps
@Override
protected void onDraw(Canvas canvas) {
//绘制一个白底背景
canvas.drawColor(Color.WHITE);
//绘制文字图层
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText("你在找什么?",mDstB.getWidth()/2,mDstB.getHeight()/2,mPaint);
//创建一块新的画布区域
int saveLayerId = canvas.saveLayer(0, 0, mDstB.getWidth(), mDstB.getHeight(), mPaint);
//绘制美女图片图层
canvas.drawBitmap(mDstB,0,0,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
//绘制手势轨迹图层
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,mPaint);
mPaint.setXfermode(null);
//回到底层画布上继续其它内容绘制
canvas.restoreToCount(saveLayerId);
}
private float startX, startY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//每次按下都要重置轨迹,以免在已有内容区域重复绘制
startX = event.getX();
startY = event.getY();
mPath.moveTo(startX, startY);
break;
case MotionEvent.ACTION_MOVE:
mPath.quadTo(startX, startY, (startX + event.getX()) / 2f, (startY + event.getY()) / 2f);
startX = event.getX();
startY = event.getY();
invalidate();
break;
}
return true;
}
}