版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/qq_37717853/article/details/86641516
通过自定义view实现屏幕手写签名效果,可以上一步,清空,可以保存签名为图片格式到本地。
效果:
思路其实很简单,肯定是自定义一个控件,然后在里面动态获取用户的触屏滑动的点的坐标并保存下来,一开始我想到的是保存path,但是又想过用canvas单纯的drawPoint(),或者drawlines()之类的,稍微试了下,不是很好,最终我还是选择了一开始想到的drawPath()的方案。
1.自定义控件customView继承自View,当然,为了使控件更灵活易拓展,将里面用到几个属性也设定为自定义,那么在values文件夹下创建一个attr_customview.xml的文件,在里面定义customView的属性: <resources>
<declare-styleable name="customView">
<!--画布背景色-->
<attr name="canvasColor" format="color"/>
<!--画笔颜色-->
<attr name="paintColor" format="color"/>
<!--画笔大小-->
<attr name="paintSrowkeWidth" format="integer"/>
</declare-styleable>
</resources>
<resources>
<declare-styleable name="customView">
<!--画布背景色-->
<attr name="canvasColor" format="color"/>
<!--画笔颜色-->
<attr name="paintColor" format="color"/>
<!--画笔大小-->
<attr name="paintSrowkeWidth" format="integer"/>
</declare-styleable>
</resources>
2.在customView的构造方法进行一些初始化,这里除了拿到自定义的属性、以及创建画笔之外,还定义了两个集合,一个用于保存路径 path,一个用于保存路径上的点 point,为什么要用集合呢,因为签名大都不可能一笔完成吧,所以肯定有多条path需要在canvas中呈现。 public customView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化
init(context,attrs);
}
public customView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化
init(context,attrs);
}
/*初始化*/ private void init(Context context,AttributeSet attrs) {
//获取TypedArray,取出自定义属性
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.customView);
//画布背景色,默认为白色
canvasColor = array.getColor(R.styleable.customView_canvasColor,Color.WHITE);
//画笔颜色,默认为黑色
paintColor = array.getColor(R.styleable.customView_paintColor,Color.BLACK);
//画笔笔触大小,默认为5
paintStrokeWidth = array.getInteger(R.styleable.customView_paintSrowkeWidth,5);
pathList = new ArrayList<>();//路径集合
pointList = new ArrayList<>();//点集合
paint = new Paint();//画笔
paint.setColor(paintColor);//画笔颜色
paint.setStyle(Paint.Style.STROKE);//描边
paint.setStrokeWidth(paintStrokeWidth);//笔触大小
paint.setAntiAlias(true);
}
private void init(Context context,AttributeSet attrs) {
//获取TypedArray,取出自定义属性
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.customView);
//画布背景色,默认为白色
canvasColor = array.getColor(R.styleable.customView_canvasColor,Color.WHITE);
//画笔颜色,默认为黑色
paintColor = array.getColor(R.styleable.customView_paintColor,Color.BLACK);
//画笔笔触大小,默认为5
paintStrokeWidth = array.getInteger(R.styleable.customView_paintSrowkeWidth,5);
pathList = new ArrayList<>();//路径集合
pointList = new ArrayList<>();//点集合
paint = new Paint();//画笔
paint.setColor(paintColor);//画笔颜色
paint.setStyle(Paint.Style.STROKE);//描边
paint.setStrokeWidth(paintStrokeWidth);//笔触大小
paint.setAntiAlias(true);
}
3.然后是重写onTouchEvent方法,通过触屏事件进行相应操作,这里我试图直接在响应事件的时候保存坐标点到pathList集合中,然后调用postInvalidate()去刷新画布,但是这样操作画布画出来的是空白,不明所以,如果有大神知道具体原因望悉知,这里我的解决方案是通过发handler消息,在onTouchEvent之外进行保存path以及刷新canvas的操作。 public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://按下时
//清空点集合
pointList.clear();
break;
case MotionEvent.ACTION_MOVE://移动时
//动态获取全部坐标点
Point point = new Point();
point.x = (int) event.getX();
point.y = (int) event.getY();
pointList.add(point);
break;
case MotionEvent.ACTION_UP://松开手时
//画路径
handler.sendEmptyMessage(1);
break;
}
return true;
}
/*收到消息,保存路径*/
@SuppressLint("HandlerLeak")
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
//创建一条新的路径
Path path = new Path();
//起始点
path.moveTo(pointList.get(0).x,pointList.get(0).y);
//画路径
for(Point point:pointList){
path.lineTo(point.x,point.y);
}
//保存路径到集合
pathList.add(path);
//刷新画布
invalidate();
}
};
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://按下时
//清空点集合
pointList.clear();
break;
case MotionEvent.ACTION_MOVE://移动时
//动态获取全部坐标点
Point point = new Point();
point.x = (int) event.getX();
point.y = (int) event.getY();
pointList.add(point);
break;
case MotionEvent.ACTION_UP://松开手时
//画路径
handler.sendEmptyMessage(1);
break;
}
return true;
}
/*收到消息,保存路径*/
@SuppressLint("HandlerLeak")
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
//创建一条新的路径
Path path = new Path();
//起始点
path.moveTo(pointList.get(0).x,pointList.get(0).y);
//画路径
for(Point point:pointList){
path.lineTo(point.x,point.y);
}
//保存路径到集合
pathList.add(path);
//刷新画布
invalidate();
}
};
4.然后是在onDraw()中将所有路径画出来: protected void onDraw(Canvas canvas) {
//设置画布底色
canvas.drawColor(canvasColor);
//遍历保存的所有路径
if(pathList!=null && pathList.size()>0){
for (Path path : pathList) {
//画路径
canvas.drawPath(path, paint);
}
}
super.onDraw(canvas);
}
protected void onDraw(Canvas canvas) {
//设置画布底色
canvas.drawColor(canvasColor);
//遍历保存的所有路径
if(pathList!=null && pathList.size()>0){
for (Path path : pathList) {
//画路径
canvas.drawPath(path, paint);
}
}
super.onDraw(canvas);
}
5.接下来是 [保存] [上一步] [清空] 这三种情况的实现,同样是在customView中用几个public方法实现它们,以便activity中直接调用。上一步和清空操作比较简单,对pathList进行操作即可,保存操作这里我用的是getDrawingCache的方式:
清空 public void cleanCanvas(){
//即清空集合中的所有路径
pathList.clear();
//刷新画布
invalidate();
}
public void cleanCanvas(){
//即清空集合中的所有路径
pathList.clear();
//刷新画布
invalidate();
}
上一步 public void lastStep(){
//去除集合中的最后一条路径,即为用户的最后一步操作
if(pathList!=null && pathList.size()>0){
pathList.remove(pathList.size()-1);
}
//刷新画布
invalidate();
}
public void lastStep(){
//去除集合中的最后一条路径,即为用户的最后一步操作
if(pathList!=null && pathList.size()>0){
pathList.remove(pathList.size()-1);
}
//刷新画布
invalidate();
}
保存 public void saveCanvas(Context context) {
long time = System.currentTimeMillis();//系统时间
@SuppressLint("SimpleDateFormat")
String picName = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time));//图片名称
File file = Environment.getExternalStorageDirectory();//保存在内部存储
String sava_file = file.getAbsolutePath()+"/"+context.getPackageName();//内部存储根目录作为创建文件夹路径
String save_path = sava_file+"/" + picName + ".jpg";//图片路径
//生成文件夹
createFile(sava_file);
//生成图片并保存
try {
getDrawingCache().compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(new File(save_path)));//保存,quality为图片大小
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(context, "文件已保存到:"+save_path, Toast.LENGTH_LONG).show();
}
/*在内部存储根目录下,生成一个以包名命名的文件夹*/
private void createFile(String filePath) {
File file;
try {
file = new File(filePath);
if(!file.exists()){//如果文件夹不存在
file.mkdir();//创建
}
}catch (Exception e){
Log.i("error:",e.toString());
}
}
public void saveCanvas(Context context) {
long time = System.currentTimeMillis();//系统时间
@SuppressLint("SimpleDateFormat")
String picName = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time));//图片名称
File file = Environment.getExternalStorageDirectory();//保存在内部存储
String sava_file = file.getAbsolutePath()+"/"+context.getPackageName();//内部存储根目录作为创建文件夹路径
String save_path = sava_file+"/" + picName + ".jpg";//图片路径
//生成文件夹
createFile(sava_file);
//生成图片并保存
try {
getDrawingCache().compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(new File(save_path)));//保存,quality为图片大小
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(context, "文件已保存到:"+save_path, Toast.LENGTH_LONG).show();
}
/*在内部存储根目录下,生成一个以包名命名的文件夹*/
private void createFile(String filePath) {
File file;
try {
file = new File(filePath);
if(!file.exists()){//如果文件夹不存在
file.mkdir();//创建
}
}catch (Exception e){
Log.i("error:",e.toString());
}
}
6.接下来是activity中的操作,其实就很简单了,初始化控件,设置监听,调用自定义控件方法,就这关键几步,当然如果是Android6.0或者以上的系统,对于文件存储权限的申请要进行动态申请,这里也做了简单的适配。
设置监听: private void setOnClickListeners() {
//保存
tvSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//保存图片到本地
savePic();
}
});
//上一步
tvLast.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
cvCanvas.lastStep();//调用自定义控件中的方法
}
});
//清空
tvClean.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
cvCanvas.cleanCanvas();//调用自定义控件中的方法
}
});
}
private void setOnClickListeners() {
//保存
tvSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//保存图片到本地
savePic();
}
});
//上一步
tvLast.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
cvCanvas.lastStep();//调用自定义控件中的方法
}
});
//清空
tvClean.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
cvCanvas.cleanCanvas();//调用自定义控件中的方法
}
});
}
动态申请权限,保存图片: /*保存图片到本地*/
private void savePic() {
//版本适配,如果是Android6.0及以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//先判断是否有权限,不等于1即没有授权
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
//申请权限
ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS_STORAGE, 9527);
}
} else {
cvCanvas.setDrawingCacheEnabled(true);//开启缓存
cvCanvas.saveCanvas(MainActivity.this);//保存画布内容
cvCanvas.setDrawingCacheEnabled(false);//关闭缓存
}
}
/*要申请的权限*/
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
/*动态申请权限返回监听*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 9527) {//返回授权成功
//保存图片
cvCanvas.setDrawingCacheEnabled(true);//开启缓存
cvCanvas.saveCanvas(MainActivity.this);//保存画布内容
cvCanvas.setDrawingCacheEnabled(false);//关闭缓存
}
}
/*保存图片到本地*/
private void savePic() {
//版本适配,如果是Android6.0及以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//先判断是否有权限,不等于1即没有授权
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
//申请权限
ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS_STORAGE, 9527);
}
} else {
cvCanvas.setDrawingCacheEnabled(true);//开启缓存
cvCanvas.saveCanvas(MainActivity.this);//保存画布内容
cvCanvas.setDrawingCacheEnabled(false);//关闭缓存
}
}
/*要申请的权限*/
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
/*动态申请权限返回监听*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 9527) {//返回授权成功
//保存图片
cvCanvas.setDrawingCacheEnabled(true);//开启缓存
cvCanvas.saveCanvas(MainActivity.this);//保存画布内容
cvCanvas.setDrawingCacheEnabled(false);//关闭缓存
}
}
到这里基本就可以收工了,layout文件就是customView的一个引用加上三个按钮,这里就不贴出来啦,对了,别忘了在AndroidManifest.xml中添加权限: <!--添加写入数据权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--添加写入数据权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
最后照例提供demo:
下载地址:https://download.csdn.net/download/qq_37717853/10937680