androidandroid-canvasmulti-touch

How to draw with multiple fingers in canvas


I am using android Canvas class from creating a drawing application. This is my first attempt to work with the Canvas class. So far the code that I used is working fine and the drawing is working fine. But what I realized in this code is that it allow the user to draw with one finger only, I mean to say if the user used more then one finger to draw on canvas it doesn't allow the user to draw with multiple fingers. I go through documentation regarding multiple touch events but failed to implement it in my code. So can anyone help me to figure this out?

The code I used for drawing on canvas:

public class DrawView extends View implements OnTouchListener 
{
    private Canvas      m_Canvas;
    
    private Path        m_Path;
    
    private Paint       m_Paint;
    
    ArrayList<Pair<Path, Paint>> arrayListPaths = new ArrayList<Pair<Path, Paint>>();
    
    ArrayList<Pair<Path, Paint>> undonePaths = new ArrayList<Pair<Path, Paint>>(); 
    
    private float mX, mY;
    
    private Bitmap bitmapToCanvas;
    
    private static final float TOUCH_TOLERANCE = 4;
        
    public DrawView(Context context) 
    {
        super(context);
        setFocusable(true);
        setFocusableInTouchMode(true);      
        this.setOnTouchListener(this);
        
        onCanvasInitialization();
    }      
    
    public void onCanvasInitialization()
    {
        m_Paint = new Paint();
        m_Paint.setAntiAlias(true);
        m_Paint.setDither(true);
        m_Paint.setColor(Color.parseColor("#37A1D1"));
        m_Paint.setStyle(Paint.Style.STROKE);
        m_Paint.setStrokeJoin(Paint.Join.ROUND);
        m_Paint.setStrokeCap(Paint.Cap.ROUND);
        m_Paint.setStrokeWidth(2);      
                
        m_Path = new Path();    
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) 
    {
        super.onSizeChanged(w, h, oldw, oldh);
        
        bitmapToCanvas = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        m_Canvas = new Canvas(bitmapToCanvas);
    }
    
    @Override
    protected void onDraw(Canvas canvas)
    {    
        canvas.drawBitmap(bitmapToCanvas, 0f, 0f, null);
        canvas.drawPath(m_Path, m_Paint);
    }
    
    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;
    }

    private void touch_start(float x, float y) 
    {
        undonePaths.clear();
        m_Path.reset();
        m_Path.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) 
        {
            m_Path.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
            mX = x;
            mY = y;
        }
    }
    private void touch_up() 
    {
        m_Path.lineTo(mX, mY);
                
        // commit the path to our offscreen
        m_Canvas.drawPath(m_Path, m_Paint);
        
        // kill this so we don't double draw                    
        Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
        arrayListPaths.add(new Pair<Path, Paint>(m_Path, newPaint));
        m_Path = new Path();
    }
}

I tried making changes in my code to support multiple touch, but it doesn't work properly. This is my changed code.


