Drawable API

An app using PSPDFKit can draw content above a displayed document using the PdfDrawable which is a subtype of Android's Drawable class and thus is very similar in use. This article guides you through the process of creating a drawable that paints a red rectangle on a document, positioning it using PDF page coordinates.

For more info on how to use Drawable API to draw watermarks, check out the WatermarkExample inside the catalog app. Another great resource is the ScreenReaderExample. It's a complete example of all the features and elements discussed in this article.

Using Drawables

To draw a PdfDrawable onto a page you need to provide it to the PdfFragment using a PdfDrawableProvider. In addition, you can provide it to PdfThumbnailBar and PdfThumbnailGrid to draw on thumbnails.

You can think of the PdfDrawableProvider like the adapter of a list view, but instead of creating and serving list view items, it serves drawables. A very simple implementation of a drawable provider that only serves a single RectDrawable which is painted on every page may look like this:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
// The drawable provider needs to return a list of drawables for a page.
// Since we're only serving a single drawable, we create a one-element list.
val drawables = Collections.singletonList(SquareDrawable(RectF(0f, 100f, 100f, 0f)))

// This is the drawable provider. You only need to implement a single method.
val provider = object : PdfDrawableProvider() {
    override fun getDrawablesForPage(context: Context, document: PdfDocument, pageIndex: Int):
        List<PdfDrawable>? {
        // Return the same drawable for every requested page.
        return drawables
    }
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The drawable provider needs to return a list of drawables for a page.
// Since we're only serving a single drawable, we create a one-element list.
private final List<? extends PdfDrawable> drawables = Collections.singletonList(
		new SquareDrawable(new RectF(0f, 100f, 100f, 0f)));

// This is the drawable provider. You only need to implement a single method.
private final PdfDrawableProvider provider = new PdfDrawableProvider() {
    @Override
	public List<? extends PdfDrawable> getDrawablesForPage(@NonNull Context context,
		@NonNull PdfDocument document, @IntRange(from = 0) int pageIndex) {

    	// Return the same drawable for every requested page.
        return drawables;
    }
};

To start drawing the PdfDrawable on the page, you need to add the provider by calling addDrawableProvider(provider) on the fragment.

1
pdfFragment.addDrawableProvider(provider)
1
getPdfFragment().addDrawableProvider(provider);

To draw on thumbnails for PdfThumbnailBar and PdfThumbnailGrid:

Copy
MyActivity.kt
1
2
3
4
// Thumbnail Grid
pspdfKitViews.thumbnailGridView?.addDrawableProvider(provider)
// Thumbnail Bar
pspdfKitViews.thumbnailBarView?.addDrawableProvider(provider)
Copy
MyActivity.java
1
2
3
4
// Thumbnail Grid
getPSPDFKitViews().getThumbnailGridView().addDrawableProvider(provider);
// Thumbnail Bar
getPSPDFKitViews().getThumbnailBarView().addDrawableProvider(provider);

Creating a Custom Drawable

Your custom drawable needs to extend the PdfDrawable which itself extends Android's abstract Drawable class and requires a couple of methods to be overridden and implemented. We will look at them one after another.

Copy
1
2
3
4
public void draw(Canvas canvas);
public void setAlpha(int alpha);
public void setColorFilter(ColorFilter colorFilter);
public int getOpacity();

The #draw(Canvas) method is where the drawable performs all of its on-screen drawing operations by issuing drawing commands on the provided Canvas. #setAlpha(int) and #setColorFilter(ColorFilter) are used to tell the drawable to adapt to presentation specifics, e.g. to enable translucency or blending modes.

Preparing to Draw

Drawing on a canvas requires a Paint object specifying drawing settings like color, thickness, opacity, and more. Since subsequent drawing passes should always happen in less than 16ms, the used Paint has to be be prepared upfront — for example in the constructor of your drawable.

Copy
RectDrawable.kt
1
2
3
4
5
6
7
8
class RectDrawable : PdfDrawable() {
	private val paint: Paint = Paint()

	init {
        paint.color = Color.RED
        paint.style = Paint.Style.FILL
    }
}
Copy
RectDrawable.java
1
2
3
4
5
6
7
8
public final class RectDrawable extends PdfDrawable {
	@NonNull private final Paint paint = new Paint();

	public RectDrawable() {
        this.paint.setColor(Color.RED);
        this.paint.setStyle(Paint.Style.FILL);
	}
}

Perform Drawing

Inside the draw() method you can use the provided Canvas, passing it the paint. There are many different call methods — this example uses the Canvas#drawRect(Rect, Paint) method.

RectDrawable.kt
1
2
3
override fun draw(canvas: Canvas) {
	canvas.drawRect(bounds, paint)
}
RectDrawable.java
1
2
3
@Override public draw(Canvas canvas) {
    canvas.drawRect(getBounds(), paint);
}

The Drawable#getBounds method used by the above snippet returns the screen rectangle that will be covered by the drawable. Thus, our RectDrawable will take all of the space it is assigned and will draw a single rectangle inside that space.

Next, a drawable must implement the #getOpacity method, which returns a flag specifying whether background shines through the drawable, or whether the drawable is completely opaque. This is mainly for improving drawing performance – which you should always aim for.

Copy
RectDrawable.kt
1
2
3
4
5
override fun int getOpacity() {
	// The drawable paints a solid rectangle. Since nothing of the background
	// inside the drawable bounds, we specify the drawable as OPAQUE.
	return PixelFormat.OPAQUE
}
Copy
RectDrawable.java
1
2
3
4
5
@Override public int getOpacity() {
	// The drawable paints a solid rectangle. Since nothing from behind the drawable
	// is visible inside the drawable bounds, we specify the drawable as OPAQUE.
	return PixelFormat.OPAQUE;
}

Note: The #getOpacity method must return one PixelFormat: UNKNOWN, OPAQUE, TRANSPARENT or TRANSLUCENT depending on the content that is drawn.

Defining Drawable Bounds

Setting new bounds of your drawable is as simple as calling this.setBounds(newDrawableBounds) inside your drawable.

RectDrawable.kt
1
2
3
// This will cause the drawable to paint a 100x100 px square
// at the very top-left corner of the page.
bounds = Rect(0, 0, 100, 100)
Copy
RectDrawable.java
1
2
3
// This will cause the drawable to paint a 100x100 px square
// at the very top-left corner of the page.
setBounds(new Rect(0, 0, 100, 100));

This works fine if you want to draw something in pixels (i.e. view coordinates). When doing so the drawable will always be at the top left of the page, keeping the same size without being influenced by the zoom.

Drawing in PDF Coordinates

If you want to draw on a page and align your drawables with page text, with annotations, or any other content on the page, you will first need to understand how the PDF coordinate space works. Since the Canvas object expects coordinates in pixel space instead of PDF coordinates, the PdfDrawable#getPdfToPageTransformation provides a transformation Matrix that you can use to convert your PDF coordinates to view coordinates.

Copy
RectDrawable.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// This is a 100x100pt square on the bottom left of the page.
// Don't confuse this with pixels. Usually 1pt is 1/72 inch.
val pageCoordinates = RectF(0f, 100f, 100f, 0f)

// This will contain the screen coordinates (in pixels).
val screenCoordinates = RectF()

private fun updateBoundingBox() {
	// Transform PDF coordinates into screen coordinates.
	pdfToPageTransformation.mapRect(screenCoordinates, pageCoordinates)

	// Since the drawable bounds are Rect (int) and our transformed
	// screen coordinates are RectF (float) we need to round them before applying.
	val newBounds = this.bounds
	screenCoordinates.roundOut(newBounds)
	this.bounds = newBounds
}
Copy
RectDrawable.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// This is a 100x100pt square on the bottom left of the page.
// Don't confuse this with pixels. Usually 1pt is 1/72 inch.
private final RectF pageCoordinates = new RectF(0f, 100f, 100f, 0f);

// This will contain the screen coordinates (in pixels).
private final RectF screenCoordinates = new RectF();

private void updateBoundingBox() {
	// Transform PDF coordinates into screen coordinates.
	getPdfToPageTransformation().mapRect(screenCoordinates, pageCoordinates);

	// Since the drawable bounds are Rect (int) and our transformed
	// screen coordinates are RectF (float) we need to round them before applying.
	final Rect bounds = getBounds();
	screenCoordinates.roundOut(bounds);
	setBounds(bounds);
}

Every time the page you're drawing to is moved—for example because of zooming or scrolling—the transformation Matrix is recalculated and the drawable is notified of this via the PdfDrawable#updatePdfToViewTransformation method. You can override this method and use it as a hook for re-calculating the screen coordinates.

Copy
RectDrawable.kt
1
2
3
4
5
6
7
override fun updatePdfToViewTransformation(matrix: Matrix) {
    super.updatePdfToViewTransformation(matrix)

    // We simply call the method we implemented earlier. It will take the new matrix
    // and use it to calculate screen coordinates.
    updateBoundingBox()
}
Copy
RectDrawable.java
1
2
3
4
5
6
7
@Override public void updatePdfToViewTransformation(@NonNull Matrix matrix) {
    super.updatePdfToViewTransformation(matrix);

    // We simply call the method we implemented earlier. It will take the new matrix
    // and use it to calculate screen coordinates.
    updateBoundingBox();
}

Important: Don't forget to call super.updatePdfToViewTransformation(matrix) first inside the method, or PdfDrawable#getPdfToPageTransformation will stop working.

Invalidating the Drawable

Whenever the visual representation of your drawable changes, you need to tell the rendering system about that change. For example, when you are updating the alpha value of your drawable (e.g. because of a call to setAlpha()) a call to Drawable#invalidateSelf will trigger an invalidation, which will re-render the updated drawable on the screen.

Copy
RectDrawable.kt
1
2
3
4
5
@UiThread override fun setAlpha(alpha: Int) {
    paint.alpha = alpha
    // Drawable invalidation is only allowed from a UI-thread.
    invalidateSelf()
}
Copy
RectDrawable.java
1
2
3
4
5
6
@UiThread
@Override public void setAlpha(int alpha) {
    paint.setAlpha(alpha);
    // Drawable invalidation is only allowed from a UI-thread.
    invalidateSelf();
}

Important: invalidateSelf() may only be called from the UI thread. Any call from a background thread will raise an exception.

The Final Drawable

Here is the final drawable. This version also supports setting new PDF coordinates by calling the RectDrawable#setPageCoordinates method.

Copy
RectDrawable.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class RectDrawable(pageCoordinates: RectF) : PdfDrawable() {

    private val paint = Paint()
    private val screenCoordinates = RectF()

    var pageCoordinates: RectF = pageCoordinates
        set(value) {
            field.set(value)
            updateScreenCoordinates()
        }

    init {
        paint.color = Color.RED
        paint.style = Paint.Style.FILL
    }

    /**
     * Here all the drawing is performed. Keep this method fast to maintain 60 fps.
     */
    override fun draw(canvas: Canvas) {
        canvas.drawRect(bounds, paint)
    }

    /**
     * PSPDFKit calls this method every time the page was moved or resized on screen.
     * It will provide a fresh transformation for calculating screen coordinates from
     * PDF coordinates.
     */
    override fun updatePdfToViewTransformation(matrix: Matrix) {
        super.updatePdfToViewTransformation(matrix)
        updateScreenCoordinates()
    }

    @UiThread override fun setAlpha(alpha: Int) {
        paint.alpha = alpha
        // Drawable invalidation is only allowed from a UI-thread.
        invalidateSelf()
    }

    @UiThread override fun setColorFilter(colorFilter: ColorFilter?) {
        paint.colorFilter = colorFilter
        // Drawable invalidation is only allowed from a UI-thread.
        invalidateSelf()
    }

    override fun getOpacity(): Int {
        return PixelFormat.OPAQUE
    }

    private fun updateScreenCoordinates() {
        // Calculate the screen coordinates by applying the PDF-to-view transformation.
        pdfToPageTransformation.mapRect(screenCoordinates, pageCoordinates)

        // Rounding out ensure no clipping of content.
        val newBounds = this.bounds
        screenCoordinates.roundOut(newBounds)
        this.bounds = newBounds
    }
}
Copy
RectDrawable.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public final class RectDrawable extends PdfDrawable {

    @NonNull private final Paint paint = new Paint();
    @NonNull private final RectF pageCoordinates;
    @NonNull private final RectF screenCoordinates = new RectF();

    public RectDrawable(@NonNull final RectF pageCoordinates) {
        this.pageCoordinates = pageCoordinates;
        this.paint.setColor(Color.RED);
        this.paint.setStyle(Paint.Style.FILL);
    }

    public void setPageCoordinates(@NonNull final RectF pageCoordinates) {
        // Copy over the value to prevent undesired mutation.
        this.pageCoordinates.set(pageCoordinates);
        updateScreenCoordinates();
    }

    /**
     * Here all the drawing is performed. Keep this method fast to maintain 60 fps.
     */
    @Override public void draw(Canvas canvas) {
        canvas.drawRect(getBounds(), paint);
    }

    /**
     * PSPDFKit calls this method every time the page was moved or resized on screen.
     * It will provide a fresh transformation for calculating screen coordinates from
     * PDF coordinates.
     */
    @Override public void updatePdfToViewTransformation(@NonNull Matrix matrix) {
        super.updatePdfToViewTransformation(matrix);
        updateScreenCoordinates();
    }

    @UiThread
    @Override public void setAlpha(int alpha) {
        paint.setAlpha(alpha);
        // Drawable invalidation is only allowed from a UI-thread.
        invalidateSelf();
    }

    @UiThread
    @Override public void setColorFilter(ColorFilter colorFilter) {
        paint.setColorFilter(colorFilter);
        // Drawable invalidation is only allowed from a UI-thread.
        invalidateSelf();
    }

    @Override public int getOpacity() {
        return PixelFormat.OPAQUE;
    }

    private void updateScreenCoordinates() {
        // Calculate the screen coordinates by applying the PDF-to-view transformation.
        getPdfToPageTransformation().mapRect(screenCoordinates, pageCoordinates);

        // Rounding out ensure no clipping of content.
        final Rect bounds = getBounds();
        screenCoordinates.roundOut(bounds);
        setBounds(bounds);
    }
}