Android 实现一个简单的画板功能
程序员文章站
2022-05-31 11:51:32
...
一.效果图:
二.快速实现:
a.支持撤销(undo)
b.支持反撤销(redo)
c.支持橡皮擦(eraser)
d.支持清除功能(clear)
e.支持保存为图像(save)
1.主函数代码:
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class MainActivity extends AppCompatActivity implements View.OnClickListener, PaletteView.Callback,Handler.Callback {
private View mUndoView;
private View mRedoView;
private View mPenView;
private View mEraserView;
private View mClearView;
private PaletteView mPaletteView;
private ProgressDialog mSaveProgressDlg;
private static final int MSG_SAVE_SUCCESS = 1;
private static final int MSG_SAVE_FAILED = 2;
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPaletteView = (PaletteView) findViewById(R.id.palette);
mPaletteView.setCallback(this);
mUndoView = findViewById(R.id.undo);
mRedoView = findViewById(R.id.redo);
mPenView = findViewById(R.id.pen);
mPenView.setSelected(true);
mEraserView = findViewById(R.id.eraser);
mClearView = findViewById(R.id.clear);
mUndoView.setOnClickListener(this);
mRedoView.setOnClickListener(this);
mPenView.setOnClickListener(this);
mEraserView.setOnClickListener(this);
mClearView.setOnClickListener(this);
mUndoView.setEnabled(false);
mRedoView.setEnabled(false);
mHandler = new Handler(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeMessages(MSG_SAVE_FAILED);
mHandler.removeMessages(MSG_SAVE_SUCCESS);
}
private void initSaveProgressDlg(){
mSaveProgressDlg = new ProgressDialog(this);
mSaveProgressDlg.setMessage("正在保存,请稍候...");
mSaveProgressDlg.setCancelable(false);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
return true;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what){
case MSG_SAVE_FAILED:
mSaveProgressDlg.dismiss();
Toast.makeText(this,"保存失败",Toast.LENGTH_SHORT).show();
break;
case MSG_SAVE_SUCCESS:
mSaveProgressDlg.dismiss();
Toast.makeText(this,"画板已保存",Toast.LENGTH_SHORT).show();
break;
}
return true;
}
private static void scanFile(Context context, String filePath) {
Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
scanIntent.setData(Uri.fromFile(new File(filePath)));
context.sendBroadcast(scanIntent);
}
private static String saveImage(Bitmap bmp, int quality) {
if (bmp == null) {
return null;
}
File appDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
if (appDir == null) {
return null;
}
String fileName = System.currentTimeMillis() + ".jpg";
File file = new File(appDir, fileName);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
bmp.compress(Bitmap.CompressFormat.JPEG, quality, fos);
fos.flush();
return file.getAbsolutePath();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.save:
if(mSaveProgressDlg==null){
initSaveProgressDlg();
}
mSaveProgressDlg.show();
new Thread(new Runnable() {
@Override
public void run() {
Bitmap bm = mPaletteView.buildBitmap();
String savedFile = saveImage(bm, 100);
if (savedFile != null) {
scanFile(MainActivity.this, savedFile);
mHandler.obtainMessage(MSG_SAVE_SUCCESS).sendToTarget();
}else{
mHandler.obtainMessage(MSG_SAVE_FAILED).sendToTarget();
}
}
}).start();
break;
}
return true;
}
@Override
public void onUndoRedoStatusChanged() {
mUndoView.setEnabled(mPaletteView.canUndo());
mRedoView.setEnabled(mPaletteView.canRedo());
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.undo:
mPaletteView.undo();
break;
case R.id.redo:
mPaletteView.redo();
break;
case R.id.pen:
v.setSelected(true);
mEraserView.setSelected(false);
mPaletteView.setMode(PaletteView.Mode.DRAW);
break;
case R.id.eraser:
v.setSelected(true);
mPenView.setSelected(false);
mPaletteView.setMode(PaletteView.Mode.ERASER);
break;
case R.id.clear:
mPaletteView.clear();
break;
}
}
}
2.主要的自定义代码:
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.Xfermode;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义画板
*/
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;
}
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 = DimenUtils.dp2pxInt(3);
mEraserSize = DimenUtils.dp2pxInt(30);
mPaint.setStrokeWidth(mDrawSize);
// mPaint.setColor(0XFF000000);//设置画笔的颜色
mPaint.setColor(Color.BLUE);
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:
//这里终点设为两点的中心点的目的在于使绘制的曲线更平滑,如果终点直接设置为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;
}
}
3.布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<com.beyondsw.palette.PaletteView
android:id="@+id/palette"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@drawable/bg"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center"
>
<ImageView
android:id="@+id/undo"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerInside"
android:src="@drawable/undo"
android:background="@drawable/toolbar_item_bg"
/>
<ImageView
android:id="@+id/redo"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerInside"
android:src="@drawable/redo"
android:background="@drawable/toolbar_item_bg"
/>
<ImageView
android:id="@+id/pen"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:layout_weight="1"
android:src="@drawable/pen"
android:background="@drawable/toolbar_item_bg"
/>
<ImageView
android:id="@+id/eraser"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:layout_weight="1"
android:src="@drawable/eraser"
android:background="@drawable/toolbar_item_bg"
/>
<ImageView
android:id="@+id/clear"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerInside"
android:src="@drawable/clear"
android:background="@drawable/toolbar_item_bg"
/>
</LinearLayout>
</LinearLayout>
4.按钮背景状态:(从左到右)
第一个按钮和第二个按钮背景一样
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/toolbar_bg"/>
<item android:state_selected="true" android:drawable="@drawable/toolbar_bg"/>
<item android:drawable="@android:color/transparent"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/toolbar_bg"/>
<item android:state_selected="true" android:drawable="@drawable/toolbar_bg"/>
<item android:drawable="@android:color/transparent"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/toolbar_bg"/>
<item android:state_selected="true" android:drawable="@drawable/toolbar_bg"/>
<item android:drawable="@android:color/transparent"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="@drawable/toolbar_bg"/>
<item android:state_selected="true" android:drawable="@drawable/toolbar_bg"/>
<item android:drawable="@android:color/transparent"/>
</selector>
5.工具类:
package com.beyondsw.palette;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.util.TypedValue;
/**
* 工具类转换
*/
public class DimenUtils {
private static final Resources sResource = Resources.getSystem();
public static float dp2px(float dp) {
DisplayMetrics dm = sResource.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, dm);
}
public static int dp2pxInt(float dp) {
return (int) dp2px( dp);
}
public static float sp2px(float sp) {
DisplayMetrics dm = sResource.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, sp, dm);
}
public static int sp2pxInt(float sp) {
return (int) sp2px(sp);
}
}
上一篇: Android中实现一个简单的逐帧动画(附代码下载)
下一篇: 3.QML布局和输入元素