需求背景
买东西付款的时候,或者银行一些业务办理的时候,我们常常需要签名。目前大多数场景都是电子签名。
我们需要拿到当前用户签名的轨迹,然后把签名的轨迹实时的发送到另外一台机器上。从而让工作人员实时看到对方签名的实况。
那么这个业务怎么实现呢?
过来看看实现它的完整思路!
定义接口,获取down和move事件获取到的坐标信息
/**
* 监听签名坐标
*/
public interface ICoordinatesListener {
/**
* click down的坐标
*/
void getDownCoordinates(int x, int y);
/**
* touch move的坐标
*/
void getCoordinates(int x,int y);
}
获取触摸事件MotionEvent
获取触摸事件,这是很自然的做法,把触摸事件的拿到的坐标点传递进到我们定义的接口中来。
对应的系统方法是,下面是伪代码,可以根据你项目实际需求进行更精细的控制:
@Override
public boolean onTouchEvent(MotionEvent event) {
if(down){
getDownCoordinates(int x, int y)
}
if(move){
getCoordinates(int x,int y);
}
}
需要注意的是,由于不同屏幕有不同的像素密度和尺寸,实际上获取到的x,y轴的坐标点可能会和你画板绘制时候拿到的view的宽高对不上,需要进行一定的转换。
对拿到的x、y坐标进行处理
我们实现接口的匿名内部类,作为监听器传递到画板View中去。当onTouchEvent来数据的时候,我们并可以拿到坐标信息。
可以脑海里想象一下,当我们画一条线,是不是一定有先点击,就是down的动作,然后我们手指移动,就是move的动作。
我们在代码中,先记录down的坐标点,
/**
* 获取当前点击down的坐标
*/
public void setTouchDownCoordinate(int touchDownX, int touchDownY) {
preX = touchDownX;
preY = touchDownY;
}
接着发送move的坐标点:
public void sendSignatureCoordinates(int xCoordinate, int yCoordinate) {
//待实现
}
接着先把监听器函数实现了:
binding.sign.setCoordinatesListener(new ICoordinatesListener() {
@Override
public void getDownCoordinates(int x, int y) {
System.out.println("down coordinate :x = " + x + " y = " + y);
getSignature().setTouchDownCoordinate(x, y);
}
@Override
public void getCoordinates(int x, int y) {
System.out.println("move coordinate :x = " + x + " y = " + y);
getSignature().sendSignatureCoordinates(x, y);
}
});
核心点在于sendSignatureCoordinates的实现。
发送实时坐标点
我们知道发送坐标数据,这是一个相对耗时的操作。更何况,当我们快速画线,并且大量快速画线的时候,想要接受对方高性能的获取准确的坐标点的话,是需要考虑一下方案的。
我们在主线程做这事情肯定不行,这然造成卡顿,甚至ARN。那么新开一个子线程呢?
效率也不高,众所周知,多线程并发是提高性能的好办法。
那么我们就使用线程池来实现:
public void sendSignatureCoordinates(int xCoordinate, int yCoordinate) {
ThreadPoolManager.getInstance().runInBackground(new Runnable() {
@Override
public void run() {
发送坐标点操作
}
});
}
需要注意是发送坐标点的时候,为了避免多线程造成线程抢占的问题。我们需要在合适的地方加个锁:
public void sendSignatureCoordinates(int xCoordinate, int yCoordinate) {
ThreadPoolManager.getInstance().runInBackground(new Runnable() {
@Override
public void run() {
同步锁(){
发送坐标点操作
}
}
});
}
跑一下程序,通过日志观察,我们得知坐标点按照绘制的先后顺序有序的发送到目标终端了,速度也还不错。
到这里我们会以为大功告成了。
结果目标终端实时绘制了我们发送过去的坐标点,发现画到线条变成了虚线。
我们获取到的坐标点是从系统触摸事件中拿到的,我们看到自己终端画线并发虚线,而是实线。为什么发送过去就是虚线了呢?
看下画板的实现代码就可以知道,画板画线的实现是通过贝塞尔曲线来实现的。3个坐标点即可连成一条实线。
所以目标终端需要实时显示实线有两个方案:
- 方案1:也通过贝塞尔曲线实现
- 方案2:我们虚线的坐标集合自动填充为实线,需要添加确实的坐标点。
考虑到目标终端需要的是完全实线的坐标点集合,我这边直接采取方案2。
算法实现:把虚线通过补偿算法填充为实线
当场手撸了一个算法,如下:
private int preX;
private int preY;
/**
* 通过算法补全当快速滑动时候touchEvent没法捕抓的,漏掉的点,防止发送的线段缺失一个个的point
* 思路:
* 1.记录前一个坐标,记录为:preX坐标,preY坐标,后一个坐标记录为:x,y
* 2.记录前后两个坐标的差值:dvalueX = x - preX,dvalueY = y - preY
*
* @param x x坐标
* @param y y坐标
* @param compensationValue 补偿值,
*/
private synchronized void fillWithEmptyPoints(int x, int y,
int compensationValue) {
//发送第一个当前获取到的坐标
/*System.out.printf("--------begin--------- (%d :%d) (%d :%d)\n", preX, preY, x, y);*/
//算法补偿
while (true) {
int dValueX = Math.abs(x - preX);
int dValueY = Math.abs(y - preY);
/*System.out.println("dValueX = " + dValueX + " dValueY = " + dValueY);*/
//假如用户画点,退出循环,假如当前没有漏点,退出循环
if (dValueX > compensationValue || dValueY > compensationValue
|| (dValueX <= 1 && dValueY <= 1)) {
preX = x;
preY = y;
/*System.out.printf("send(%d,%d)\n", preX, preY);*/
sendRealtimeCoordinate(preX, preY);
break;
}
if (dValueX > dValueY) {
if (x > preX) {
++preX;
} else {
--preX;
}
} else if (dValueX < dValueY) {
if (y > preY) {
++preY;
} else {
--preY;
}
} else {
if (x > preX) {
++preX;
} else {
--preX;
}
if (y > preY) {
++preY;
} else {
--preY;
}
}
/*System.out.printf("send(%d,%d)\n", preX, preY);*/
sendRealtimeCoordinate(preX, preY);
}
/*System.out.println("--------end----------");*/
}
说说这个算法的使用方法,入参传入了当前手指触摸到并相应的x,y坐标点,compensationValue作为填充系数,我们根据不同的屏幕像素密度和尺寸等信息,填入不同的系数。需要同时兼顾到目标终端拿到的实线坐标集合的效果和性能表现。根据测试效果调整即可。
思路很简单,我们实时绘制的时候,首先会有一个落脚点,拿到它,我们移动的时候,可能会直接拿到坐标点(0,0)和坐标点(10,10),这样的话我们看着就是很明显的虚线了,我们计算两个点距离的绝对值,假如绝对值大于入参compensationValue的话,那么就需要补偿发送缺失的坐标点。
从而达到补偿的效果。
需要注意的是,我们由于使用了多线程,需要做好线程同步。
完成
测试,从目标终端可以看到很不错的实时签名效果。
以上便是实现方案的完整内容了。如有什么疑问,可以评论区交流讨论。