一、背景
如果有这样一个需求,需要擦除的是素材而保留背景图片,并且保存的时候能将背景图片和素材重新绘制到一起生成一张新的图片,这张图片大小比例就和原图一样,只不过上面多了素材。
之前在网上找过橡皮檫的实现,大多数是以整张view作为画板,但是这个需求情况下就不能使用整张view作为画板了,不然保存的时候还要裁剪出素材,一不小心还把素材大小什么的搞乱了,这时候就需要另一种办法了
二、实现
先重写view的手势监听事件
@Override
public boolean onTouchEvent(MotionEvent event) {
if (materialEntities.size() < 1) {
return;
}
float x =event.getX();
float y =event.getY();
MaterialEntity materialEntity = materialEntities.get(0);
x -= materialEntity.mBorderDstPoint[4];
y -= materialEntity.mBorderDstPoint[5];
float[]disPoint = new float[2];
mMatrix.mapPoints(disPoint,new float[]{x, y});
x = disPoint[0];
y = disPoint[1];
switch (event.getAction()& MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mPaths.add(new Path());
mPath = mPaths.getLast();
mLastX = x;
mLastY = y;
mPath.moveTo(mLastX, mLastY);
break;
case MotionEvent.ACTION_MOVE:
mPath.quadTo(mLastX, mLastY, x, y);//quardTo适合画曲线,lineTo适合直线
mLastX = x;
mLastY = y;
drawPath();
break;
}
}
可以看到监听手势滑动时启动橡皮檫功能,上面我们说到橡皮檫要针对素材而不是整个view,这边就体现在mMatrix这个变量里面了,我们先来看一下这个变量的定义:
public void setMode(int mode) {
mMode = mode;
if (mMode != MODE_NORMAL) {
setEraserListener(this);
mMatrix.reset();
int size = materialEntities.size();
MaterialEntity materialEntity = size > 0 ? materialEntities.get(size - 1) : null;
if (materialEntity!= null) {
float scale = 1.0f / materialEntity.mImageScale;
mMatrix.postScale(scale, scale);
mMatrix.postRotate(360 - materialEntity.mImageDegree);
}
invalidate();
} else {
setEraserListener(null);
}
}
可以看到这个矩阵的作用其实就是素材矩阵的逆变换,简单来说,比如素材被放大了两倍,然后你的手指指向了素材的中间,这时候我们怎么计算点击的位置相对素材的坐标呢,以下两个步骤:
1) 先把onTouchEvent获取的坐标点减去素材左上角坐标,求出触点相对素材左上角的向量
2) 把这个向量实行素材矩阵的逆变换,转换成以素材左上角为原点的坐标
有了坐标后就简单了,这时候就和以整张view作为画板的操作一样了,继续执行以下几个步骤:
1) 创建一个新的Canvas,并且以素材大小作为画板
2) 开始在这个新的Canvas上面画轨迹:
private void init() {
mPaths = new LinkedBlockingDeque<>();
mMatrix = new Matrix();
mPaint = new Paint();
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
mEraserPaint = new Paint();
mEraserPaint.setAntiAlias(true);
mEraserPaint.setDither(true);
mEraserPaint.setStyle(Paint.Style.STROKE);
mEraserPaint.setStrokeJoin(Paint.Join.ROUND);
mEraserPaint.setStrokeWidth(30);
mRecoverPaint = new Paint();
mRecoverPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
mRecoverPaint.setAntiAlias(true);
mRecoverPaint.setDither(true);
mRecoverPaint.setStyle(Paint.Style.STROKE);
mRecoverPaint.setStrokeJoin(Paint.Join.ROUND);
mRecoverPaint.setStrokeWidth(30);
}
private void drawPath() {
if (mMaskBitmap == null) {
Bitmap dragImage = materailEntities.get(materailEntities.size() - 1).mImage;
mMaskBitmap = Bitmap.createBitmap(dragImage.getWidth(), dragImage.getHeight(), dragImage.getConfig());
mMaskCanvas = new Canvas(mMaskBitmap);
}
if (mMode == MODE_ERASER) {
mMaskCanvas.drawPath(mPath, mEraserPaint);
} else if (mMode == MODE_RECOVER) {
mMaskCanvas.drawPath(mPath, mRecoverPaint);
}
if (mPathListener != null) {
mPathListener.onDrawPath();
}
invalidate();
}
最终橡皮檫的实现就是用的图像混合的理念了,这边用的是PorterDuff.Mode.DST_OUT,即所画到的地方目标图片透明度变为0
这边你可能看到奇怪的地方,那个mMaskBitmap是干嘛用的呢,这个作用可大了,因为我们不仅要能擦除素材,还要能恢复擦除的部分,但是像drawPath这种是画下去就直接改变图片了,想要恢复是不可能了,那怎么办呢,就要用到mMaskBitmap了,原理如下:
1) 生成一张素材的蒙层,所有橡皮檫的轨迹用画笔画在上面,想要恢复的话用另外一个画笔在蒙层里面擦除橡皮檫画笔的轨迹
2) 应用效果时候,先画上素材原图,然后画上蒙层,蒙层使用的画笔还是PorterDuff.Mode.DST_OUT,这样的话蒙层上面橡皮檫的轨迹就会把素材原图上面对应的部位的透明度变为0,即达到了擦除素材的目的:
private void initCanvas() {
mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
mClearPaint = new Paint();
mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Canvas tempCanvas;
tempCanvas = mCanvas;
tempCanvas.drawPaint(mClearPaint);
tempCanvas.save();
if (drawFilter == null) {
drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
}
tempCanvas.setDrawFilter(drawFilter);
int i = 0;
int totalDragImageCount = materailEntities.size();
for (MaterailEntity materialEntity : materailEntities) {
i++;
final int saveCount = tempCanvas.save();
tempCanvas.rotate(materialEntity.mImageShowDegree, materialEntity.centerX,materialEntity.centerY);
tempCanvas.drawBitmap(materialEntity.mImage, materialEntity.mImageMatrix, mImagePaint);
if (mMaskBitmap != null) {
tempCanvas.drawBitmap(mMaskBitmap, materialEntity.mImageMatrix, mPaint);
}
if (mCanvas != null) {
canvas.drawBitmap(mBitmap, 0, 0, null);
tempCanvas.restore();
}
}
可以看到onDraw的时候先创建一张新的画布,然后在这张画布上面绘制好素材原图后再绘制素材的蒙层,最后把这张新的画布生成的图片绘制到屏幕上就实现了我们擦除素材的目的。
注意的是mImageMatrix里面只有缩放和平移,不包括旋转,所以绘制的时候要将画布先旋转,然后再画上素材
最后我们怎么保存呢:
public Bitmap saveDragImage(int position, float scale, float degree) {
MaterialEntity materailEntity =
materialEntities.size() > 0 && position < materialEntities.size() ? materialEntities.get(position) : null;
if (materailEntity == null || (materailEntity.mImage == null)) {
return null;
}
Bitmap dragBitmap;
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
matrix.postRotate(degree);
dragBitmap = Bitmap.createBitmap(dragImageEntity.mImage.getWidth(), dragImageEntity.mImage.getHeight(), dragImageEntity.mImage.getConfig());
dragBitmap = Bitmap.createBitmap(dragBitmap, 0, 0, materailEntity.mImage.getWidth(),
materailEntity.mImage.getHeight(), matrix, true);
if (!dragBitmap.isMutable()) {
dragBitmap = dragBitmap.copy(dragBitmap.getConfig(), true);
}
Canvas canvas = new Canvas(dragBitmap);
matrix.reset();
matrix.postTranslate(dragBitmap.getWidth() / 2 - dragImageEntity.mImage.getWidth() / 2.0f,
dragBitmap.getHeight() / 2 - dragImageEntity.mImage.getHeight() / 2.0f);
matrix.postScale(scale, scale, dragBitmap.getWidth() / 2, dragBitmap.getHeight() / 2);
matrix.postRotate(degree, dragBitmap.getWidth() / 2, dragBitmap.getHeight() / 2);
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
canvas.drawBitmap(dragImageEntity.mDragImage, matrix, mDragImagePaint);
if (mMaskBitmap != null) {
canvas.drawBitmap(mMaskBitmap, matrix, mPaint);
}
return dragBitmap;
}
其实就是和onDraw类似,重新绘制一遍素材生成一张素材效果图,然后把这张图画到原图上面,最后就是处理好的图片了