需要让特殊线条能够平滑地跟随用户的手指移动,同时还具备其他行为。

8

首先,这不是其他“平滑线”问题的重复,因为我还需要能够随意删除我的线条的部分,因此我需要一种特殊的方式来存储我的线条。

我需要让一条线条跟随用户的手指移动。然而,我也需要能够随意删除这条线的末端部分。

基本上,我需要这条线的行为看起来像是在这个游戏中跟随用户鼠标移动的蓝线:

http://hakim.se/experiments/html5/coil/

为了实现这一点,我在我的onTouch方法中加入了一些代码,每次用户手指移动时向数组中添加一个点。

@Override
public boolean onTouch(View v, MotionEvent event) {



    //This for loop is supposed to add all points that were in between this 
    //motion event and the previous motion event to the "linePoints" array.

    for(int i = 0; i < event.getHistorySize(); i++) {
        linePoints[arrayIndex] = new Point((int) event.getHistoricalX(i), (int) event.getHistoricalY(i));
        arrayIndex++;
    }

     //This adds the current location of the user's finger to "linePoints"
    // array
    linePoints[arrayIndex] = new Point((int) event.getX(), (int) event.getY());
    arrayIndex++;

     //This switch statement makes it so that if the user lifts their finger
     // off the screen the line will get deleted.
     switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            screenPressed = true;
            setEventTime(); //Ignore setEventTime(); 
            break;
        case MotionEvent.ACTION_UP:
            screenPressed = false;
            linePoints = new Point[10000]; 
            arrayIndex = 0;
            break;
    }
 return true;


}

然后在 onDraw() 方法中,游戏会绘制每条线上的每个点:
 @Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);


    //This code loops through all of linePoints and then draws every point
    // in linePoints to create a line on screen.
    for(Point p : linePoints) {
       if(p == null) {
           break;
       }

        canvas.drawRect(p.x, p.y, p.x+ 2, p.y + 2, black);
        invalidate();

    // I have not added in the deletion behavior yet, because my current
    // method cannot create a smooth line.
    }

我选择使用点绘制线而不是使用Android的Path()类的原因是,我想随时删除线的部分(通过从数组“linePoints”中删除点)。
问题是,如果我移动手指太快,那么点会散开,看起来就不像一条线了。
如何确保线条保持平滑,同时以这种方式存储,可以删除其中的部分?
编辑:有人要求更具体地描述线条的详细信息,因此我将提供。
我希望在用户画了“X”秒以上的线后开始删除该线。我想删除线的方式是:
线的末端将开始消失,直到(同时用户仍在画它),直到线完全删除或用户将手指从屏幕上拿开。
编辑2:我还需要知道线是否相交或创建了某种封闭形状(这就是为什么我选择点存储系统的原因,我认为如果数组中的2个点具有相同的坐标,则我会知道线是否相交)。我目前不知道如何实现这一点(因为点不是连续的),但如果我想出了什么,我将提供进一步的编辑。
编辑3:我已经找到了一个解决方案,可以确定线是否相交(即使点间隔不规则)!但是我仍然没有解决创建无间隙平滑线条的问题。
解决方案:
每次游戏将新点添加到数组中时,它将将其与数组中上一个添加到的点进行比较,并建模线段“A”。然后,它将比较线段“A”与数组中由2个点组成的所有先前线段,并确定比较的线段是否相交。如果它们相交,则我知道该线中存在交点。
编辑4:这是我目前正在使用的完整代码。
在这段代码中,我(尝试)提供详细的注释和概述,解释我的目标以及迄今为止所做的事情。
作为引言,我当前的问题是能够按照一定的速度(例如每秒10毫米)删除线条,如果用户已经画了一段时间。
  package com.vroy.trapper;

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


public class GameView extends View implements View.OnTouchListener {

    // I am basically trying to replicate the game: http://hakim.se/experiments/html5/coil/
    // without any of the lighting effects and my version will have slightly
    // different behavior.

    // Right now all I am concerned with is allowing the line to be deleted at a constant pace
    // if the user has been drawing a line for more than "X" seconds.


