原笔迹手写SVG绘图算法的改善

最近折腾了一下SVG,对SVG绘图的效果做了重大的改善,效果可以媲美之前的分段画点算法,这个是一个很鼓舞的成果,SVG绘图的优点很多,例如绘图手指跟随效果很流畅,同时生成文件更小,更重要的是浏览器原生支持SVG,这样客户可以很方便的把SVG数据应用到网络应用,以及同步书写的应用。

后面有空的话,要把这个算法移植到 iOS。

好了,不多说,看效果图:

那,下一步应该是什么呢?

集成手写控件到 mupdf?手写控件javascript 版本?手写识别功能?

路漫漫其修远兮~~~

原笔迹手写技术与智能绣花机的第一次亲密接触

0. 前言

是的,之前一直没有想象过原笔迹手写技术可以应用在绣花机上,更多的只是在教育行业有合作的需求。
直到客户提出需求,才恍然大悟,试想将自己酷炫的签名秀在自己的鸭舌帽上,将自己得意的书法作品秀在绸缎上,将孩子萌萌达的涂鸦秀在家庭亲子装上,等等,这是何等提升幸福感的事情啊!

1. 需求

  • 原笔迹手写,支持电磁屏/主动电容屏压感以及笔锋;普通电容屏的的压感以及笔锋
  • 生成笔迹svg,绣花机通过生成的svg来进行刺绣
  • 生成笔迹png,激光绣花级通过生成的png来进行刺绣
  • 其他

2. 技术实现

通过在平板上书写,绘制,能很好的还原原笔迹,压感,笔锋,平滑,优美。原笔迹手写技术已经比较成熟,实现效果如下:

唯一的问题就是svg的需求。之前生成的svg不符合客户的需求,绣花机需要的是不是一条贝塞尔曲线,而是形成笔迹的两条贝塞尔曲线,然后根据这两条塞尔曲线进行刺绣。

技术上有一定的难度,但是因为之前已经有过相当长的一段时间的研究,所有实现也并不是太难。svg文件的效果可以很好地满足客户的需求。

svg文件生成效果如下:

3. 视频实例

最终期望效果~~~

4. 展望

相信随着智能手机,平板的普及,原笔迹手写在消费行业定会大有所为,同时也会在工业行业找到更多展现自己价值的机会。

Android 原笔迹手写实现普通触摸屏的压感笔锋效果

Android 要实现原笔迹手写的压感,笔锋,必须要配合更好的压感触摸屏,例如电磁屏。三星的 note 系统,微软的 surface 系列,都是需要加装电磁屏的。E人E本,好记星的平板,有电磁屏的加成,也能很好的实现压感笔锋的效果,而且效果并不比三星微软的差。

如果没有电磁屏,也可以依靠最近兴起的主动电容屏来实现压感笔锋的效果,但是效果略差。

如果连主动电容屏也没有,那就是普通的电容屏,这样一般只能实现没有压感笔锋的效果,只能勉强用用,无法体会平板手写的优雅~~~

之前,手写控件也受限于此,无法实现普通电容屏的压感笔锋效果,效果如下:
android-paintview-finger-pressure-not-ok

幸好,现在我的手写控件已经克服这个问题,通过算法实现了普通电容屏的压感笔锋效果,而且整体效果也很好。
看图:

android-paintview-finger-pressure

android.graphics.Path 的序列化

序列化 (Serialization) 的相关知识可以参考相关的百科,这里只要是学习 android.graphics.Path 的序列化。

工作中有需求要保存绘制的 paths, 这个时候就需要用到对象的序列化的功能,但是 Android 上没有对 Path 做序列化的工作,所以只能自己进行序列化了。

Path 类序列化的原理就是通过将 path 绘制的每一个点都保存下来,需要还原的时候就进行反序列化,再通过这些保存的点重新绘制 path。

实例1:

继承 android.graphics.Path 类,重写 moveTo, lineTo 等方法,记录下 path 绘制的每一点的坐标数据;反序列化的时候调用 “drawThisPath” 重绘 path。

