如何在你的手指后面画一条平滑的线

Http://marakana.com/tutorials/android/2d-graphics-example.html

我正在使用下面的例子。但是当我的手指在屏幕上快速移动时,这条线就变成了单个的点。

我不确定我是否能加快绘图速度。或者我应该用一条直线把最后两点连接起来。这两个解决方案中的第二个似乎是一个很好的选择,除非当你的手指移动非常快,你将有一个直线的长段,然后锐利的曲线。

如果有任何其他的解决方案,听到他们将是伟大的。

提前谢谢你的帮助。

95076 次浏览

MotionEvent中,您可能有比您意识到的更多的可用信息,这些信息可以提供中间的一些数据。

链接中的示例忽略事件中包含的历史接触点。请参阅 MotionEvent文档顶部附近的“批处理”部分: http://developer.android.com/reference/android/view/MotionEvent.html除此之外,将点与线连接起来可能不是一个坏主意。

当你调用 onTouch 方法时,你也应该使用方法(在 onTouch (MotionEvent 事件)中)

event.getHistorySize();

差不多吧

int histPointsAmount = event.getHistorySize();
for(int i = 0; i < histPointsAmount; i++){
// get points from event.getHistoricalX(i);
// event.getHistoricalY(i); and use them for your purpouse
}

正如你提到的,一个简单的解决方案就是简单地用一条直线把这些点连接起来。下面是这样做的代码:

public void onDraw(Canvas canvas) {
Path path = new Path();
boolean first = true;
for(Point point : points){
if(first){
first = false;
path.moveTo(point.x, point.y);
}
else{
path.lineTo(point.x, point.y);
}
}
canvas.drawPath(path, paint);
}

请确保您的油漆从填充改为中风:

paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.WHITE);

另一种方法是使用 quadTo 方法将这些点与迭代连接起来:

public void onDraw(Canvas canvas) {
Path path = new Path();
boolean first = true;
for(int i = 0; i < points.size(); i += 2){
Point point = points.get(i);
if(first){
first = false;
path.moveTo(point.x, point.y);
}


else if(i < points.size() - 1){
Point next = points.get(i + 1);
path.quadTo(point.x, point.y, next.x, next.y);
}
else{
path.lineTo(point.x, point.y);
}
}


canvas.drawPath(path, paint);
}

这仍然会导致一些尖锐的边缘。

如果你真的很有雄心壮志,你可以开始计算三次样条函数如下:

public void onDraw(Canvas canvas) {
Path path = new Path();


if(points.size() > 1){
for(int i = points.size() - 2; i < points.size(); i++){
if(i >= 0){
Point point = points.get(i);


if(i == 0){
Point next = points.get(i + 1);
point.dx = ((next.x - point.x) / 3);
point.dy = ((next.y - point.y) / 3);
}
else if(i == points.size() - 1){
Point prev = points.get(i - 1);
point.dx = ((point.x - prev.x) / 3);
point.dy = ((point.y - prev.y) / 3);
}
else{
Point next = points.get(i + 1);
Point prev = points.get(i - 1);
point.dx = ((next.x - prev.x) / 3);
point.dy = ((next.y - prev.y) / 3);
}
}
}
}


boolean first = true;
for(int i = 0; i < points.size(); i++){
Point point = points.get(i);
if(first){
first = false;
path.moveTo(point.x, point.y);
}
else{
Point prev = points.get(i - 1);
path.cubicTo(prev.x + prev.dx, prev.y + prev.dy, point.x - point.dx, point.y - point.dy, point.x, point.y);
}
}
canvas.drawPath(path, paint);
}

此外,我发现你需要改变以下内容,以避免重复的运动事件:

public boolean onTouch(View view, MotionEvent event) {
if(event.getAction() != MotionEvent.ACTION_UP){
Point point = new Point();
point.x = event.getX();
point.y = event.getY();
points.add(point);
invalidate();
Log.d(TAG, "point: " + point);
return true;
}
return super.onTouchEvent(event);
}

并将 dx & dy 值添加到 Point 类:

class Point {
float x, y;
float dx, dy;


@Override
public String toString() {
return x + ", " + y;
}
}

这会产生平滑的线条,但有时不得不使用一个循环来连接这些点。 此外,对于长期绘图会议,这变得计算密集型计算。

希望这能帮上忙,好玩的东西。

剪辑

我匆匆完成了一个快速的项目,演示了这些不同的技术,包括 Square 建议的签名实现

这对你来说可能已经不重要了,但是我努力解决它,我想分享,可能对其他人有用。

