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>
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!