在信用卡支付流程中,使用手写签名能够提高支付的安全性,并有效降低过程成本。使用Square在手机上进行支付,用户可以用手指在屏幕上签名,无需拿出笔来在收据上签字。

小窍门:该界面中提供了手机摇一摇清屏的功能

用户在该界面提供的签名,将签署在电子邮件收据中,以帮助Square监测和防止消费欺诈。

下面我们尝试在Android客户端上实现该界面,先尝试从最简单可行的方式开始:生成一个自定义View,能够监听触屏事件,并根据触摸路径画出点。

Java代码  收藏代码
public class SignatureView extends View {  private Paint paint = new Paint();  private Path path = new Path();  public SignatureView(Context context, AttributeSet attrs) {  super(context, attrs);  paint.setAntiAlias(true);  paint.setColor(Color.BLACK);  paint.setStyle(Paint.Style.STROKE);  paint.setStrokeJoin(Paint.Join.ROUND);  paint.setStrokeWidth(5f);  }  @Override  protected void onDraw(Canvas canvas) {  canvas.drawPath(path, paint);  }  @Override  public boolean onTouchEvent(MotionEvent event) {  float eventX = event.getX();  float eventY = event.getY();  switch (event.getAction()) {  case MotionEvent.ACTION_DOWN:  path.moveTo(eventX, eventY);  return true;  case MotionEvent.ACTION_MOVE:  case MotionEvent.ACTION_UP:  path.lineTo(eventX, eventY);  break;  default:  return false;  }  // Schedules a repaint.  invalidate();  return true;  }
}

可以看到实现出来的效果与预期有一定的差距——签名的笔画呈硬邦邦的锯齿状,而且与用户交互迟钝。

下面我们尝试从两个不同的途径解决这个问题。

触屏事件丢失

该实现效果的问题之一是,自定义View的响应与绘制未能跟上用户手指的触屏动作。我们一开始的顾虑是:

1.Android对触屏事件的采样率过低

2.绘制事件阻塞了触屏事件的采样

幸运的是,经过实验考证,这两个顾虑都没有发生。同时,我们发现Android对触屏事件进行批量处理。传递给onTouchEvent()的每一个MotionEvent都包含上至前一个onTouchEvent()调用之间捕获的若干个坐标点。如果将这些点都加入到绘制中,可使签名效果更加平滑。

隐藏的坐标数组可以通过以下MotionEvent类的方法获取

·getHistorySize()

·getHistoricalX(int)

·getHistoricalY(int)

下面我们利用这些方法,将中间点包含进SignatureView的绘制:

Java代码  收藏代码
public class SignatureView extends View {  public boolean onTouchEvent(MotionEvent event) {  ...  switch (event.getAction()) {  case MotionEvent.ACTION_MOVE:  case MotionEvent.ACTION_UP:  // When the hardware tracks events faster than they are delivered,  // the event will contain a history of those skipped points.  int historySize = event.getHistorySize();  for (int i = 0; i < historySize; i++) {  float historicalX = event.getHistoricalX(i);  float historicalY = event.getHistoricalY(i);  path.lineTo(historicalX, historicalY);  }  // After replaying history, connect the line to the touch point.  path.lineTo(eventX, eventY);  break;  ...  }
}

这个简单的改进,使签名效果外观有了显著的提升。但该View对用户触屏的响应能力仍然不足。

局部刷新

我们的SignatureView在每一次调用onTouchEvent()时,会在触屏坐标之间画线,并进行全屏刷新——即使只是很小的像素级变动,也需要全屏重绘。

显然,全屏重绘效率低下且没有必要。我们可以使用 View.invalidate(Rect) 方法,选择性地对新添画线的矩形区域进行局部刷新,可以显著提高绘制性能。

采用的算法思路如下:

1.创建一个代表脏区域的矩形;

2.获得 ACTION_DOWN 事件的 X 与 Y 坐标,用来设置矩形的顶点;

3.获得 ACTION_MOVE 和 ACTION_UP 事件的新坐标点,加入到矩形中使之拓展开来(别忘了上文说过的历史坐标点);

4.刷新脏区域。

采用该算法后,我们能够明显感觉到触屏响应性能的大幅提升。

出炉

以上我们对SignatureView进行了两方面的改造提升:将触屏事件的中间点加入绘制,使笔画更加流畅逼真;以局部刷新取代全屏刷新,提高绘图性能,使触屏响应更加迅速。

最终出炉的效果:


下面是SignatureView的最终完成代码,我们去掉了一些无关的方法(如摇动检测)

Java代码  收藏代码
public class SignatureView extends View {  private static final float STROKE_WIDTH = 5f;  /** Need to track this so the dirty region can accommodate the stroke. **/  private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;  private Paint paint = new Paint();  private Path path = new Path();  /** * Optimizes painting by invalidating the smallest possible area. */  private float lastTouchX;  private float lastTouchY;  private final RectF dirtyRect = new RectF();  public SignatureView(Context context, AttributeSet attrs) {  super(context, attrs);  paint.setAntiAlias(true);  paint.setColor(Color.BLACK);  paint.setStyle(Paint.Style.STROKE);  paint.setStrokeJoin(Paint.Join.ROUND);  paint.setStrokeWidth(STROKE_WIDTH);  }  /** * Erases the signature. */  public void clear() {  path.reset();  // Repaints the entire view.  invalidate();  }  @Override  protected void onDraw(Canvas canvas) {  canvas.drawPath(path, paint);  }  @Override  public boolean onTouchEvent(MotionEvent event) {  float eventX = event.getX();  float eventY = event.getY();  switch (event.getAction()) {  case MotionEvent.ACTION_DOWN:  path.moveTo(eventX, eventY);  lastTouchX = eventX;  lastTouchY = eventY;  // There is no end point yet, so don't waste cycles invalidating.  return true;  case MotionEvent.ACTION_MOVE:  case MotionEvent.ACTION_UP:  // Start tracking the dirty region.  resetDirtyRect(eventX, eventY);  // When the hardware tracks events faster than they are delivered, the  // event will contain a history of those skipped points.  int historySize = event.getHistorySize();  for (int i = 0; i < historySize; i++) {  float historicalX = event.getHistoricalX(i);  float historicalY = event.getHistoricalY(i);  expandDirtyRect(historicalX, historicalY);  path.lineTo(historicalX, historicalY);  }  // After replaying history, connect the line to the touch point.  path.lineTo(eventX, eventY);  break;  default:  debug("Ignored touch event: " + event.toString());  return false;  }  // Include half the stroke width to avoid clipping.  invalidate(  (int) (dirtyRect.left - HALF_STROKE_WIDTH),  (int) (dirtyRect.top - HALF_STROKE_WIDTH),  (int) (dirtyRect.right + HALF_STROKE_WIDTH),  (int) (dirtyRect.bottom + HALF_STROKE_WIDTH));  lastTouchX = eventX;  lastTouchY = eventY;  return true;  }  /** * Called when replaying history to ensure the dirty region includes all * points. */  private void expandDirtyRect(float historicalX, float historicalY) {  if (historicalX < dirtyRect.left) {  dirtyRect.left = historicalX;  } else if (historicalX > dirtyRect.right) {  dirtyRect.right = historicalX;  }  if (historicalY < dirtyRect.top) {  dirtyRect.top = historicalY;  } else if (historicalY > dirtyRect.bottom) {  dirtyRect.bottom = historicalY;  }  }  /** * Resets the dirty region when the motion event occurs. */  private void resetDirtyRect(float eventX, float eventY) {  // The lastTouchX and lastTouchY were set when the ACTION_DOWN  // motion event occurred.  dirtyRect.left = Math.min(lastTouchX, eventX);  dirtyRect.right = Math.max(lastTouchX, eventX);  dirtyRect.top = Math.min(lastTouchY, eventY);  dirtyRect.bottom = Math.max(lastTouchY, eventY);  }
}

在上文中,我们讨论了Square如何在Android设备上把签名效果做的平滑。在最新发布的Android版Square Card Reader应用中,我们将签名效果更上一层楼,更平滑,更美观,响应更灵敏!改进主要来自于以下三个方面:使用改进的曲线算法、笔划粗细变化、以bitmap缓存提升响应能力。

曲弧之美

当用户在屏幕滑动手指进行签名时,Android将一序列的触屏事件传递给Square客户端,每个触屏事件都包含独立的 (x,y) 坐标。要创建出一个签名的图像,客户端需要重建这些采样触点之间的连接线段。计算连接一序列离散点之间连接线段的过程,称为样条插值。

最简单的样条插值策略是直线连接每一对触点。这也是之前版本的Square客户端采用的策略。

可以看到,即使有足够多的触点去模拟签名的曲线,线性插值方法呈现的效果仍显得又硬又挫。仔细观察图中的签名曲线,可以发现连接线在触点处出现了硬角,原本应该是外圆弧状的地方呈现出难看的扁平状。

问题原因在于,用户签名时手指并不是直愣愣地作点到点直线划动,更多情况下是曲线式的移动。但我们的SignatureView只能捕捉到签名过程中的采样点,再通过猜测采样点间连线来模拟用户签名的完整轨迹。显然,直线连接并不是很好的模拟。

这里较为合适的一个插值方法是曲线拟合。我们发现三次Bezier插值曲线是最理想的插值算法。我们能够利用Bezier控制点精确地确定曲线形状,更赞的是我们能够在网上轻松地找到很多高效的Bezier曲线绘制算法。

Bezier曲线绘制算法需要输入一组用于生成曲线的控制点,但我们目前得到的只有在曲线上的采样点本身,没有Bezier控制点。由此,我们的样条插值计算归结为,利用现有的采样触点,计算出一组用来作为Bezier绘制算法输入的控制点,画出目标曲线。

这里对平滑的三次方曲线绘制的相关数学知识不作详细讨论。有兴趣的朋友可以阅读Kirby Baker的UCLA计算机课程讲义。

完成了从线性插值到曲线插值,乍看差异很细微,但整体的圆滑效果提升却相当明显。

笔划粗细变化
如果你仔细研究下写在纸上的手写签名,不难发现笔划的粗细并不是一成不变的。笔划的粗细是随着笔的速度和用力程度而改变的。尽管Android提供了一个跟踪触屏力度的API,但其效果并没有达到我们用于签名所需的灵敏度与连贯性。还好,跟踪笔划速度是可以实现的,我们仅需要将每个触点的采集时间作tag标记,然后就可以计算点到点之间的速度了。

Java代码  收藏代码
public class Point {  private final float x;  private final float y;  private final long timestamp;  // ...  public float velocityFrom(Point start) {  return distanceTo(start) / (this.time - start.time);  }
}

由于我们的绘制了签名的每个Bezier曲线,笔划的粗细依据可为每段曲线的起止点间的速度。

Java代码  收藏代码
lastVelocity = initialVelocity;
lastWidth = intialStrokeWidth;  public void addPoint(Point newPoint) {  points.add(newPoint);  Point lastPoint = points.get(points.size() - 1);  Bezier bezier = new Bezier(lastPoint, newPoint);  float velocity = newPoint.velocityFrom(lastPoint);  // A simple lowpass filter to mitigate velocity aberrations.  velocity = VELOCITY_FILTER_WEIGHT * velocity   + (1 - VELOCITY_FILTER_WEIGHT) * lastVelocity;  // The new width is a function of the velocity. Higher velocities  // correspond to thinner strokes.  float newWidth = strokeWidth(velocity);  // The Bezier's width starts out as last curve's final width, and  // gradually changes to the stroke width just calculated. The new  // width calculation is based on the velocity between the Bezier's   // start and end points.  addBezier(bezier, lastWidth, newWidth);  lastVelocity = velocity;  lastWidth = strokeWidth;
}

当我们动手实现的时候,却碰到了一个棘手的问题——Android的canvas API没有绘制曲线宽度可变的Bezier曲线的相关方法。这意味着我们必需以点成线,自己点画出目标曲线。

Java代码  收藏代码
/** Draws a variable-width Bezier curve. */
public void draw(Canvas canvas, Paint paint, float startWidth, float endWidth) {  float originalWidth = paint.getStrokeWidth();  float widthDelta = endWidth - startWidth;  for (int i = 0; i < drawSteps; i++) {  // Calculate the Bezier (x, y) coordinate for this step.  float t = ((float) i) / drawSteps;  float tt = t * t;  float ttt = tt * t;  float u = 1 - t;  float uu = u * u;  float uuu = uu * u;  float x = uuu * startPoint.x;  x += 3 * uu * t * control1.x;  x += 3 * u * tt * control2.x;  x += ttt * endPoint.x;  float y = uuu * startPoint.y;  y += 3 * uu * t * control1.y;  y += 3 * u * tt * control2.y;  y += ttt * endPoint.y;  // Set the incremental stroke width and draw.  paint.setStrokeWidth(startWidth + ttt * widthDelta);  canvas.drawPoint(x, y, paint);  }  paint.setStrokeWidth(originalWidth);
}

可以看到,笔划粗细变化的签名,更加接近真实的手写效果。

响应能力
影响一个签名过程愉悦程度的另外一个重要因素是对输入的响应能力。使用纸笔签名时,笔的移动与纸上笔划出现是没有任何延迟的。而在触摸屏设备上,出现响应延迟在所难免。我们要做的是尽可能地减少这种延迟感,缩短用户手指在屏幕上滑动与签名笔划出现之间的时间间隔。
一种简单渲染策略是将所有的Bezier曲线在我们signatureView的onDraw()方法中绘制。

Java代码  收藏代码
@Override protected void onDraw(Canvas canvas) {  for (Bezier curve : signature) {  curve.draw(canvas, paint, curve.startWidth(), curve.endWidth());  }
}

之前提到,我们绘制Bezierq曲线的方法是多次调用canvas.drawPoint(…)方法来以点成线。每个曲线重绘,对于笔划简单的签名还算可行,但对笔划较为复杂的签名则明显感觉到很慢。即使采用指定区域刷新的方法,绘制重叠线段仍然会严重拖慢签名响应。
解决方法是当签名每增加一个曲线时,将相应的Bezier曲线绘制到一个内存中的Bitmap中。之后只需要在onDraw()方法中画出该bitmap,而不需要在整个签名过程中对每条曲线重复运行Bezier曲线绘制算法。

Java代码  收藏代码
Bitmap bitmap = null;
Canvas bitmapCanvas = null;  private void addBezier(Bezier curve, float startWidth, float endWidth) {  if (bitmap == null) {  bitmap = Bitmap.createBitmap(getWidth(), getHeight(),   Bitmap.Config.ARGB_8888);  bitmapCanvas = new Canvas(bitmap);  }  curve.draw(bitmapCanvas, paint, startWidth, endWidth);
}  @Override protected void onDraw(Canvas canvas) {  canvas.drawBitmap(bitmap, 0, 0, paint);
}

使用该方法能保证签名的绘制响应,不受签名复杂度的影响。
最终成品
综上所述,我们采用了三次样条插值来使签名效果更平滑,基于笔划速度的笔划粗细可变效果使签名更真实,bitmap缓存使得绘制响应得到优化。最终的成果是用户能够得到一个愉悦的签名体验和一个漂亮的签名。

Android应用中平滑的手写效果实现相关推荐

  1. android 自定义刷新控件,Android开发中MJRefresh自定义刷新动画效果

    有时候我们对自己开发的项目经常不满意,但是我们要达到自定义刷新动画的效果有一定的难度,别着急,下面爱站技术频道和大家分享Android开发中MJRefresh自定义刷新动画效果,一起来学习吧! [一] ...

  2. android led闪烁功能,如何在Android应用层中制作一个LED指示灯效果

    如何在Android应用层中制作一个LED指示灯效果 发布时间:2020-12-08 16:12:59 来源:亿速云 阅读:86 作者:Leah 本篇文章给大家分享的是有关如何在Android应用层中 ...

  3. android l 效果,[原]Android L中水波纹点击效果的实现

    博主参加了2014 CSDN博客之星评选,帮我投一票吧. 前言 前段时间android L(android 5.0)出来了,界面上做了一些改动,主要是添加了若干动画和一些新的控件,相信大家对view的 ...

  4. Android L中水波纹点击效果的实现

    博主参加了2014 CSDN博客之星评选,帮我投一票吧. 点击给我投票 前言 前段时间android L(android 5.0)出来了,界面上做了一些改动,主要是添加了若干动画和一些新的控件,相信大 ...

  5. Android Activity中实现Fragment切换功能效果

    一个最简单的Activity中实现Fragment切换功能效果: 一.效果图: 二.快速实现: 一个Activity中添加多个Fragment进行切换实现相应的功能需求逻辑,比如在activity中有 ...

  6. 识别图中模糊的手写数字(菜鸟做法)

    预备知识 python语言基础 目标 导入图片数据集,分析图片的特点.定义变量,构建模型,训练模型并输出中间状态参数,测试.保存.读取模型 如何搞定它 1.1导入图片数据集 首先来看看数据集是什么样的 ...

  7. office_handwriting 手写字体生成脚本 手写文章 打印手写效果 windows office word 宏脚本 模仿手写 模拟手写 一键生成 代码生成 任意文本 多种字体 手写字体

    目录 效果图 安装方法 使用方法 效果图 先上效果图 手写文章生成脚本,可模仿手写字体. 安装方法 1.下载脚本地址:https://github.com/DaviesGit/office_handw ...

  8. android 层叠轮播,vue手写一个卡片化层叠轮播(支持滑动,移动端连续滚动,点击)...

    项目需求,需要写一个卡片化层叠的轮播,找了下插件都没有合适的,于是写了一个展示5个卡片的轮播 先看效果图: 卡片化层叠轮播 5个卡片要计算各自的高度,宽度,利用相对定位计算出各自的位置 然后trans ...

  9. Android fragment中广告图片轮播效果的实现(附图 )

    作者刚刚接触android小白一枚,这是本人在CSDN上写的第一篇博客..出于写博客的目的:一也是最重要的想让像我这样的android初学者少走点弯路,本以为这个功能实现起来挺简单的,但是项目要求在f ...

最新文章

  1. 大盘点|轻量级人脸检测算法实现,快到没朋友的都在这里了~
  2. http://blog.csdn.net/luoshengyang/article/details/6651971
  3. python 遍历删除
  4. markdown demo 学习
  5. [转]创建一个JavaScript弹出DIV窗口层的效果
  6. python绘制基因结构图_Python调用graphviz绘制结构化图形网络示例
  7. 再等等!华为折叠屏手机Mate X预计在7月底至8月初开售
  8. DOTNET零碎要点---vb.net获取combox的选中值,删除Datagridview,选中值,处理提示框...
  9. 附加作业:源自邹老师的作业“链接”
  10. 剪切板记录管理工具:Paste Mac
  11. VS Code Css格式化插件
  12. npoi 删除多行 操作excel_使用NPOI导出Excel ICell调用过剩 内存溢出
  13. word2019如何删除段落文字带颜色的背景
  14. 复杂网络是怎么应用于神经网络上
  15. Win11保留的存储空间怎么关闭?Win11释放系统保留存储空间教程
  16. 微信公众平台开发[5] —— 微信扫码支付介绍
  17. Autojs: 坚果云文本文件上传/下载
  18. 更改MTU解决docker pull不成功的问题
  19. Fcoin事件背后的良心与底线
  20. 算法:js 数组 array 去重,并显示所有重复的元素

热门文章

  1. windows 花式关机
  2. java基于vue+springboot 的体育用品销售购物网站 多商家 nodejs
  3. [附源码]Java计算机毕业设计SSM党史知识竞赛系统
  4. Josephus 问题(2)
  5. 采购员掌握的计算机知识,电子商务导论复习题
  6. SQL server日期转换——年月日合并成日期格式/取每月第一天或最后一天
  7. 外国小哥总结各国旅行好用App,中国区榜首心服口服
  8. 【向题看齐】408之数据结构DS概念记忆总结
  9. 互联网+先进制造业工业互联网智能转型升级中的三大巨坑
  10. U8W/U8W-Mini使用与常见问题解决