kishu27
kishu27

Reputation: 3120

Painting by touchscreen skips (discontiguous lines)

My Activity Class file:

package com.drawing.test;

import android.app.Activity;
import android.os.Bundle;


import android.app.Activity;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.LinearLayout;

public class TstActivity extends Activity implements OnTouchListener
{
    float x1 = 0, y1 = 0, x2 = 0, y2 = 0;
    public static boolean action=false;
    private Bitmap mBitmap;
    private Canvas mCanvas;
    private Paint mBitmapPaint;

    Drawer mDrawer;

    @Override
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        LinearLayout mLinearLayout = (LinearLayout) findViewById(R.id.drawView);
        mLinearLayout.setOnTouchListener((OnTouchListener) this);
        mLinearLayout.addView(new Drawer(this));
    }

    public boolean onTouch(View v, MotionEvent event) 
    {
        switch (event.getAction()) 
        {
            case MotionEvent.ACTION_DOWN:
                x1 = event.getX();
                y1 = event.getY();
                action=false;
                //v.invalidate();
            return true;
            case MotionEvent.ACTION_MOVE:
                x1=x2;
                y1=y2;
                x2 = event.getX();
                y2 = event.getY();
                v.invalidate();
                action=true;
            return true;
            case MotionEvent.ACTION_UP:
                x2 = event.getX();
                y2 = event.getY();
                v.invalidate();
                action=true;
            return true;

        }
        return false;
    }

    public class Drawer extends View
    {

        public Drawer(Context context)
        {
            super(context);
            mBitmap = Bitmap.createBitmap(400, 800, Bitmap.Config.ARGB_8888);
            mCanvas = new Canvas(mBitmap);
            mBitmapPaint = new Paint(Paint.DITHER_FLAG);
            mBitmapPaint.setColor(Color.MAGENTA);
            invalidate();
        }

        protected void onDraw(Canvas canvas)
        {
            if(x1==x2 && y1==y2)return;
            Paint p = new Paint();
            // Canvas mCanvas1=new Canvas(mBitmap);
            p.setColor(Color.parseColor("#7CFC00"));
            canvas.drawBitmap(mBitmap, 0, 0, p);
            //  canvas.drawLine(x1, y1, x2 , y2, p);
            p.setColor(Color.RED);
            // mCanvas1.drawLine(x1, y1, x2, y2,p);
            if(action==true)mCanvas.drawLine(x1, y1, x2, y2, mBitmapPaint);

        }
    }
}

My Layout XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:id="@+id/drawView">

</LinearLayout>

When I try this out on emulator, the line breaks in middle but continues drawing. I tried on device too, and same problems happen but lesser in number. Also, the display sometimes flickers, or goes completely blank. When its completely black, it comes back if I touch once.

Is there anything wrong that I am doing, shall I change my approach.

Any suggestions are appreciated. Thanks

Upvotes: 1

Views: 417

Answers (2)

Raffaele
Raffaele

Reputation: 20885

It's a synchronization problem. Basically, invalidate() is not a blocking call: it just tells the system to redraw at some point in the future. So what happens:

  • You set a value for (x1, y1) and (x2, y2)
  • invaldate() schedules a redraw as soon as possible
  • but another touch event overwrites (x1, y1) and (x2, y2) before the line is drawn: the old values are lost forever

You can prove it simply by adding two hits counter, one for invalidate() and one for onDraw(). After some time you see that the number of calls to invalidate() is greater than the hits for onDraw(). My suggestion is to keep the points from touch events in a Queue. Also note that every time you allocate a Bitmap you need to call recycle when you have done to avoid memory leaks. In fact, the pixels are stored in native memory and are not garbage collected when the view is destroyed. I usually call recycle when my activity stops. Here is my code:

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    public static class Drawer extends View  {

        private Bitmap cache;
        private Queue<PointF> points;
        private PointF from;

        public Drawer(Context ctx, AttributeSet attrs) {
            super(ctx, attrs);
            points = new ConcurrentLinkedQueue<PointF>();
        }

        @Override
        public boolean onTouchEvent(MotionEvent evt) {
            switch (evt.getAction()) {
            case MotionEvent.ACTION_DOWN: from = new PointF(evt.getX(), evt.getY()); break;
            case MotionEvent.ACTION_MOVE: points.add(new PointF(evt.getX(), evt.getY())); invalidate(); break;
            case MotionEvent.ACTION_UP: from = null; break;
            default: from = null;
            }
            return true;
        }

        @Override
        public void onSizeChanged(int w, int h, int oldw, int oldh) {
            if (w == 0 || h == 0)
                return;
            cache = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        }

        @Override
        public void onDraw(Canvas systemCanvas) {
            int w = getWidth();
            int h = getHeight();
            if (w == 0 || h == 0)
                return;
            if (cache == null)
                cache = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
            // Draw on the cache
            Canvas canvas = new Canvas(cache);
            Paint paint = new Paint();
            paint.setStrokeWidth(4);
            paint.setColor(Color.MAGENTA);
            paint.setFlags(Paint.ANTI_ALIAS_FLAG);

            drawPoints(points, canvas, paint);

            // Draw the cache with the system canvas
            systemCanvas.drawBitmap(cache, 0, 0, paint);
        }

        private void drawPoints(Queue<PointF> points, Canvas canvas, Paint paint) {
            if (from == null)
                return;
            PointF to;
            while ((to = points.poll()) != null) {
                canvas.drawLine(from.x, from.y, to.x, to.y, paint);
                from = to;
            }
        }

    }
}

and this is the layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:id="@+id/drawView">
    <view 
        class="com.zybnet.test.MainActivity$Drawer"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>

You can see that customs view can be used in XML, too :)

Upvotes: 2

eyespyus
eyespyus

Reputation: 1576

For motionEvent.ACTION_MOVE they can and will be batched. I believe this is why there are breaks in the lines.

See this from documentation:

Batching

For efficiency, motion events with ACTION_MOVE may batch together multiple movement samples within a single object. The most current pointer coordinates are available using getX(int) and getY(int). Earlier coordinates within the batch are accessed using getHistoricalX(int, int) and getHistoricalY(int, int). The coordinates are "historical" only insofar as they are older than the current coordinates in the batch; however, they are still distinct from any other coordinates reported in prior motion events. To process all coordinates in the batch in time order, first consume the historical coordinates then consume the current coordinates.

Upvotes: 0

Related Questions