android 涂鸦上帝闪电合成表 撤销功能如何实现

是男儿总要走向远方,走向远方是为了让生命更辉煌
android图片涂鸦,具有设置画笔,撤销,缩放移动等功能(一)
之前做过图片涂鸦的功能,前段时间很多人问我要源码,但因为当时技术水平有限,功能虽然实现了,但代码却不利于阅读,实现原理也过于“新手”,于是为了方便他人学习参考,本人在工作之余,经过漫长过程,终于把原有的代码重构,使用更加简洁明了的原理,实现图片涂鸦功能。实现效果如下:
主要功能如下:
设置画笔及形状
画笔可以选择手绘、仿制、橡皮擦、文字,其中仿制功能跟PS中的类似,复制图片中的某处地方。形状可以选择手绘、箭头、直线、圆、矩形等。画笔的底色可以选择颜色,或者选择一张画布。
撤销及清屏
每一步的操作都可以撤销,清屏时将清除所有的操作。
放缩、移动及旋转
在涂鸦的过程中,可以自由地通过手势缩放和移动图片,同时在多次缩放后,可快速居中图片;支持旋转图片。
源码在Github上的地址为: , 欢迎大家反馈问题,我会及时在上面更新代码,谢谢支持。
这篇文章主要是介绍图片涂鸦的功能,接下来我会抽空把实现的原理梳理出来,方便大家理解。基础好的同学也可以先自行阅读代码,我已经写好了注释。
没有更多推荐了,是男儿总要走向远方,走向远方是为了让生命更辉煌
android图片涂鸦,具有设置画笔,撤销,缩放移动等功能(二)
上一篇文章,讲了涂鸦的主要功能,还没看过的同学建议去看一遍,方便接下来的分析。现在,为大家讲解涂鸦功能的实现原理。
放缩与移动
当我们进入涂鸦界面后,发现图片刚好适应屏幕居中,这是因为进行了处理,下面三个变量记录此时图片的位置信息,当确定这三个值后,不会再改变,除非显示区域大小发生变化。
private float mPrivateS // 图片适应屏幕时的缩放倍数
private int mPrivateHeight, mPrivateW// 图片适应屏幕时的大小(View窗口坐标系上的大小)
private float mCentreTranX, mCentreTranY;// 图片在适应屏幕时,位于居中位置的偏移(View窗口坐标系上的偏移)
之后我们对图片进行的缩放与移动操作,都是相对于这个状态下的图片,位置信息记录下面两个变量中。
private float mScale = 1; // 在适应屏幕时的缩放基础上的缩放倍数 ( 图片真实的缩放倍数为 mPrivateScale*mScale )
private float mTransX = 0, mTransY = 0; // 图片在适应屏幕且处于居中位置的基础上的偏移量( 图片真实偏移量为mCentreTranX + mTransX,View窗口坐标系上的偏移)
会发现,当缩放移动图片时,相应的涂鸦操作也会被缩放移动,这里是通过缩放移动GraffitiVIew的画布,来实现整体的变化,而不用分别单独对图片和涂鸦进行变化操作。
private void doDraw(Canvas canvas) {
float left = mCentreTranX + mTransX;
float top = mCentreTranY + mTransY;
// 画布和图片共用一个坐标系,只需要处理屏幕坐标系到图片(画布)坐标系的映射关系
canvas.translate(left, top); // 偏移画布
canvas.scale(mPrivateScale * mScale, mPrivateScale * mScale); // 缩放画布
(不熟悉矩阵变换的同学可以查看我的另一篇文章)
经过多次优化后,这里选择画布和图片共用一个坐标系,了解图片的位置信息后,最后需要处理的就是,屏幕坐标系与图片(画布)坐标系的映射,即把屏幕上滑动的轨迹投射到图片中。当手指在屏幕上滑动做绘图操作,此时看到的绘图轨迹其实是画在GraffitiView的画布上的,所以能看到画笔的实时变化,而图片上暂未被改动,即还未真正在图片上涂鸦。当结束此次绘图(松开手指)时,才把真实的绘图操作画在图片上。
从上图的分析中,我们可以得出如下映射关系:
图片坐标x=(屏幕坐标x-图片在x轴上的偏移量)/图片缩放倍数
图片坐标y=(屏幕坐标y-图片在y轴上的偏移量)/图片缩放倍数
* 将屏幕触摸坐标x转换成在图片中的坐标
public final float toX(float touchX) {
return (touchX - mCentreTranX - mTransX) / (mPrivateScale * mScale);
* 将屏幕触摸坐标y转换成在图片中的坐标
public final float toY(float touchY) {
return (touchY - mCentreTranY - mTransY) / (mPrivateScale * mScale);
可见,屏幕坐标投射到图片上时,需要减去偏移量,因为图片的位置是一直不变的,我们对图片进行偏移,其实是对GraffitView的画布进行偏移。
设置画笔及形状通过Paint类充当画笔,根据不同选择来设置画笔属性,当画笔的底色为颜色值时,通过Paint.setColor(int)设置颜色,当画笔的底色为一张图片时,则通过Paint.setShader(BitmapShader)把图片设置为画笔底色。代码中我定义了一个类GraffitiColor表示画笔底色,定义如下:
public static class GraffitiColor {
// 底色类型
public enum Type {
COLOR, // 颜色值
BITMAP // 图片
private int mC // 颜色值
private Bitmap mB // 图片
private Type mT
private Shader.TileMode mTileX = Shader.TileMode.MIRROR;
private Shader.TileMode mTileY = Shader.TileMode.MIRROR;
}同理,当画笔为仿制(COPY)和橡皮擦(ERASER)时,通过Paint.setShader(BitmapShader)直接把画笔底色设置为当前涂鸦的图片,即可实现擦除的效果,只不过当为仿制时,需要对图片进行偏移处理,相关信息记录在CopyLocation类中:
private class CopyLocation {
private float mCopyStartX, mCopyStartY; // 仿制的坐标
private float mTouchStartX, mTouchStartY; // 开始触摸的坐标
private float mX, mY; // 当前位置
private Paint mP
private boolean isRelocating = // 正在定位中
private boolean isCopying = // 正在仿制绘图中
}每当偏移位置发生改变时,重新设置需要仿制的图片的偏移位置,可通过下面的方法设置:
ShaderMatrix.postTranslate(float, float);
BitmapShader.setLocalMatrix(ShaderMatrix); 
由上图的分析,绿色虚线为仿制图片的偏移量,我们可以得出图片X轴上偏移的位置(图中的黑色虚线)为
CopyLocation.mTouchStartX - CopyLocation.mCopyStartX ,
Y轴上偏移的位置(图中的黑色虚线长度)为
CopyLocation.mTouchStartY - CopyLocation.mCopyStartY 。
每一次的涂鸦操作(从手指点击到抬起)都会记录在GraffitiPath类中:
private static class GraffitiPath {
Pen mP // 画笔类型
Shape mS // 画笔形状
float mStrokeW // 大小
GraffitiColor mC // 颜色
Path mP // 画笔的路径
float mSx, mSy; // 映射后的起始坐标,(手指点击)
float mDx, mDy; // 映射后的终止坐标,(手指抬起)
Matrix mM // 仿制图片的偏移矩阵
撤销及清屏上面说到了GraffitiPath的作用,因此我们可以通过该类还原出每一次的涂鸦操作,我们把操作记录在集合CopyOnWriteArrayList&GraffitiPath&中,通过删除集合的最后一个元素即可实现撤销上一步操作的功能,同理,清除集合内的元素即可实现清屏,当然,删除后需要在原始图片上按次序还原集合中所有操作。
public void undo() {
if (mPathStack.size() & 0) {
mPathStack.remove(mPathStack.size() - 1);
initCanvas();
draw(mBitmapCanvas, mPathStack, false);
invalidate();
最新源码在Github上的地址为: ,
欢迎大家反馈问题,我会及时在上面更新代码,谢谢支持。
建议大家先阅读v3.0的代码,因为3.0之后加入了旋转等功能,原理变得更为复杂。v3.0的代码:
没有更多推荐了,Android简单涂鸦以及撤销、重做的实现方法
前段时间研究了下涂鸦功能的实现,其实单独的涂鸦实现起来还是挺简单的,关键的技术难点是撤销与重做功能的实现。但是这里暂时只说明下涂鸦功能的实现,高手勿喷哈,而且该功能在 SDK提供的APIDemo当中就有的,但是如果能够将该地方的知识点搞懂的话,我认为View画图基本上是难不倒你了,特别是里面为什么要用一个中间的Bitmap。老规矩,还是先看看效果图吧:
package cn.ych.import java.io.Fimport java.io.FileNotFoundEimport java.io.FileOutputSimport java.io.IOEimport java.util.ArrayLimport java.util.Iimport java.util.Limport .content.Cimport android.graphics.Bimport android.graphics.Cimport android.graphics.Pimport android.graphics.Pimport android.graphics.Bitmap.CompressFimport android.os.Eimport android.view.MotionEimport android.view.V/**** @category: View实现涂鸦、撤销以及重做功能* @author: 锋翼* @link: www.apkstory.com* @date: **/public class TuyaView extends View {private Bitmap mBprivate Canvas mCprivate Path mPprivate Paint mBitmapP// 画布的画笔private Paint mP// 真实的画笔private float mX, mY;//临时点坐标private static final float TOUCH_TOLERANCE = 4;private int screenWidth, screenH// 屏幕長寬public TuyaView(Context context, int w, int h) {
super(context);
screenWidth =
screenHeight =
mBitmap = Bitmap.createBitmap(screenWidth, screenHeight,
Bitmap.Config.ARGB_8888);
// 保存一次一次绘制出来的图形
mCanvas = new Canvas(mBitmap);
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);// 设置外边缘
mPaint.setStrokeCap(Paint.Cap.SQUARE);// 形状
mPaint.setStrokeWidth(5);// 画笔宽度}@Overridepublic void onDraw(Canvas canvas) {
canvas.drawColor(0xFFAAAAAA);
// 将前面已经画过得显示出来
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (mPath != null) {
// 实时的显示
canvas.drawPath(mPath, mPaint);
}}private void touch_start(float x, float y) {
mPath.moveTo(x, y);
mY =}private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(mY - y);
触摸间隔大于阈值才绘制路径
if (dx &= TOUCH_TOLERANCE || dy &= TOUCH_TOLERANCE) {
// 从x1,y1到x2,y2画一条贝塞尔曲线,更平滑(直接用mPath.lineTo也是可以的)
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
}}private void touch_up() {
mPath.lineTo(mX, mY);
mCanvas.drawPath(mPath, mPaint);
}@Overridepublic boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 每次down下去重新new一个Path
mPath = new Path();
touch_start(x, y);
invalidate();
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
上一讲当中,已经讲解了普通View实现涂鸦的功能,现在再来给涂鸦添加上撤销与重做的功能吧。撤销与重做在很多地方都是很重要的功能,比如PS里面、Word里面等等,而且大部分童鞋都能够想到要实现该功能应该需要用到堆栈,对于一些大牛的话可能就直接想到设计模式上面去了,比如命令模式就可以解决撤销与重做的问题。我们这里要讲解的是利用集合来完成该功能,其实也就是模拟栈,我相信你懂得。
老规矩,先上效果图:
package cn.ych.import java.io.Fimport java.io.FileNotFoundEimport java.io.FileOutputSimport java.io.IOEimport java.util.ArrayLimport java.util.Iimport java.util.Limport android.content.Cimport android.graphics.Bimport android.graphics.Cimport android.graphics.Pimport android.graphics.Pimport android.graphics.Bitmap.CompressFimport android.os.Eimport android.view.MotionEimport android.view.V/**** @category: View实现涂鸦、撤销以及重做功能* @author: 锋翼* @link: www.apkstory.com* @date: **/public class TuyaView extends View {private Bitmap mBprivate Canvas mCprivate Path mPprivate Paint mBitmapP// 画布的画笔private Paint mP// 真实的画笔private float mX, mY;// 临时点坐标private static final float TOUCH_TOLERANCE = 4;// 保存Path路径的集合,用List集合来模拟栈private static List&DrawPath& saveP// 记录Path路径的对象private DrawPprivate int screenWidth, screenH// 屏幕長寬private class DrawPath {
public P// 路径
public P// 画笔}public TuyaView(Context context, int w, int h) {
super(context);
screenWidth =
screenHeight =
mBitmap = Bitmap.createBitmap(screenWidth, screenHeight,
Bitmap.Config.ARGB_8888);
// 保存一次一次绘制出来的图形
mCanvas = new Canvas(mBitmap);
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);// 设置外边缘
mPaint.setStrokeCap(Paint.Cap.SQUARE);// 形状
mPaint.setStrokeWidth(5);// 画笔宽度
savePath = new ArrayList&DrawPath&();}@Overridepublic void onDraw(Canvas canvas) {
canvas.drawColor(0xFFAAAAAA);
// 将前面已经画过得显示出来
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (mPath != null) {
// 实时的显示
canvas.drawPath(mPath, mPaint);
}}private void touch_start(float x, float y) {
mPath.moveTo(x, y);
mY =}private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(mY - y);
if (dx &= TOUCH_TOLERANCE || dy &= TOUCH_TOLERANCE) {
// 从x1,y1到x2,y2画一条贝塞尔曲线,更平滑(直接用mPath.lineTo也是可以的)
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
}}private void touch_up() {
mPath.lineTo(mX, mY);
mCanvas.drawPath(mPath, mPaint);
//将一条完整的路径保存下来(相当于入栈操作)
savePath.add(dp);
mPath =// 重新置空}/**
* 撤销的核心思想就是将画布清空,
* 将保存下来的Path路径最后一个移除掉,
* 重新将路径画在画布上面。
*/public void undo() {
mBitmap = Bitmap.createBitmap(screenWidth, screenHeight,
Bitmap.Config.ARGB_8888);
mCanvas.setBitmap(mBitmap);// 重新设置画布,相当于清空画布
// 清空画布,但是如果图片有背景的话,则使用上面的重新初始化的方法,用该方法会将背景清空掉...
if (savePath != null && savePath.size() & 0) {
// 移除最后一个path,相当于出栈操作
savePath.remove(savePath.size() - 1);
Iterator&DrawPath& iter = savePath.iterator();
while (iter.hasNext()) {
DrawPath drawPath = iter.next();
mCanvas.drawPath(drawPath.path, drawPath.paint);
invalidate();// 刷新
/*在这里保存图片纯粹是为了方便,保存图片进行验证*/
String fileUrl = Environment.getExternalStorageDirectory()
.toString() + "/android/data/test.png";
FileOutputStream fos = new FileOutputStream(new File(fileUrl));
mBitmap.compress(CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
* 重做的核心思想就是将撤销的路径保存到另外一个集合里面(栈),
* 然后从redo的集合里面取出最顶端对象,
* 画在画布上面即可。
*/public void redo(){
//如果撤销你懂了的话,那就试试重做吧。}@Overridepublic boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 每次down下去重新new一个Path
mPath = new Path();
//每一次记录的路径对象是不一样的
dp = new DrawPath();
dp.path = mP
dp.paint = mP
touch_start(x, y);
invalidate();
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
没有更多推荐了,1.Activity:
package com.example.
import java.beans.PropertyChangeE
import java.beans.PropertyChangeL
import java.io.F
import java.io.FileNotFoundE
import java.io.FileOutputS
import java.util.ArrayL
import java.util.L
import com.example.hello.adapter.ColorA
import com.example.hello.ui.CanvasV
import com.example.hello.utils.BitmapU
import com.example.hello.utils.CanvasGlobelM
import com.example.hello.utils.ColorT
import android.app.A
import android.app.AlertD
import android.app.D
import android.content.ContentR
import android.content.DialogI
import android.content.I
import android.graphics.B
import android.graphics.C
import android.graphics.drawable.ColorD
import android.net.U
import android.os.B
import android.util.DisplayM
import android.view.D
import android.view.KeyE
import android.view.V
import android.view.View.OnClickL
import android.view.ViewGroup.LayoutP
import android.view.W
import android.view.WindowM
import android.widget.AdapterV
import android.widget.AdapterView.OnItemClickL
import android.widget.B
import android.widget.GridV
import android.widget.ImageV
import android.widget.LinearL
import android.widget.T
public class CanvasActivity extends Activity implements OnClickListener {
private CanvasView canvasV
private Button saveB
private Button cancelB
private ImageView recoverB
private ImageView colorB
private ImageView pictureB
private ImageView releaseB
private boolean isShowColorCanvas =
private LinearLayout canvasViewL
private View colorL
private GridView colorsGridV
private static final int ACTIVITY_GET_IMAGE = 0;
private static final int ACTIVITY_FROM_CAMERA = 1;
static List&Integer& colors = new ArrayList&Integer&();
private ColorAdapter colorsA
colors.add(R.drawable.canvas_color_grid_black_selector);
colors.add(R.drawable.canvas_color_grid_blue_selector);
colors.add(R.drawable.canvas_color_grid_green_selector);
colors.add(R.drawable.canvas_color_grid_purple_selector);
colors.add(R.drawable.canvas_color_grid_red_selector);
colors.add(R.drawable.canvas_color_grid_yellow_selector);
colors.add(R.drawable.canvas_color_grid_white_selector);
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.canvas_activity);
this.saveButton = (Button) findViewById(R.id.save_btn);
this.cancelButton = (Button) findViewById(R.id.save_btn);
this.recoverButtton = (ImageView) findViewById(R.id.canvas_recover_btn);
this.colorButtton = (ImageView) findViewById(R.id.canvas_color_btn);
this.pictureButtton = (ImageView) findViewById(R.id.canvas_picture_btn);
this.releaseButtton = (ImageView) findViewById(R.id.canvas_release_btn);
this.canvasViewLayout = (LinearLayout) findViewById(R.id.canvas_layout);
this.saveButton.setOnClickListener(this);
this.cancelButton.setOnClickListener(this);
this.recoverButtton.setOnClickListener(this);
this.colorButtton.setOnClickListener(this);
this.pictureButtton.setOnClickListener(this);
this.releaseButtton.setOnClickListener(this);
this.releaseButtton.setClickable(false);
this.invalidateCanvasView();
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == ACTIVITY_GET_IMAGE) {
if (data != null) {
Uri uri = data.getData();
ContentResolver resolver = this.getContentResolver();
byte[] buffer = BitmapUtil.getBytesFromInputStream(resolver.openInputStream(Uri.parse(uri.toString())));
Bitmap upBitmap = BitmapUtil.getPicFromBytes(buffer, null);
this.canvasView.updateImage(upBitmap);
} catch (Exception e) {
Toast.makeText(this, "The image not found, Please try again", 1).show();
e.printStackTrace();
public void onClick(View v) {
switch (v.getId()) {
case R.id.save_btn:
Bitmap bitmap = this.canvasView.saveBitmap();
// Show the image in dialog
if (bitmap != null) {
Dialog d = new Dialog(CanvasActivity.this);
ImageView imageView = new ImageView(CanvasActivity.this);
imageView.setImageBitmap(bitmap);
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
d.addContentView(imageView, layoutParams);
Toast.makeText(CanvasActivity.this, "图片保存失败", 1).show();
// Create a JPEG ant save it to local.
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(new File("mnt/sdcard/arun.jpg")));
} catch (FileNotFoundException e) {
e.printStackTrace();
case R.id.back_btn:
CanvasGlobelManager.getInstanse().resetColorsSelectedItem();
this.finish();
case R.id.canvas_recover_btn:
this.canvasView.clear();
case R.id.canvas_color_btn:
this.showColorView();
case R.id.canvas_picture_btn:
final CharSequence[] picItems = { "本地图片", "拍照" };
AlertDialog dlg = new AlertDialog.Builder(CanvasActivity.this).setTitle("选择图片类型").setItems(picItems, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int picItem) {
if (picItem == 0) {
Intent getImage = new Intent(Intent.ACTION_GET_CONTENT);
getImage.addCategory(Intent.CATEGORY_OPENABLE);
getImage.setType("image/*");
startActivityForResult(getImage, ACTIVITY_GET_IMAGE);
// TODO No have complete the model
Intent getImageByCamera = new Intent("android.media.action.IMAGE_CAPTURE");
startActivityForResult(getImageByCamera, ACTIVITY_FROM_CAMERA);
}).create();
dlg.show();
case R.id.canvas_release_btn:
if (this.canvasView.getPathsSize() & 0)
this.canvasView.releaseLastPath();
this.releaseButtton.setClickable(false);
private void showColorView() {
this.colorLayout = this.findViewById(R.id.color_grid_view_layout);
this.colorsGridView = (GridView) this.findViewById(R.id.color_grid_view);
this.colorsGridView.setSelector(new ColorDrawable(Color.TRANSPARENT));
// colorsGridView.setStretchMode(GridView.NO_STRETCH);
colorsGridView.setNumColumns(colors.size());
colorsAdapter = new ColorAdapter(this, R.layout.color_grid_view_row, colors);
this.colorsGridView.setAdapter(colorsAdapter);
this.controlShowColorCanvas();
this.colorsGridView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView&?& arg0, View view, int postion, long arg3) {
CanvasGlobelManager.getInstanse().setColorsSelectedItem(postion);
switch (postion) {
canvasView.setColor(ColorType.COLOR_BLACK);
canvasView.setColor(ColorType.COLOR_BLUE);
canvasView.setColor(ColorType.COLOR_GREEN);
canvasView.setColor(ColorType.COLOR_PURPLE);
canvasView.setColor(ColorType.COLOR_RED);
canvasView.setColor(ColorType.COLOR_YELLOW);
canvasView.setColor(ColorType.COLOR_WHITE);
controlShowColorCanvas();
private void invalidateCanvasView() {
Display display = getWindowManager().getDefaultDisplay();
int height = display.getHeight();
int width = display.getWidth();
DisplayMetrics dm = new DisplayMetrics();
dm= getResources().getDisplayMetrics();
display.getMetrics(dm);
int heightPixels = dm.heightP
int widthPixels = dm.widthP
// TODO the height will be modify
int canvasViewHeight = height - 60 - 40;
this.canvasView = new CanvasView(this, canvasViewHeight, width);
canvasViewLayout.addView(this.canvasView);
this.canvasView.addpropertChange(new PropertyChangeListener() {
//监听path列表中是否还有path ,如果没有,则撤销不可点击
public void propertyChange(PropertyChangeEvent event) {
if ((Integer) event.getNewValue() & 0) {
releaseButtton.setClickable(true);
private void controlShowColorCanvas() {
if (!isShowColorCanvas && this.colorLayout != null) {
this.colorLayout.setVisibility(View.VISIBLE);
this.colorButtton.setImageResource(R.drawable.canvas_color_image1);
this.colorLayout.setVisibility(View.GONE);
this.colorButtton.setImageResource(R.drawable.canvas_color_image);
this.isShowColorCanvas = !isShowColorC
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
CanvasGlobelManager.getInstanse().resetColorsSelectedItem();
this.finish();
return super.onKeyDown(keyCode, event);
&?xml version="1.0" encoding="utf-8"?&
&LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" &
&RelativeLayout
android:layout_width="fill_parent"
android:layout_height="@dimen/canvasHeaderHeight"
android:background="@drawable/label_bground" &
android:id="@+id/back_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginLeft="2dip"
android:background="@drawable/label_save_bg"
android:gravity="center"
android:text="取消"
android:textColor="#FFFFFF" /&
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="18dip"
android:text="涂鸦画版"
android:layout_centerInParent="true"
android:textColor="#FFFFFF" /&
android:id="@+id/save_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="2dip"
android:background="@drawable/label_save_bg"
android:gravity="center"
android:text="发送"
android:textColor="#FFFFFF" /&
&/RelativeLayout&
&RelativeLayout
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:background="@drawable/background"
android:gravity="center" &
&LinearLayout
android:id="@+id/canvas_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center" &
&/LinearLayout&
&LinearLayout
android:id="@+id/color_grid_view_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/canvas_layout"
android:layout_alignParentBottom="true"
android:background="@drawable/scroll_background"
android:orientation="horizontal"
android:visibility="gone" &
android:id="@+id/color_grid_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:layout_gravity="center"
android:layout_marginTop="@dimen/canvasColorGridMargin"
android:layout_marginBottom="@dimen/canvasColorGridMargin"
android:horizontalSpacing="@dimen/canvasColorGridMargin"
android:divider="@android:color/transparent" &
&/GridView&
&/LinearLayout&
&/RelativeLayout&
&LinearLayout
android:layout_width="fill_parent"
android:layout_height="@dimen/canvasFootHeight"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:background="@drawable/canvas_foot_background"
android:orientation="horizontal" &
&ImageView
android:id="@+id/canvas_recover_btn"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_weight="1"
android:src="@drawable/canvas_recover_selector" /&
&ImageView
android:id="@+id/canvas_color_btn"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_weight="1"
android:src="@drawable/canvas_color_selector" /&
&ImageView
android:id="@+id/canvas_picture_btn"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_weight="1"
android:src="@drawable/canvas_picture_selector" /&
&ImageView
android:id="@+id/canvas_release_btn"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_weight="1"
android:src="@drawable/canvas_release_selector" /&
&/LinearLayout&
&/LinearLayout&
package com.example.hello.
import java.beans.PropertyChangeL
import java.beans.PropertyChangeS
import java.util.ArrayL
import java.util.EventL
import android.content.C
import android.graphics.B
import android.graphics.BitmapF
import android.graphics.C
import android.graphics.M
import android.graphics.P
import android.graphics.P
import android.util.L
import android.view.MotionE
import android.view.V
import com.example.hello.R;
import com.example.hello.utils.ColorT
public class CanvasView extends View implements EventListener {
private Bitmap bgB
private Bitmap mB
private Canvas mC
private Path mP
@SuppressWarnings("unused")
private Paint mBitmapP// 预定义两个Paint,背景和Bitmap分开画
private Paint mP
private boolean isOverRawXY =
* It will set the mPaint color after release path
private int currentColor = ColorType.COLOR_BLACK;
ArrayList&CanvasPath& pathList = new ArrayList&CanvasPath&(40);
PropertyChangeSupport support =
* CanvasView组件大小
private int viewHeight = 0;
private int viewWidth = 0;
* 画图离顶部的距离
private int top = 0;
private int left = 0;
public CanvasView(Context c, int height, int width) {
mPaint = new Paint();
support = new PropertyChangeSupport(this);
bgBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas();
mCanvas.setBitmap(bgBitmap);
mPath = new Path();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(this.currentColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(5);
this.viewHeight =
this.viewWidth =
Log.i("sys", "原始高" + this.viewHeight + "宽" + this.viewWidth);
this.drawMBitmapByResource(R.drawable.background);
private void drawMBitmapByResource(int id) {
this.mBitmap = BitmapFactory.decodeResource(getResources(), id);
drawMBitmap();
private void drawMBitmap() {
this.measureXY();
this.mCanvas.drawBitmap(this.mBitmap, this.left, this.top, this.mPaint);
* 缩小图片,使用期Scale方式
private void measureXY() {
// 初始大小
float oldWidth = this.mBitmap.getWidth();
float oldHeight = this.mBitmap.getHeight();
this.left = (int) ((this.viewWidth - oldWidth) / 2);
this.top = (int) ((this.viewHeight - oldHeight) / 2);
Matrix matrix =
if (left & 0 || top & 0) {
matrix = new Matrix();
Log.i("sys", "原" + oldWidth + "-" + oldHeight);
// 创建操作图片用的matrix对象
boolean isVertical = left &
float scaleXY = 0f;
if (isVertical) {
// 计算宽高缩放率
scaleXY = ((float) this.viewHeight) / oldH
this.left = (int) ((this.viewWidth - scaleXY * oldWidth) / 2);
this.top = 0;
scaleXY = ((float) this.viewWidth) / oldW
this.left = 0;
this.top = (int) ((this.viewHeight - scaleXY * oldHeight) / 2);
// 缩放图片动作
matrix.setScale(scaleXY, scaleXY);
this.mBitmap = Bitmap.createBitmap(this.mBitmap, 0, 0, (int) (oldWidth), (int) (oldHeight), matrix, true);
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
protected void onDraw(Canvas canvas) {
canvas.drawColor(0xFFFFFFFF);// TODO set background color
canvas.drawBitmap(bgBitmap, 0, 0, mPaint);
canvas.drawPath(mPath, mPaint);
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// widthMeasureSpec = widthMeasureSpec - 2 * spacW
// heightMeasureSpec = heightMeasureSpec - 2 * spacH
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;
private void touch_start(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx &= TOUCH_TOLERANCE || dy &= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
private void touch_up() {
mPath.lineTo(mX, mY);
// commit the path to our offscreen
mCanvas.drawPath(mPath, mPaint);
// kill this so we don't double draw
pathList.add(new CanvasPath(mPath, mPaint.getColor(), this.isOverRawXY));
mPath = new Path();// mPath.reset()不可行,
int size = this.pathList.size();
this.support.fireIndexedPropertyChange("length", 0, size - 1, size);
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (this.isNullBitmap() || x & this.left || y & this.top || x & this.left + this.mBitmap.getWidth() || y & this.top + this.mBitmap.getHeight())
this.isOverRawXY =
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
return super.onTouchEvent(event);
public Bitmap saveBitmap() {
Log.i("sys", "开始X:"+left+"Y:"+top);
Log.i("sys", "计算后开始X:"+(this.viewWidth-mBitmap.getWidth())/2+"Y:"+(this.viewHeight-mBitmap.getHeight())/2);
if ((this.top & 0 || this.left & 0) && !this.isOverRawXY && !this.isNullBitmap()) {
return Bitmap.createBitmap(this.bgBitmap, this.left, this.top, this.mBitmap.getWidth(), this.mBitmap.getHeight());
// 注意:后来两个参数不是指结束位置,而是指要取的图片长度
return this.bgB
public void clear() {
mCanvas.drawColor(0xFFFFFFFF);
invalidate();
this.mBitmap =
this.pathList.clear();
public void setColor(int color) {
this.mPaint.setColor(color);
this.currentColor =
public void updateImage(Bitmap bitmap) {
this.isOverRawXY =
this.clear();
this.mBitmap =
this.drawMBitmap();
public void releaseLastPath() {
this.isOverRawXY =
this.mCanvas.drawColor(0xFFFFFFFF);// set background color
if (this.mBitmap != null)
this.mCanvas.drawBitmap(this.mBitmap, this.left, this.top, this.mPaint);
int size = this.pathList.size();
for (int i = 0; i & size - 1; i++) {
CanvasPath canvasPath = this.pathList.get(i);
this.mPaint.setColor(canvasPath.colorId);
this.mCanvas.drawPath(canvasPath.path, this.mPaint);
if (canvasPath.isTrue) {
this.isOverRawXY =
this.mPaint.setColor(this.currentColor);
this.pathList.remove(size - 1);
invalidate();
public boolean isNullBitmap() {
if (this.mBitmap != null) {
public int getPathListSize() {
return this.pathList.size();
public void addpropertChange(PropertyChangeListener pl) {
support.addPropertyChangeListener(pl);
public void removePropertChangListener(PropertyChangeListener PL) {
support.removePropertyChangeListener(PL);
private class CanvasPath {
private int colorId;
private boolean isT
public CanvasPath(Path path, int colorId, boolean isTrue) {
this.path =
this.colorId = colorId;
this.isTrue = isT
3. 全局数据保存,用于当前使用画笔颜色:
package com.example.hello.
public class CanvasGlobelManager {
private static CanvasGlobelManager instance =
private int colorsSelectedItem = 0;
instance = new CanvasGlobelManager();
private CanvasGlobelManager() {
public static CanvasGlobelManager getInstanse() {
public synchronized int getColorsSelectedItem() {
return this.colorsSelectedI
public synchronized void setColorsSelectedItem(int colorsSelected) {
this.colorsSelectedItem = colorsS
public synchronized void resetColorsSelectedItem() {
this.colorsSelectedItem = 0;
4.工具类:
package com.example.hello.
import java.io.IOE
import java.io.InputS
import java.nio.ByteB
import android.graphics.B
import android.graphics.BitmapF
public class BitmapUtil {
public static byte[] getBytesFromInputStream(InputStream is) throws IOException {
int total = 0;
byte[] bytes = new byte[2048];
ByteBuffer byteBuffer = ByteBuffer.allocate(3500000);
while (true) {
int read = is.read(bytes);
if (read == -1)
byteBuffer.put(bytes, 0, read);
byte[] content = new byte[total];
byteBuffer.flip();
byteBuffer.get(content, 0, total);
public static Bitmap getPicFromBytes(byte[] bytes, BitmapFactory.Options opts) {
if (bytes != null)
if (opts != null)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opts);
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
import android.graphics.C
public class ColorType {
// COLOR_BLACK,COLOR_BLUE,COLOR_GREEN,COLOR_PURPLE,COLOR_RED,COLOR_YELLOW, COLOR_WHITE;
public static final int COLOR_BLACK = Color.BLACK;
public static final int COLOR_BLUE = Color.BLUE;
public static final int COLOR_GREEN = Color.GREEN;
public static final int COLOR_PURPLE = Color.rgb(0x99, 0x32, 0xCD);
public static final int COLOR_RED = Color.RED;
public static final int COLOR_YELLOW = Color.YELLOW;
public static final int COLOR_WHITE = Color.WHITE;
浏览: 23835 次
来自: 成都
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'

我要回帖

更多关于 涂鸦上帝 草怎么合成 的文章

 

随机推荐