前言
促进小飞哥写代码的动力只有两个。第一个是为了挣钱钱,第二个是为了挣更多的钱。所以毫无疑问,电子签名又是公司最近需要开发的新功能。
应用场景:以前去银行办理业务都得去柜台,填N张表格,写N多个签名。随着智能机的普及、移动终端app的使用场景越来越广泛。手机银行app也给越来越多的人办理银行业务提供便捷,妈妈再也不用但是我们去银行排队浪费时间了。然而,有的业务该走的流程必须得走,所以电子签名的需求也就凸显出来。
这是一篇技术博客,那废话就不多说了。
原理
在Android开发中,所有用户可见的页面都是由一个个View(视图)拼接而成。Google 已经提供了很多的基础的View组件,如:显示图片的ImageView,显示文字的TextView,这些已经被造好的*能够让我们很方便的去开发绝大多数app。除此之外,有些*还得自己造,不然要程序员干嘛呢?虽然网上已经有很多已经实现过电子签名,但是小飞哥一直以来都崇尚自己动手,哪怕是Hello Word 也绝不copy。(又扯远了!!!)
说了那么多,实现电子签名的组件(在程序中命名SignView)需要继承View,这也是面向对象编程的一大特色,直接继承View,为我们省去不少麻烦。记录用户在触屏上滑动的轨迹、重写onDraw方法将轨迹在屏幕上绘制出来,就实现了我们需要的效果了。然而一切并没有结束,还需要保存为图片,先上图:
定义属性
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;// 笔画宽度
- 画笔:在屏幕上绘制出我们写下的笔迹,画笔主要有两个属性,颜色和粗细。这也是在程序中只开放设置接口的两个属性。
- 笔迹集合:不否认有人写字时喜欢一气呵成,但是支持多笔输入,可以让程序支持更多输入场景。
- 默认值:默认字迹颜色黑色,字迹宽度10 个像素点(这是一个很细的线,随便写的,不要介意)
接受输入信息
/** * @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
方法,清空当前视图的图像信息并通知系统刷新视图,增加显示刚刚输入的信息。
显示输入信息
/** * @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(画布)将之前保存的笔迹绘制出现。参数中,指定了一个画笔。
定义画笔
/** * 初始化画笔 * * @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对象或存文件。
- 当书写错误,可以清空屏幕重新书写等。
设置画笔属性
在Android中,使用View有两种方式。
1. 在xml布局文件中添加view并指定组件属性
2. 在代码中动态添加view,最不被开发者所接受的方式
代码设置
/** * @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方法,可以在任意时候设置画笔的属性。
自定义组件属性
既然可以通过再xml布局中使用自定义的组件,那么我们当然也希望可以在xml布局中静态的指定画笔颜色和宽度。10像素点的粗细是不被使用者所接受的。
通过如下的代码,给SignView声明两个新属性。
<?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>
使用自定义属性
<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的四个构造函数的作用。
/** * @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
参数。除了第四个是在代码中动态添加组件使用,其余三个可以映射到布局文件中的代码。
/** * 解析类型数组 * * @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);
}
获取图像信息
/** * 保存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;
}
保存图像到文件
/** * 将图像保存到文件 * * @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;
}
}
清空输入
public void clearPath() {
lines.removeAll(lines);
invalidate();
}