4
votes

I have a list of points that represents a curve which I draw on a canvas using a Path object.

path.moveTo(x, y);
for (int i = 0; i < points.size(); i++) {
    path.lineTo(points.get(i).x, points.get(i).y);
}
canvas.drawPath(path, paint);

What I want to achieve is ability to set control points that user can touch and move and based on that movement my points will get transformed. Same thing that photoshop does with Pen Tool see image: enter image description here

Note: android Path is only used for drawing, I don't need to modify Path I need to modify coordinates. so code above can be replaced with

canvas.drawLine();

It nothing has to do with Path object.

1
read android.graphics.Path docs, they explain how to deal with bezier curves - pskink
I've read that, it only modifies path thru matrices, and as you can see from the picture what I need is completely different and cannot be achieved with matrix transform - Vilen
you need to build your path from the scratch (reset method followed by moveTo and couple of cubicTo methods) whenever any control point changes - pskink
please read question again, what you are suggesting cannot solve the problem I have. It is about points modification so that path can be transformed in any place, it is not about scale/shift/skew - Vilen
I am not asking about how to redraw / re-draw the path I am asking for algorithm that modifies points so that path behaves like a string in real life when you drag it it changes the shape. If you still don't get the question try to open photoshop and see how pen tool works - Vilen

1 Answers

5
votes

this is the simple view that uses one "anchor" point and two control points, if you need more anchors, add another cubicTo to your path:

class V extends View {
    static final float RADIUS = 32;
    Path path = new Path();
    Paint pathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    Paint controlPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    PointF ctrl1 = new PointF();
    PointF ctrl2 = new PointF();
    PointF ctrl3 = new PointF();
    PointF ctrl4 = new PointF();
    PointF anchor = new PointF();
    GestureDetector detector;
    Layout layout;

    public V(Context context) {
        super(context);
        pathPaint.setColor(Color.RED);
        pathPaint.setStyle(Paint.Style.STROKE);
        pathPaint.setStrokeWidth(16);
        controlPaint.setColor(Color.GREEN);
        controlPaint.setAlpha(128);
        detector = new GestureDetector(context, listener);
    }

    GestureDetector.OnGestureListener listener = new GestureDetector.SimpleOnGestureListener() {
        PointF target;

        @Override
        public boolean onDown(MotionEvent e) {
            PointF[] targets = { ctrl2, ctrl3, anchor };
            for (PointF t : targets) {
                if (Math.hypot(t.x - e.getX(), t.y - e.getY()) < RADIUS) {
                    target = t;
                    return true;
                }
            }
            target = null;
            return false;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (target == null) return false;

            target.offset(-distanceX, -distanceY);
            if (target == ctrl2 || target == ctrl3) {
                PointF otherControl = target == ctrl2 ? ctrl3 : ctrl2;
                // anchor just between points
                double a = Math.atan2(anchor.y - target.y, anchor.x - target.x);
                double r = Math.hypot(otherControl.x - anchor.x, otherControl.y - anchor.y);
                otherControl.set((float) (anchor.x + r * Math.cos(a)), (float) (anchor.y + r * Math.sin(a)));

                // anchor always in the center
//                otherControl.set(2 * anchor.x - target.x, 2 * anchor.y - target.y);
            } else {
                ctrl2.offset(-distanceX, -distanceY);
                ctrl3.offset(-distanceX, -distanceY);
            }
            rebuildPath();
            invalidate();
            return true;
        }
    };

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        ctrl1.set(w * 0.0f, h * 1.0f);
        ctrl2.set(w * 0.1f, h * 0.5f);
        ctrl3.set(w * 0.9f, h * 0.5f);
        ctrl4.set(w * 1.0f, h * 1.0f);
        anchor.set(w * 0.5f, h * 0.5f);
        rebuildPath();
        CharSequence src = "you can drag any green circle: the both control points or the anchor point\n\n" +
                "notice that the control points can be adjusted individually - the only constraint for a smooth line is that the anchor point is between them (but not necessarily in the exact center)";
        TextPaint tp = new TextPaint();
        tp.setColor(Color.WHITE);
        tp.setTextSize(32);
        layout = new StaticLayout(src, tp, w - 64, Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    }

    private void rebuildPath() {
        path.reset();
        path.moveTo(ctrl1.x, ctrl1.y);
        path.cubicTo(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, anchor.x, anchor.y);
        path.cubicTo(ctrl3.x, ctrl3.y, ctrl4.x, ctrl4.y, ctrl4.x, ctrl4.y);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        detector.onTouchEvent(event);
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.translate(32, 32);
        layout.draw(canvas);
        canvas.restore();
        canvas.drawPath(path, pathPaint);
        controlPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(anchor.x, anchor.y, RADIUS, controlPaint);
        canvas.drawCircle(ctrl2.x, ctrl2.y, RADIUS, controlPaint);
        canvas.drawCircle(ctrl3.x, ctrl3.y, RADIUS, controlPaint);
        controlPaint.setStyle(Paint.Style.STROKE);
        canvas.drawLine(ctrl2.x, ctrl2.y, ctrl3.x, ctrl3.y, controlPaint);
    }
}