16

I am working on an Android application which should allow user to draw a circle on canvas and drag it . I have been able to draw lines and circles on it.But I cant seem to be able to drag it.

Is it possible to drag an object drawn on canvas in Android?.For example if I detect a touch within the circle, how do I delete the circle from a previous position and move it to the current position? I have tried invalidate().But it wont work if the user has drawn multiple circle and wants to move all of them.

Simon Sarris
  • 62,212
  • 13
  • 141
  • 171
Shravz
  • 183
  • 1
  • 1
  • 6
  • How do You store information about drawn circles? Is it exact circles (drawn by drawCircle())? – sandrstar Jul 24 '13 at 06:41
  • A bean class is created for circle which stores the X,Y co-ordinates of the centre and the radius. – Shravz Jul 24 '13 at 07:06
  • 1
    Some code might be useful. I'm assuming that You have some kind of drawing view which stores and draws these 'circles' in onDraw() method. If so, then invalidate() should work (like in this question with single circle http://stackoverflow.com/q/17289576/657487). Am I right? Is it problem with multiple touch pointers? – sandrstar Jul 24 '13 at 07:11

1 Answers1

45

It seems that you might have issues with handling of multi-touch / drawing. There's some usefull tutorials about it on Android Developer site and on Android Blog.

Based on this I was able to create an example which I think quite similar to that You're trying to achieve (without complete circle drawing - circles get generated by single touch):

public class CirclesDrawingView extends View {

    private static final String TAG = "CirclesDrawingView";

    /** Main bitmap */
    private Bitmap mBitmap = null;

    private Rect mMeasuredRect;

    /** Stores data about single circle */
    private static class CircleArea {
        int radius;
        int centerX;
        int centerY;

        CircleArea(int centerX, int centerY, int radius) {
            this.radius = radius;
            this.centerX = centerX;
            this.centerY = centerY;
        }

        @Override
        public String toString() {
            return "Circle[" + centerX + ", " + centerY + ", " + radius + "]";
        }
    }

    /** Paint to draw circles */
    private Paint mCirclePaint;

    private final Random mRadiusGenerator = new Random();
    // Radius limit in pixels
    private final static int RADIUS_LIMIT = 100;

    private static final int CIRCLES_LIMIT = 3;

    /** All available circles */
    private HashSet<CircleArea> mCircles = new HashSet<CircleArea>(CIRCLES_LIMIT);
    private SparseArray<CircleArea> mCirclePointer = new SparseArray<CircleArea>(CIRCLES_LIMIT);

    /**
     * Default constructor
     *
     * @param ct {@link android.content.Context}
     */
    public CirclesDrawingView(final Context ct) {
        super(ct);

        init(ct);
    }

    public CirclesDrawingView(final Context ct, final AttributeSet attrs) {
        super(ct, attrs);

        init(ct);
    }

    public CirclesDrawingView(final Context ct, final AttributeSet attrs, final int defStyle) {
        super(ct, attrs, defStyle);

        init(ct);
    }

    private void init(final Context ct) {
        // Generate bitmap used for background
        mBitmap = BitmapFactory.decodeResource(ct.getResources(), R.drawable.up_image);

        mCirclePaint = new Paint();

        mCirclePaint.setColor(Color.BLUE);
        mCirclePaint.setStrokeWidth(40);
        mCirclePaint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void onDraw(final Canvas canv) {
        // background bitmap to cover all area
        canv.drawBitmap(mBitmap, null, mMeasuredRect, null);

        for (CircleArea circle : mCircles) {
            canv.drawCircle(circle.centerX, circle.centerY, circle.radius, mCirclePaint);
        }
    }

    @Override
    public boolean onTouchEvent(final MotionEvent event) {
        boolean handled = false;

        CircleArea touchedCircle;
        int xTouch;
        int yTouch;
        int pointerId;
        int actionIndex = event.getActionIndex();

        // get touch event coordinates and make transparent circle from it
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // it's the first pointer, so clear all existing pointers data
                clearCirclePointer();

                xTouch = (int) event.getX(0);
                yTouch = (int) event.getY(0);

                // check if we've touched inside some circle
                touchedCircle = obtainTouchedCircle(xTouch, yTouch);
                touchedCircle.centerX = xTouch;
                touchedCircle.centerY = yTouch;
                mCirclePointer.put(event.getPointerId(0), touchedCircle);

                invalidate();
                handled = true;
                break;

            case MotionEvent.ACTION_POINTER_DOWN:
                Log.w(TAG, "Pointer down");
                // It secondary pointers, so obtain their ids and check circles
                pointerId = event.getPointerId(actionIndex);

                xTouch = (int) event.getX(actionIndex);
                yTouch = (int) event.getY(actionIndex);

                // check if we've touched inside some circle
                touchedCircle = obtainTouchedCircle(xTouch, yTouch);

                mCirclePointer.put(pointerId, touchedCircle);
                touchedCircle.centerX = xTouch;
                touchedCircle.centerY = yTouch;
                invalidate();
                handled = true;
                break;

            case MotionEvent.ACTION_MOVE:
                final int pointerCount = event.getPointerCount();

                Log.w(TAG, "Move");

                for (actionIndex = 0; actionIndex < pointerCount; actionIndex++) {
                    // Some pointer has moved, search it by pointer id
                    pointerId = event.getPointerId(actionIndex);

                    xTouch = (int) event.getX(actionIndex);
                    yTouch = (int) event.getY(actionIndex);

                    touchedCircle = mCirclePointer.get(pointerId);

                    if (null != touchedCircle) {
                        touchedCircle.centerX = xTouch;
                        touchedCircle.centerY = yTouch;
                    }
                }
                invalidate();
                handled = true;
                break;

            case MotionEvent.ACTION_UP:
                clearCirclePointer();
                invalidate();
                handled = true;
                break;

            case MotionEvent.ACTION_POINTER_UP:
                // not general pointer was up
                pointerId = event.getPointerId(actionIndex);

                mCirclePointer.remove(pointerId);
                invalidate();
                handled = true;
                break;

            case MotionEvent.ACTION_CANCEL:
                handled = true;
                break;

            default:
                // do nothing
                break;
        }

        return super.onTouchEvent(event) || handled;
    }