    /*
    OVERVIEW:

    array of points "linePoints" stores all locations of user touching screen
    that are captured by system.

    Each time a new point is added to "linePoints" I draw a path from the previous point
    to the new point. (Sidenote: While this does make the line look very smooth it can still look odd sometimes)

    The game also checks for intersections in the line to see if the line has made a
    polygon. I do this because this is important for a feature that will be implemented.

    The system then draws the path on screen.

    The system also checks if the user has lifted their finger off the screen,
    if the user has then the system deletes the current line on screen and resets all variables.

    TO BE IMPLEMENTED:

    If the line has formed a polygon then the game will check if that polygon contains certain
    objects that will randomly spawn onscreen.


    PROBLEMS:

    1. Currently I want the line to start deleting itself from the back if the user
    has been drawing the line for more then "X" seconds. However I am not sure how to do this.

    */


    // General variables.
    private int screenWidth;
    private int screenHeight;
    public static boolean screenPressed; //Might not need.
    //    public static float contactLocX;
    //    public static float contactLocY;

        //Time variables.
        private static long startTime; //This variable is used in conjunction with the
                                       //elapsedTime() method to determine if the user
                                       // has been drawing a line for more then "X" seconds.


    //Game variables.
    private static int orbWidth; //Not used currently. This will be the width of the randomly spawned tokens.
    private Point[] linePoints; //The array that holds all captured points.
    private int arrayIndex;
    private Path linePath; //The path that the canvas draws.
    private boolean firstPoint; //If firstPoint is true then that means is 1st point in current line.
                                //I need this for the path.MoveTo() method.

    //Debug values. (Not used currently)
    private int debug;
    private String strdebug;

    //Paints
    Paint black = new Paint();


    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);


        black.setARGB(255, 0, 0, 0);  //Paint used to draw line.
        black.setStyle(Paint.Style.STROKE);
        black.setStrokeWidth(3);


        linePoints = new Point[10000];

        GameView gameView = (GameView) findViewById(R.id.GameScreen); //Setting up onTouch listener.
        gameView.setOnTouchListener(this);

        arrayIndex = 0;
        linePath = new Path(); //Setting up initial path.
        firstPoint = true;
    }


    //Currently OnSizeChanged is not needed, I only keep it for the future when I implement
    // the random object spawning system.
    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        screenHeight = getHeight();
        screenWidth = getWidth();
        orbWidth = screenHeight / 20;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawPath(linePath, black);

        //Currently "1000000000" is a placeholder value (in nano-seconds)
        if(elapsedTime() > 1000000000 ) {

            //Code that evenly deletes the line starting from the back
            //(this is where I most need your assistance).


        }

        invalidate(); //I don't know if this is the best way to refresh the screen

    }



    @Override
    public boolean onTouch(View v, MotionEvent event) {

        //Sets up starting point of path
        if(firstPoint) {
            firstPoint = false;
            linePath.moveTo(event.getX(),event.getY());
            linePoints.add(new TimeStampedPoint((int)event.getX(),              (int)event.getY(),event.getEventTime()));



        }

        //Adds points to path & linePoints that were missed.
        for(int i = 0; i < event.getHistorySize(); i++) {
            linePoints[arrayIndex] = new Point((int) event.getHistoricalX(i), (int) event.getHistoricalY(i));
            linePath.lineTo(linePoints[arrayIndex].x,linePoints[arrayIndex].y);
            if(arrayIndex >= 1) {
                checkForIntersections(linePoints[arrayIndex - 1], linePoints[arrayIndex]);
            }
            arrayIndex++;
        }

        //Adds current point to path & linePath();
        linePoints[arrayIndex] = new Point((int) event.getX(), (int) event.getY());
        if (arrayIndex >= 1) {
            checkForIntersections(linePoints[arrayIndex - 1] ,linePoints[arrayIndex]);
        }
        linePath.lineTo(linePoints[arrayIndex].x,linePoints[arrayIndex].y);
        arrayIndex++;

        //This switch statements creates initial actions for when the finger is pressed/lifted.
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                screenPressed = true;
                setEventTime(); //This starts the timer that will eventually reach "X" seconds.
                break;
            case MotionEvent.ACTION_UP: //The primary purpose of this "switch" is to delete the old line
                                        // & reset variables in preparation for new line
                screenPressed = false;
                linePoints = new Point[10000]; //Possibly filling heap with empty arrays.
                linePath = new Path();
                arrayIndex = 0;
                firstPoint = true;
                break;
        }

        return true;
    }


    private void checkForIntersections(Point p, Point p2) {

        for(int i = arrayIndex - 3; i > 0; i--) {
            if(intersect(p,p2,linePoints[i],linePoints[i-1])) {
                //RETURN POINTS IN THE POLYGON THAT WILL BE USED TO DETERMINE IF "TOKENS"
                // ARE IN POLYGON.
            }
        }
    }

    private void setEventTime() {
    startTime = System.nanoTime();
    }

    //Checks current time since setEventTime
    private long elapsedTime() {
    return  System.nanoTime() - startTime;
    }

    // Things used to determine intersections.

    //Used to determine orientation of <something>
    private static int orientation(Point p, Point q, Point r) {
        double val = (q.y - p.y) * (r.x - q.x)
                - (q.x - p.x) * (r.y - q.y);

        if (val == 0.0)
            return 0; // colinear
        return (val > 0) ? 1 : 2; // clock or counterclock wise
    }

    //Determines intersection of 2 lines (P1,Q1) & (P2,Q2).
    private static boolean intersect(Point p1, Point q1, Point p2, Point q2) {

        int o1 = orientation(p1, q1, p2);
        int o2 = orientation(p1, q1, q2);
        int o3 = orientation(p2, q2, p1);
        int o4 = orientation(p2, q2, q1);

        if (o1 != o2 && o3 != o4)
            return true;

        return false;
    }

    //Will shorten checking process by determining if 2 lines do/don't have the same bounding box.
    //Not yet implemented.
    private static boolean boundBoxCheck(Point p1, Point q1, Point p2, Point q2) {
        return true; //Placeholder code
    }


    }