相关代码:

public class CustomPath extends Path implements Serializable {

private static final long serialVersionUID = -5974912367682897467L;

private ArrayList<PathAction> actions = new ArrayList<CustomPath.PathAction>();

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
    in.defaultReadObject();
    drawThisPath();
}

@Override
public void moveTo(float x, float y) {
    actions.add(new ActionMove(x, y));
    super.moveTo(x, y);
}

@Override
public void lineTo(float x, float y){
    actions.add(new ActionLine(x, y));
    super.lineTo(x, y);
}

private void drawThisPath(){
    for(PathAction p : actions){
        if(p.getType().equals(PathActionType.MOVE_TO)){
            super.moveTo(p.getX(), p.getY());
        } else if(p.getType().equals(PathActionType.LINE_TO)){
            super.lineTo(p.getX(), p.getY());
        }
    }
}

public interface PathAction {
    public enum PathActionType {LINE_TO,MOVE_TO};
    public PathActionType getType();
    public float getX();
    public float getY();
}

public class ActionMove implements PathAction, Serializable{
    private static final long serialVersionUID = -7198142191254133295L;

    private float x,y;

    public ActionMove(float x, float y){
        this.x = x;
        this.y = y;
    }

    @Override
    public PathActionType getType() {
        return PathActionType.MOVE_TO;
    }

    @Override
    public float getX() {
        return x;
    }

    @Override
    public float getY() {
        return y;
    }

}

public class ActionLine implements PathAction, Serializable{
    private static final long serialVersionUID = 8307137961494172589L;

    private float x,y;

    public ActionLine(float x, float y){
        this.x = x;
        this.y = y;
    }

    @Override
    public PathActionType getType() {
        return PathActionType.LINE_TO;
    }

    @Override
    public float getX() {
        return x;
    }

    @Override
    public float getY() {
        return y;
    }

}
}

实例2:

创建 PathInfo 类,方法 lineStart, lineMove, lineEnd 专门用来保存 path 绘制的每一个点的坐标数据;反序列化的时候重绘 path。原理跟实例1差不多。

源码下载

在 stackoverflow 回答问题,被外国友人吐槽语法

在 stackoverflow 上回答了一个关于 FileLock 在 Android 上的用法的问题,尽管我已经有所注意,而且只是简单的几句话,没想到还是出现了语法问题。被外国友人吐槽语法,无地自容啊。

语法知识都还给英语老师了。本来以为自己虽然是哑巴英语,但是好歹能读能写,现在写也成问题了。学习一门语言,周围环境真的很重要,平时学了多少单词,多少语法,实际上找个实践的机会都没有。工作生活都是用普通话,我现在讲广东话都有点生疏了。

不说了,上图纪念。好好学习英语,积极实践,错得越多,学得越多。

如图,红色是被修正的错误:

grammer-error

Android adb offline 的解决方案

之前在 Android adb 网络连接 中总结了一次 adb 的网络连接,而最近工作中遇到 Android 设备连接 adb 的时候出现 offline 的提示,无法正常连接上设备,这个情况应该如何解决呢?

这里总结一下解决方案:

  1. 重新插拔 USB 线
  2. 换一个 USB 口,有些 PC 的 USB 口不都是一样的;或者换一根 USB 线,低质量的 USB 线缆可能会引起这个问题
  3. 取消 adb 调试模式,再重新勾选
  4. 在 eclipse 的 DDMS 模式下点击 “reset adb”
  5. 在 PC 端重启 adb。adb kill-server 然后 adb start-server
  6. 重启 PC手机,不行的话再试几次
  7. 升级 android SDK,对于开发者来说,一般都是系统与 adb 工具版本不一致引起的。例如我的 Nexus 7 升级到 Android 4.3之后,只能通过升级 Android SDK 来解决这个问题
  8. 将xx手机助手之类的相关进程杀掉就正常了(感谢“梁c”的补充)。xx助手之类的也会将 adb 进程锁定,导致无法正常连接 adb.

