您的位置:首页 > 移动开发 > Android开发

Android 系列 5.4徒手绘制平滑曲线

2017-01-10 09:25 429 查看
5.4徒手绘制平滑曲线

问题

您希望允许用户绘制平滑的曲线,如手绘贝塞尔曲线,法律签名等。



使用仔细编写的OnTouchListener创建自定义视图,处理输入的速度比代码可以处理的速度快的情况;将结果保存在数组中,并在onDraw()中绘制它们。

讨论

此代码最初由Square Inc.的Eric Burke编写,用于在人们使用Square应用捕获信用卡购买时捕获签名。为了在法律上可接受作为购买意图的证明,所捕获的签名必须具有良好的质量。 Square已经把这个代码放在Apache Software License 2.0下面,但是不能提供它作为这个菜谱的一部分。

我已经适应的签名代码用于JabaGator,我的非常简单的,通用的绘图程序的Java桌面和Android,但事实上,名称rhymes与着名的插图程序从Adobe是,当然,纯粹巧合。

埃里克的初始,“通过书”绘图代码工作,但非常干燥,非常缓慢。根据调查,Square了解到Android的图形层以“批次”发送触摸事件,当它无法单独快速传递时。传递到onTouchEvent()的每个MotionEvent可能包含多个触摸坐标,与自上次onTouchEvent()调用以来捕获的数量一样多。要绘制一条平滑的曲线,你必须得到所有的点。你可以使用TouchEvent方法getHistorySize()的坐标数,迭代该计数,并调用getHistoricalX(int)和getHistoricalY(int)来获取点的位置(见例5-8)。

实例5-8。绘制所有的点
// in onTouchEvent(TouchEvent):
for (int i=0; i < event.getHistorySize(); i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
... add point (historicalX, historicalY) to your path ...
}
... add point (eventX, eventY) to your path ...


这提供了显着的改进,但是它仍然是太慢,人们绘制与 - 许多非极客将等待绘图代码赶上他们的手指,如果它不够快速绘制足够!问题是,一个简单的解决方案在每个线段之后调用invalidate(),这是正确的但很慢,因为它强制Android重绘整个屏幕。这个问题的解决方案是调用invalidate()只是你绘制线段的区域,并涉及一些算术来获得区域正确;请参见示例5-9中的expandDirtyRect()方法。这里是Eric对脏区算法的描述:

1.“创建一个表示脏区域的矩形。

2.“从ACTION_DOWN事件将四个角的点设置为X和Y坐标。

3.“对于ACTION_MOVE和ACTION_UP,展开矩形以包含新点。 (不要忘记历史坐标!)

4.“只传递脏矩形为invalidate()。 Android不会重绘其余的。

这组步骤使绘图代码响应,并且应用程序可用。

例5-9显示了我的版本的最终代码。我有几个OnTouchListener,一个用于绘制曲线,一个用于选择对象,一个用于绘制矩形,等等。该代码目前不完整,但曲线绘图部分工作得很好。

实例5-9。 DrawingView.java
// This code is dual-licensed under Creative Commons and Apache Software License 2.0
public class DrawingView 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();
final OnTouchListener selectionAndMoveListener = // not shown;
final OnTouchListener drawRectangleListener = // not shown;
final OnTouchListener drawOvalListener = // not shown;
final OnTouchListener drawPolyLineListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// Log.d("jabagator", "onTouch: " + event);
float eventX = event.getX();
float eventY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(eventX, eventY);
lastTouchX = eventX;
lastTouchY = eventY;
// 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 can be delivered to the app, 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:
Log.d("jabagator", "Unknown 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);
}
};
/** DrawingView Constructor */
public DrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(STROKE_WIDTH);
setMode(MotionMode.DRAW_POLY);
}
public void clear() {
path.reset();
// Repaints the entire view.
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(path, paint);
}
/**
* Sets the DrawingView into one of several modes, such
* as "select" mode (e.g., for moving or resizing objects),
* or "Draw polyline" (smooth curve), "draw rectangle", etc.
*/
private void setMode(MotionMode motionMode) {
switch(motionMode) {
case SELECT_AND_MOVE:
setOnTouchListener(selectionAndMoveListener);
break;
case DRAW_POLY:
setOnTouchListener(drawPolyLineListener);
break;
case DRAW_RECTANGLE:
setOnTouchListener(drawRectangleListener);
break;
case DRAW_OVAL:
setOnTouchListener(drawOvalListener);
break;
default:
throw new IllegalStateException("Unknown MotionMode " + motionMode);
}
}
}


图5-3显示了JabaGator正在运行,显示我尝试清晰的笔迹(不要担心,这不是我的法律签名)。

图5-3。触摸绘图示例

这提供了良好的拉丝性能和平滑的曲线。将曲线捕获到绘图数据模型中的代码未显示,因为它是特定于应用程序的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息