Android

Android通过.nomedia文件禁止多媒体库扫描指定文件夹下的多媒体文件 Android应用内存泄露分析、改善经验总结 修改Eclipse导入项目的默认工程名 自定义Android Studio工程模板 使用Nexus Repository搭建属于自己公司的私有maven服务器 Android Studio编译过程中mergeDebugResources时报“png-cruncher_*”异常的解决方案 Eclipse转Android Studio的过程中有必要弄明白的一些问题 Android开发经验总结 Android Studio使用过程中遇到的一些问题及解决方案 Android各个Support Library介绍 调用AsyncTask的excute方法不能立即执行程序的原因分析及改善方案 提升进入界面的速度 使用软引用解决Handler内存泄露和显示Popupwindow、Dialog时提示"Unable to add Window-token is null"的问题 SharedPreferences在多进程中的使用及注意事项 Android性能测试工具列表 Android View双缓冲绘制时清除Bitmap上的内容的方法 解决JPinyin在APK被加密后不能正常使用的问题 Android APP内存优化之图片优化 Android EditText的使用及值得注意的地方 Android应用内多进程的使用及注意事项 Android设置应用内文字的默认颜色和大小 关于APK瘦身值得分享的一些经验 Android通过ClipDrawable实现图片裁剪功能 Android通过广播更新文件和文件夹到媒体库 每个Android开发者都应该了解的资源列表 selector的使用方法及注意事项 通过批处理批量clone代码 Android清除数据、清除缓存、一键清理的区别 Android将数据库保存到SD卡的实现 Android多分辨率适配经验总结 通过观察者模式监听媒体库的变化实现APP本地数据自动更新 Android ADB命令大全(通过ADB命令查看wifi密码、MAC地址、设备信息、操作文件、查看文件、日志信息、卸载、启动和安装APK等) Android通过ADB查看wifi密码 Android一个APK多个入口(多个桌面图标)的实现 使用Python脚本批量卸载第三方应用和清除log缓存 Android CheckList Android模仿打字机效果的自定义View实现 在Activity的onCreate方法中显示PopupWindow导致异常的原因分析及解决方案 Android手写优化-更为平滑的签名效果实现 Android手写优化-平滑的签名效果实现 不要在Android的Application对象中缓存数据! 大量Android面试题目来袭 一种不需要Google账号、不需要关联手机、不需要在手机上安装Google的服务直接能够下载Google Play上APK的方法 在Android library中不能使用switch-case语句访问资源ID的原因分析及解决方案 Android程序和数据分离的实现方案 按Home按键退出应用后重新启动该应用无法返回到最后打开页面的解决方案 Eclipse下Android项目不能生成R.java的解决方法汇总 android:descendantFocusability属性在ListView中的妙用 去掉SrollView、GrdiView、ListView、ViewPager等滑动到边缘的光晕效果 Android开发经验谈-Eclipse使用技巧 Android开发经验谈-很少有人会告诉你的Android开发基本常识 Android开发经验谈-Android工程目录介绍 在Android的string.xml中使用转义字符实现想要的显示效果 修改ViewPager调用setCurrentItem时,滑屏的速度 Android监听Home按键消息 Android手写开源项目和资料搜集 Android通过资源文件名获取资源ID Android中Bitmap、Drawable、bytes数组之间相互转换 想过但未实现的一些Idea 读写文件编码方式不一致导致文件乱码的解决方案 Android字符串格式化开源库phrase介绍 Android实现带箭头的自定义Progressbar Android模拟键盘输入功能的实现 与Android应用程序相关的各种文件存储路径介绍 Android开发者网址导航

标签

Android 65

Android通过.nomedia文件禁止多媒体库扫描指定文件夹下的多媒体文件 Android应用内存泄露分析、改善经验总结 修改Eclipse导入项目的默认工程名 自定义Android Studio工程模板 使用Nexus Repository搭建属于自己公司的私有maven服务器 Android Studio编译过程中mergeDebugResources时报“png-cruncher_*”异常的解决方案 Eclipse转Android Studio的过程中有必要弄明白的一些问题 Android开发经验总结 Android Studio使用过程中遇到的一些问题及解决方案 Android各个Support Library介绍 调用AsyncTask的excute方法不能立即执行程序的原因分析及改善方案 提升进入界面的速度 使用软引用解决Handler内存泄露和显示Popupwindow、Dialog时提示"Unable to add Window-token is null"的问题 SharedPreferences在多进程中的使用及注意事项 Android性能测试工具列表 Android View双缓冲绘制时清除Bitmap上的内容的方法 解决JPinyin在APK被加密后不能正常使用的问题 Android APP内存优化之图片优化 Android EditText的使用及值得注意的地方 Android应用内多进程的使用及注意事项 Android设置应用内文字的默认颜色和大小 关于APK瘦身值得分享的一些经验 Android通过ClipDrawable实现图片裁剪功能 Android通过广播更新文件和文件夹到媒体库 每个Android开发者都应该了解的资源列表 selector的使用方法及注意事项 通过批处理批量clone代码 Android清除数据、清除缓存、一键清理的区别 Android将数据库保存到SD卡的实现 Android多分辨率适配经验总结 通过观察者模式监听媒体库的变化实现APP本地数据自动更新 Android ADB命令大全(通过ADB命令查看wifi密码、MAC地址、设备信息、操作文件、查看文件、日志信息、卸载、启动和安装APK等) Android通过ADB查看wifi密码 Android一个APK多个入口(多个桌面图标)的实现 使用Python脚本批量卸载第三方应用和清除log缓存 Android CheckList Android模仿打字机效果的自定义View实现 在Activity的onCreate方法中显示PopupWindow导致异常的原因分析及解决方案 Android手写优化-更为平滑的签名效果实现 Android手写优化-平滑的签名效果实现 不要在Android的Application对象中缓存数据! 大量Android面试题目来袭 一种不需要Google账号、不需要关联手机、不需要在手机上安装Google的服务直接能够下载Google Play上APK的方法 在Android library中不能使用switch-case语句访问资源ID的原因分析及解决方案 Android程序和数据分离的实现方案 按Home按键退出应用后重新启动该应用无法返回到最后打开页面的解决方案 Eclipse下Android项目不能生成R.java的解决方法汇总 android:descendantFocusability属性在ListView中的妙用 去掉SrollView、GrdiView、ListView、ViewPager等滑动到边缘的光晕效果 Android开发经验谈-Eclipse使用技巧 Android开发经验谈-很少有人会告诉你的Android开发基本常识 Android开发经验谈-Android工程目录介绍 在Android的string.xml中使用转义字符实现想要的显示效果 修改ViewPager调用setCurrentItem时,滑屏的速度 Android监听Home按键消息 Android手写开源项目和资料搜集 Android通过资源文件名获取资源ID Android中Bitmap、Drawable、bytes数组之间相互转换 想过但未实现的一些Idea 读写文件编码方式不一致导致文件乱码的解决方案 Android字符串格式化开源库phrase介绍 Android实现带箭头的自定义Progressbar Android模拟键盘输入功能的实现 与Android应用程序相关的各种文件存储路径介绍 Android开发者网址导航

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

2014年12月29日

前言

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

正文

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

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

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

  触屏事件丢失

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

  • 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

下面是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);
  }
}