具有@johncarl 提供的解决方案的教程对于绘图非常有用,但是它们对于我的目的提供了一个限制。如果你把你的手指从屏幕上拿出来放回去,这个解决方案会在最后一次点击和你的新点击之间画一条线,使整个画面总是连接在一起。所以我试图找到一个解决办法,最后我找到了!(对不起,如果听起来很明显,我是一个初学者与图形)

public class MainActivity extends Activity {
DrawView drawView;


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set full screen view
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);


drawView = new DrawView(this);
setContentView(drawView);
drawView.requestFocus();
}
}




public class DrawingPanel extends View implements OnTouchListener {
private static final String TAG = "DrawView";


private static final float MINP = 0.25f;
private static final float MAXP = 0.75f;


private Canvas  mCanvas;
private Path    mPath;
private Paint       mPaint;
private LinkedList<Path> paths = new LinkedList<Path>();


public DrawingPanel(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);


this.setOnTouchListener(this);


mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(6);
mCanvas = new Canvas();
mPath = new Path();
paths.add(mPath);
}


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}


@Override
protected void onDraw(Canvas canvas) {
for (Path p : paths){
canvas.drawPath(p, mPaint);
}
}


private float mX, mY;
private static final float TOUCH_TOLERANCE = 4;


private void touch_start(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}


private void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
}
}


private void touch_up() {
mPath.lineTo(mX, mY);
// commit the path to our offscreen
mCanvas.drawPath(mPath, mPaint);
// kill this so we don't double draw
mPath = new Path();
paths.add(mPath);
}


@Override
public boolean onTouch(View arg0, MotionEvent event) {
float x = event.getX();
float y = event.getY();


switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
}

我把用你的手指画的机器人样本和修改它一点,以存储每条路径,而不是只有最后一个!希望能帮到别人!

干杯。

我有个问题,我画的是一个点,而不是一条线。您应该首先创建一个路径来保持线条。仅在第一次触摸事件中调用 path.moveto。然后在你的画布上绘制路径,然后重置或倒带的路径后,你完成(路径。重置) ..。

我已经尝试了几种方法来渲染运动事件的累积点。 最后,我通过计算两点之间的中点,并将列表中的点作为二次 Bezier 曲线的锚点(除了第一点和最后一点,它们通过简单的直线连接到下一个中点) ,得到了最好的结果。

这给了一个平滑的曲线没有任何角落。绘制的路径不会触及列表中的实际点,而是通过每个中点。

Path path = new Path();
if (points.size() > 1) {
Point prevPoint = null;
for (int i = 0; i < points.size(); i++) {
Point point = points.get(i);


if (i == 0) {
path.moveTo(point.x, point.y);
} else {
float midX = (prevPoint.x + point.x) / 2;
float midY = (prevPoint.y + point.y) / 2;


if (i == 1) {
path.lineTo(midX, midY);
} else {
path.quadTo(prevPoint.x, prevPoint.y, midX, midY);
}
}
prevPoint = point;
}
path.lineTo(prevPoint.x, prevPoint.y);
}

我最近不得不对此做了一些修改,现在已经开发出了我认为最好的解决方案,因为它有三个功能:

  1. 它允许你画不同的线
  2. 它可以使用较大的笔触,而不需要使用复杂的三次样条
  3. 它比这里的许多解决方案都要快,因为 canvas.drawPath()方法是 在外面 for 循环,所以它不会被多次调用。

public class DrawView extends View implements OnTouchListener {
private static final String TAG = "DrawView";


List<Point> points = new ArrayList<Point>();
Paint paint = new Paint();
List<Integer> newLine = new ArrayList<Integer>();


public DrawView(Context context, AttributeSet attrs){
super(context, attrs);
setFocusable(true);
setFocusableInTouchMode(true);
setClickable(true);


this.setOnTouchListener(this);


paint.setColor(Color.WHITE);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(20);


}


public void setColor(int color){
paint.setColor(color);
}
public void setBrushSize(int size){
paint.setStrokeWidth((float)size);
}
public DrawView(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);


this.setOnTouchListener(this);




paint.setColor(Color.BLUE);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(20);
}


@Override
public void onDraw(Canvas canvas) {
Path path = new Path();
path.setFillType(Path.FillType.EVEN_ODD);
for (int i = 0; i<points.size(); i++) {
Point newPoint = new Point();
if (newLine.contains(i)||i==0){
newPoint = points.get(i)
path.moveTo(newPoint.x, newPoint.y);
} else {
newPoint = points.get(i);


path.lineTo(newPoint.x, newPoint.y);
}


}
canvas.drawPath(path, paint);
}


