Search code examples
androidandroid-canvaspaintondrawandroidpdfviewer

How to initialise onDraw in Android via a button


In my project, users can view PDF documents and I want them to have the option to annotate each page in the document via onDraw and Paint. I would like the document to open for viewing first with the option to turn the drawing/painting function on and off via a button like the WhatsApp paint function.

I have a PaintView class extending my PDFView but when I open a PDF, the onDraw is called straight away, allowing me to draw over the PDF but not being able to then turn off this function and swipe between pages. When I move initDraw to a button I get a null pointer in my PaintView class.

java.lang.NullPointerException: Attempt to invoke virtual method 'void android.graphics.Canvas.drawColor(int)' on a null object reference
    at com.example.dissertation814.pdfViewer.PaintView.onDraw(PaintView.java:60)

My viewer activity:

public class PdfViewerActivity extends AppCompatActivity {

private boolean isDrawInit = false;
private PaintView paintView;

//firebase auth
private FirebaseAuth mAuth;

//variables
public String currentUserAccount;
public String teacherAccountNav = "Teacher";

PDFView pdfView;

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_pdf_viewer);

    //PDFView to display PDFs
    pdfView = findViewById(R.id.pdfView);

    //use best quality
    pdfView.useBestQuality(true);

    //get data from intent
    Intent i = this.getIntent();
    Uri uri = i.getParcelableExtra("FILE_PATH_URI");

    //Get the pdf file
    assert uri != null;
    File file = new File(Objects.requireNonNull(uri.getPath()));

    if(file.canRead()){
        //load pdf file
        pdfView.fromFile(file)
                .defaultPage(0)
                .enableSwipe(true)
                .swipeHorizontal(true)
                .pageSnap(true)

                .onDrawAll(new OnDrawListener() {
                    @Override
                    public void onLayerDrawn(Canvas canvas, float pageWidth, float pageHeight, int displayedPage) {
                        Bitmap  bitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);

                        Paint paint = new Paint();
                        paint.setColor(Color.BLACK);
                        canvas.drawBitmap(bitmap, 0,0, paint);
                    }
                })

                .onLoad(new OnLoadCompleteListener() {
            @Override
            public void loadComplete(int nbPages) {
                Toast.makeText(PdfViewerActivity.this, "No. of pages: " + nbPages, Toast.LENGTH_SHORT).show();
            }
        }).load();
    }

}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
public void onInitDrawClick(View view){

}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
protected void onResume() {
    super.onResume();
    if(!isDrawInit){
        initDraw();
        isDrawInit = true;
    }
}

//initialise paint view
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
private void initDraw(){
        paintView = findViewById(R.id.paintView);
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
        paintView.init(metrics);
}

//user finger path from paint view class
static class FingerPath{
    int colour;
    int strokeWidth;
    Path path;

    FingerPath(int colour, int strokeWidth, Path path){
        this.colour = colour;
        this.strokeWidth = strokeWidth;
        this.path = path;
    }
}

My PaintView class:

public class PaintView extends PDFView {
private Paint mPaint;
private Canvas mCanvas;
private Bitmap mBitmap;

private ArrayList<PdfViewerActivity.FingerPath> paths = new ArrayList<>();
private Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG);

private static final float TOUCH_TOLERANCE = 4;

private Path mPath;
private float mX;
private float mY;
public int brushColour = Color.BLACK;
public int brushSize = 10;


public PaintView(Context context, AttributeSet set) {
    super(context, set);
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setDither(true);
    mPaint.setColor(Color.BLACK);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeJoin(Paint.Join.ROUND);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    mPaint.setXfermode(null);
    mPaint.setAlpha(0xff);
}

public void init (DisplayMetrics metrics){
    int height = (int) (metrics.heightPixels);
    int width = metrics.widthPixels;

    mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    mCanvas = new Canvas(mBitmap);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    mCanvas.drawColor(Color.TRANSPARENT);

    for(PdfViewerActivity.FingerPath fp : paths){
        mPaint.setColor(fp.colour);
        mPaint.setStrokeWidth(fp.strokeWidth);
        mPaint.setMaskFilter(null);

        mCanvas.drawPath(fp.path, mPaint);
    }

    canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
    canvas.restore();
}

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

    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            touchStart(x,y);
            invalidate();
            break;
            case MotionEvent.ACTION_MOVE:
                touchMove(x,y);
                invalidate();
                break;
                case MotionEvent.ACTION_UP:
                    touchUp();
                    break;
    }
    return true;
}

