Android 画布使用之电子签名

促进小飞哥写代码的动力只有两个。第一个是为了挣钱钱,第二个是为了挣更多的钱。所以毫无疑问,电子签名又是公司最近需要开发的新功能。

应用场景:以前去银行办理业务都得去柜台,填N张表格,写N多个签名。随着智能机的普及、移动终端app的使用场景越来越广泛。手机银行app也给越来越多的人办理银行业务提供便捷,妈妈再也不用但是我们去银行排队浪费时间了。然而,有的业务该走的流程必须得走,所以电子签名的需求也就凸显出来。

这是一篇技术博客,那废话就不多说了。

1 原理

在Android开发中,所有用户可见的页面都是由一个个View(视图)拼接而成。Google 已经提供了很多的基础的View组件,如:显示图片的ImageView,显示文字的TextView,这些已经被造好的轮子能够让我们很方便的去开发绝大多数app。除此之外,有些轮子还得自己造,不然要程序员干嘛呢?虽然网上已经有很多已经实现过电子签名,但是小飞哥一直以来都崇尚自己动手,哪怕是Hello Word 也绝不copy。(又扯远了!!!)

说了那么多,实现电子签名的组件(在程序中命名SignView)需要继承View,这也是面向对象编程的一大特色,直接继承View,为我们省去不少麻烦。记录用户在触屏上滑动的轨迹、重写onDraw方法将轨迹在屏幕上绘制出来,就实现了我们需要的效果了。然而一切并没有结束,还需要保存为图片,先上图:

2 定义属性

1
2
3
4
5
6
7
8
9
private Paint linePaint;// 画笔

private ArrayList<Path> lines;// 写字的笔迹,支持多笔画
private int lineCount;// 记录笔画数目

private final int DEFAULT_LINE_WIDTH = 10;// 默认笔画宽度

private int lineColor = Color.BLACK;// 默认字迹颜色(黑色)
private float lineWidth = DEFAULT_LINE_WIDTH;// 笔画宽度
  1. 画笔:在屏幕上绘制出我们写下的笔迹,画笔主要有两个属性,颜色和粗细。这也是在程序中只开放设置接口的两个属性。
  2. 笔迹集合:不否认有人写字时喜欢一气呵成,但是支持多笔输入,可以让程序支持更多输入场景。
  3. 默认值:默认字迹颜色黑色,字迹宽度10 个像素点(这是一个很细的线,随便写的,不要介意)

3 接受输入信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @file Framework:com.flueky.android.view.SignView.java
* @author flueky zuokefei0217@163.com
* @time 2016年12月19日 上午10:49:58
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
/**
* 考虑到这个view会出现在lib工程里,因此使用if else
*/
if (event.getAction() == MotionEvent.ACTION_DOWN) {// 按下这个屏幕
Path path = new Path();
path.moveTo(event.getX(), event.getY());
lines.add(path);
lineCount = lines.size();
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {// 在屏幕上移动
lines.get(lineCount - 1).lineTo(event.getX(), event.getY());
invalidate();
} else {

}
return true;
}

用户点击和在屏幕上移动都会触发该方法。MotionEvent指的是手指在屏幕上的运动事件。包含动作类型:按下、移动、抬起。点击屏幕上的位置,通过event.getX(),event.getY()方法获取。

用Path(路径)类,按下屏幕为记录笔画的开始,在屏幕上移动记录笔画的轨迹。调用invalidate方法,清空当前视图的图像信息并通知系统刷新视图,增加显示刚刚输入的信息。

4 显示输入信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @file Framework:com.flueky.android.view.SignView.java
* @author flueky zuokefei0217@163.com
* @time 2016年12月19日 上午10:47:19
* @see android.view.View#onDraw(android.graphics.Canvas)
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (lines != null && lines.size() > 0) {
for (Path path : lines)
canvas.drawPath(path, linePaint);
}
}

onDraw方法,多数情况下为系统调用,(用户也可以自己调用,后面用到),通过Canvas(画布)将之前保存的笔迹绘制出现。参数中,指定了一个画笔。

5 定义画笔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 初始化画笔
*
* @file Framework:com.flueky.android.view.SignView.java
* @author flueky zuokefei0217@163.com
* @time 2016年12月19日 下午5:23:26
*/
private void initLinePaint() {
linePaint = new Paint();
linePaint.setColor(lineColor);
linePaint.setStrokeWidth(lineWidth);
linePaint.setStrokeCap(Cap.ROUND);
linePaint.setPathEffect(new CornerPathEffect(50));
linePaint.setStyle(Style.STROKE);
}

简简单单的几行,每行作用都极大。
首先new一个Paint对象,(程序猿间经常自嘲,我们的对象都是new出来的)。其次是设置画笔颜色和宽度。除了最后一行,设置画笔风格为stroke(绳子,这里命名很形象,还有其他两个风格就不多做表述,很抽象的概念)。其余两行是设置笔迹平滑的重要方法。Cap.ROUND使笔迹起始、结束位置为圆形,PahtEfect指笔迹的风格,CornerPathEffect在拐角处添加弧度,弧度半径50像素点。

