Drawing content above displayed documents
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 }}getDrawablesForPage() is a Kotlin suspend function, so Java code can’t override it. Enable Kotlin support in your app module, and declare the provider in a Kotlin file:
class RectDrawableProvider(private val drawables: List<PdfDrawable>) : PdfDrawableProvider() { override suspend fun getDrawablesForPage( context: Context, document: PdfDocument, pageIndex: Int, ): List<PdfDrawable> = drawables}Then create it from Java:
// 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 RectDrawable(new RectF(0f, 100f, 100f, 0f)));
private final PdfDrawableProvider provider = new RectDrawableProvider(drawables);Register the provider on the fragment with addDrawableProvider(provider) to start drawing:
requirePdfFragment().addDrawableProvider(provider)requirePdfFragment().addDrawableProvider(provider);Register the provider on PdfThumbnailBar and PdfThumbnailGrid to draw on thumbnails:
// Thumbnail GridpspdfKitViews.thumbnailGridView?.addDrawableProvider(provider)// Thumbnail BarpspdfKitViews.thumbnailBarView?.addDrawableProvider(provider)// Thumbnail GridgetPSPDFKitViews().getThumbnailGridView().addDrawableProvider(provider);// Thumbnail BargetPSPDFKitViews().getThumbnailBarView().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 }}public final class RectDrawable extends PdfDrawable { @NonNull private final Paint paint = new Paint();
public RectDrawable() { paint.setColor(Color.RED); this.paint.setStyle(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)}@Override public void draw(Canvas canvas) { canvas.drawRect(getBounds(), 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}@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;}Return a PixelFormat(opens in a new tab) matching the content: UNKNOWN(opens in a new tab), OPAQUE(opens in a new tab), TRANSPARENT(opens in a new tab), or TRANSLUCENT(opens in a new tab).
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)// 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));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}// 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 the coordinates before applying. final Rect bounds = new Rect(); screenCoordinates.roundOut(bounds); setBounds(bounds);}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()}@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();}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()}@UiThread@Override public void setAlpha(int alpha) { paint.setAlpha(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 }}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(); }
/** * All the drawing is performed here. Keep this method fast to maintain 60 fps. */ @Override public void draw(Canvas canvas) { canvas.drawRect(getBounds(), 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 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 ensures no clipping of content. final Rect bounds = new Rect(); screenCoordinates.roundOut(bounds); setBounds(bounds); }}