编辑5:

我已经实现了stKent/Titan的代码,但是我的代码由于索引超出范围而崩溃。

我将尝试找到问题并修复它,但在此之前,我会把我的代码发布在这里,以便其他人想要修复它。

   package com.vroy.trapper;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.sql.Time;
import java.util.ArrayList;
import java.util.List;


public class GameView extends View implements View.OnTouchListener {

    // I am basically trying to replicate the game: http://hakim.se/experiments/html5/coil/
    // without any of the lighting effects and my version will have slightly
    // different behavior.

    // Right now all I am concerned with is allowing the line to be deleted at a constant pace
    // if the user has been drawing a line for more than "X" seconds.


    /*
    OVERVIEW:

    array of points "linePoints" stores all locations of user touching screen
    that are captured by system.

    Each time a new point is added to "linePoints" I draw a path from the previous point
    to the new point. (Sidenote: While this does make the line look very smooth it can still look odd sometimes)

    The game also checks for intersections in the line to see if the line has made a
    polygon. I do this because this is important for a feature that will be implemented.

    The system then draws the path on screen.

    The system also checks if the user has lifted their finger off the screen,
    if the user has then the system deletes the current line on screen and resets all variables.

    TO BE IMPLEMENTED:

    If the line has formed a polygon then the game will check if that polygon contains certain
    objects that will randomly spawn onscreen.


    PROBLEMS:

    1. Currently I want the line to start deleting itself from the back if the user
    has been drawing the line for more then "X" seconds. However I am not sure how to do this.

    */


    // General variables.
    private int screenWidth;
    private int screenHeight;
    public static boolean screenPressed; //Might not need.
//    public static float contactLocX;
//    public static float contactLocY;

        //Time variables.
        private static long startTime; //This variable is used in conjunction with the
                                       //elapsedTime() method to determine if the user
                                       // has been drawing a line for more then "X" seconds.


    //Game variables.
    private static int orbWidth; //Not used currently. This will be the width of the randomly spawned tokens.
    private List<TimeStampedPoint> linePoints; //The array that holds all captured points.
    private int arrayIndex;
    private Path linePath; //The path that the canvas draws.
    private List<TimeStampedPoint> validPoints;
    private boolean firstPoint; //If firstPoint is true then that means is 1st point in current line.
                                //I need this for the path.MoveTo() method.

    //Debug values. (Not used currently)
    private int debugint;
    private String strdebug;

