Search code examples
androiddrawingphotoandroid-camera-intent

Android: nullponterException after activating camera


I am developing simple drawing application for android. My goal is to:

  1. open camera from my application
  2. take a photo
  3. draw this photo into canvas
  4. draw over this photo

After activating camera:

Intent takePhoto = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                if (takePhoto.resolveActivity(getPackageManager()) != null) {
                    startActivityForResult(takePhoto, CAMERA_INTENT_CALLED);
                }

when i try to draw photo into canvas:

else if (requestCode == CAMERA_INTENT_CALLED) {
            if (resultCode == RESULT_OK) {
                Bundle extras = returnedIntent.getExtras();
                Bitmap imageBitmap = (Bitmap) extras.get("data");
                drawView.drawImage(imageBitmap);
            }
        }

I get NullPointerException: Attempt to invoke virtual method saying that my canvas is null. Loading photo from gallery into canvas works fine. Probably the reason is that my activity is destroyed after activating camera so I made a wrapper to make my canvas serializable:

public class SerializableCanvas extends Canvas implements Serializable {
    public SerializableCanvas(Bitmap canvasBitmap) {
        super(canvasBitmap);
    }
}

and tried to save my canvas:

   protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSerializable("drawCanvas", drawView.getDrawCanvas());
    }

and restoring it:

protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    if (savedInstanceState != null) {
        if (savedInstanceState.containsKey("drawCanvas")) {
            drawView.setDrawCanvas((SerializableCanvas)savedInstanceState.getSerializable("drawCanvas"));
        }
    }
}

but now I get an error saying that width and height must be >0 in this fragment:

public void drawImage(Bitmap image) {
    drawCanvas.drawBitmap(resize(image, drawCanvas.getWidth(), drawCanvas.getHeight()), 0, 0, canvasPaint);
    invalidate();
}

I tried also saving whole DrawingView object but it's too big and has a Bitmap inside. I have 2 classes: MainActivity and DrawingView. If sameone knows how to overcome this problem I would be gratefull for help. Full code below.

MainActivity class:

public class MainActivity extends Activity implements View.OnClickListener {

    private DrawingView drawView;
    private ImageButton currPaint;
    private ImageButton drawBtn;
    private ImageButton eraseBtn;
    private ImageButton newBtn;
    private ImageButton saveBtn;
    private ImageButton openBtn;
    private ImageButton cameraBtn;
    private float smallBrush;
    private float mediumBrush;
    private float largeBrush;
    private Drawable image;

    private final int GALLERY_INTENT_CALLED = 1;
    private final int CAMERA_INTENT_CALLED = 2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        drawView = (DrawingView)findViewById(R.id.drawing);
        LinearLayout paintLayout = (LinearLayout)findViewById(R.id.paint_colors);
        currPaint = (ImageButton)paintLayout.getChildAt(0);
        currPaint.setImageDrawable(getResources().getDrawable(R.drawable.paint_pressed));
        smallBrush = getResources().getInteger(R.integer.small_size);
        mediumBrush = getResources().getInteger(R.integer.medium_size);
        largeBrush = getResources().getInteger(R.integer.large_size);

        drawBtn = (ImageButton)findViewById(R.id.draw_btn);
        drawBtn.setOnClickListener(this);
        eraseBtn = (ImageButton)findViewById(R.id.erase_btn);
        eraseBtn.setOnClickListener(this);
        newBtn = (ImageButton)findViewById(R.id.new_btn);
        newBtn.setOnClickListener(this);
        saveBtn = (ImageButton)findViewById(R.id.save_btn);
        saveBtn.setOnClickListener(this);
        openBtn = (ImageButton)findViewById(R.id.open_btn);
        openBtn.setOnClickListener(this);
        cameraBtn = (ImageButton)findViewById(R.id.camera_btn);
        cameraBtn.setOnClickListener(this);