如果这个能帮到你,请留言告诉我。

Android Path 类运用实践

理论学习了 Android Path 类之后,对 Path 类的属性方法都有所了解,写两个例子实践一下。理论与实践结合嘛。

实例1,五环 + 文字:

useesee

源码:

package com.example.pathstudy;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Bundle;
import android.view.View;

public class PathStudyActivity extends Activity {

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(new PathStudyView(getApplicationContext()));
	}

	private class PathStudyView extends View {

		public PathStudyView(Context context) {
			super(context);
			// TODO Auto-generated constructor stub
		}

		@Override
		protected void onDraw(Canvas canvas) {
			// TODO Auto-generated method stub
			// super.onDraw(canvas);
			canvas.drawColor(Color.WHITE);
			Paint mPaint = new Paint();
			mPaint.setStyle(Paint.Style.STROKE);
			mPaint.setStrokeWidth(20);

			int originX = 0;
			int originY = 0;
			int offsetX = 0;
			int offsetY = 0;
			int offsetLengthX = 240;
			int offsetLengthY = 110;

			float circleX = 150;
			float circleY = 200;
			float radius = 100;

			// first circle
			mPaint.setColor(Color.BLUE);
			Path mPath = new Path();
			mPath.addPath(getDrawCirclePath(circleX, circleY, radius));
			canvas.drawPath(mPath, mPaint);

			// second circle
			mPaint.setColor(Color.BLACK);
			canvas.save();
			offsetX = originX + offsetLengthX;
			canvas.translate(offsetX, offsetY);
			mPath.reset();
			mPath.addPath(getDrawCirclePath(circleX, circleY, radius));
			canvas.drawPath(mPath, mPaint);
			canvas.restore();

			// thid circle
			mPaint.setColor(Color.RED);
			canvas.save();
			offsetX += offsetLengthX;
			canvas.translate(offsetX, offsetY);
			mPath.reset();
			mPath.addPath(getDrawCirclePath(circleX, circleY, radius));
			canvas.drawPath(mPath, mPaint);
			canvas.restore();

			// forth circle
			mPaint.setColor(Color.YELLOW);
			canvas.save();
			offsetX = originX + offsetLengthX / 2;
			offsetY = originY + offsetLengthY;
			canvas.translate(offsetX, offsetY);
			mPath.reset();
			mPath.addPath(getDrawCirclePath(circleX, circleY, radius));
			canvas.drawPath(mPath, mPaint);
			canvas.restore();

			// fifth circle
			mPaint.setColor(Color.GREEN);
			canvas.save();
			offsetX += offsetLengthX;
			canvas.translate(offsetX, offsetY);
			mPath.reset();
			mPath.addPath(getDrawCirclePath(circleX, circleY, radius));
			canvas.drawPath(mPath, mPaint);
			canvas.restore();

			// draw text kevinems.com
			// fifth circle
			mPaint.setColor(Color.BLACK);
			mPaint.setStyle(Paint.Style.FILL);
			mPaint.setTextSize(100);
			mPaint.setStrokeWidth(5);
			String websiteStr = "kevinems.com";
			canvas.drawText(websiteStr, 110, 500, mPaint);
			canvas.restore();

		}

		private Path getDrawCirclePath(float x, float y, float radius) {
			Path mPath = new Path();
			mPath.setFillType(Path.FillType.EVEN_ODD);
			mPath.addCircle(x, y, radius, Path.Direction.CCW);
			return mPath;
		}
	}
}

实例2,可以随意绘画 path 的手写板:

paint-pad

源码:

package com.example.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class PathView extends View {
	private static final String LOG_TAG = "PathView";
	private Path mPath;
	private Paint mPaint;
	private float mPosX;
	private float mPosY;

	public PathView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
		init();
	}

	private void init() {
		// TODO Auto-generated method stub
		mPath = new Path();
		mPaint = new Paint();

		mPaint.setColor(Color.GREEN);
		mPaint.setStyle(Paint.Style.STROKE);
		mPaint.setStrokeWidth(5);

	}

	@Override
	protected void onDraw(Canvas canvas) {
		// TODO Auto-generated method stub
		// super.onDraw(canvas);
		Log.i(LOG_TAG, "onDraw");
		canvas.drawColor(Color.WHITE);
		canvas.drawPath(mPath, mPaint);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		// return super.onTouchEvent(event);
		int action = event.getAction();
		float x = event.getX();
		float y = event.getY();

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mPath.moveTo(x, y);
			break;

		case MotionEvent.ACTION_MOVE:
			mPath.quadTo(mPosX, mPosY, x, y);

			invalidate();
			break;

		default:
			break;
		}

		mPosX = x;
		mPosY = y;

		return true;
	}
}

Android Path 类学习

这是 Kevin 的一篇 Google Android API 的翻译习作,原文请移步:

Android Deveolopers Path 类 api


public class
公共类

Path

extends Object
继承 Object

java.lang.Object
   ↳ android.graphics.Path

Class Overview
类 概貌


The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves. It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint’s Style), or it can be used for clipping or to draw text on a path.

Path类封装了由直线段,二次曲线,三次曲线混合(多轮廓)组成的几何路径。它可以通过 canvas.drawPath(path, paint) 进行绘制,包括填充和描边(取决于 paint’s Style 这个参数),或者它可以用于裁剪或绘制路径上的文本。

Summary
概要


Nested Classes
嵌套类
enum Path.Direction Specifies how closed shapes (e.g. rects, ovals) are oriented when they are added to a path.
指定封闭的形状(如矩形的,椭圆形),当它们被添加到路径时的方向。有顺时针和逆时针两个数值。
enum Path.FillType Enum for the ways a path may be filled
填充方式的枚举类型:Path.FillType EVEN_ODD,用奇偶规则填充
Path.FillType INVERSE_EVEN_ODD,顾名思义,和EVEN_ODD规则恰好相反。
Path.FillType INVERSE_WINDING,同样,WINDING的反效果。
Path.FillType WINDING,用非零环绕数规则填充。
Public Constructors
公共构造函数
Path()

Create an empty path
创建一个空的 path
Path(Path src)

Create a new path, copying the contents from the src path.
复制 Path src 到一个新的 path
Public Methods
公共类
void addArc(RectF oval, float startAngle, float sweepAngle)

Add the specified arc to the path as a new contour.
将一个指定的弧形作为新的轮廓添加到 path
void addCircle(float x, float y, float radius, Path.Direction dir)

Add a closed circle contour to the path
添加一个闭合圆形轮廓到 path
void addOval(RectF oval, Path.Direction dir)

Add a closed oval contour to the path
添加一个闭合的椭圆形轮廓到 path
void addPath(Path src, float dx, float dy)

Add a copy of src to the path, offset by (dx,dy)
将源 path 拷贝一份并添加到  path(当前),dx, dy 为偏移
void addPath(Path src)

Add a copy of src to the path
将源 path 拷贝一份并添加到  path(当前)
void addPath(Path src, Matrix matrix)

Add a copy of src to the path, transformed by matrix
将源 path 拷贝一份并添加到  path(当前),由 matrix 进行转换
void addRect(float left, float top, float right, float bottom, Path.Direction dir)

Add a closed rectangle contour to the path
添加一个闭合的矩形轮廓到 path
void addRect(RectF rect, Path.Direction dir)

Add a closed rectangle contour to the path
添加一个闭合的矩形轮廓到 path
void addRoundRect(RectF rect, float[] radii, Path.Direction dir)

Add a closed round-rectangle contour to the path.
添加一个闭合的圆角矩形轮廓到 path
void addRoundRect(RectF rect, float rx, float ry, Path.Direction dir)

Add a closed round-rectangle contour to the path
添加一个闭合的圆角矩形轮廓到 path
void arcTo(RectF oval, float startAngle, float sweepAngle)

