自定义view实现涂鸦(画板)功能
Posted 原来如此丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义view实现涂鸦(画板)功能相关的知识,希望对你有一定的参考价值。
项目一直在做,现在空闲下来才有空整理剩下的。
进入正文:
需求修改之后,需要添加画椭圆、画矩形以及画箭头的方法,之后需要将在白色背景上作图改为在图片上进行编辑
总体来说:完成方式类似,只需要在外部设置按钮用标记去标识,在画板中变化画图方式即可
该注释的地方我都加在代码里,所以就不作太多的额外说明了
先定义一下画图方式:
//设置画图样式
private static final int DRAW_PATH = 01;
private static final int DRAW_CIRCLE = 02;
private static final int DRAW_RECTANGLE = 03;
private static final int DRAW_ARROW = 04;
private int[] graphics = new int[]DRAW_PATH,DRAW_CIRCLE,DRAW_RECTANGLE,DRAW_ARROW;
private int currentDrawGraphics = graphics[0];//默认画线
画椭圆和画矩形比较简单:
画椭圆:
if(currentDrawGraphics == DRAW_CIRCLE)
mPath.reset();//清空以前的路径,否则会出现无数条从起点到末位置的线
RectF rectF = new RectF(startX,startY,x,y);
mPath.addOval(rectF, Path.Direction.CCW);//画椭圆
// mPath.addCircle((startX + x) / 2, (startY + y) / 2, (float) Math.sqrt(Math.pow(newx, 2) + Math.pow(newy, 2)) / 2, Path.Direction.CCW)//画圆
画矩形:
if(currentDrawGraphics == DRAW_RECTANGLE)
mPath.reset();
RectF rectF = new RectF(startX,startY,x,y);
mPath.addRect(rectF, Path.Direction.CCW);
稍微麻烦的是画箭头:
思路是:先画线,再以线的另一端为端点按照一个角度来偏移并计算偏移坐标,并得到箭头的两个端点,然后再将端点画线连接起来,这样就会形成一个箭头
if (currentDrawGraphics == DRAW_ARROW)
mPath.reset();
drawAL((int) startX, (int) startY, (int) x, (int) y);
drawAL:
/**
* 画箭头
* @param startX 开始位置x坐标
* @param startY 开始位置y坐标
* @param endX 结束位置x坐标
* @param endY 结束位置y坐标
*/
public void drawAL(int startX, int startY, int endX, int endY)
double lineLength = Math.sqrt(Math.pow(Math.abs(endX-startX),2) + Math.pow(Math.abs(endY-startY),2));//线当前长度
double H = 0;// 箭头高度
double L = 0;// 箭头长度
if(lineLength < 320)//防止箭头开始时过大
H = lineLength/4 ;
L = lineLength/6;
else //超过一定线长箭头大小固定
H = 80;
L = 50;
double arrawAngle = Math.atan(L / H); // 箭头角度
double arraowLen = Math.sqrt(L * L + H * H); // 箭头的长度
double[] pointXY1 = rotateAndGetPoint(endX - startX, endY - startY, arrawAngle, true, arraowLen);
double[] pointXY2 = rotateAndGetPoint(endX - startX, endY - startY, -arrawAngle, true, arraowLen);
int x3 = (int) (endX - pointXY1[0]);//(x3,y3)为箭头一端的坐标
int y3 = (int) (endY - pointXY1[1]);
int x4 = (int) (endX - pointXY2[0]);//(x4,y4)为箭头另一端的坐标
int y4 = (int) (endY - pointXY2[1]);
// 画线
mPath.moveTo(startX,startY);
mPath.lineTo(endX,endY);
mPath.moveTo(x3, y3);
mPath.lineTo(endX, endY);
mPath.lineTo(x4, y4);
// mPath.close();闭合线条
rotateAndGetPoint():这个方法就是用来计算箭头端点的,我们需要提供x以及y方向的长度,当然还有箭头偏转角度
/**
* 矢量旋转函数,计算末点的位置
* @param x x分量
* @param y y分量
* @param ang 旋转角度
* @param isChLen 是否改变长度
* @param newLen 箭头长度长度
* @return 返回末点坐标
*/
public double[] rotateAndGetPoint(int x, int y, double ang, boolean isChLen, double newLen)
double pointXY[] = new double[2];
double vx = x * Math.cos(ang) - y * Math.sin(ang);
double vy = x * Math.sin(ang) + y * Math.cos(ang);
if (isChLen)
double d = Math.sqrt(vx * vx + vy * vy);
pointXY[0] = vx / d * newLen;
pointXY[1] = vy / d * newLen;
return pointXY;
以上通过注释应能比较清晰的看出来思路
之后我就开始着手做成在背景图片上进行涂鸦,这样一来很多东西就需要修改了,原来能在白色背景上做的东西都需要进行修改。
去图库选择图片很简单:
Intent picIntent = new Intent();
picIntent.setType("image/*");
picIntent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(picIntent, OPEN_PHOTO);
但是当你在保存图片时会出现很多各种莫名其妙的错误包括:保存背景为黑色,笔迹完全显示不出来等问题
所以我想的是很可能保存时候没有对图片处理好,决定采用将两张bitmap进行合并的方法,于是就有了:
if(srcBitmap != null)//有背景图
Bitmap bitmap = Bitmap.createBitmap(screenWidth,screenHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
// canvas.drawBitmap(srcBitmap, new Matrix(), null);
// paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
Bitmap pathBitmap = drawBitmapPath();
canvas.drawBitmap(pathBitmap,new Matrix(),null);
*//*Iterator<DrawPath> iter = savePath.iterator();
while (iter.hasNext())
DrawPath drawPath = iter.next();
canvas.drawPath(drawPath.path, drawPath.paint);
*//*
//将合并后的bitmap保存为png图片到本地
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
else //没有背景图就画在一张白色为背景的bitmap上
*//*Bitmap bitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(0,0,screenWidth,screenHeight,paint);
Iterator<DrawPath> iter = savePath.iterator();
while (iter.hasNext())
DrawPath drawPath = iter.next();
canvas.drawPath(drawPath.path, drawPath.paint);
*//*
mBitmap.compress(CompressFormat.PNG, 100, fos);
但是在使用橡皮擦时出错误了,原因到现在理解的还是不是很透彻,可能是渲染模式只是覆盖的关系,但是橡皮擦仍会作为一种触摸痕迹也被保留了下来,所以发现使用橡皮擦后保存下来的图片不但没有擦掉原痕迹,连橡皮擦的痕迹也显示出来了,于是偷懒之下就想到了一个很便捷的方法:
我们只需要像截图一样把当前界面截取下来就行了,而且android也提供了view层的截取方法,步骤如下:
//view层的截图
view.setDrawingCacheEnabled(true);
Bitmap newBitmap = Bitmap.createBitmap(view.getDrawingCache());
viewt.setDrawingCacheEnabled(false);
view表示你需要截取的控件,注意步骤一定要按照上面实现,那么只需要将截取的图片直接保存就可以了,于是在保存到sd卡的方法中修改
//保存到sd卡
public String saveToSDCard(Bitmap bitmap)
//获得系统当前时间,并以该时间作为文件名
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
Date curDate = new Date(System.currentTimeMillis());//获取当前时间
String str = formatter.format(curDate) + "paint.png";
String path= "sdcard/" + str;
File file = new File(path);
FileOutputStream fos = null;
try
fos = new FileOutputStream(file);
catch (Exception e)
e.printStackTrace();
bitmap.compress(CompressFormat.PNG, 100, fos);
return path;
另外要注意的是,图片要进行压缩,要不然连试几次就oom了:
/**
* 通过uri获取图片并进行压缩
*
* @param uri
*/
public Bitmap getBitmapFormUri(Activity ac, Uri uri) throws FileNotFoundException, IOException
InputStream input = ac.getContentResolver().openInputStream(uri);
BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
onlyBoundsOptions.inJustDecodeBounds = true;
onlyBoundsOptions.inDither = true;//optional
onlyBoundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
input.close();
//图片宽高
int originalWidth = onlyBoundsOptions.outWidth;
int originalHeight = onlyBoundsOptions.outHeight;
if ((originalWidth == -1) || (originalHeight == -1))
return null;
//图片分辨率以480x800为标准
float hh = 800f;//这里设置高度为800f
float ww = 480f;//这里设置宽度为480f
//缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
int be = 1;//be=1表示不缩放
if (originalWidth > originalHeight && originalWidth > ww) //如果宽度大的话根据宽度固定大小缩放
be = (int) (originalWidth / ww);
else if (originalWidth < originalHeight && originalHeight > hh) //如果高度高的话根据宽度固定大小缩放
be = (int) (originalHeight / hh);
if (be <= 0)
be = 1;
//比例压缩
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = be;//设置缩放比例
bitmapOptions.inDither = true;//optional
bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
input = ac.getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
input.close();
return compressImageByQuality(bitmap);//再进行质量压缩
// 质量压缩方法
public Bitmap compressImageByQuality(Bitmap image) throws IOException
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
int options = 100;
while (baos.toByteArray().length / 1024 > 100) //循环判断如果压缩后图片是否大于100kb,大于继续压缩
baos.reset();//重置baos即清空baos
//第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差 ,第三个参数:保存压缩后的数据的流
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
options -= 10;//每次都减少10
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中
Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片
isBm.close();
baos.close();
return bitmap;
压缩方法网上资料很多,就不赘述了,需要注意的是我们需要根据所在画图控件的大小来调整图片宽高压缩比
以上基本上所有需要的东西已经全部做完了,以下就是锦上添花的功能:
例如:添加保存时的progressDialog以便好看一点:
public class CustomProgressDialog extends Dialog
private Context context = null;
private static CustomProgressDialog customProgressDialog = null;
public CustomProgressDialog(Context context)
super(context);
this.context = context;
public CustomProgressDialog(Context context, int theme)
super(context, theme);
public static CustomProgressDialog createDialog(Context context)
customProgressDialog = new CustomProgressDialog(context,R.style.CustomProgressDialog);
customProgressDialog.setContentView(R.layout.customprogressdialog);
return customProgressDialog;
public void onWindowFocusChanged(boolean hasFocus)
if (customProgressDialog == null)
return;
ImageView imageView = (ImageView) customProgressDialog.findViewById(R.id.loadingImageView);
AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getBackground();
animationDrawable.start();
public void setMessage(String strMessage)
TextView tvMsg = (TextView)customProgressDialog.findViewById(R.id.id_tv_loadingmsg);
if (tvMsg != null)
tvMsg.setText(strMessage);
progressDialog的xml文件:
<?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:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/loadingImageView"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="@drawable/progress_round"/>
<TextView
android:id="@+id/id_tv_loadingmsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="正在保存..."
android:textSize="16sp"
android:layout_marginTop="10dp"/>
</LinearLayout>
CustomProgressDialog 的样式:
<style name="CustomDialog" parent="@android:style/Theme.Dialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
</style>
<style name="CustomProgressDialog" parent="@style/CustomDialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
</style>
那么就只需要在保存时CustomProgressDialog.createDialog就行了,在ondestroy时dismiss即可
效果图:
下面就是所有的代码了:
PaintBoardActivity:
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.sanhai.android.util.ABIOUtil;
import com.sanhai.psdapp.R;
import com.sanhai.psdapp.service.BanhaiActivity;
import com.sanhai.psdapp.view.CustomProgressDialog;
import com.sanhai.psdapp.view.TuyaView;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by zpxiang.
*/
public class PaintBoardActivity extends BanhaiActivity implements View.OnClickListener,RadioGroup.OnCheckedChangeListener
private FrameLayout frameLayout;
private View common_view;
private RadioGroup rg_drawgraphics;
private RadioButton btn_undo;
private RadioButton btn_redo;
private RadioButton btn_save;
private RadioButton btn_picselect;
private RadioButton btn_drawcycle;
private RadioButton btn_drawrec;
private RadioButton btn_drawarrow;
private RadioButton btn_null;
private TuyaView tuyaView;//自定义涂鸦板
private LinearLayout ll_paintcolor;
private LinearLayout ll_paintsize;
private LinearLayout ll_paintstyle;
private SeekBar sb_size;
//三个弹出框的相关控件
private LinearLayout ll_paintstyle_state;
private ImageView iv_paintstyle;
private LinearLayout ll_paintcolor_state;
private RadioGroup rg_paint_color;
private int OPEN_PHOTO = 9999;
private int screenWidth;
private int screenHeight;
private TextView tv_com_title;
//画线,圆,矩形,以及箭头
private static final int DRAW_PATH = 0;
private static final int DRAW_CIRCLE = 1;
private static final int DRAW_RECTANGLE = 2;
private static final int DRAW_ARROW = 3;
private int[] drawGraphicsStyle = new int[]DRAW_PATH, DRAW_CIRCLE, DRAW_RECTANGLE, DRAW_ARROW;
private int realHeight;//控件真实高度,去除头部标题后的
private Bitmap photoBmp;
private CustomProgressDialog customProgressDialog;//自定义进度条对话框
private Button btn_text_save;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_paintboard);
initView();
initData();
initListener();
private void initView()
common_view = findViewById(R.id.common_view);
frameLayout = (FrameLayout) findViewById(R.id.fl_boardcontainer);
rg_drawgraphics = (RadioGroup) findViewById(R.id.rg_drawgraphics);
btn_undo = (RadioButton) findViewById(R.id.btn_revoke);
btn_redo = (RadioButton) findViewById(R.id.btn_clean);
btn_save = (RadioButton) findViewById(R.id.btn_savesd);
btn_picselect = (RadioButton) findViewById(R.id.btn_picselect);//图片选择
btn_drawcycle = (RadioButton) findViewById(R.id.btn_drawcycle);
btn_drawrec = (RadioButton) findViewById(R.id.btn_drawrec);
btn_drawarrow = (RadioButton) findViewById(R.id.btn_drawarrow);
btn_null = (RadioButton) findViewById(R.id.btn_null);
btn_text_save = (Button) findViewById(R.id.but_submit);
ll_paintcolor = (LinearLayout) findViewById(R.id.ll_paint_color);
ll_paintsize = (LinearLayout) findViewById(R.id.ll_paint_size);
ll_paintstyle = (LinearLayout) findViewById(R.id.ll_paint_style);
sb_size = (SeekBar) findViewById(R.id.sb_size);
ll_paintcolor_state = (LinearLayout) findViewById(R.id.ll_paintcolor_state);
rg_paint_color = (RadioGroup) findViewById(R.id.rg_paint_color);
iv_paintstyle = (ImageView) findViewById(R.id.iv_paintstyle);
tv_com_title = (TextView) findViewById(R.id.tv_com_title);
private void initData()
tv_com_title.setText("画板");
btn_text_save.setText("保存");
//虽然此时获取的是屏幕宽高,但是我们可以通过控制framlayout来实现控制涂鸦板大小
Display defaultDisplay = getWindowManager().getDefaultDisplay();
screenWidth = defaultDisplay.getWidth();
screenHeight = defaultDisplay.getHeight();
realHeight = (int) (screenHeight - getResources().getDimension(R.dimen.DIMEN_100PX) - getResources().getDimension(R.dimen.DIMEN_100PX));
tuyaView = new TuyaView(this, screenWidth, realHeight);
frameLayout.addView(tuyaView);
tuyaView.requestFocus();
tuyaView.selectPaintSize(sb_size.getProgress());
//设置画笔默认背景
iv_paintstyle.setBackgroundResource(R.drawable.paint_style);
//创建对话框进度条
customProgressDialog = CustomProgressDialog.createDialog(this);
private void initListener()
ll_paintcolor.setOnClickListener(this);
ll_paintsize.setOnClickListener(this);
ll_paintstyle.setOnClickListener(this);
btn_picselect.setOnClickListener(this);
btn_save.setOnClickListener(this);
btn_redo.setOnClickListener(this);
btn_undo.setOnClickListener(this);
btn_null.setOnClickListener(this);
btn_text_save.setOnClickListener(this);
sb_size.setOnSeekBarChangeListener(new MySeekChangeListener());
ll_paintcolor_state.setOnClickListener(this);
iv_paintstyle.setOnClickListener(this);
rg_drawgraphics.setOnCheckedChangeListener(this);
class MySeekChangeListener implements SeekBar.OnSeekBarChangeListener
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
tuyaView.selectPaintSize(seekBar.getProgress());
@Override
public void onStartTrackingTouch(SeekBar seekBar)
tuyaView.selectPaintSize(seekBar.getProgress());
@Override
public void onStopTrackingTouch(SeekBar seekBar)
sb_size.setVisibility(View.GONE);
private boolean isPaint = true;
@Override
public void onClick(View v)
switch (v.getId())
case R.id.ll_paint_color:
changeColorAndSizeState(View.VISIBLE, View.GONE);
rg_paint_color.setOnCheckedChangeListener(this);
break;
case R.id.ll_paint_size:
changeColorAndSizeState(View.GONE, View.VISIBLE);
break;
case R.id.iv_paintstyle:
ll_paintstyle.setEnabled(true);
aboutPaintStyleSetting();
break;
case R.id.ll_paint_style:
aboutPaintStyleSetting();
break;
//文字保存
case R.id.but_submit:
aboutSaveToSd();
break;
//上方四个按钮
case R.id.btn_revoke://撤销
tuyaView.undo();
break;
case R.id.btn_clean://重做
tuyaView.redo();
frameLayout.setBackgroundResource(R.color.white);
//恢复成画笔状态
tuyaView.setSrcBitmap(null);
paintStyleSettingDesc(R.drawable.paint_style, select_paint_style_paint, true);
tuyaView.drawGraphics(DRAW_PATH);
break;
case R.id.btn_picselect://图片选择器
Intent picIntent = new Intent();
picIntent.setType("image/*");
picIntent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(picIntent, OPEN_PHOTO);
break;
case R.id.btn_savesd://保存
aboutSaveToSd();
break;
//保存到sd卡
private void aboutSaveToSd()
if (tuyaView.getSavePath().size() <= 0 || tuyaView.getSavePath() == null)
Toast.makeText(this, "涂鸦为空,无法保存", Toast.LENGTH_SHORT).show();
else if(tuyaView.getSavePath().size() > 0)
if (customProgressDialog != null)
customProgressDialog.setCanceledOnTouchOutside(false);//设置屏幕触摸失效
customProgressDialog.show();
new Thread()
@Override
public void run()
SystemClock.sleep(1000);
//view层的截图
frameLayout.setDrawingCacheEnabled(true);
Bitmap newBitmap = Bitmap.createBitmap(frameLayout.getDrawingCache());
frameLayout.setDrawingCacheEnabled(false);
//压缩
Bitmap scaleBitmap = com.sanhai.android.util.ABImageProcess.compBitmapByScale(newBitmap, screenWidth, realHeight);
String path = tuyaView.saveToSDCard(scaleBitmap);
final String[] images = new String[]path;
runOnUiThread(new Runnable()
@Override
public void run()
//Log.e("现在图片的路径", path);
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putStringArray("images", images);
intent.putExtras(bundle);
setResult(RESULT_OK, intent);
tuyaView.redo();
Toast.makeText(PaintBoardActivity.this, "当前涂鸦已保存", Toast.LENGTH_SHORT).show();
finish();
);
.start();
//画笔样式设置
private void aboutPaintStyleSetting()
changeColorAndSizeState(View.GONE, View.GONE);
if (isPaint) //当前为画笔,点击后变为橡皮擦
paintStyleSettingDesc(R.drawable.reaser_style,select_paint_style_eraser,false);
btn_null.setChecked(true);//使单选消失
else
paintStyleSettingDesc(R.drawable.paint_style,select_paint_style_paint,true);
tuyaView.drawGraphics(DRAW_PATH);
//画笔样式设置详情
private void paintStyleSettingDesc(int paintStyleResouce,int paintStyle,boolean styleTarget)
iv_paintstyle.setBackgroundResource(paintStyleResouce);
tuyaView.selectPaintStyle(paintStyle);
isPaint = styleTarget;
//切换画笔颜色和画笔尺寸显隐状态
private void changeColorAndSizeState(int visible, int gone)
ll_paintcolor_state.setVisibility(visible);
sb_size.setVisibility(gone);
@Override
public void onCheckedChanged(RadioGroup group, int checkedId)
switch (checkedId)
//处理颜色
case R.id.rb_purple:
selectPaintColorAndSetting(0);
break;
case R.id.rb_orange:
selectPaintColorAndSetting(1);
break;
case R.id.rb_green:
selectPaintColorAndSetting(2);
break;
case R.id.rb_yellow:
selectPaintColorAndSetting(3);
break;
case R.id.rb_black:
selectPaintColorAndSetting(4);
break;
//以下为画图形状按钮
case R.id.btn_drawarrow:
tellPaintStyleAndSetDrawGraphics(DRAW_ARROW, select_paint_style_paint);
break;
case R.id.btn_drawrec:
tellPaintStyleAndSetDrawGraphics(DRAW_RECTANGLE, select_paint_style_paint);
break;
case R.id.btn_drawcycle:
tellPaintStyleAndSetDrawGraphics(DRAW_CIRCLE, select_paint_style_paint);
break;
//判断画笔样式并切换画图样式
private void tellPaintStyleAndSetDrawGraphics(int drawArrow, int select_paint_style_paint)
if (isPaint)
tuyaView.drawGraphics(drawArrow);
else //当前为橡皮擦
iv_paintstyle.setBackgroundResource(R.drawable.paint_style);
tuyaView.selectPaintStyle(select_paint_style_paint);
tuyaView.drawGraphics(drawArrow);
isPaint = true;
//选择画笔颜色
private void selectPaintColorAndSetting(int which)
tuyaView.selectPaintColor(which);
ll_paintcolor_state.setVisibility(View.GONE);
//图片选择后的回传
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
if (resultCode == RESULT_OK)
if (requestCode == OPEN_PHOTO && data != null)
Uri mImageCaptureUri = data.getData();
photoBmp = null;
if (mImageCaptureUri != null)
try
photoBmp = getBitmapFormUri(this, mImageCaptureUri);//根据uri获取压缩后的bitmap
catch (IOException e)
e.printStackTrace();
frameLayout.setBackground(new BitmapDrawable(photoBmp));
tuyaView.redo();
//压缩bitmap并将它传入到view中
tuyaView.setSrcBitmap(photoBmp);
Log.e("tag", "走到这了。。。。。。。。。。。。。。。");
super.onActivityResult(requestCode, resultCode, data);
private int select_paint_style_paint = 0;
private int select_paint_style_eraser = 1;
/**
* 通过uri获取图片并进行压缩
*
* @param uri
*/
public Bitmap getBitmapFormUri(Activity ac, Uri uri) throws FileNotFoundException, IOException
InputStream input = ac.getContentResolver().openInputStream(uri);
BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
onlyBoundsOptions.inJustDecodeBounds = true;
onlyBoundsOptions.inDither = true;//optional
onlyBoundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
input.close();
//图片宽高
int originalWidth = onlyBoundsOptions.outWidth;
int originalHeight = onlyBoundsOptions.outHeight;
if ((originalWidth == -1) || (originalHeight == -1))
return null;
//图片分辨率以480x适配高度为标准
float hh = 480 * realHeight / screenWidth;//这里设置高度为800f
float ww = 480f;//这里设置宽度为480f
//缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
int be = 1;//be=1表示不缩放
if (originalWidth > originalHeight && originalWidth > ww) //如果宽度大的话根据宽度固定大小缩放
be = (int) (originalWidth / ww);
else if (originalWidth < originalHeight && originalHeight > hh) //如果高度高的话根据宽度固定大小缩放
be = (int) (originalHeight / hh);
if (be <= 0)
be = 1;
//比例压缩
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = be;//设置缩放比例
bitmapOptions.inDither = true;//optional
bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
input = ac.getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
input.close();
return compressImage(bitmap);//再进行质量压缩
/**
* 质量压缩方法
*
* @param image
* @return
*/
public Bitmap compressImage(Bitmap image) throws IOException
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
int options = 100;
while (baos.toByteArray().length / 1024 > 100) //循环判断如果压缩后图片是否大于100kb,大于继续压缩
baos.reset();//重置baos即清空baos
//第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差 ,第三个参数:保存压缩后的数据的流
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
options -= 10;//每次都减少10
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中
Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片
isBm.close();
baos.close();
return bitmap;
@Override
protected void onStop()
super.onStop();
ABIOUtil.recycleBitmap(photoBmp);//释放资源
@Override
protected void onDestroy()
//无法再onstop()中dismiss因为会造成无法弹出进度对话框
if (customProgressDialog != null)
customProgressDialog.dismiss();
customProgressDialog = null;
super.onDestroy();
TuyaView:
/**
* Created by zpxiang.
* 类描述:View实现涂鸦、橡皮擦、撤销以及重做功能
* 进行保存以及图片选择
* 小功能包括画笔颜色、尺寸调整
* 之后添加了画椭圆,画矩形以及画箭头的功能
*/
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
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.view.MotionEvent;
import android.view.View;
import com.sanhai.psdapp.util.AddImageUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TuyaView extends View
private Context context;
private Bitmap mBitmap;
private Canvas mCanvas;
private Path mPath;
private Paint mBitmapPaint;// 画布的画笔
private Paint mPaint;// 真实的画笔
private float mX, mY;// 临时点坐标
private static final float TOUCH_TOLERANCE = 4;
// 保存Path路径的集合
private static List<DrawPath> savePath;
// 保存已删除Path路径的集合
private static List<DrawPath> deletePath;
// 记录Path,paint路径的对象
private DrawPath dp;
private int screenWidth, screenHeight;//控件传进来的宽高,用来表示该tuyaView的宽高
private int currentColor = Color.RED;
private int currentSize = 5;
private int currentStyle = 1;
private int[] paintColor;//颜色集合
private Bitmap srcBitmap;//传递过来的背景图转换成的bitmap
//设置画图样式
private static final int DRAW_PATH = 01;
private static final int DRAW_CIRCLE = 02;
private static final int DRAW_RECTANGLE = 03;
private static final int DRAW_ARROW = 04;
private int[] graphics = new int[]DRAW_PATH,DRAW_CIRCLE,DRAW_RECTANGLE,DRAW_ARROW;
private int currentDrawGraphics = graphics[0];//默认画线
private class DrawPath
public Path path;// 路径
public Paint paint;// 画笔
public TuyaView(Context context, int w, int h)
super(context);
this.context = context;
screenWidth = w;
screenHeight = h;
paintColor = new int[]
Color.parseColor("#E372FF"),Color.parseColor("#FE7C2E"),Color.parseColor("#6CD685"),Color.parseColor("#FFB42B"),Color.parseColor("#000000")
;//画笔颜色的数组
setLayerType(LAYER_TYPE_SOFTWARE, null);//设置默认样式,去除dis-in的黑色方框以及clear模式的黑线效果
initCanvas();
srcBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), mBitmap.getConfig());
savePath = new ArrayList<DrawPath>();//存储画下的路径
deletePath = new ArrayList<DrawPath>();//存储删除的路径,用来恢复,此方法已弃
//初始化画笔画板
public void initCanvas()
setPaintStyle();
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
//不能在ondraw()上的canvas直接作画,需要单独一套的bitmap以及canvas记录路径
mBitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888);
mBitmap.eraseColor(Color.argb(0, 0, 0, 0));//橡皮擦的设置
mCanvas = new Canvas(mBitmap); //所有mCanvas画的东西都被保存在了mBitmap中
mCanvas.drawColor(Color.TRANSPARENT);//设置透明是为了之后的添加背景
//初始化画笔样式
private void setPaintStyle()
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);// 设置外边缘
mPaint.setStrokeCap(Paint.Cap.ROUND);// 形状
mPaint.setAntiAlias(true);
mPaint.setDither(true);
if (currentStyle == 1) //普通画笔功能
mPaint.setStrokeWidth(currentSize);
mPaint.setColor(currentColor);
else //橡皮擦
mPaint.setAlpha(0);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));//橡皮擦效果
mPaint.setColor(Color.TRANSPARENT);
mPaint.setStrokeWidth(50);
currentDrawGraphics = DRAW_PATH;//使用橡皮擦时默认用线的方式擦除
@Override
public void onDraw(Canvas canvas)
// 在之前画板上画过得显示出来
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);
mX = x;
mY = y;
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也可以)
if(currentDrawGraphics == DRAW_PATH)//画线
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
else if(currentDrawGraphics == DRAW_CIRCLE)
mPath.reset();//清空以前的路径,否则会出现无数条从起点到末位置的线
RectF rectF = new RectF(startX,startY,x,y);
mPath.addOval(rectF, Path.Direction.CCW);//画椭圆
// mPath.addCircle((startX + x) / 2, (startY + y) / 2, (float) Math.sqrt(Math.pow(newx, 2) + Math.pow(newy, 2)) / 2, Path.Direction.CCW);
else if(currentDrawGraphics == DRAW_RECTANGLE)
mPath.reset();
RectF rectF = new RectF(startX,startY,x,y);
mPath.addRect(rectF, Path.Direction.CCW);
else if (currentDrawGraphics == DRAW_ARROW)
mPath.reset();
drawAL((int) startX, (int) startY, (int) x, (int) y);
isEmpty = false;//此时证明有path存在,即涂鸦板不会为空
//能连起来
mX = x;
mY = y;
private void touch_up()
if(currentDrawGraphics == DRAW_PATH)
mPath.lineTo(mX, mY);
mCanvas.drawPath(mPath, mPaint);
//将一条完整的路径保存下来
savePath.add(dp);
mPath = null;// 重新置空
private float startX;
private float startY;
private boolean isEmpty = true;//用来处理点击了涂鸦板未生成路径但保存时已不再报空的情况
@Override
public boolean onTouchEvent(MotionEvent event)
float x = event.getX();
float y = event.getY();
switch (event.getAction())
case MotionEvent.ACTION_DOWN:
//记录最初始的点,画圆矩形箭头会用到
startX = event.getX();
startY = event.getY();
// 每次down下去重新new一个Path
mPath = new Path();
//每一次记录的路径对象是不一样的
dp = new DrawPath();
dp.path = mPath;
dp.paint = mPaint;
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
if(isEmpty)
savePath.clear();
invalidate();
break;
return true;
/**
* 撤销
* 撤销的核心思想就是将画布清空,
* 将保存下来的Path路径最后一个移除掉,
* 重新将路径画在画布上面。
*/
public void undo()
if (savePath != null && savePath.size() > 0)
DrawPath drawPath = savePath.get(savePath.size() - 1);
deletePath.add(drawPath);
savePath.remove(savePath.size() - 1);
redrawOnBitmap();
/**
* 重做
*/
public void redo()
if (savePath != null && savePath.size() > 0)
savePath.clear();
redrawOnBitmap();
//将剩下的path重绘
private void redrawOnBitmap()
initCanvas();
Iterator<DrawPath> iter = savePath.iterator();
while (iter.hasNext())
DrawPath drawPath = iter.next();
mCanvas.drawPath(drawPath.path, drawPath.paint);
invalidate();// 刷新
/**
* 恢复,恢复的核心就是将删除的那条路径重新添加到savapath中重新绘画即可
*/
public void recover()
if (deletePath.size() > 0)
//将删除的路径列表中的最后一个,也就是最顶端路径取出(栈),并加入路径保存列表中
DrawPath dp = deletePath.get(deletePath.size() - 1);
savePath.add(dp);
//将取出的路径重绘在画布上
mCanvas.drawPath(dp.path, dp.paint);
//将该路径从删除的路径列表中去除
deletePath.remove(deletePath.size() - 1);
invalidate();
/**
* 以下为样式修改内容
* */
//设置画笔样式
public void selectPaintStyle(int which)
if (which == 0)
currentStyle = 1;
setPaintStyle();
else if (which == 1) //which为1时,选择的是橡皮擦
currentStyle = 2;
setPaintStyle();
//选择画笔大小
public void selectPaintSize(int which)
currentSize = which;
setPaintStyle();
//设置画笔颜色
public void selectPaintColor(int which)
currentColor = paintColor[which];
setPaintStyle();
//保存到sd卡
public String saveToSDCard(Bitmap bitmap)
//获得系统当前时间,并以该时间作为文件名
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
Date curDate = new Date(System.currentTimeMillis());//获取当前时间
String str = formatter.format(curDate) + "paint.png";
String path= "sdcard/" + str;
File file = new File(path);
FileOutputStream fos = null;
try
fos = new FileOutputStream(file);
catch (Exception e)
e.printStackTrace();
//不论是否有背景将所截图保存
bitmap.compress(CompressFormat.PNG, 100, fos);
return tempPath;
//设置背景图
public void setSrcBitmap(Bitmap bitmap)
this.srcBitmap = bitmap;
/**
* 以下为画图方式
* @param which 通过传过来的int类型来修改画图样式
*/
//画线,圆,矩形,以及箭头
public void drawGraphics(int which)
currentDrawGraphics = graphics[which];
/**
* 画箭头
* @param startX 开始位置x坐标
* @param startY 开始位置y坐标
* @param endX 结束位置x坐标
* @param endY 结束位置y坐标
*/
public void drawAL(int startX, int startY, int endX, int endY)
double lineLength = Math.sqrt(Math.pow(Math.abs(endX-startX),2) + Math.pow(Math.abs(endY-startY),2));//线当前长度
double H = 0;// 箭头高度
double L = 0;// 箭头长度
if(lineLength < 320)//防止箭头开始时过大
H = lineLength/4 ;
L = lineLength/6;
else //超过一定线长箭头大小固定
H = 80;
L = 50;
double arrawAngle = Math.atan(L / H); // 箭头角度
double arraowLen = Math.sqrt(L * L + H * H); // 箭头的长度
double[] pointXY1 = rotateVec(endX - startX, endY - startY, arrawAngle, true, arraowLen);
double[] pointXY2 = rotateVec(endX - startX, endY - startY, -arrawAngle, true, arraowLen);
int x3 = (int) (endX - pointXY1[0]);//(x3,y3)为箭头一端的坐标
int y3 = (int) (endY - pointXY1[1]);
int x4 = (int) (endX - pointXY2[0]);//(x4,y4)为箭头另一端的坐标
int y4 = (int) (endY - pointXY2[1]);
// 画线
mPath.moveTo(startX,startY);
mPath.lineTo(endX,endY);
mPath.moveTo(x3, y3);
mPath.lineTo(endX, endY);
mPath.lineTo(x4, y4);
// mPath.close();闭合线条
/**
* 矢量旋转函数,计算末点的位置
* @param x x分量
* @param y y分量
* @param ang 旋转角度
* @param isChLen 是否改变长度
* @param newLen 箭头长度长度
* @return 返回末点坐标
*/
public double[] rotateVec(int x, int y, double ang, boolean isChLen, double newLen)
double pointXY[] = new double[2];
double vx = x * Math.cos(ang) - y * Math.sin(ang);
double vy = x * Math.sin(ang) + y * Math.cos(ang);
if (isChLen)
double d = Math.sqrt(vx * vx + vy * vy);
pointXY[0] = vx / d * newLen;
pointXY[1] = vy / d * newLen;
return pointXY;
public List getSavePath()
return savePath;
Xml布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<FrameLayout
android:id="@+id/fl_boardcontainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/common_view"
android:layout_above="@+id/ll_bottom_boardstyle"
android:background="@color/white"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/DIMEN_20PX"
android:orientation="vertical"
>
<RadioGroup
android:id="@+id/rg_drawgraphics"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/btn_savesd"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:background="@drawable/tuya_save_selector"
android:layout_marginTop="@dimen/DIMEN_160PX"
android:button="@null"/>
<RadioButton
android:id="@+id/btn_revoke"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:background="@drawable/tuya_revoke_selector"
android:layout_marginTop="@dimen/DIMEN_30PX"
android:button="@null"
/>
<RadioButton
android:id="@+id/btn_clean"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:background="@drawable/tuya_clean_selector"
android:layout_marginTop="@dimen/DIMEN_30PX"
android:button="@null"/>
<RadioButton
android:id="@+id/btn_picselect"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:background="@drawable/tuya_picselect_selector"
android:layout_marginTop="@dimen/DIMEN_30PX"
android:button="@null"/>
<RadioButton
android:id="@+id/btn_drawcycle"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:background="@drawable/tuya_circle_selector"
android:layout_marginTop="@dimen/DIMEN_30PX"
android:button="@null"/>
<RadioButton
android:id="@+id/btn_drawrec"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:background="@drawable/tuya_rectangle_selector"
android:layout_marginTop="@dimen/DIMEN_30PX"
android:button="@null"/>
<RadioButton
android:id="@+id/btn_drawarrow"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:background="@drawable/tuya_arrow_selector"
android:layout_marginTop="@dimen/DIMEN_30PX"
android:button="@null"/>
<RadioButton
android:visibility="gone"
android:id="@+id/btn_null"
android:layout_width="@dimen/DIMEN_68PX"
android:layout_height="@dimen/DIMEN_68PX"
android:layout_marginTop="@dimen/DIMEN_10PX"
android:background="@drawable/tuya_save_selector"
android:button="@null"/>
</RadioGroup>
</LinearLayout>
<SeekBar
android:id="@+id/sb_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/ll_bottom_boardstyle"
android:progress="5"
android:max="30"
android:visibility="gone"/>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/DIMEN_2PX"
android:background="#33000000"
android:layout_above="@id/ll_bottom_boardstyle"/>
<LinearLayout
android:id="@+id/ll_bottom_boardstyle"
android:layout_width="match_parent"
android:layout_height="@dimen/DIMEN_100PX"
android:layout_alignParentBottom="true"
android:background="#F0F5D1"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/ll_paint_style"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_horizontal"
android:background="@drawable/bottom_color_selector"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_paintstyle"
android:layout_width="@dimen/DIMEN_50PX"
android:layout_height="@dimen/DIMEN_50PX"
android:background="@drawable/paint_style"
android:layout_marginTop="@dimen/DIMEN_25PX" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_paint_color"
android:background="@drawable/bottom_color_selector"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_marginTop="@dimen/DIMEN_25PX"
android:layout_width="@dimen/DIMEN_50PX"
android:layout_height="@dimen/DIMEN_50PX"
android:background="@drawable/paint_color" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_paint_size"
android:background="@drawable/bottom_color_selector"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_marginTop="@dimen/DIMEN_25PX"
android:layout_width="@dimen/DIMEN_50PX"
android:layout_height="@dimen/DIMEN_50PX"
android:background="@drawable/paint_size" />
</LinearLayout>
</LinearLayout>
<include layout="@layout/layout_paintcolor_popup"/>
</RelativeLayout>
以上是关于自定义view实现涂鸦(画板)功能的主要内容,如果未能解决你的问题,请参考以下文章
DrawBoard 是一个自定义 View 实现的画板;方便对图片进行各种编辑或涂鸦相关操作