        drawView.setBrushSize(mediumBrush);
    }

    public void paintClicked(View view){
        drawView.setErase(false);
        drawView.setBrushSize(drawView.getLastBrushSize());
        if(view!=currPaint){
            ImageButton imgView = (ImageButton)view;
            String color = view.getTag().toString();
            drawView.setColor(color);
            imgView.setImageDrawable(getResources().getDrawable(R.drawable.paint_pressed));
            currPaint.setImageDrawable(getResources().getDrawable(R.drawable.paint));
            currPaint=(ImageButton)view;
        }
    }

    @Override
    public void onClick(View view) {
        if(view.getId()==R.id.draw_btn){
            proceedDrawButtonAction();
        }else if(view.getId()==R.id.erase_btn){
            proceedEraseButtonAction();
        }else if(view.getId()==R.id.new_btn){
            proceedNewButtonAction();
        }else if(view.getId()==R.id.save_btn){
            proceedSaveButtonAction();
        }else if(view.getId()==R.id.open_btn){
            proceedOpenButtonAction();
        }else if(view.getId()==R.id.camera_btn){
            proceedCameraButtonAction();
        }
    }

    public void proceedDrawButtonAction(){
        final Dialog brushDialog = new Dialog(this);
        brushDialog.setTitle("Brush size:");
        brushDialog.setContentView(R.layout.brush_chooser);
        brushDialog.show();

        ImageButton smallBtn = (ImageButton)brushDialog.findViewById(R.id.small_brush);
        smallBtn.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                drawView.setErase(false);
                drawView.setBrushSize(smallBrush);
                drawView.setLastBrushSize(smallBrush);
                brushDialog.dismiss();
            }
        });
        ImageButton mediumBtn = (ImageButton)brushDialog.findViewById(R.id.medium_brush);
        mediumBtn.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                drawView.setErase(false);
                drawView.setBrushSize(mediumBrush);
                drawView.setLastBrushSize(mediumBrush);
                brushDialog.dismiss();
            }
        });
        ImageButton largeBtn = (ImageButton)brushDialog.findViewById(R.id.large_brush);
        largeBtn.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                drawView.setErase(false);
                drawView.setBrushSize(largeBrush);
                drawView.setLastBrushSize(largeBrush);
                brushDialog.dismiss();
            }
        });
    }

    public void proceedEraseButtonAction(){
        final Dialog brushDialog = new Dialog(this);
        brushDialog.setTitle("Eraser size:");
        brushDialog.setContentView(R.layout.brush_chooser);
        brushDialog.show();

        ImageButton smallBtn = (ImageButton)brushDialog.findViewById(R.id.small_brush);
        smallBtn.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                drawView.setErase(true);
                drawView.setBrushSize(smallBrush);
                brushDialog.dismiss();
            }
        });
        ImageButton mediumBtn = (ImageButton)brushDialog.findViewById(R.id.medium_brush);
        mediumBtn.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                drawView.setErase(true);
                drawView.setBrushSize(mediumBrush);
                brushDialog.dismiss();
            }
        });
        ImageButton largeBtn = (ImageButton)brushDialog.findViewById(R.id.large_brush);
        largeBtn.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View v) {
                drawView.setErase(true);
                drawView.setBrushSize(largeBrush);
                brushDialog.dismiss();
            }
        });
    }

    public void proceedNewButtonAction(){
        AlertDialog.Builder yesNoDialog = new AlertDialog.Builder(this);
        yesNoDialog.setTitle("New drawing");
        yesNoDialog.setMessage("Start new drawing (you will lose the current drawing)?");
        yesNoDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                drawView.startNew();
                dialog.dismiss();
            }
        });
        yesNoDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                dialog.cancel();
            }
        });
        yesNoDialog.show();
    }

    public void proceedSaveButtonAction(){
        AlertDialog.Builder saveDialog = new AlertDialog.Builder(this);
        saveDialog.setTitle("Save drawing");
        saveDialog.setMessage("Save drawing to device Gallery?");
        saveDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                drawView.setDrawingCacheEnabled(true);
                String imgSaved = MediaStore.Images.Media.insertImage(
                        getContentResolver(), drawView.getDrawingCache(),
                        UUID.randomUUID().toString()+".png", "drawing");
                if(imgSaved!=null){
                    Toast savedToast = Toast.makeText(getApplicationContext(),
                            "Drawing saved to Gallery!", Toast.LENGTH_SHORT);
                    savedToast.show();
                }
                else{
                    Toast unsavedToast = Toast.makeText(getApplicationContext(),
                            "Image could not be saved.", Toast.LENGTH_SHORT);
                    unsavedToast.show();
                }
                drawView.destroyDrawingCache();
            }
        });
        saveDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                dialog.cancel();
            }
        });
        saveDialog.show();
    }

    public void proceedOpenButtonAction(){
        AlertDialog.Builder yesNoDialog = new AlertDialog.Builder(this);
        yesNoDialog.setTitle("Load image");
        yesNoDialog.setMessage("Load image from gallery (you will lose the current drawing)?");
        yesNoDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                Intent pickPhoto = new Intent(
                        Intent.ACTION_PICK,
                        android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
                startActivityForResult(pickPhoto, GALLERY_INTENT_CALLED);
            }
        });
        yesNoDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                dialog.cancel();
            }
        });
        yesNoDialog.show();
    }

    public void proceedCameraButtonAction(){
        AlertDialog.Builder yesNoDialog = new AlertDialog.Builder(this);
        yesNoDialog.setTitle("Take a photo");
        yesNoDialog.setMessage("Take a photo and start drawing (you will lose the current drawing)?");
        yesNoDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                Intent takePhoto = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                if (takePhoto.resolveActivity(getPackageManager()) != null) {
                    startActivityForResult(takePhoto, CAMERA_INTENT_CALLED);
                }
            }
        });
        yesNoDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener(){
            public void onClick(DialogInterface dialog, int which){
                dialog.cancel();
            }
        });
        yesNoDialog.show();
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
        super.onActivityResult(requestCode, resultCode, returnedIntent);
        if (requestCode == GALLERY_INTENT_CALLED) {
            if (resultCode == RESULT_OK) {
                try {
                    Uri selectedImage = returnedIntent.getData();
                    InputStream inputStream = getContentResolver().openInputStream(selectedImage);
                    image = Drawable.createFromStream(inputStream, selectedImage.toString());
                } catch (FileNotFoundException e) {}
                drawView.drawImage(drawableToBitmap(image));
            }
        }else if (requestCode == CAMERA_INTENT_CALLED) {
            if (resultCode == RESULT_OK) {
                Bundle extras = returnedIntent.getExtras();
                Bitmap imageBitmap = (Bitmap) extras.get("data");
                drawView.drawImage(imageBitmap);
            }
        }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey("drawCanvas")) {
                drawView.setDrawCanvas((SerializableCanvas)savedInstanceState.getSerializable("drawCanvas"));
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSerializable("drawCanvas", drawView.getDrawCanvas());
    }

}

