通過自定義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