    //Paints
    Paint black = new Paint();


    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);


        black.setARGB(255, 0, 0, 0);  //Paint used to draw line.
        black.setStyle(Paint.Style.STROKE);
        black.setStrokeWidth(3);


        linePoints = new ArrayList<>();
        validPoints = new ArrayList<>();

        GameView gameView = (GameView) findViewById(R.id.GameScreen); //Setting up onTouch listener.
        gameView.setOnTouchListener(this);

        arrayIndex = 0;
        linePath = new Path(); //Setting up initial path.
        validPoints = new ArrayList<>();
        firstPoint = true;
    }


    //Currently OnSizeChanged is not needed, I only keep it for the future when I implement
    // the random object spawning system.
    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        screenHeight = getHeight();
        screenWidth = getWidth();
        orbWidth = screenHeight / 20;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        linePath.rewind();

        validPoints = removeExpiredPoints();
        updatePathUsingPoints(validPoints);

        canvas.drawPath(linePath, black);


        linePoints = validPoints;


        invalidate(); //I don't know if this is the best way to refresh the screen

    }



    @Override
    public boolean onTouch(View v, MotionEvent event) {
        debugint = arrayIndex;
        strdebug = Integer.toString(debugint);

        Log.i("ARRAY INDEX: ",strdebug);

        debugint = linePoints.size();
        strdebug = Integer.toString(debugint);

        Log.i("LIST SIZE: ",strdebug);



        //Sets up starting point of path
        if(firstPoint) {
            firstPoint = false;
            linePath.moveTo(event.getX(),event.getY());
            linePoints.add(new TimeStampedPoint((int)event.getX(),(int)event.getY(),event.getEventTime()));
        }

        //Adds points to path & linePoints that were missed.
        for(int i = 0; i < event.getHistorySize(); i++) {
            linePoints.add(new TimeStampedPoint((int) event.getHistoricalX(i), (int) event.getHistoricalY(i), event.getHistoricalEventTime(i)));
            linePath.lineTo(linePoints.get(arrayIndex).x,linePoints.get(arrayIndex).y);
            if(arrayIndex >= 1) {
                checkForIntersections(linePoints.get(arrayIndex), linePoints.get(arrayIndex));
            }
            arrayIndex++;
        }

        //Adds current point to path & linePath();

        debugint = linePoints.size();
        strdebug = Integer.toString(debugint);
        Log.i("Before" , strdebug);



        linePoints.add(new TimeStampedPoint((int) event.getX(), (int) event.getY(),event.getEventTime()));

        debugint = linePoints.size();
        strdebug = Integer.toString(debugint);
        Log.i("After:", strdebug);


        if (arrayIndex >= 1) {
            checkForIntersections(linePoints.get(arrayIndex - 1) ,linePoints.get(arrayIndex));
        }
        linePath.lineTo(linePoints.get(arrayIndex).x,linePoints.get(arrayIndex).y);
        arrayIndex++;

        //This switch statements creates initial actions for when the finger is pressed/lifted.
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                screenPressed = true;
                setEventTime(); //This starts the timer that will eventually reach "X" seconds.
                break;
            case MotionEvent.ACTION_UP: //The primary purpose of this "switch" is to delete the old line
                                        // & reset variables in preparation for new line
                screenPressed = false;
                linePoints.clear();
                linePath = new Path();
                arrayIndex = 0;
                firstPoint = true;
                break;
        }

        return true;
    }


    private void checkForIntersections(TimeStampedPoint p, TimeStampedPoint p2) {

        for(int i = arrayIndex - 3; i > 0; i--) {
            if(intersect(p,p2,linePoints.get(i),linePoints.get(i-1))) {
                //RETURN POINTS IN THE POLYGON THAT WILL BE USED TO DETERMINE IF "TOKENS"
                // ARE IN POLYGON.
            }
        }
    }

    private void setEventTime() {
    startTime = System.nanoTime();
    }

    //Checks current time since setEventTime
    private long elapsedTime() {
    return  System.nanoTime() - startTime;
    }

    // Things used to determine intersections.

    //Used to determine orientation of <something>
    private static int orientation(Point p, Point q, Point r) {
        double val = (q.y - p.y) * (r.x - q.x)
                - (q.x - p.x) * (r.y - q.y);

        if (val == 0.0)
            return 0; // colinear
        return (val > 0) ? 1 : 2; // clock or counterclock wise
    }

    //Determines intersection of 2 lines (P1,Q1) & (P2,Q2).
    private static boolean intersect(TimeStampedPoint p1, TimeStampedPoint q1, TimeStampedPoint p2, TimeStampedPoint q2) {

        int o1 = orientation(p1, q1, p2);
        int o2 = orientation(p1, q1, q2);
        int o3 = orientation(p2, q2, p1);
        int o4 = orientation(p2, q2, q1);

        if (o1 != o2 && o3 != o4)
            return true;

        return false;
    }

    //Will shorten checking process by determining if 2 lines do/don't have the same bounding box.
    //Not yet implemented.
    private static boolean boundBoxCheck(Point p1, Point q1, Point p2, Point q2) {
        return true; //Placeholder code
    }


    //Point class that also stores time of creation
    @SuppressLint("ParcelCreator")
    private static class TimeStampedPoint extends Point {

        private final long timeStamp;

        private TimeStampedPoint(final int x, final int y, final long timeStamp) {
            super(x, y);
            this.timeStamp = timeStamp;
        }
    }


    private List<TimeStampedPoint> removeExpiredPoints() {
        final List<TimeStampedPoint> result = new ArrayList<>();

        for (final TimeStampedPoint point: linePoints) {
            if (System.currentTimeMillis() - point.timeStamp <= 10000) {
                // We only include points in the result if they are not expired.
                result.add(point);
            }
        }

        return result;
    }

    private void updatePathUsingPoints(final List<TimeStampedPoint> validPoints) {
        if (validPoints.size() < 2) {
            return; // Return the empty path here; nothing to draw.
        }

        linePath.moveTo(validPoints.get(0).x,validPoints.get(0).y);

        for (int i = 1; i < validPoints.size(); i++) {
            final Point targetPoint = validPoints.get(i);
            linePath.lineTo(targetPoint.x, targetPoint.y);
        }


    }

}

