Android Как нарисовать плавную линию по пальцу - PullRequest
66 голосов
/ 27 ноября 2011

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

Я использую этот пример ниже.Но когда я слишком быстро перемещаю пальцы по экрану, линия превращается в отдельные точки.

Я не уверен, смогу ли я ускорить рисование.Или я должен соединить две последние точки прямой линией.Второе из этих двух решений кажется хорошим вариантом, за исключением того, что при очень быстром движении пальца у вас будут длинные участки прямой линии, а затем острые изгибы.

Если есть какие-либо другие решения, было бы здорово услышатьим.

Спасибо за любую помощь заранее.

Ответы [ 9 ]

108 голосов
/ 28 ноября 2011

Как вы упомянули, простое решение - просто соединить точки прямой линией. Вот код для этого:

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. Наслаждайтесь: https://github.com/johncarl81/androiddraw

34 голосов
/ 10 марта 2012

Это может быть не важно для вас, но я изо всех сил пытался ее решить и хочу поделиться, может быть полезным для кого-то еще.

Учебное пособие с предложенным решением @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;
    } 
}  

Я взял образец андроида для рисования пальцем и немного изменил его, чтобы сохранить каждый путь, а не только последний! Надеюсь, это кому-нибудь поможет!

Приветствие.

19 голосов
/ 05 ноября 2012

Я экспериментировал с несколькими способами визуализации накопленных точек событий движения.В итоге я получил лучшие результаты, рассчитав средние точки между двумя точками и рассматривая точки в списке как точки привязки квадратичных кривых Безье (кроме первой и последней точек, которые связаны простыми линиями со следующей средней точкой).).

Это дает плавную кривую без углов.Нарисованный путь не будет касаться фактических точек в списке, а будет проходить через каждую среднюю точку.

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);
}
6 голосов
/ 28 марта 2016

Если хотите просто:

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

Чтобы стереть все рисунки, просто поверните экран.

4 голосов
/ 27 ноября 2011

У меня была очень похожая проблема. Когда вы вызываете метод 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
}
3 голосов
/ 07 апреля 2013

Мне пришлось внести некоторые изменения в это недавно, и теперь я разработал то, что я считаю лучшим решением здесь, потому что оно делает три вещи:

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

Это позволяет достаточно хорошо рисовать линии,Единственная проблема заключается в том, что если вы сделаете линию более толстой, что делает нарисованные линии немного странными, и в действительности я бы порекомендовал использовать первую в любом случае.

2 голосов
/ 16 января 2015

События движения с 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

0 голосов
/ 14 апреля 2012

У меня была эта проблема, я рисовал точку вместо линии. Вы должны сначала создать путь, чтобы удерживать вашу линию. вызывайте path.moveto только для первого события касания. Затем на холсте нарисуйте путь и затем сбросьте или перемотайте путь после того, как вы сделали (path.reset) ...

0 голосов
/ 27 ноября 2011

В вашем MotionEvent может быть гораздо больше информации, чем вы думаете, которая может предоставить некоторые данные между ними.

Пример в вашей ссылке игнорирует исторические точки соприкосновения, включенные в событие. См. Раздел «Пакетирование» в верхней части документации MotionEvent: http://developer.android.com/reference/android/view/MotionEvent.html Кроме того, соединение точек с линиями может быть неплохой идеей.

...