Append the specified arc to the path as a new contour.
追加指定的弧形到 path 作为一个新的轮廓
void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)

Append the specified arc to the path as a new contour.
追加指定的弧形到 path 作为一个新的轮廓
void close()

Close the current contour.
闭合当前轮廓
void computeBounds(RectF bounds, boolean exact)

Compute the bounds of the control points of the path, and write the answer into bounds.
计算路径的控制点的边界,并把答案写进边界
void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

Add a cubic bezier from the last point, approaching control points (x1,y1) and (x2,y2), and ending at (x3,y3).
从上一个点添加一个三次贝塞尔曲线,经过控制点(x1, y1), (x2, y2),在(x3, y3) 结束
Path.FillType getFillType()

Return the path’s fill type.
返回 path 的填充类型
void incReserve(int extraPtCount)

Hint to the path to prepare for adding more points.
对 path 暗示将会添加更多的点
boolean isEmpty()

Returns true if the path is empty (contains no lines or curves)
当 path 为空时返回 true (包括没有线和曲线)
boolean isInverseFillType()

Returns true if the filltype is one of the INVERSE variants
如果填充类型为 INVERSE 变量的其中一种,返回 true
boolean isRect(RectF rect)

Returns true if the path specifies a rectangle.
如果 path 指定一个矩形,返回 true
void lineTo(float x, float y)

Add a line from the last point to the specified point (x,y).
从上一点到 (x, y) 画线
void moveTo(float x, float y)

Set the beginning of the next contour to the point (x,y).
将下一个轮廓的起点设置为 (x, y)
void offset(float dx, float dy, Path dst)

Offset the path by (dx,dy), returning true on success
将 path 偏移 (x, y),成功则返回 true
void offset(float dx, float dy)

Offset the path by (dx,dy), returning true on success
将 path 偏移 (x, y),成功则返回 true
void quadTo(float x1, float y1, float x2, float y2)

Add a quadratic bezier from the last point, approaching control point (x1,y1), and ending at (x2,y2).
从上一个点添加一个二次贝塞尔曲线,经过控制点(x1, y1),在(x2, y2) 结束
void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

Same as cubicTo, but the coordinates are considered relative to the current point on this contour.
同 cublicTo,但坐标被认为是相对于当前轮廓上的当前点。
void rLineTo(float dx, float dy)

Same as lineTo, but the coordinates are considered relative to the last point on this contour.
同 lineTo,但坐标被认为是相对于当前轮廓上的上一点。
void rMoveTo(float dx, float dy)

Set the beginning of the next contour relative to the last point on the previous contour.
设置下一个轮廓的开始点相当于上一个轮廓的最后一点
void rQuadTo(float dx1, float dy1, float dx2, float dy2)

Same as quadTo, but the coordinates are considered relative to the last point on this contour.
同 quadTo,但坐标被认为是相对于当前轮廓上的上一点。
void reset()

Clear any lines and curves from the path, making it empty.
清空 path 里面的所有线和曲线
void rewind()

Rewinds the path: clears any lines and curves from the path but keeps the internal data structure for faster reuse.
清空 path 中的所有线和曲线,但是保留其内部数据以便快速重用
void set(Path src)

Replace the contents of this with the contents of src.
用源 path 代替当前 path 的内容
void setFillType(Path.FillType ft)

Set the path’s fill type.
设置 path 填充类型
void setLastPoint(float dx, float dy)

Sets the last point of the path.
设置 path 的最后一点
void toggleInverseFillType()

Toggles the INVERSE state of the filltype
切换 filltype 的逆状态
void transform(Matrix matrix, Path dst)

Transform the points in this path by matrix, and write the answer into dst.
应用矩阵变换当前 path, 并把结果写到目的 path
void transform(Matrix matrix)

Transform the points in this path by matrix.
应用矩阵变换当前 path
Protected Methods
私有方法
void finalize()

Invoked when the garbage collector has detected that this instance is no longer reachable.
当垃圾收集器检测到该实例不再可达时调用