项目中想要实现一个简易画图板的需求,功能并不复杂,就是6个很常用的功能
陈小默同学有一个比较复杂,强大,高效的CrazyPalette,同学间商业互吹下,哈哈。里面基本常用的操作都有,代码写的很好,只是用的Kotlin
,不过我需要的只是一个简单的绘图板,我参考了他的一些思路以及另外一篇android项目 之 记事本 ----- 画板功能之撤销、恢复和清空,做了一个简单的PaintView
1. PaintView
之前在网上看到别的博客说写的双缓冲是这个思路,这里感觉有错误,不清楚我写的这种方式算不算双缓冲。等过了这段加班,我再查查问问确认下 20170524 21:13
思路:使用双缓冲思路,有一个mBitmap
,来记录最终的绘制。在手指滑动过程中,屏幕上会实时显示出手指滑动时的绘制轨迹。当手指离开屏幕后,显示最终存有内容的mBitmap
- 撤销和恢复利用
LinkedList
来模拟两个储存记录的栈
- 清空,这里偷懒,直接绘制白色,将之前绘制的内容盖住。也可以考虑使用
new PorterDuffXfermode(PorterDuff.Mode.CLEAR)
。但有些时候,个人感觉这种方式会出现些莫名其妙的情况,能直接绘制成单一纯色,就不使用PorterDuffXfermode
- 橡皮擦,这里使用了
PorterDuffXfermode
,并setBackgroundColor(Color.WHITE)
以及把硬件加速关闭了
关于橡皮擦得额外说明下:
橡皮擦使用new PorterDuffXfermode(PorterDuff.Mode.CLEAR)
是为了复习下PorterDuffXfermode
,踩踩坑
这里有两个坑,硬件加速和背景穿透。当使用PorterDuff.Mode.CLEAR
时,利用的是把上次绘制的东西给清除掉,这就导致在保存绘制的图片后,橡皮擦轨迹是透明的,而之前绘制的内容又被擦除了,就会把图片下方的当前系统背景色显示出来
当我在电脑打开保存的图片时,橡皮擦轨迹会透出我电脑桌面背景的颜色。在手机打开就会透出手机背景颜色
解决办法:
在init()
方法中,setBackgroundColor(Color.WHITE)
,绘制了一个白色背景,但这样也就导致了过度绘制
根据需求,这里更好的思路就是把橡皮擦的颜色也直接设置成白色,更加简单而且没有PorterDuffXfermode
的坑。但既然是练习,就踩踩坑
代码:
public class PaintView extends View {
private Paint mPaint;
private Path mPath;
private Path eraserPath;
private Paint eraserPaint;
private Canvas mCanvas;
private Bitmap mBitmap;
private float mLastX, mLastY;//上次的坐标
private Paint mBitmapPaint;
//使用LinkedList 模拟栈,来保存 Path
private LinkedList<PathBean> undoList;
private LinkedList<PathBean> redoList;
private boolean isEraserModel;
public PaintView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/***
* 初始化
*/
private void init() {
//关闭硬件加速
//否则橡皮擦模式下,设置的 PorterDuff.Mode.CLEAR ,实时绘制的轨迹是黑色
setBackgroundColor(Color.WHITE);//设置白色背景
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
//画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStrokeWidth(4f);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);//使画笔更加圆润
mPaint.setStrokeCap(Paint.Cap.ROUND);//同上
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
//保存签名的画布
post(new Runnable() {//拿到控件的宽和高
@Override
public void run() {
//获取PaintView的宽和高
//由于橡皮擦使用的是 Color.TRANSPARENT ,不能使用RGB-565
mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
mCanvas = new Canvas(mBitmap);
//抗锯齿
mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
//背景色
mCanvas.drawColor(Color.WHITE);
}
});
undoList = new LinkedList<>();
redoList = new LinkedList<>();
}
/**
* 绘制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBitmap != null) {
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);//将mBitmap绘制在canvas上,最终的显示
if (!isEraserModel) {
if (null != mPath) {//显示实时正在绘制的path轨迹
canvas.drawPath(mPath, mPaint);
}
} else {
if (null != eraserPath) {
canvas.drawPath(eraserPath, eraserPaint);
}
}
}
}
/**
* 撤销操作
*/
public void undo() {
if (!undoList.isEmpty()) {
clearPaint();//清除之前绘制内容
PathBean lastPb = undoList.removeLast();//将最后一个移除
redoList.add(lastPb);//加入 恢复操作
//遍历,将Path重新绘制到 mCanvas
for (PathBean pb : undoList) {
mCanvas.drawPath(pb.path, pb.paint);
}
invalidate();
}
}
/**
* 恢复操作
*/
public void redo() {
if (!redoList.isEmpty()) {
PathBean pathBean = redoList.removeLast();
mCanvas.drawPath(pathBean.path, pathBean.paint);
invalidate();
undoList.add(pathBean);
}
}
/**
* 设置画笔颜色
*/
public void setPaintColor(@ColorInt int color) {
mPaint.setColor(color);
}
/**
* 清空,包括撤销和恢复操作列表
*/
public void clearAll() {
clearPaint();
mLastY = 0f;
//清空 撤销 ,恢复 操作列表
redoList.clear();
undoList.clear();
}
/**
* 设置橡皮擦模式
*/
public void setEraserModel(boolean isEraserModel) {
this.isEraserModel = isEraserModel;
if (eraserPaint == null) {
eraserPaint = new Paint(mPaint);
eraserPaint.setStrokeWidth(15f);
eraserPaint.setColor(Color.TRANSPARENT);
eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
}
/**
* 保存到指定的文件夹中
*/
public boolean saveImg(String filePath, String imgName) {
boolean isCanSave = mBitmap != null && mLastY != 0f && !undoList.isEmpty();
if (isCanSave) {//空白板时,就不保存
//保存图片
File file = new File(filePath + File.separator + imgName);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
if (mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)) {
fileOutputStream.flush();
return true;
}
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
closeStream(fileOutputStream);
}
}
return false;
}
/**
* 是否可以撤销
*/
public boolean isCanUndo() {
return undoList.isEmpty();
}
/**
* 是否可以恢复
*/
public boolean isCanRedo() {
return redoList.isEmpty();
}
/**
* 清除绘制内容
* 直接绘制白色背景
*/
private void clearPaint() {
mCanvas.drawColor(Color.WHITE);
invalidate();
}
/**
* 触摸事件 触摸绘制
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEraserModel) {
commonTouchEvent(event);
} else {
eraserTouchEvent(event);
}
invalidate();
return true;
}
/**
* 橡皮擦事件
*/
private void eraserTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
//路径
eraserPath = new Path();
mLastX = x;
mLastY = y;
eraserPath.moveTo(mLastX, mLastY);
break;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(x - mLastX);
float dy = Math.abs(y - mLastY);
if (dx >= 3 || dy >= 3) {//绘制的最小距离 3px
eraserPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2);
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mCanvas.drawPath(eraserPath, eraserPaint);//将路径绘制在mBitmap上
eraserPath.reset();
eraserPath = null;
break;
}
}
/**
* 普通画笔事件
*/
private void commonTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
//路径
mPath = new Path();
mLastX = x;
mLastY = y;
mPath.moveTo(mLastX, mLastY);
break;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(x - mLastX);
float dy = Math.abs(y - mLastY);
if (dx >= 3 || dy >= 3) {//绘制的最小距离 3px
//利用二阶贝塞尔曲线,使绘制路径更加圆滑
mPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2);
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mCanvas.drawPath(mPath, mPaint);//将路径绘制在mBitmap上
Path path = new Path(mPath);//复制出一份mPath
Paint paint = new Paint(mPaint);
PathBean pb = new PathBean(path, paint);
undoList.add(pb);//将路径对象存入集合
mPath.reset();
mPath = null;
break;
}
}
/**
* 关闭流
*/
private void closeStream(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 测量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, 200);
}
}
/**
* 路径对象
*/
class PathBean {
Path path;
Paint paint;
PathBean(Path path, Paint paint) {
this.path = path;
this.paint = paint;
}
}
}
代码中,重要地方都有注释
2. Activity
布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="15dp"
android:layout_weight="1"
app:cardElevation="4dp"
app:cardUseCompatPadding="true">
<com.example.gcc.okhttpl.richeditor.PaintView
android:id="@+id/activity_paint_pv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v7.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/activity_paint_undo"
style="@style/paint_menu_text_view_style"
android:text="撤销" />
<TextView
android:id="@+id/activity_paint_redo"
style="@style/paint_menu_text_view_style"
android:text="恢复" />
<TextView
android:id="@+id/activity_paint_color"
style="@style/paint_menu_text_view_style"
android:text="红色" />
<TextView
android:id="@+id/activity_paint_clear"
style="@style/paint_menu_text_view_style"
android:text="清空" />
<TextView
android:id="@+id/activity_paint_eraser"
style="@style/paint_menu_text_view_style"
android:text="橡皮擦" />
<TextView
android:id="@+id/activity_paint_save"
style="@style/paint_menu_text_view_style"
android:text="保存" />
</LinearLayout>
</LinearLayout>
Activity代码
public class PaintViewActivity extends AppCompatActivity {
private PaintView paintView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_paint_view);
initView();
initMenu();
}
/**
* 初始化
*/
private void initView() {
paintView = (PaintView) findViewById(R.id.activity_paint_pv);
}
/**
* 初始化底部菜单
*/
private void initMenu() {
//撤销
menuItemSelected(R.id.activity_paint_undo, new MenuSelectedListener() {
@Override
public void onMenuSelected() {
ToastUtils.show(PaintViewActivity.this, "撤销");
paintView.undo();
}
});
//恢复
menuItemSelected(R.id.activity_paint_redo, new MenuSelectedListener() {
@Override
public void onMenuSelected() {
ToastUtils.show(PaintViewActivity.this, "恢复");
paintView.redo();
}
});
//颜色
menuItemSelected(R.id.activity_paint_color, new MenuSelectedListener() {
@Override
public void onMenuSelected() {
ToastUtils.show(PaintViewActivity.this, "红色");
paintView.setPaintColor(Color.RED);
}
});
//清空
menuItemSelected(R.id.activity_paint_clear, new MenuSelectedListener() {
@Override
public void onMenuSelected() {
ToastUtils.show(PaintViewActivity.this, "清空");
paintView.clearAll();
}
});
//橡皮擦
menuItemSelected(R.id.activity_paint_eraser, new MenuSelectedListener() {
@Override
public void onMenuSelected() {
ToastUtils.show(PaintViewActivity.this, "橡皮擦");
paintView.setEraserModel(true);
}
});
//保存
menuItemSelected(R.id.activity_paint_save, new MenuSelectedListener() {
@Override
public void onMenuSelected() {
String path = Environment.getExternalStorageDirectory().getPath()
+ File.separator + Strings.FILE_PATH + File.separator + Strings.CACHE_PATH;
String imgName = "paint.jpg";
if (paintView.saveImg(path,imgName)) {
ToastUtils.show(PaintViewActivity.this, "保存成功");
}
}
});
}
/**
* 选中底部 Menu 菜单项
*/
private void menuItemSelected(int viewId, final MenuSelectedListener listener) {
findViewById(viewId).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onMenuSelected();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
ToastUtils.cancel();
}
interface MenuSelectedListener {
void onMenuSelected();
}
}
这是橡皮擦使用PorterDuffXfermode
踩坑思路
2.1 橡皮擦直接绘制白色背景思路
简单修改PaintView
代码:
1. 首先把硬件加速打开,也就是把init()方法里,下面行代码注释掉:
//setBackgroundColor(Color.WHITE);
//setLayerType(View.LAYER_TYPE_SOFTWARE, null);
2.修改橡皮擦颜色
eraserPaint.setColor(Color.WHITE);
//eraserPaint.setColor(Color.TRANSPARENT);
//eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
这种思路即不会导致过度绘制,也不会有硬件加速的坑,但前提是绘图板背景颜色是纯色的
3. 最后
即使在使用过渡绘制思路的情况下,暂时感觉效率也可以,在低端机上也没有明显的卡顿感,绘制轨迹蛮跟手的。个人感觉这种绘图板并不需要SurfaceView
有错误,请指出
共勉 : )