72 votes

Android Comment dessiner une ligne lisse en suivant votre doigt

http://marakana.com/tutorials/Android/2d-graphics-example.html

J'utilise l'exemple ci-dessous. Mais lorsque je déplace mes doigts trop rapidement sur l'écran, la ligne se transforme en points individuels.

Je ne sais pas si je peux accélérer le dessin. Ou bien je devrais relier les deux derniers points par une ligne droite. La seconde de ces deux solutions semble être une bonne option, sauf qu'en déplaçant votre doigt très rapidement, vous aurez de longues sections de ligne droite puis des courbes prononcées.

S'il existe d'autres solutions, je serais ravi de les entendre.

Merci d'avance pour toute aide.

0 votes

Si les lignes droites s'avèrent "insuffisantes" pour votre objectif, vous pouvez envisager l'ajustement des courbes : stackoverflow.com/questions/878200/java-curve-fitting-library

0 votes

Merci, cela semble utile. Je n'avais pas pensé à utiliser des splines. Principalement parce que je suppose que cela utilise beaucoup plus de ressources. Est-ce que c'est aussi disponible pour Android

2 votes

Vous avez Path.quadTo , Path.cubicTo ... stackoverflow.com/questions/3811529/

116voto

johncarl Points 5449

Une solution facile, comme vous l'avez mentionné, consiste à relier simplement les points par une ligne droite. Voici le code pour le faire :

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

Assurez-vous que vous changez votre peinture de remplissage à course :

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

Une autre option consiste à relier les points par iterpolation en utilisant la méthode 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);
}

Il en résulte tout de même quelques arêtes vives.

Si vous êtes vraiment ambitieux, vous pouvez commencer à calculer les splines cubiques comme suit :

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

J'ai également constaté qu'il fallait modifier les éléments suivants pour éviter de dupliquer les événements de mouvement :

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

et ajoutez les valeurs dx et dy à la classe Point :

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

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

Cela produit des lignes lisses, mais il faut parfois relier les points à l'aide d'une boucle. En outre, pour les longues sessions de dessin, cette méthode devient gourmande en ressources informatiques.

J'espère que cela vous aidera... c'est amusant de jouer avec.

Modifier

J'ai réalisé un projet rapide démontrant ces différentes techniques, y compris l'implémentation de la signature suggérée de Square. Profitez-en : https://github.com/johncarl81/androiddraw

11 votes

Voici un article intéressant sur le sujet. Il semble qu'Android regroupe les événements tactiles : corner.squareup.com/2010/07/signatures-lisse.html

4 votes

Le premier exemple de boucle cubicTo() semble être erroné. Il devrait boucler sur tous les points, pas seulement sur le dernier.

0 votes

Eric Obermühlner : Je passe en boucle sur tous les points dans la boucle for autour de l'appel cubicTo().

35voto

caiocpricci2 Points 3681

Ce n'est peut-être plus important pour vous, mais j'ai eu beaucoup de mal à le résoudre et je veux le partager, cela pourrait être utile à quelqu'un d'autre.

Le tutoriel et la solution proposée par @johncarl sont excellents pour dessiner mais ils offrent une limitation pour mes besoins. Si vous retirez votre doigt de l'écran et le remettez, cette solution tracera une ligne entre le dernier clic et votre nouveau clic, rendant le dessin entier toujours connecté. J'ai donc essayé de trouver une solution à ce problème, et je l'ai finalement trouvée (désolé si cela semble évident, je suis un débutant en graphisme).

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

J'ai pris l'exemple d'Android pour dessiner avec votre doigt et je l'ai modifié un peu pour stocker chaque chemin au lieu du dernier seulement ! J'espère que cela aidera quelqu'un !

A la vôtre.

1 votes

Hé... Essayez de vider la liste quelque part, sinon elle va ralentir progressivement, car vous continuez à dessiner de plus en plus à la fois...

0 votes

Ce n'était pas un problème pour les applications qui ne fonctionnent pas longtemps (comme celle que j'ai faite), pour mon implémentation, cela ne fonctionne jamais plus d'une minute environ. Mais merci d'avoir signalé ce problème !

1 votes

Avec le stockage des chemins pour le dessin, il est facile d'implémenter les fonctions "undo" ou "redo". Mais comment peut-on implémenter la fonction d'effacement si on ne stocke pas le dessin dans le bitmap ?

23voto

Eric Obermühlner Points 271

J'ai expérimenté plusieurs façons de rendre les points accumulés des événements de mouvement. Au final, j'ai obtenu les meilleurs résultats en calculant les points médians entre deux points et en traitant les points de la liste comme des points d'ancrage de courbes de Bézier quadratiques (à l'exception du premier et du dernier point qui sont reliés par des lignes simples au point médian suivant).

Cela permet d'obtenir une courbe lisse sans aucun angle. Le chemin tracé ne touchera pas les points réels de la liste, mais passera par chaque point central.

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

0 votes

Merci. Il a fait exactement ce que j'attendais.

0 votes

Grâce à vous, c'est ce que j'ai cherché !

1 votes

J'ai dû retirer l'instruction path.lineTo(prevPoint.x, prevPoint.y) ; en dehors de la boucle for pour obtenir une ligne lisse et nette. Pouvez-vous expliquer pourquoi elle est là ?

6voto

J'ai eu un problème très similaire. Lorsque vous appelez la méthode onTouch, vous devriez également utiliser la méthode (inside onTouch(MotionEvent event))

event.getHistorySize();

et quelque chose comme ça

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
}

0 votes

Je n'étais pas au courant... Je suis curieux de voir combien de données historiques il y a dans l'événement.

3voto

jcw Points 3047

J'ai dû y apporter quelques modifications récemment, et j'ai maintenant développé ce que je crois être la meilleure solution ici car elle fait trois choses :

  1. Il vous permet de dessiner différentes lignes
  2. Il fonctionne avec des coups de pinceau plus larges et sans utiliser de splines cubiques compliquées.
  3. C'est plus rapide que la plupart des solutions proposées ici car le canvas.drawPath() La méthode est à l'extérieur de la boucle for, afin qu'elle ne soit pas appelée plusieurs fois.

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

Cela fonctionne aussi, mais pas aussi 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;
    }
}

Il vous permet de dessiner des lignes raisonnablement bien, le seul problème est que si vous rendez la ligne plus épaisse, les lignes dessinées ont l'air un peu étranges, et vraiment, je recommanderais d'utiliser le premier de toute façon.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X