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

前言

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

正文

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

after
after

提示:该界面中提供了摇一摇清屏的功能

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

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

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;
  }
}

可以看到实现出来的效果还是与预期有一定差距的,签名的笔画有锯齿,并且明显反应迟钝,笔迹跟不上手指。

before
before

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

触屏事件丢失

笔迹跟不上手指这个问题,可能的原因是:

  • Android对触屏事件的采样率过低;

  • 绘制事件阻塞了触屏事件的采样;

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

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

  • ·getHistorySize()

  • ·getHistoricalX(int)

  • ·getHistoricalY(int)

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

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) 方法,选择性地对新添画线的矩形区域进行局部刷新,可以显著提高绘制性能。

采用的算法思路如下:

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

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

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

  • 刷新脏区域。

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

以上我们对SignatureView进行了两方面的改造提升:

  • 将触屏事件的中间点加入绘制,使笔画更加流畅逼真;

  • 以局部刷新取代全屏刷新,提高绘图性能,使触屏响应更加迅速。

最终的效果出炉:

after
after

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

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 前言   这是一篇翻译至squareup的文章,这是原文,之前有人在TIEYE上翻译过这篇文章,但现在链接已经失效...
    张明云阅读 9,923评论 2 27
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,350评论 0 17
  • 早先,前辈李医生嘴边常念叨一句老话:人是一截截死的。意思是任你鲜花着锦、烈火烹油,人一旦落了魄,没几步就走完蛋了。...
    autumnwater阅读 516评论 0 0
  • 文/闲敲棋子 贺飞认识朱小北已有9年时光了。 朱小北是那种天生有着超强优越感的女生,长着一张让所有女人羡慕嫉妒恨的...
    四土闲时敲棋子阅读 871评论 0 11