Android Как нарисовать плавную линию вслед за пальцем

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

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

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

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

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


person Somk    schedule 27.11.2011    source источник
comment
Если прямых линий недостаточно для вашей цели, вы можете изучить подгонку кривой: stackoverflow.com/questions/878200/java-curve-fitting-library   -  person HostileFork says dont trust SE    schedule 27.11.2011
comment
Спасибо, это звучит полезно. Я не думал об использовании шлицев. В основном потому, что я предполагаю, что он использует гораздо больше ресурсов. Также это доступно для Android   -  person Somk    schedule 27.11.2011
comment
У вас есть Path.quadTo, _2 _... stackoverflow.com/questions/3811529/   -  person HostileFork says dont trust SE    schedule 27.11.2011
comment
stackoverflow.com/questions/11328848/   -  person Yogendra    schedule 01.02.2015


Ответы (11)


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

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

person John Ericksen    schedule 27.11.2011
comment
Это интересная статья на эту тему. Похоже, что Android объединяет события касания: corner.squareup.com/2010/07/ smooth-signatures.html - person John Ericksen; 19.06.2012
comment
Первый пример цикла cubicTo () кажется неправильным. Он должен проходить по всем точкам, а не только по последней. - person Eric Obermühlner; 05.11.2012
comment
Эрик Обермюльнер: Я перебираю все точки цикла for вокруг вызова cubicTo (). - person John Ericksen; 06.11.2012
comment
@johncarl Я согласен с Эриком. Я считаю, что код кривых неправильный - первый цикл сглаживает только последние несколько точек. Я предлагаю запустить первый цикл с 0, а не с points.size () - 2. (это основано на том факте, что я фактически скопировал этот код в свое графическое приложение и протестировал его) - person Richard Le Mesurier; 30.03.2013
comment
хммм, я все еще не вижу проблемы. У меня есть код на github, если вы хотите отправить запрос на перенос: github.com/johncarl81/androiddraw - person John Ericksen; 30.03.2013
comment
@johncarl, я пытаюсь нарисовать плавную линию, как здесь stackoverflow.com/questions/16455896/, но не работает .. Любая идея? - person kyogs; 09.05.2013
comment
Я использовал этот код в своем классе, и он отлично работает, но дело в том, что когда одна линия рисуется, затем при рисовании второго класса она начинается с конечной точки 1-й строки ... означает, что рисунок подключен ... поэтому, пожалуйста, помогите мне в этом бесплатно практика дрваинга ..! - person jigar; 13.08.2013
comment
@johncarl, пожалуйста, посмотрите на мой вопрос и посмотрите, есть ли какое-нибудь решение для этого - person AndroidDev; 07.01.2014
comment
Это отличный ответ, но он должен быть для (int i = 0; i ‹points.size (); i ++) {при вычислении кубических сплайнов. - person QuinnBaetz; 04.03.2014
comment
Android уже имеет встроенную функциональность, такую ​​же, как этот, с гораздо большим количеством функций, так что вы можете сослаться на здесь. Просто используйте первый пример johncarl, и вы сможете добиться плавных краевых линий. - person mr5; 09.06.2014
comment
подскажите, как удалить и очистить весь холст? Я имею в виду, что я когда-либо рисовал, как я могу убрать с него все нарисованные вещи? - person Coas Mckey; 14.10.2015

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

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

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

Ваше здоровье.

person caiocpricci2    schedule 10.03.2012
comment
Эй ... Попытайтесь очистить список где-нибудь, иначе он будет постепенно замедляться, так как вы будете рисовать все больше и больше одновременно .. - person Arun Abraham; 17.07.2012
comment
Это не было проблемой для приложений, которые не запускают это долго (например, то, что я сделал), для моей реализации это никогда не выполняется дольше 1 минуты или около того. Но спасибо, что указали на эту проблему! - person caiocpricci2; 17.07.2012
comment
Привет. Сохраняя пути для рисования, легко реализовать функции отмены или повтора. Но как мы можем реализовать функцию стирания, если мы не сохраняем рисунок в растровое изображение? - person Yeung; 09.01.2013
comment
Я не думаю, что ты сможешь. Если вы его не храните, как узнать, какой удалить? - person caiocpricci2; 09.01.2013
comment
Просто любопытно, а для чего mCanvas? Вы упомянули фиксацию пути к нашему внеэкранному режиму в touch_up(), но не похоже, что mCanvas когда-либо использовался где-либо еще. - person Jake Stoeffler; 01.06.2013
comment
Не могли бы вы помочь мне с моим вопросом ?? - person Leonardo Sapuy; 11.06.2013
comment
Это лучшее решение, которое я искал .. Спасибо - person DkPathak; 20.06.2013
comment
тоже хорошо, но вы можете мне сказать, как удалить и очистить весь холст? Я имею в виду, что я когда-либо рисовал, как я могу убрать с него все нарисованные вещи? - person Coas Mckey; 14.10.2015
comment
отличный ответ. Очень помогло: D - person Abir Hasan; 24.01.2017

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

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

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);
}
person Eric Obermühlner    schedule 05.11.2012
comment
Спасибо. он сделал именно то, что я ожидал. - person Mario Lenci; 09.01.2013
comment
Благодаря вам я искал именно это! - person Aurel; 26.06.2013
comment
Пришлось вытащить path.lineTo (prevPoint.x, prevPoint.y); за пределами цикла for, чтобы получить чистую гладкую линию. Вы можете объяснить, почему он там? - person wheels53; 22.05.2014
comment
Path.lineTo (prevPoint.x, prevPoint.y) в конце соединяет последнюю среднюю точку с последней точкой. Он уже находится за пределами петли и не должен мешать гладкости линии. - person Eric Obermühlner; 11.07.2015

