java - trucos - Android Cómo dibujar una línea suave siguiendo tu dedo
trucos para dibujar (9)
He experimentado con varias formas de representar los puntos acumulados de los eventos de movimiento. Al final tuve los mejores resultados al calcular los puntos medios entre dos puntos y tratar los puntos en la lista como puntos de anclaje de las curvas cuadradas de Bezier (excepto el primer y último punto que están conectados por líneas simples al siguiente punto medio )
Esto le da una curva suave sin esquinas. La ruta dibujada no tocará los puntos reales en la lista, sino que atravesará cada punto medio.
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);
}
http://marakana.com/tutorials/android/2d-graphics-example.html
Estoy usando este ejemplo a continuación. Pero cuando muevo los dedos demasiado rápido por la pantalla, la línea se convierte en puntos individuales.
No estoy seguro de si puedo acelerar el sorteo. O debería conectar los dos últimos puntos con una línea recta. La segunda de estas dos soluciones parece una buena opción, excepto cuando mueva el dedo muy rápido tendrá secciones largas de una línea recta y luego curvas pronunciadas.
Si hay otras soluciones, sería genial escucharlas.
Gracias por cualquier ayuda de antemano.
Los eventos de movimiento con ACTION_MOVE
pueden agrupar varias muestras de movimiento dentro de un solo objeto. Las coordenadas del puntero más actuales están disponibles usando getX (int) y getY (int). Se accede a las coordenadas anteriores dentro del lote utilizando getHistoricalX(int, int)
y getHistoricalY(int, int)
. Usarlos para construir ruta hace que sea mucho más suave:
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);
Aquí hay un buen tutorial sobre esto desde Square: corner.squareup.com/2010/07/smooth-signatures.html
Puede que ya no sea importante para usted, pero me costó mucho resolverlo y quiero compartirlo, podría serle útil a otra persona.
El tutorial con la solución @johncarl ofrecida es excelente para el dibujo, pero ofrecieron una limitación para mis propósitos. Si saca su dedo de la pantalla y lo vuelve a colocar, esta solución dibujará una línea entre el último clic y el nuevo clic, haciendo que todo el dibujo esté siempre conectado. Así que estaba tratando de encontrar una solución para eso, y finalmente lo conseguí (lo siento si suena obvio, soy un principiante con gráficos)
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;
}
}
Tomé la muestra de Android para dibujar con el dedo y la modifiqué un poco para almacenar cada ruta en lugar de solo la última. Espero que ayude a alguien!
Aclamaciones.
Puede tener mucha más información disponible en su MotionEvent
de lo que MotionEvent
que puede proporcionar algunos datos intermedios.
El ejemplo en su enlace ignora los puntos de contacto históricos incluidos en el evento. Consulte la sección " MotionEvent
lotes" cerca de la parte superior de la documentación de MotionEvent
: http://developer.android.com/reference/android/view/MotionEvent.html Más allá de eso, conectar los puntos con líneas puede no ser una mala idea.
Si lo quieres simple:
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;
}
}
En la actividad simplemente:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new DrawByFingerCanvas(this));
}
Resultado:
Para borrar todos los dibujos solo gira la pantalla.
Tuve este problema, estaba dibujando un punto en lugar de una línea. Primero debe crear una ruta para mantener su línea. llame a path.moveto solo en su primer evento táctil. Luego, en su lienzo, dibuje la ruta y luego reinicie o rebobine la ruta después de su hecho (path.reset) ...
Tuve que hacer algunas modificaciones a esto recientemente, y ahora he desarrollado lo que creo que es la mejor solución porque hace tres cosas:
- Le permite dibujar diferentes líneas
- Funciona con pinceladas más grandes y sin utilizar complicadas cúbicas
- Es más rápido que muchas de las soluciones aquí porque el método
canvas.drawPath()
está fuera del ciclo for, por lo que no se llama varias veces.
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;
}
}
Esto también funciona, pero no tan bien
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;
}
}
Te permite dibujar líneas razonablemente bien, el único problema es si haces que la línea sea más gruesa, lo que hace que las líneas dibujadas se vean un poco raras, y realmente, recomendaría usar la primera de todos modos.
Tuve un problema muy similar. Cuando llamas al método Toque, también debes usar el método (dentro de Toque (evento MotionEvent))
event.getHistorySize();
y algo así
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
}
Una solución fácil, como usted mencionó, es simplemente conectar los puntos con una línea recta. Aquí está el código para hacerlo:
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);
}
asegúrese de cambiar su pintura de relleno a golpe:
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.WHITE);
Otra opción es conectar los puntos con iterpolation usando el método 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);
}
Esto todavía resulta en algunos bordes filosos.
Si eres realmente ambicioso, puedes comenzar a calcular las splines cúbicos de la siguiente manera:
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);
}
Además, descubrí que necesitaba cambiar lo siguiente para evitar eventos de movimiento duplicados:
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);
}
y agrega los valores dx y dy a la clase Point:
class Point {
float x, y;
float dx, dy;
@Override
public String toString() {
return x + ", " + y;
}
}
Esto produce líneas suaves, pero a veces tiene que conectar los puntos mediante un bucle. Además, para largas sesiones de dibujo, esto se vuelve computacionalmente intenso para calcular.
Espero que ayude ... cosas divertidas para jugar.
Editar
Lancé un proyecto rápido que demuestra estas diferentes técnicas, incluida la implementación de sugerencia de Square. Disfruta: https://github.com/johncarl81/androiddraw