DrawingView class:

public class DrawingView extends View implements Serializable{

    private Path drawPath;
    private Paint drawPaint;
    private Paint canvasPaint;
    private int paintColor = 0xFFFF0000;
    private SerializableCanvas drawCanvas;
    private Bitmap canvasBitmap;
    private float brushSize;
    private float lastBrushSize;
    private boolean erase = false;

    public DrawingView(Context context, AttributeSet attrs){
        super(context, attrs);
        setupDrawing();
    }

    private void setupDrawing(){
        brushSize = getResources().getInteger(R.integer.medium_size);
        lastBrushSize = brushSize;
        drawPath = new Path();
        drawPaint = new Paint();
        drawPaint.setColor(paintColor);
        drawPaint.setAntiAlias(true);
        drawPaint.setStrokeWidth(brushSize);
        drawPaint.setStyle(Paint.Style.STROKE);
        drawPaint.setStrokeJoin(Paint.Join.ROUND);
        drawPaint.setStrokeCap(Paint.Cap.ROUND);
        canvasPaint = new Paint(Paint.DITHER_FLAG);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float touchX = event.getX();
        float touchY = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawPath.lineTo(touchX, touchY);
                if(erase){
                    drawCanvas.drawPath(drawPath, drawPaint);
                }
                break;
            case MotionEvent.ACTION_UP:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
        }

        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
        if(erase){ return;}
        canvas.drawPath(drawPath, drawPaint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        drawCanvas = new SerializableCanvas(canvasBitmap);
    }