绘制和显示笔迹的原理部分就介绍完了。在使用中,有以上的代码还远远不够。

  • 需要开放出接口,使别人可以自主的设置笔迹颜色、宽度。
  • 可以让别人获取到输入的图像信息,转成Bitmap对象或存文件。
  • 当书写错误,可以清空屏幕重新书写等。

6 设置画笔属性

在Android中,使用View有两种方式。

  1. 在xml布局文件中添加view并指定组件属性
  2. 在代码中动态添加view,最不被开发者所接受的方式

6.1 代码设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @param lineColor
* the lineColor to set
*/
public void setLineColor(int lineColor) {
this.lineColor = lineColor;
linePaint.setColor(lineColor);
}

/**
* @param lintWidth
* the lintWidth to set
*/
public void setLineWidth(float lineWidth) {
this.lineWidth = lineWidth;
linePaint.setStrokeWidth(lineWidth);
}

通过在代码中调用组件的set方法,可以在任意时候设置画笔的属性。

6.2 自定义组件属性

既然可以通过再xml布局中使用自定义的组件,那么我们当然也希望可以在xml布局中静态的指定画笔颜色和宽度。10像素点的粗细是不被使用者所接受的。

通过如下的代码,给SignView声明两个新属性。

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="SignView">
<attr name="lineColor" format="color" />
<attr name="lineWidth" format="dimension" />
</declare-styleable>

</resources>

6.3 使用自定义属性

1
2
3
4
5
6
7
<com.flueky.android.view.SignView
android:id="@+id/main_sign"
android:layout_width="400dp"
android:layout_height="400dp"
app:lineColor="#FF0000"
app:lineWidth="6dp" >
</com.flueky.android.view.SignView>

眼尖的读者们很定奇怪,app是怎么来的,怎么通过 app:lineColor就能使用我们之前新声明的两个属性。

如图所所示,需要在布局文件的根节点定义app属性。同第一行定义android属性一样。我们只需要将res后面被涂改的部分替换成我们自己应用的包名即可。如果是在lib工程里,需要写成 xmlns:app="http://schemas.android.com/apk/res-auto"

最后,还需要在代码中,获取到在xml布局布局中设置的属性值。需要介绍下view的四个构造函数的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@SuppressLint("NewApi")
public SignView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
if (attrs != null) {
TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.SignView, defStyleAttr, defStyleRes);
parseTyepdArray(tArray);
}
initLinePaint();
lines = new ArrayList<Path>();
}

/**
* @param context
* @param attrs
* @param defStyleAttr
*/
public SignView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.SignView, defStyleAttr, 0);
parseTyepdArray(tArray);
}
initLinePaint();
lines = new ArrayList<Path>();
}

/**
* @param context
* @param attrs
*/
public SignView(Context context, AttributeSet attrs) {
super(context, attrs);
if (attrs != null) {
TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.SignView);
parseTyepdArray(tArray);
}
initLinePaint();
lines = new ArrayList<Path>();
}

/**
* @param context
*/
public SignView(Context context) {
super(context);
initLinePaint();
lines = new ArrayList<Path>();
}

以上4个构造函数中,三个都包含AttributeSet参数。除了第四个是在代码中动态添加组件使用,其余三个可以映射到布局文件中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 解析类型数组
*
* @file Framework:com.flueky.android.view.SignView.java
* @author flueky zuokefei0217@163.com
* @time 2016年12月19日 下午5:15:25
* @param tArray
*/
private void parseTyepdArray(TypedArray tArray) {
lineColor = tArray.getColor(R.styleable.SignView_lineColor, Color.BLACK);
lineWidth = tArray.getDimension(R.styleable.SignView_lineWidth, DEFAULT_LINE_WIDTH);
}

7 获取图像信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 保存view视图的bitmap信息
*
* @file Framework:com.flueky.android.view.SignView.java
* @author flueky zuokefei0217@163.com
* @time 2016年12月19日 下午7:00:01
* @return
*/
public Bitmap getImage() {
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
/**
* 绘制背景
*/
Drawable bgDrawable = getBackground();
if (bgDrawable != null)
bgDrawable.draw(canvas);
else
canvas.drawColor(Color.WHITE);
draw(canvas);// 绘制view视图的内容
return bitmap;
}

8 保存图像到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 将图像保存到文件
*
* @file Framework:com.flueky.android.view.SignView.java
* @author flueky zuokefei0217@163.com
* @time 2016年12月19日 下午7:00:49
* @param filePath
* @return 返回false表示保存失败
*/
public boolean saveImageToFile(String filePath) {
try {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
getImage().compress(CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
return true;
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) {
return false;
}
}

9 清空输入

1
2
3
4
public void clearPath() {
lines.removeAll(lines);
invalidate();
}

觉得有用?那打赏一个呗。[去打赏](/donate/)

Author: flueky
Link: http://example.com/229/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.