转载并改编自 https://www.jianshu.com/p/b243e2ba2462
PaletteView.java
package com.university.sailist.goodmanager.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Xfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.university.sailist.goodmanager.activity.MainActivity;
import java.util.ArrayList;
import java.util.List;
import afu.org.checkerframework.checker.nullness.qual.Nullable;
public class PaletteView extends View {
private Paint mPaint;
private Path mPath;
private float mLastX;
private float mLastY;
private Bitmap mBufferBitmap;
private Canvas mBufferCanvas;
private static final int MAX_CACHE_STEP = 20;
private List<DrawingInfo> mDrawingList;
private List<DrawingInfo> mRemovedList;
private Xfermode mXferModeClear;
private Xfermode mXferModeDraw;
private int mDrawSize;
private int mEraserSize;
private int mPenAlpha = 255;
private boolean mCanEraser;
private Callback mCallback;
public enum Mode {
DRAW,
ERASER
}
private Mode mMode = Mode.DRAW;
public PaletteView(Context context) {
super(context);
init();
}
public PaletteView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public PaletteView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public interface Callback {
void onUndoRedoStatusChanged();
}
public void setCallback(Callback callback) {
mCallback = callback;
}
/** dip转换成px */
public static int dip2px(float dipValue) {
float scale = MainActivity.appCompatActivity.getResources().getDisplayMetrics().scaledDensity;
return (int) (dipValue * scale + 0.5f);
}
private void init() {
setDrawingCacheEnabled(true);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setFilterBitmap(true);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mDrawSize = dip2px(3);
mEraserSize = dip2px(30);
mPaint.setStrokeWidth(mDrawSize);
mPaint.setColor(Color.BLACK);
mXferModeDraw = new PorterDuffXfermode(PorterDuff.Mode.SRC);
mXferModeClear = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
mPaint.setXfermode(mXferModeDraw);
}
private void initBuffer() {
mBufferBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
mBufferCanvas = new Canvas(mBufferBitmap);
}
private abstract static class DrawingInfo {
Paint paint;
abstract void draw(Canvas canvas);
}
private static class PathDrawingInfo extends DrawingInfo {
Path path;
@Override
void draw(Canvas canvas) {
canvas.drawPath(path, paint);
}
}
public Mode getMode() {
return mMode;
}
public void setMode(Mode mode) {
if (mode != mMode) {
mMode = mode;
if (mMode == Mode.DRAW) {
mPaint.setXfermode(mXferModeDraw);
mPaint.setStrokeWidth(mDrawSize);
} else {
mPaint.setXfermode(mXferModeClear);
mPaint.setStrokeWidth(mEraserSize);
}
}
}
public void setEraserSize(int size) {
mEraserSize = size;
}
public void setPenRawSize(int size) {
mDrawSize = size;
if (mMode == Mode.DRAW) {
mPaint.setStrokeWidth(mDrawSize);
}
}
public void setPenColor(int color) {
mPaint.setColor(color);
}
private void reDraw() {
if (mDrawingList != null) {
mBufferBitmap.eraseColor(Color.TRANSPARENT);
for (DrawingInfo drawingInfo : mDrawingList) {
drawingInfo.draw(mBufferCanvas);
}
invalidate();
}
}
public int getPenColor() {
return mPaint.getColor();
}
public int getPenSize() {
return mDrawSize;
}
public int getEraserSize() {
return mEraserSize;
}
public void setPenAlpha(int alpha) {
mPenAlpha = alpha;
if (mMode == Mode.DRAW) {
mPaint.setAlpha(alpha);
}
}
public int getPenAlpha() {
return mPenAlpha;
}
public boolean canRedo() {
return mRemovedList != null && mRemovedList.size() > 0;
}
public boolean canUndo() {
return mDrawingList != null && mDrawingList.size() > 0;
}
public void redo() {
int size = mRemovedList == null ? 0 : mRemovedList.size();
if (size > 0) {
DrawingInfo info = mRemovedList.remove(size - 1);
mDrawingList.add(info);
mCanEraser = true;
reDraw();
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
}
public void undo() {
int size = mDrawingList == null ? 0 : mDrawingList.size();
if (size > 0) {
DrawingInfo info = mDrawingList.remove(size - 1);
if (mRemovedList == null) {
mRemovedList = new ArrayList<>(MAX_CACHE_STEP);
}
if (size == 1) {
mCanEraser = false;
}
mRemovedList.add(info);
reDraw();
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
}
public void clear() {
if (mBufferBitmap != null) {
if (mDrawingList != null) {
mDrawingList.clear();
}
if (mRemovedList != null) {
mRemovedList.clear();
}
mCanEraser = false;
mBufferBitmap.eraseColor(Color.TRANSPARENT);
invalidate();
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
}
public Bitmap buildBitmap() {
Bitmap bm = getDrawingCache();
Bitmap result = Bitmap.createBitmap(bm);
destroyDrawingCache();
return result;
}
private void saveDrawingPath() {
if (mDrawingList == null) {
mDrawingList = new ArrayList<>(MAX_CACHE_STEP);
} else if (mDrawingList.size() == MAX_CACHE_STEP) {
mDrawingList.remove(0);
}
Path cachePath = new Path(mPath);
Paint cachePaint = new Paint(mPaint);
PathDrawingInfo info = new PathDrawingInfo();
info.path = cachePath;
info.paint = cachePaint;
mDrawingList.add(info);
mCanEraser = true;
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBufferBitmap != null) {
canvas.drawBitmap(mBufferBitmap, 0, 0, null);
}
}
@SuppressWarnings("all")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
final int action = event.getAction() & MotionEvent.ACTION_MASK;
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
if (mPath == null) {
mPath = new Path();
}
mPath.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
if (mMode == Mode.DRAW) {
//橡皮擦模式,绘图模式
resetDirtyRect(x, y);
int historySize = event.getHistorySize();
for (int i = 0; i < historySize; i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
getDirtyRect(historicalX, historicalY);
mPath.lineTo(historicalX, historicalY);
}
mPath.lineTo(x, y);
if (mBufferBitmap == null) {
initBuffer();
}
mBufferCanvas.drawPath(mPath, mPaint);
invalidate((int) (mDirtyRect.left - HALF_STROKE_WIDTH),
(int) (mDirtyRect.top - HALF_STROKE_WIDTH),
(int) (mDirtyRect.right + HALF_STROKE_WIDTH),
(int) (mDirtyRect.bottom + HALF_STROKE_WIDTH));
} else {
//橡皮擦模式
//这里终点设为两点的中心点的目的在于使绘制的曲线更平滑,如果终点直接设置为x,y,效果和lineto是一样的,实际是折线效果
mPath.quadTo(mLastX, mLastY, (x + mLastX) / 2, (y + mLastY) / 2);
if (mBufferBitmap == null) {
initBuffer();
}
if (mMode == Mode.ERASER && !mCanEraser) {
break;
}
mBufferCanvas.drawPath(mPath, mPaint);
invalidate();
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
if (mMode == Mode.DRAW || mCanEraser) {
saveDrawingPath();
}
mPath.reset();
break;
}
return true;
}
private void resetDirtyRect(float eventX, float eventY) {
mDirtyRect.left = Math.min(mLastX, eventX);
mDirtyRect.right = Math.max(mLastX, eventX);
mDirtyRect.top = Math.min(mLastY, eventY);
mDirtyRect.bottom = Math.max(mLastY, eventY);
}
private void getDirtyRect(float historicalX, float historicalY) {
if (historicalX < mDirtyRect.left) {
mDirtyRect.left = historicalX;
} else if (historicalX > mDirtyRect.right) {
mDirtyRect.right = historicalX;
}
if (historicalY < mDirtyRect.top) {
mDirtyRect.top = historicalY;
} else if (historicalY > mDirtyRect.bottom) {
mDirtyRect.bottom = historicalY;
}
}
private static final float STROKE_WIDTH = 5f;
private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;
private final RectF mDirtyRect = new RectF();
}
使用
- 在布局文件中添加即可直接实现绘制
- 使用
clear()
方法清空 setPenColor()/setPenAlpha()/setMode(绘制/增添/擦除...)/setPenRawSize()
buildBitmap()