首先,这不是其他“平滑线”问题的重复,因为我还需要能够随意删除我的线条的部分,因此我需要一种特殊的方式来存储我的线条。
我需要让一条线条跟随用户的手指移动。然而,我也需要能够随意删除这条线的末端部分。
基本上,我需要这条线的行为看起来像是在这个游戏中跟随用户鼠标移动的蓝线:
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提供的当前代码以一致的速度删除了该行中的点,但这并不意味着该行本身将以相同的速度被删除(因为点分布不均)。
非常感谢大家一直支持我,直到现在我希望可以找到一个解决方案,使该行也能以一致的速度被删除。
arrayIndex
。现在你正在使用列表,完全可以将其删除,而且由于我们在点过期时调整了列表大小,这可能是索引越界问题的原因。关于平滑删除 - 我明天会更新我的答案,并提供我第一次提到的那些细节。 - stkent