    /**
     * Clears all CircleArea - pointer id relations
     */
    private void clearCirclePointer() {
        Log.w(TAG, "clearCirclePointer");

        mCirclePointer.clear();
    }

    /**
     * Search and creates new (if needed) circle based on touch area
     *
     * @param xTouch int x of touch
     * @param yTouch int y of touch
     *
     * @return obtained {@link CircleArea}
     */
    private CircleArea obtainTouchedCircle(final int xTouch, final int yTouch) {
        CircleArea touchedCircle = getTouchedCircle(xTouch, yTouch);

        if (null == touchedCircle) {
            touchedCircle = new CircleArea(xTouch, yTouch, mRadiusGenerator.nextInt(RADIUS_LIMIT) + RADIUS_LIMIT);

            if (mCircles.size() == CIRCLES_LIMIT) {
                Log.w(TAG, "Clear all circles, size is " + mCircles.size());
                // remove first circle
                mCircles.clear();
            }

            Log.w(TAG, "Added circle " + touchedCircle);
            mCircles.add(touchedCircle);
        }

        return touchedCircle;
    }

    /**
     * Determines touched circle
     *
     * @param xTouch int x touch coordinate
     * @param yTouch int y touch coordinate
     *
     * @return {@link CircleArea} touched circle or null if no circle has been touched
     */
    private CircleArea getTouchedCircle(final int xTouch, final int yTouch) {
        CircleArea touched = null;

        for (CircleArea circle : mCircles) {
            if ((circle.centerX - xTouch) * (circle.centerX - xTouch) + (circle.centerY - yTouch) * (circle.centerY - yTouch) <= circle.radius * circle.radius) {
                touched = circle;
                break;
            }
        }

        return touched;
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mMeasuredRect = new Rect(0, 0, getMeasuredWidth(), getMeasuredHeight());
    }
}

Activity contains only setContentView(R.layout.main) there main.xml is the following:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:id="@+id/scroller">
    <com.example.TestApp.CirclesDrawingView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>
Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
sandrstar
  • 12,503
  • 8
  • 58
  • 65
  • I have been stuck in the similar problem, i am trying to implement this too. Finger Crossed. – Salman Khakwani Oct 21 '13 at 11:12
  • I am getting these errors while implementing this code http://pastebin.com/4mHdHh6W: – Salman Khakwani Oct 21 '13 at 11:54
  • Any idea how to implement your idea into mine? http://stackoverflow.com/questions/21662397/how-to-keep-an-image-within-layout (I am getting a FC app in my layout) – Si8 Feb 09 '14 at 17:33
  • @SiKni8 Sorry, was on vacation, but seems it's already answered. – sandrstar Feb 20 '14 at 04:00
  • I am having another issue now. To call a function once if not touched on a view. http://stackoverflow.com/questions/21841670/how-to-call-a-function-only-when-not-focused-in-a-framelayout-view. If you can please take a look. – Si8 Feb 20 '14 at 11:50
  • How to remove circle when action_up.Plz Help Thanx in Advance – Sanket990 Mar 24 '14 at 07:10
  • @Sanket990 you can just clear mCircles array, so drawing will not happen. – sandrstar Mar 25 '14 at 01:02
  • @sandrstar thanks, it's useful. But there is some important thing. Why it draws all the circles each time we draw a circle. It's too CPU consumer if we have many circles. Is there any solution that the new circle added to the view without clearing other? – Behzad Aug 29 '16 at 20:44
  • 1
    @Behzad yes, it's possible to improve by tracking which circle should be really redraw and clear only that circle area (by redrawing it with transparent colour). Probably drawBitmap is quite fast, so we just redraw existing circles, it's not too expensive and will work for any number of circles without additional logic. I'm suggesting to check if there's really any performance gain from that approach. – sandrstar Aug 30 '16 at 07:10
  • @sandrstar Thanks. So do you want to edit your code? Could you help me more to edit your code to what you said in last comment. I mean redrawing just the circle should be redraw. – Behzad Aug 31 '16 at 20:51
  • @Behzad it requires much more logic for touches handling which might make the answer more difficult to understand. Considering that it might not give any sensible performance advantage I'm not going to rework it. – sandrstar Sep 01 '16 at 07:59