Solution

  • See Making Sense of Multitouch, it helped me a lot. It explanes how to handle multi touches

    Points to remember

    1.Make sure that you switch on action & MotionEvent.ACTION_MASK

    2.if you want to draw multiple lines at same time, follow PointerId of each pointer which comes in MotionEvent.ACTION_POINTER_DOWN and release it in MotionEvent.ACTION_POINTER_UP by comparing pointer ids.

    private static final int INVALID_POINTER_ID = -1;
    
    // The ‘active pointer’ is the one currently moving our object.
    private int mActivePointerId = INVALID_POINTER_ID;
    
    // Existing code ...
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();
    
            mLastTouchX = x;
            mLastTouchY = y;
    
            // Save the ID of this pointer
            mActivePointerId = ev.getPointerId(0);
            break;
        }
    
        case MotionEvent.ACTION_MOVE: {
            // Find the index of the active pointer and fetch its position
            final int pointerIndex = ev.findPointerIndex(mActivePointerId);
            final float x = ev.getX(pointerIndex);
            final float y = ev.getY(pointerIndex);
    
            final float dx = x - mLastTouchX;
            final float dy = y - mLastTouchY;
    
            mPosX += dx;
            mPosY += dy;
    
            mLastTouchX = x;
            mLastTouchY = y;
    
            invalidate();
            break;
        }
    
        case MotionEvent.ACTION_UP: {
            mActivePointerId = INVALID_POINTER_ID;
            break;
        }
    
        case MotionEvent.ACTION_CANCEL: {
            mActivePointerId = INVALID_POINTER_ID;
            break;
        }
    
        case MotionEvent.ACTION_POINTER_UP: {
            // Extract the index of the pointer that left the touch sensor
            final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) 
                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
            final int pointerId = ev.getPointerId(pointerIndex);
            if (pointerId == mActivePointerId) {
                // This was our active pointer going up. Choose a new
                // active pointer and adjust accordingly.
                final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                mLastTouchX = ev.getX(newPointerIndex);
                mLastTouchY = ev.getY(newPointerIndex);
                mActivePointerId = ev.getPointerId(newPointerIndex);
            }
            break;
        }
        }
    
        return true;
    }
    

    Edit

    Please see this code... This still has some issues but i think you can debug it and fix those ... Also the logic is not there persisting lines please implement that...

    package com.example.stackgmfdght;
    
    import java.util.ArrayList;
    
    
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.Path;
    import android.util.AttributeSet;
    import android.util.Pair;
    import android.view.MotionEvent;
    import android.view.View;
    
    public class JustDoIt extends View
    {
        private Canvas          m_Canvas;
    
     //   private Path            m_Path;
    
        int current_path_count=-1; 
        ArrayList <Path> m_Path_list =  new ArrayList<Path>();
        ArrayList <Float> mX_list =  new ArrayList<Float>();
        ArrayList <Float> mY_list =  new ArrayList<Float>();
        ArrayList <Integer> mActivePointerId_list =  new ArrayList<Integer>();
    
        private Paint       m_Paint;
    
        ArrayList<Pair<Path, Paint>> arrayListPaths = new ArrayList<Pair<Path, Paint>>();
    
        //ArrayList<Pair<Path, Paint>> undonePaths = new ArrayList<Pair<Path, Paint>>();
    
        private float mX, mY;
    
        private Bitmap bitmapToCanvas;
    
        private static final float TOUCH_TOLERANCE = 4;
    
        public JustDoIt (Context context)
        {
                super(context);
                setFocusable(true);
                setFocusableInTouchMode(true);      
    
                onCanvasInitialization();
        }     
    
        public JustDoIt(Context context, AttributeSet attributeSet) {
            super(context, attributeSet);
            setFocusable(true);
            setFocusableInTouchMode(true);      
    
            onCanvasInitialization();
        }
    
        public void onCanvasInitialization()
        {
                m_Paint = new Paint();
                m_Paint.setAntiAlias(true);
                m_Paint.setDither(true);
                m_Paint.setColor(Color.parseColor("#37A1D1"));
                m_Paint.setStyle(Paint.Style.STROKE);
                m_Paint.setStrokeJoin(Paint.Join.ROUND);
                m_Paint.setStrokeCap(Paint.Cap.ROUND);
                m_Paint.setStrokeWidth(2);            
    
             //   m_Path = new Path();  
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh)
        {
                super.onSizeChanged(w, h, oldw, oldh);
    
                bitmapToCanvas = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
                m_Canvas = new Canvas(bitmapToCanvas);
        }
    
        @Override
        protected void onDraw(Canvas canvas)
        {    
                canvas.drawBitmap(bitmapToCanvas, 0f, 0f, null);
                for(int i=0;i<=current_path_count;i++)
                {
                canvas.drawPath(m_Path_list.get(i), m_Paint);
                }
        }
    
    
        public void onDrawCanvas()
        {
                for (Pair<Path, Paint> p : arrayListPaths)
                {
                    m_Canvas.drawPath(p.first, p.second);
                }
        }
    
        private static final int INVALID_POINTER_ID = -1;
    
        // The ‘active pointer’ is the one currently moving our object.
        private int mActivePointerId = INVALID_POINTER_ID;
    
    
    
        @Override
        public boolean onTouchEvent(MotionEvent event)
        {
                super.onTouchEvent(event);
    
                final int action = event.getAction();  
    
                switch (action & MotionEvent.ACTION_MASK)
                {
                        case MotionEvent.ACTION_DOWN:
                        {
                                float x = event.getX();
                                float y = event.getY();
    
    
                                current_path_count=0;
                                mActivePointerId_list.add ( event.getPointerId(0),current_path_count);                                              
                                touch_start((x ),(y ),current_path_count );
                        }
                        break;
    
                        case MotionEvent.ACTION_POINTER_DOWN:
                        {
    
               if(event.getPointerCount()>current_path_count)
               {
    
                                current_path_count++;
                                float x = event.getX(current_path_count);
                                float y = event.getY(current_path_count);
    
    
                                mActivePointerId_list.add ( event.getPointerId(current_path_count),current_path_count);                                              
                                    touch_start((x ),(y ),current_path_count);  
               } 
                        }
                        break;
    
                        case MotionEvent.ACTION_MOVE:
                        {
                             for(int i=0;i<=current_path_count;i++)
                             { try{
                                         int pointerIndex = event
                                        .findPointerIndex(mActivePointerId_list.get(i));
    
                                        float x = event.getX(pointerIndex);
                                        float y = event.getY(pointerIndex);
    
                                        touch_move((x ),(y ),i);
                             }
                             catch(Exception e)
                             {
                                 e.printStackTrace();
                             }
                             }
    
    
                        }
                        break;
    
                        case MotionEvent.ACTION_UP:
                        { current_path_count=-1; 
                             for(int i=0;i<=current_path_count;i++)
                             {
    
                                        touch_up(i);
                             }
                             mActivePointerId_list =  new ArrayList<Integer>();
    
    
                        }
                        break;
    
                        case MotionEvent.ACTION_CANCEL:
                        {
                                mActivePointerId = INVALID_POINTER_ID;  
                                current_path_count=-1; 
                        }
                        break;
    
                        case MotionEvent.ACTION_POINTER_UP:
                        {
                                final int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                                final int pointerId = event.getPointerId(pointerIndex);
                                for(int i=0;i<=current_path_count;i++)
                             {
                                if (pointerId == mActivePointerId_list.get(i))
                                {
                                        // This was our active pointer going up. Choose a new
                                        // active pointer and adjust accordingly.
    
                                         mActivePointerId_list.remove(i);
                                        touch_up(i);
                                        break;
                                }              
                             }
                        }    
                        break;
    
                        case MotionEvent.ACTION_OUTSIDE:
                        break;
        }
    
        invalidate();
        return true;
    }
    
        private void touch_start(float x, float y, int count)
        {
            //    undonePaths.clear();
               Path  m_Path=new Path();
    
               m_Path_list.add(count,m_Path);
    
               m_Path_list.get(count).reset();
    
    
               m_Path_list.get(count).moveTo(x, y);
    
                mX_list.add(count,x);
                mY_list.add(count,y);
    
        }
    
        private void touch_move(float x, float y,int count)
        {
                float dx = Math.abs(x - mX_list.get(count));
                float dy = Math.abs(y - mY_list.get(count));
                if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE)
                {
                        m_Path_list.get(count).quadTo(mX_list.get(count), mY_list.get(count), (x + mX_list.get(count))/2, (y + mY_list.get(count))/2);
                        try{
    
                            mX_list.remove(count);
                            mY_list.remove(count);
                            }
                            catch(Exception e)
                            {
                               e.printStackTrace();
                            }
                        mX_list.add(count,x);
                        mY_list.add(count,y);
                }
        }
        private void touch_up(int count)
        {
             m_Path_list.get(count).lineTo(mX_list.get(count), mY_list.get(count));
    
                // commit the path to our offscreen
                m_Canvas.drawPath( m_Path_list.get(count), m_Paint);
    
                // kill this so we don't double draw                          
                Paint newPaint = new Paint(m_Paint); // Clones the mPaint object
                arrayListPaths.add(new Pair<Path, Paint>( m_Path_list.get(count), newPaint));
                m_Path_list.remove(count);
                mX_list.remove(count);
                mY_list.remove(count);
        }
    }