Если вы хотите, чтобы все было просто:

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

Результат:

введите описание изображения здесь

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

person Andrew    schedule 28.03.2016
comment
Это должен быть принятый ответ - именно то, что запрашивает OP, с помощью наименее сложного кода. - person Jacob Kaddoura; 05.04.2019
comment
Спасибо за это. Подскажите, пожалуйста, как добавить большой значок плюса, показывающий, где находится курсор? - person Faizan Haidar Khan; 17.06.2021

У меня была очень похожая проблема. Когда вы вызываете метод 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
}
person y434y    schedule 27.11.2011
comment
не знал об этом ... любопытно посмотреть, сколько исторических данных есть в событии. - person Yevgeny Simkin; 01.02.2012

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

person amukhachov    schedule 16.01.2015
comment
У меня возникла проблема с неправильным рисованием кривых. Это решение устранило мою проблему. Спасибо! - person channae; 10.05.2021

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

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

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

person jcw    schedule 07.04.2013

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

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

person adamp    schedule 27.11.2011

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

person j2emanue    schedule 14.04.2012

Вот простой метод сглаживания точек, нарисованных с помощью Path. lineTo

fun applySmoothing(smoothingIterations: Int) {
    for (z in 1..smoothingIterations) {
        for (i in graphPoints.indices) {
            if (i > 0 && i < graphPoints.size-1) {
                val previousPoint = graphPoints[i-1]
                val currentPoint = graphPoints[i]
                val nextPoint = graphPoints[i+1]
                val midX = (previousPoint.x + currentPoint.x + nextPoint.x) / 3
                val midY = (previousPoint.y + currentPoint.y + nextPoint.y) / 3
                graphPoints[i].x = midX
                graphPoints[i].y = midY
            }
        }
    }
}
person Adam Johns    schedule 07.04.2021

Вот упрощенное решение, которое рисует линию, идущую вдоль вашего пальца и всегда прямую:

https://stackoverflow.com/a/68076519/15463816

person Cloud Town    schedule 22.06.2021
comment
Пожалуйста, опубликуйте минимальный пример для спрашивающего. - person Daniel.Wang; 22.06.2021