public boolean onTouch(View view, MotionEvent event) {
Point point = new Point();
point.x = event.getX();
point.y = event.getY();
points.add(point);
invalidate();
Log.d(TAG, "point: " + point);
if(event.getAction() == MotionEvent.ACTION_UP){
// return super.onTouchEvent(event);
newLine.add(points.size());
}
return true;
}
}


class Point {
float x, y;


@Override
public String toString() {
return x + ", " + y;
}
}

这也可以,只是不太好

  import java.util.ArrayList;
import java.util.List;


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


public class DrawView extends View implements OnTouchListener {
private static final String TAG = "DrawView";
List<Point> points = new ArrayList<Point>();
Paint paint = new Paint();
List<Integer> newLine = new ArrayList<Integer>();


public DrawView(Context context, AttributeSet attrs){
super(context, attrs);
setFocusable(true);
setFocusableInTouchMode(true);


this.setOnTouchListener(this);


paint.setColor(Color.WHITE);
paint.setAntiAlias(true);
}
public DrawView(Context context) {
super(context);
setFocusable(true);
setFocusableInTouchMode(true);


this.setOnTouchListener(this);


paint.setColor(Color.WHITE);
paint.setAntiAlias(true);
}


@Override
public void onDraw(Canvas canvas) {
for (int i = 0; i<points.size(); i++) {
Point newPoint = new Point();
Point oldPoint = new Point();
if (newLine.contains(i)||i==0){
newPoint = points.get(i);
oldPoint = newPoint;
} else {
newPoint = points.get(i);
oldPoint = points.get(i-1);
}
canvas.drawLine(oldPoint.x, oldPoint.y, newPoint.x, newPoint.y, paint);
}
}


public boolean onTouch(View view, MotionEvent event) {
Point point = new Point();
point.x = event.getX();
point.y = event.getY();
points.add(point);
invalidate();
Log.d(TAG, "point: " + point);
if(event.getAction() == MotionEvent.ACTION_UP){
// return super.onTouchEvent(event);
newLine.add(points.size());
}
return true;
}
}


class Point {
float x, y;


@Override
public String toString() {
return x + ", " + y;
}
}

它可以很好地绘制直线,唯一的问题是如果你把直线画得更粗,这会使得所绘制的直线看起来有点奇怪,而且,我还是建议使用第一条直线。

使用 ACTION_MOVE的运动事件可以在一个对象中批处理多个运动样本。使用 getX (int)和 getY (int)可以获得最新的指针坐标。使用 getHistoricalX(int, int)getHistoricalY(int, int)访问批处理中的早期坐标。将它们用于建设道路,会使事情变得更加顺利:

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

下面是来自 Square 的一个很好的教程: http://corner.squareup.com/2010/07/smooth-signatures.html

如果你想要简单:

public class DrawByFingerCanvas extends View {


private Paint brush = new Paint(Paint.ANTI_ALIAS_FLAG);
private Path path = new Path();


public DrawByFingerCanvas(Context context) {
super(context);
brush.setStyle(Paint.Style.STROKE);
brush.setStrokeWidth(5);
}


@Override
protected void onDraw(Canvas c) {
c.drawPath(path, brush);
}


@Override
public boolean onTouchEvent(MotionEvent event) {


float x = event.getX();
float y = event.getY();


switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(x,y);
break;
case MotionEvent.ACTION_MOVE:
path.lineTo(x, y);
break;
default:
return false;
}
invalidate();
return true;
}
}

在活动中只有:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new DrawByFingerCanvas(this));
}

结果:

enter image description here

要擦除所有的图画只需旋转屏幕。

下面是一个用 Path.排队绘制平滑点的简单方法

fun applySmoothing(smoothingIterations: Int) {
for (z in 1..smoothingIterations) {
for (i in graphPoints.indices) {
if (i > 0 && i < graphPoints.size-1) {
val previousPoint = graphPoints[i-1]
val currentPoint = graphPoints[i]
val nextPoint = graphPoints[i+1]
val midX = (previousPoint.x + currentPoint.x + nextPoint.x) / 3
val midY = (previousPoint.y + currentPoint.y + nextPoint.y) / 3
graphPoints[i].x = midX
graphPoints[i].y = midY
}
}
}
}

这里有一个简化的解决方案,画一条线,沿着你的手指,并始终是直的:

Https://stackoverflow.com/a/68076519/15463816