    public void setColor(String newColor){
        invalidate();
        paintColor = Color.parseColor(newColor);
        drawPaint.setColor(paintColor);
    }

    public void setBrushSize(float newSize){
        float pixelAmount = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                newSize, getResources().getDisplayMetrics());
        brushSize = pixelAmount;
        drawPaint.setStrokeWidth(brushSize);
    }

    public void setLastBrushSize(float lastSize){
        lastBrushSize=lastSize;
    }
    public float getLastBrushSize(){
        return lastBrushSize;
    }

    public void setErase(boolean erase){
        this.erase = erase;
        if(erase) {
            drawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        } else {
            drawPaint.setXfermode(null);
        }
    }

    public void startNew(){
        drawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
        invalidate();
    }

    public void drawImage(Bitmap image) {
        drawCanvas.drawBitmap(resize(image, drawCanvas.getWidth(), drawCanvas.getHeight()), 0, 0, canvasPaint);
        invalidate();
    }

    public static Bitmap drawableToBitmap (Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable)drawable).getBitmap();
        }
        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }

    private static Bitmap resize(Bitmap image, int maxWidth, int maxHeight) {
        image = rotateImage(image, 90);
        image = Bitmap.createScaledBitmap(image, maxWidth, maxHeight, true);
        return image;
    }

    public static Bitmap rotateImage(Bitmap source, float angle) {
        Matrix matrix = new Matrix();
        matrix.postRotate(angle);
        return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(),
                matrix, true);
    }

    public SerializableCanvas getDrawCanvas(){
        return drawCanvas;
    }

    public void setDrawCanvas(SerializableCanvas drawCanvas){
        this.drawCanvas = drawCanvas;
    }

    public Bitmap getCanvasBitmap(){
        return canvasBitmap;
    }

    public void setCanvasBitmap(Bitmap canvasBitmap){
        this.canvasBitmap = canvasBitmap;
    }

    public Paint getCanvasPaint(){
        return canvasPaint;
    }

    public void setCanvasPaint(Paint canvasPaint){
        this.canvasPaint = canvasPaint;
    }

}

Solution

  • You are absolutely right; the root cause of your NPE is that your Activity was closed. Most likely, the Camera app forces landscape orientation, and your Activity, which does not support configuration change, is restarted (maybe more than once) to account for this 'rotation'. Another common scenario is that the system decided to close your app because Camera app needed more RAM.

    At any rate, you cannot save/restore a Canvas or View in the bundle of onSaveInstanceState(). You must connect to the new DrawingView and its drawingCanvas after the activity resumes.

    The catch is that onActivityResult() may be invoked before the Activity resumes, which means that maybe you cannot draw the bitmap when you receive it. In DrawingView.drawBitmap(), if drawingCanvas == null, save the bitmap to a local variable, and draw this bitmap as soon as the canvas becomes available.

    PS note that for CAMERA_INTENT_CALLED your app will draw a low-res thumbnail bitmap of the picture taken by camera. If you want the full quality image, you must retrieve it from the file created by the Camera app.

    Here is a piece of code for DrawingView to achieve delayed draw:

    private Bitmap savedBitmap;
    
    public void drawImage(Bitmap image) {
        if (drawCanvas == null) {
            savedBitmap = image;
        }
        else {
            drawCanvas.drawBitmap(resize(image, drawCanvas.getWidth(), drawCanvas.getHeight()), 0, 0, canvasPaint);
            invalidate();
        }
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        drawCanvas = new SerializableCanvas(canvasBitmap);
        if (savedBitmap != null) {
            drawBitmap(savedBitmap);
            savedBitmap = null;
        }
    }
    

    Note that you don't need serialization for the Canvas or for the DrawingView.