Search code examples
androidgraphicsandroid-2.3-gingerbread

View.onDraw called with wrong Canvas


My app supports API level 10 (Gingerbread) upwards. One of the activities draws a chart which works perfectly on later versions, but when running a level 10 emulator I get an extra call to View.onDraw with the wrong canvas ID and this causes the screen to go blank. (It's not just the emulator - the problem was reported by someone running on their Gingerbread phone.)

Normal operation is for onDraw to be called twice - the first time from the framework, where I take the canvas ID from, and the second time from my call to invalidate(), which passes the same canvas ID. These two calls happen with the level 10 emulator, but then there is a third call with a different canvas ID - i.e. not belonging to the view, and this blanks it out.

The activity is derived from SherlockActivity to provide an action bar, and I believe this is what is causing the problem.

Relevant code from my activity class:

public class Chart extends SherlockActivity implements OnGestureListener, OnDoubleTapListener, OnScaleGestureListener
{
    public static boolean   mDataSet = false;

    private ChartView              mView;
    private Menu                   mMenu;
    private GestureDetector        mDetector;
    private ScaleGestureDetector   mScaleDetector;
    private ActionBar              mActionBar;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        mDataSet = false;

        mActionBar = getSupportActionBar();
        mActionBar.setHomeButtonEnabled(true);
        mActionBar.setDisplayHomeAsUpEnabled(true);
        mActionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);

        mView = new ChartView(this, mCentreLat, mCentreLong, mRadius);
        setContentView(mView);
        setTitle("");

        Context context = getApplicationContext();

        mDetector   = new GestureDetector(context, this);
        mScaleDetector = new ScaleGestureDetector(context, this);
    }

    @Override
    public void onConfigurationChanged(Configuration config)
    {
        super.onConfigurationChanged(config);
        mDataSet = false;
    }

    // Menu handling
    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        mMenu = menu;
        MenuInflater inflater = getSupportMenuInflater();
        inflater.inflate(R.menu.chart_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        boolean handled = true;

        switch (item.getItemId())
        {
        case android.R.id.home:
        finish();
        break;

        case R.id.viewOptions:
            mDataSet = false;
            Intent i = new Intent(getBaseContext(), ChartSettings.class);
            startActivity(i);
            break;

        // Other menu options here...

        default:
            handled = false;
        }
        return handled;
    }

Relevant code from my View class:

public class ChartView extends View
{

    public ChartView(Context context, float centreLat, float centreLong, float radius)
    {
        super(context);
        mContext    = context;
        mCentreLat  = centreLat;
        mCentreLong = centreLong;
        mRadius = radius;
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        // First time
        // (We pick up the canvas id mCanvas from the parameter.)
        // (Nothing much else relevant here, except this is where stuff
        // gets initialized, then in setDataInfo(), mDataSet is set true
        // and invalidate() is called at the end.)
        if (!Chart.mDataSet)
        {
            setBackgroundColor(Color.WHITE);
            mCanvas = canvas;
            initPaint();
            setDataInfo();      // invalidate() called at end
        }
        else
        {
            // Second call to onDraw() with same canvas id comes here,
            // then an unwanted third call with a different canvas id, only
            // with the Level 10 (Gingerbread) emulator.

            // This is where the various lines and shapes are plotted
            // on the canvas (mCanvas)
            plotLinesAndShapes();
        }

Please can anyone explain why the third call happens with the Gingerbread emulator (or phone)? The effect is that the screen is blanked (completely white).

By the time it gets to this function it's too late and the call cannot just be ignored - the screen goes blank as the call stack unwinds.

There is a workround - if the user chooses view options from the menu, then immediately returns to the chart, it is redrawn and behaves normally from then on.


Solution

  • The problem is that you are keeping a reference to the Canvas you receive as a parameter. There is no guarantee whatsoever that this Canvas instance will be valid after the current frame is done. You could for instance receive a different Canvas instance on every frame. You will typically receive a different Canvas when the View is rendered into a Bitmap (View.setDrawingCacheEnabled() for instance.)

    Instead of keeping a reference in mCanvas, just pass the canvas you receive to plotLinesAndShapes().