Android手写优化-更为平滑的签名效果实现

前言

  这是一篇翻译至squareup的文章,这是原文,之前有人在TIEYE上翻译过这篇文章,但现在链接已经失效,手写效率问题是一直是Android平台上一个比较棘手的问题,所以有必要将这篇文章带给Android开发者,这篇文章在ITEYE那篇译文的基础上有所改动,如果英语还可以,请尽量阅读原文。

正文

  在上一篇文章中,我们讨论了Square如何在Android设备上把签名效果做的平滑。在最新发布的Android版Square Card Reader应用中,我们将签名效果更上一层楼,更平滑,更美观,响应更灵敏!

主要通过以下三个方面来改进用户体验效果:

  • 使用改进的曲线算法;

  • 笔划粗细变化;

  • 以bitmap缓存提升响应能力。

  曲弧之美:

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

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

Splining0
Splining0

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

Splining1
Splining1

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

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

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

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

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

Splining2
Splining2

  笔划粗细变化:

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

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曲线,笔划的粗细依据可为每段曲线的起止点间的速度。

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曲线的相关方法。这意味着我们必需以点成线,自己点画出目标曲线。

/** 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);
}

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

Variable Stroke Width
Variable Stroke Width

  响应能力:

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

  一种简单渲染策略是将所有的Bezier曲线在我们signatureView的onDraw()方法中绘制。

@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曲线绘制算法。

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缓存使得绘制响应得到优化。最终的成果是用户能够得到一个愉悦的签名体验和一个漂亮的签名。

final
final

注:在github上可以下载到项目的源码:

android-signaturepad

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容