private void touchUp(){
    mPath.lineTo(mX,mY);
}

private void touchMove(float x, float y){
    float dx = Math.abs(x - mX);
    float dy = Math.abs(y - mY);


    if(dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE){
        mPath.quadTo(mX, mY, (x+mX)/2, (y+mY)/2);
        mX = x;
        mY = y;
    }
}

private void touchStart(float x, float y){
    mPath = new Path();
    PdfViewerActivity.FingerPath fp = new PdfViewerActivity.FingerPath(brushColour, brushSize, mPath);
    paths.add(fp);

    mPath.reset();
    mPath.moveTo(x,y);


    mX = x;
    mY = y;
}

public void clear(){
    paths.clear();
    invalidate();
}

My XML:

 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/black"
     tools:context=".pdfViewer.PdfViewerActivity">

<Button
    android:id="@+id/initDraw"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="init"
    app:layout_constraintBottom_toTopOf="@+id/relativeLayout"
    app:layout_constraintEnd_toStartOf="@+id/homeButton"
    app:layout_constraintStart_toEndOf="@+id/backButton"
    app:layout_constraintTop_toTopOf="parent"
    android:onClick="onInitDrawClick"/>

<ImageButton
    android:id="@+id/backButton"
    android:layout_width="60dp"
    android:layout_height="60dp"
    android:background="@color/black"
    android:contentDescription="@string/back_button"
    android:onClick="onBackClicked"
    android:src="@drawable/backward_arrow"
    app:layout_constraintBottom_toTopOf="@+id/relativeLayout"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.112"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.8" />

<ImageButton
    android:id="@+id/homeButton"
    android:layout_width="60dp"
    android:layout_height="60dp"
    android:layout_marginEnd="64dp"
    android:layout_marginRight="64dp"
    android:background="@color/black"
    android:onClick="onHomeClicked"
    android:src="@drawable/ic_home_black_24dp"
    app:layout_constraintBottom_toTopOf="@+id/relativeLayout"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="1.0"
    app:layout_constraintStart_toEndOf="@+id/backButton"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.8" />

<RelativeLayout
    android:id="@+id/relativeLayout"
    android:layout_width="match_parent"
    android:layout_height="800dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.0"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.919"
    android:paddingLeft="16dp"
    android:paddingRight="16dp">

    <com.github.barteksc.pdfviewer.PDFView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.example.dissertation814.pdfViewer.PaintView
        android:id="@+id/paintView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/transparent"/>


</RelativeLayout>

 </androidx.constraintlayout.widget.ConstraintLayout>

Solution

  • onDraw is a method, thus it's not something you initialize. I don't think you should try to disable the method either. Although you can override it, which puts you in control of what is drawn.

    Consider another solution to you problem. Instead of enabling or disabling the onDraw method, you control which view gets to process the user input.


    Solution:

    When returning true in the method onTouchEvent, you state that no views above this one -- in the view hierarcy -- needs to process this input.

    What you instead should do is to check whether or not the drawing feature should be on. If the drawing feature is disabled, you return false. Else if the drawing feature is enabled, you process the input, then return true.

    Example:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Check whether or not the drawing feature is disabled
        if (drawingIsEnabled == false) { 
             // Let parent views process this input
             return false;
        }
    
        float x = event.getX();
        float y = event.getY();
    
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                touchStart(x,y);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                touchMove(x,y);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                touchUp();
                break;
        }
    
        // Prevent parent views from processing this input
        return true;
    }
    

    When returning false, the input is passed further up the view hierarchy so that the parent view will get the chance to process the input. (This will allow you to swipe pages)

    If you however return true, you are preventing parent views from processing the input. (This will prevent the parent view from swiping pages while you're drawing, which would be quite annoying)


    Hope this helps!