还有一件非常非常重要的事情,我必须注意到。我认为直到第四次编辑为止,我没有注意到这一点,虽然我希望将该行从结尾删除,但我也希望它能够均匀地被删除,我认为由stkent和Titan提供的当前代码以一致的速度删除了该行中的点,但这并不意味着该行本身将以相同的速度被删除(因为点分布不均)。
非常感谢大家一直支持我,直到现在我希望可以找到一个解决方案,使该行也能以一致的速度被删除。

我已经想出了如何使线条平滑,通过在每个点之间绘制路径。然而,我仍然没有解决以恒定速度删除线条的方法,因为不同的路径长度不同,导致删除速度不均匀。 - Foobar
你能沿着那些绘制的路径进行插值,找到一个新的端点吗?这样至少可以营造出部分删除的假象。不记得路径是否可以通过弧长轻松参数化了。 - stkent
@stkent我不太知道如何沿着绘制的路径插值。也许您可以将代码发布为答案? - Foobar
关于崩溃 - 我仍然在你的代码中看到 arrayIndex。现在你正在使用列表,完全可以将其删除,而且由于我们在点过期时调整了列表大小,这可能是索引越界问题的原因。关于平滑删除 - 我明天会更新我的答案,并提供我第一次提到的那些细节。 - stkent
“删除线条均匀”:您需要更仔细地定义此内容,以便我们知道您的确切目标。我们如何确定应删除线条的速度?假设从曲线上最早的点开始删除,但由于我们不再使用第二个点的时间信息来确定第一条线段的删除,因此没有单一的“恒定删除速度”概念可遵循。 - stkent
@stkent 我还没有考虑过删除速度,但理想情况下,解决方案应该是通用的,这样我就可以轻松地调整删除速度,直到达到最佳效果。 - Foobar
2个回答

3

我建议使用ArrayList而不是静态数组,因为您可能并不总是需要存储10000个点。我还建议创建Point的子类,并在实例化时存储时间戳。请考虑:

public class TimedPoint extends Point {
    private static final int KEEP_ALIVE_TIME_MS = 200; //tweak this value to your needs
    private final long time;

    public TimedPoint(int x, int y) {
        super(x, y);
        time = System.currentTimeMillis();
    }

    public TimedPoint(int x, int y, long time) {
        super(x, y);
        this.time = time;
    }

    public boolean hasExpired(long time) {
        return (time-this.time>KEEP_ALIVE_TIME_MS);
    }
}

public class GameView extends View ... {
    ArrayList<TimedPoint> linePoints = new ArrayList<>(); //Lists can grow and shrink to demand
    //this implementation is backed by an array.
    ...

