This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/android/features/drawable-api.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Draw content above displayed document on Android | Nutrient

Use PdfDrawable to draw content over a document. This class extends Android’s Drawable(opens in a new tab). The following examples draw a red rectangle positioned in PDF coordinates. For coordinate details, refer to the PDF page coordinates guide.

The Catalog app’s WatermarkExample demonstrates watermarks. ScreenReaderExample provides a complete implementation of the features in this guide.

Using drawables

Supply a PdfDrawable to PdfFragment through a PdfDrawableProvider. The same provider can draw on PdfThumbnailBar and PdfThumbnailGrid thumbnails.

A provider returns drawables for each requested page. The following provider returns one RectDrawable for every page:

// 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(RectDrawable(RectF(0f, 100f, 100f, 0f)))
// This is the drawable provider. You only need to implement a single method.
val provider = object : PdfDrawableProvider() {
override suspend fun getDrawablesForPage(
context: Context,
document: PdfDocument,
pageIndex: Int): List<PdfDrawable>? {
// Return the same drawable for every requested page.
return drawables
}
}

Register the provider on the fragment with addDrawableProvider(provider) to start drawing:

requirePdfFragment().addDrawableProvider(provider)

Register the provider on PdfThumbnailBar and PdfThumbnailGrid to draw on thumbnails:

// Thumbnail Grid
pspdfKitViews.thumbnailGridView?.addDrawableProvider(provider)
// Thumbnail Bar
pspdfKitViews.thumbnailBarView?.addDrawableProvider(provider)

Call notifyDrawablesChanged() on PdfDrawableProvider when its drawables change, such as after asynchronous loading. Nutrient then requests updated drawables and redraws the affected pages.

Creating a custom drawable

Extend PdfDrawable to create a custom drawable. Implement these inherited Drawable(opens in a new tab) methods:

public void draw(Canvas canvas);
public void setAlpha(int alpha);
public void setColorFilter(ColorFilter colorFilter);
public int getOpacity();

Implement drawing commands in #draw(Canvas)(opens in a new tab) using the supplied Canvas(opens in a new tab). Use #setAlpha(int)(opens in a new tab) for translucency and #setColorFilter(ColorFilter)(opens in a new tab) for color filtering and blending.

Preparing to draw

A Paint(opens in a new tab) object defines settings such as color, thickness, and opacity. Prepare it before drawing to keep drawing passes under 16 ms(opens in a new tab). The following example initializes it in the drawable’s constructor:

class RectDrawable : PdfDrawable() {
private val paint: Paint = Paint()
init {
paint.color = Color.RED
paint.style = Paint.Style.FILL
}
}

Perform drawing

Inside draw(), pass your paint to drawing methods on the supplied Canvas(opens in a new tab). This example uses Canvas#drawRect(Rect, Paint)(opens in a new tab):

override fun draw(canvas: Canvas) {
canvas.drawRect(bounds, paint)
}

Drawable#getBounds(opens in a new tab) returns the drawable’s screen rectangle. RectDrawable fills this entire area.

Implement #getOpacity(opens in a new tab) to report whether the drawable obscures its background. Android deprecated this method in API level 29 and no longer uses it for drawing optimizations. It remains abstract, so your drawable still requires an implementation:

override fun getOpacity(): Int {
// 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
}

Defining drawable bounds

Set the drawable’s bounds with this.setBounds(newDrawableBounds)(opens in a new tab):

// 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)

These bounds use view coordinates in pixels. The rectangle stays at the page’s top-left and keeps its size as the page zooms.

Drawing in PDF coordinates

To align drawables with page content, refer to the PDF coordinate space guide. Canvas(opens in a new tab) expects pixel coordinates rather than PDF coordinates.

Use the Matrix(opens in a new tab) from PdfDrawable#getPdfToPageTransformation() to convert PDF coordinates to view coordinates:

// 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 the coordinates before applying.
val newBounds = Rect()
screenCoordinates.roundOut(newBounds)
this.bounds = newBounds
}

Nutrient recalculates the transformation Matrix(opens in a new tab) when the page moves, including during zooming or scrolling. It notifies the drawable through PdfDrawable#updatePdfToViewTransformation. Override this method to recalculate screen coordinates:

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()
}

Call super.updatePdfToViewTransformation(matrix) first. Otherwise, PdfDrawable#getPdfToPageTransformation stops working.

Invalidating the drawable

Call Drawable#invalidateSelf(opens in a new tab) when the drawable’s appearance changes. This requests a redraw. For example, invalidate after changing alpha in setAlpha():

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

Call invalidateSelf() only on the UI thread. A background-thread call throws an exception.

Refreshing drawables after data changes

Call PdfDrawableProvider#notifyDrawablesChanged() when the drawable set changes. For one page, call PdfDrawableProvider#notifyDrawablesChanged(pageIndex) instead.

Use provider invalidation for data changes, such as loading overlay metadata after a page jump. Use Drawable#invalidateSelf(opens in a new tab) for appearance changes within an existing drawable, including alpha, color, or cached bitmap updates.

Nutrient calls PdfDrawable.draw() only when rendering the drawable’s page. It can query the provider before drawing that page, particularly after large page jumps or layout changes. Don’t use draw() as a page visibility callback.

Call PdfFragment#notifyLayoutChanged() when the surrounding layout changes. Use PdfFragment#refreshPages() only when the document view has no visible pages and requires reinitialization.

The final drawable

The complete drawable below also supports changing PDF coordinates through RectDrawable#setPageCoordinates:

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
}
/**
* All the drawing is performed here. Keep this method fast to maintain 60 fps.
*/
override fun draw(canvas: Canvas) {
canvas.drawRect(bounds, paint)
}
/**
* Nutrient calls this method every time the page is moved or resized on the 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 ensures no clipping of content.
val newBounds = Rect()
screenCoordinates.roundOut(newBounds)
this.bounds = newBounds
}
}