    public void addPoint(int x, int y) {
        linePoints.add(new TimedPoint(x, y);
    }

    public void removeOldPoints() {
        long time = System.currentTimeMillis();
        Iterator<TimedPoint> i = linePoints.iterator();
        while(i.hasNext()) {
            TimedPoint point = i.next();
            if(point.hasExpired(time))
                i.remove();
        }
    }
}

removeOldPoints()函数将删除linePoints中时间差大于TimedPoint定义的阈值的任何点。这意味着您可以定期调用removeOldPoints()函数。提示一下,如果在onDraw()中调用,效果会很好。

如果在绘制线条之前在onDraw()中调用removeOldPoints()函数,则可以保证linePoints中保留的任何点都应该被绘制。此时只需遍历列表并将点绘制为线条即可,"尾巴"将在绘制时开始消失。


您还可以将linePoints传递给TimedPoint,并在构造时设置一个Timer,并为每个TimedPoint调用schedule()以在未来的某个时间自动移除自身。这不需要您定期调用removeOldPoints()函数。请考虑:

public class TimedPoint extends Point {
    private static final long KEEP_ALIVE_TIME_MS = 200; //tweak this value to your needs
    //we don't need a timestamp, because every point disposes of itself. We do need a timer, though.
    private final Timer lifetime = new Timer();

    public TimedPoint(final List<TimedPoint> linePoints, int x, int y) {
        super(x, y);
        lifetime.schedule(new TimerTask() {
            @Override
            public void run() {
                linePoints.remove(TimedPoint.this);
            }
        }, KEEP_ALIVE_TIME_MS);
    }
}

 public class GameView extends View ... {
    List<TimedPoint> linePoints = Collections.synchronizedList(new ArrayList<>()); //Lists can grow and shrink to demand
    //this implementation is backed by an array.
    //and is thread safe for Timer
    ...

    public void addPoint(int x, int y) {
        linePoints.add(new TimedPoint(x, y);
    }
   //notice removeOldPoints() is gone! Each point now disposes of itself, no calls needed.
}

这种方法还有一些可以调整的地方。例如,点在“出生”后立即开始“死亡”。如果更合适,我们可以将其更改为只在添加到列表时才这样做。
此外,可能还有优化的空间,因为我认为这可能会为每个点生成一个新线程。这应该会提高性能(如果removeOldPoints()是瓶颈),直到您的CPU被上下文切换所束缚。如果您感到苛刻,或者性能成为问题;你可以使用线程池和队列。 这里ArrayList的文档,以帮助您适应新类。
开心编码 :)
编辑 看起来您仍然遇到麻烦。尝试这个,让我知道它对您有何影响。
public class GameView ... {
    ArrayList<TimedPoint> linePoints = new ArrayList<>(); //Lists can grow and shrink to demand
    //this implementation is backed by an array.
    ...

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        removeOldPoints();
        Path path = linePointsToPath(); //I'm not sure if you need to store path, let's generate it.
        if(path != null)
            canvas.drawPath(path, black);
    }

    public void addPoint(int x, int y) {
        linePoints.add(new TimedPoint(x, y);
        invalidate();
    }

    public void removeOldPoints() {
        int oldLen = linePoints.size();
        long time = System.currentTimeMillis();
        Iterator<TimedPoint> i = linePoints.iterator();
        while(i.hasNext()) {
            TimedPoint point = i.next();
            if(point.hasExpired(time))
                i.remove();
        }
        int newLen = linePoints.size();
        if(newLen != oldLen) //if we removed items from list
            invalidate();
    }

    //small tweaks to stKents method
    private Path linePointsToPath() {
        if(linePoints.size() < 2)
            return null;
        Path path = new Path();
        Point p = points.get(0);
        Path.moveTo(p.x, p.y);
        for(Point point : linePoints) {
            if(p != point)
                path.lineTo(point.x, point.y); //skip first point, because of moveTo
        }
        return path;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        ...
        addPoint(...);
    }
}

这也是我的问题的一个非常好的解决方案,我在你的代码中看到的一个问题是System.currenttimemillis()会给出错误的时间戳,因为我还要检查历史点(实际上在onTouch方法执行时没有被捕获的点)。 - Foobar
我没有想到那个。我已经给'TimedPoint'添加了另一个构造函数,这样您就可以为历史时间设置自己的时间。只需将所有必要的'Point'用'TimedPoint'替换,并将时间作为构造函数的最后一个参数传递,或者省略它以获得自动“现在”时间戳;就像我在'addPoint()'中所做的那样。 - MeetTitan
onTouch中调用invalidate是我的第一个想法,但我相信这将会阻止用户在抬起手指后删除该行 :/ - stkent

2
根据您最近的代码,这是我首先尝试的方法。在此答案中,我做出以下假设:
  • 每次只会绘制一条线/路径(如果不是,则需要针对每个路径执行下面概述的过程,通过迭代一些路径集合)

创建一个围绕Point类的包装器,添加时间戳:

private static class TimeStampedPoint extends Point {

    private final long timeStamp;

    private TimeStampedPoint(final int x, final int y, final long timeStamp) {
        super(x, y);
        this.timeStamp = timeStamp;
    }
}

然后将您的点数存储更新为以下内容:
List<TimeStampedPoint> linePoints = new ArrayList<>();

由于这个原因,你需要对代码进行大量更改。特别地,你可以使用List方法中的add来将新的点追加到列表的末尾,而不是显式跟踪arrayIndex

在你的onTouchEvent方法中,用以下代码块替换:

for(int i = 0; i < event.getHistorySize(); i++) {
    linePoints[arrayIndex] = new Point((int) event.getHistoricalX(i), (int) event.getHistoricalY(i));
    linePath.lineTo(linePoints[arrayIndex].x,linePoints[arrayIndex].y);
    if(arrayIndex >= 1) {
        checkForIntersections(linePoints[arrayIndex - 1], linePoints[arrayIndex]);
    }
    arrayIndex++;
}

使用类似以下的东西:

这样的东西:

for(int i = 0; i < event.getHistorySize(); i++) {
    TimeStampedPoint point = new TimeStampedPoint((int) event.getHistoricalX(i), (int) event.getHistoricalY(i), event.getHistoricalEventTime(i));

    linePoints.add(point);
    linePath.lineTo(point.x, point.y);

    int numberOfPoints = linePoints.size();

    if(numberOfPoints >= 2) {
        checkForIntersections(linePoints.get(numberOfPoints - 2), linePoints.get(numberOfPoints - 1));
    }
}

请在添加数值到linePoints数组的每个位置进行类似的调整。同时请注意,在循环过程中我们不再逐步创建Path,因为我们将在构建Path之前对其进行一些清理(例如删除已过期的点)。为此,请每次准备绘制时清除linePath(如果性能不佳,您可能需要将此方法移动到其他位置;我只是建议在onDraw中进行以使生命周期更加明确)。然后您的onDraw方法应如下所示:
@Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Reset the Path.
    linePath.rewind();

    validPoints = removeExpiredPoints();
    updatePathUsingPoints(validPoints);
    canvas.drawPath(linePath, black);

    linePoints = validPoints;

    invalidate(); //I don't know if this is the best way to refresh the screen
}

这里的validPoints是另一个类型为List<TimeStampedPoint>的字段。[一般来说,从onDraw中调用invalidate可能不是最好的选择,但这超出了本问题的范围。]

这里引入了两个新方法:

private List<TimeStampedPoint> removeExpiredPoints() {
    final List<TimeStampedPoint> result = new ArrayList<>();

    for (final TimeStampedPoint point: linePoints) {
        if (System.uptimeMillis() - point.getTimeStamp <= 10000) {
            // We only include points in the result if they are not expired.
            result.add(point);
        }
    }

    return result;
}

并且

private void updatePathUsingPoints(final List<TimeStampedPoint> validPoints) {
    if (validPoints.size() < 2) {
        return linePath; // Return the empty path here; nothing to draw.
    }

    linePath.moveTo(validPoints.get(0));

    for (int i = 1; i < validPoints.size(); i++) {
        final Point targetPoint = validPoints.get(i);
        linePath.lineTo(targetPoint.x, targetPoint.y);
    }
}

希望这个框架能让你有足够的起点。如果你注意到行尾消失不流畅,我有一些想法可以帮助,但需要打更多字——所以我们不要过早地优化 :)


另外,我想问一下,updatePathUsingPoints()方法不应该返回任何内容,对吗?因为对于这一行“linePath = updatePathUsingPoints(validPoints);”,该方法必须返回某些内容,但这似乎不正确。 - Foobar
没错;我最初写的方法是返回一个 Path,但后来想起可以使用 rewind。现在正在回答中进行修复! - stkent
对于那些给出负评的人,你们有什么建设性的意见吗? - stkent
linePoints[arrayIndex] = new TimeStampedPoint... 更改为适用于将 linePoints 更改为 List 后可以编译的内容,我会为您抵消那个匆忙的踩踏。 - MeetTitan
我的代码实现似乎崩溃了,我不确定原因。我会继续研究,但与此同时,我会发布我的版本,以防您想要查看。 - Foobar
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接