Creating PDFs programmatically, especially on an Android device, is not an everyday task. However, many situations require it - when an organisation needs to capture, share or even print dynamically created content. For example, you may need to generate and print an invoice or receipt for a customer using a Bluetooth printer, or quickly create and print custom labels for individual packages in a distribution centre. But how do you use a PDF generator to create a user interface on Android and fill an empty PDF object with data before saving it? This article will give you the details!

What are the methods for creating a UI on Android?

PDF creation is not a common task, but it has significant practical value. If you want to build a user interface (UI) on Android, there are currently two popular approaches to choose from:
  • Classic XML-based UI building: This method defines layouts and views in an XML file. With this approach, you can easily position elements and apply styling to create a functional UI.
  • Jetpack Compose: Jetpack Compose is a newer, declarative approach to UI construction. Its declarative approach allows you to create UI components faster and easier.
How does this relate to PDF creation? Before we look at the relationship between these methods and PDF creation, let's take a closer look at how the two approaches work.
Both XML-based user interfaces and Jetpack Compose render elements on a blank canvas in Android. However, the way they work is quite different.

XML-based UI construction

The views defined in an XML file (e.g. Button, TextView, etc.) are objects derived from the Android View class. When the system reads the XML file and attempts to build the UI based on it, the following steps are performed for each view:
  • Layout Inflating: The system creates the appropriate View objects.
  • Rendering: Based on the rendering data (size, position, style) of the created views, the Android SDK draws each view on a canvas. This process is implicit.
The UI is constructed using different view classes as defined in the XML. However, the drawing on the canvas is hidden from the developer's view.

Jetpack Compose

Jetpack Compose works on a completely different principle. Instead of using the traditional view hierarchy, Compose UI elements follow a declarative UI model. While Compose also uses a canvas for rendering, the process and structure are very different.
  • Declarative UI: In Jetpack Compose, the UI is defined declaratively. This means that the state of the components directly determines what is displayed. There is no explicit view hierarchy or XML file, instead the UI is described using composable functions.
  • Background rendering: Behind the scenes, Compose also draws elements onto a canvas, but the process is different from the view system. Compose uses its rendering mechanism and efficiently updates the UI based on state changes.
Jetpack Compose interacts directly with the canvas, but the elements are unique components. These elements are rendered on the canvas using a different model and rendering mechanism to XML-based views.
Both methods share a key feature: the Canvas class. In both cases, the user interface is ultimately drawn on a canvas.
Can we access the canvas? Can we create arbitrary UI elements using Canvas? The answer is yes! And this is exactly the technique we use to build our PDF file.

What exactly is the canvas?

The canvas is a basic drawing surface used in Android graphics that allows you to render different graphical elements such as lines, circles, text, images and other shapes. It acts as a "canvas" on which you can "draw" different graphical components. The canvas is one of the key elements of the 2D graphics API in Android.
A canvas object is essentially a blank drawing surface that you can use to draw various graphics and text using a paint object. The canvas can be the entire screen or the specific area you want to draw on. After each drawing operation, the result is displayed on the screen.

Drawing on the canvas

1. Create a canvas object
Normally you do not create a canvas directly. Instead, the canvas is associated with a view or a bitmap object. When drawing in a view, the canvas is automatically available within the onDraw() method.
2. Using a paint object
To draw, you need a paint object, which defines the appearance properties of the drawn elements (e.g. colour, thickness, style, etc.).
3. Drawing methods
The canvas offers several drawing methods. These methods allow you to draw various shapes, images and text:
  • drawLine(): Draws a line.
  • drawRect(): Draws a rectangle.
  • drawCircle(): Draws a circle.
  • drawText(): Draws text.
  • drawBitmap(): Displays an image.
  • drawPath(): Draws custom shapes using a path object.
Simple drawing example
Here's an example of drawing on the canvas by creating a custom View class and overriding the onDraw() method:
class MyCanvasView(context: Context) : View(context) {
    private val paint = Paint()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        paint.color = Color.RED
        paint.strokeWidth = 10f

        canvas.drawLine(100f, 100f, 400f, 400f, paint)

        paint.color = Color.BLUE
        canvas.drawCircle(200f, 200f, 100f, paint)

        paint.color = Color.BLACK
        paint.textSize = 50f
        canvas.drawText("Hello Canvas", 150f, 600f, paint)
    }
}
By using the Paint object and the drawing methods of the canvas, you can easily draw lines, circles, text or even complex shapes.

Steps to create a PDF

What is a PDF?

A PDF (Portable Document Format) is a widely used file format for storing and sharing documents, images, text, graphics and other content across devices and platforms. Developed by Adobe, the PDF format is popular because it ensures that content is displayed consistently across devices, maintaining its layout and appearance.
How do you create a PDF on Android?
On Android, you can create PDFs using the PdfDocument class. This class allows you to add pages, draw content on them, and save the entire document to a file.
Let's look at a simple example of creating and saving an empty PDF object:
<
fun cratePdf(): File? {
    val pdfDocument = PdfDocument()

    val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create() // A4 size

    val page = pdfDocument.startPage(pageInfo)

    val canvas = page.canvas
    // Draw 

    pdfDocument.finishPage(page)

    pdfDocument.close()

    return saveInfoFile("sample", pdfDocument)
}

fun saveInfoFile(titleText: String, pdfDocument: PdfDocument): File? {
    val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
    val fileName = "${titleText}_document.pdf"
    val pdfFile = File(storageDir, fileName)

    try {
        val outputStream = FileOutputStream(pdfFile)
        pdfDocument.writeTo(outputStream)
        pdfDocument.close()
        outputStream.close()
    } catch (e: IOException) {
        return null
    }

    return pdfFile
}
This snippet demonstrates how to use the PdfDocument class to create a simple PDF object, add a blank page and save it to a file using the saveIntoFile() method. It also shows how to access the empty canvas on which elements can be drawn to appear in the document.

A practical example of creating a label-like layout

Let's look at a simple example of drawing some common elements, such as a bitmap or some text. Note that you cannot position elements here as easily as you can in XML. If you want to centre an element, you have to give exact coordinates - it is not enough to simply set something like gravity="centre".
fun createPdf(
    qrCode: String,
    titleText: String,
    data: ArrayList<Pair<String, String>>,
): File? {
    val resources = context.resources
    val pdfDocument = PdfDocument()

    val rowHeight = resources.getDimensionPixelSize(R.dimen.pdf_row_height)
    val normalGapSize = resources.getDimensionPixelSize(R.dimen.pdf_gap_size)
    val qrCodeSize = resources.getDimensionPixelSize(R.dimen.pdf_qrcode_size)

    val titleTextSize = resources.getDimensionPixelSize(R.dimen.text_size_normal)
    val contentTextSize = resources.getDimensionPixelSize(R.dimen.text_size_little)

    val pdfWidth = resources.getDimensionPixelSize(R.dimen.pdf_width)
    val pdfHeight =
        (qrCodeSize + normalGapSize + (normalGapSize + (rowHeight * data.size)) + normalGapSize)

    val pageInfo = PdfDocument.PageInfo.Builder(pdfWidth, pdfHeight, 1).create()
    val page = pdfDocument.startPage(pageInfo)
    val canvas = page.canvas

    var currentVerticalPosition = normalGapSize + rowHeight

    val qrCodeBitmap = barcodeEncoder.encodeBitmap(
        qrCode,
        BarcodeFormat.QR_CODE,
        qrCodeSize,
        qrCodeSize,
        mapOf(EncodeHintType.MARGIN to 0),
    )
    canvas.drawBitmap(
        qrCodeBitmap,
        (canvas.width - qrCodeSize),
        normalGapSize,
        null
    )

    val titlePaint = Paint()
    titlePaint.textSize = titleTextSize.toFloat()
    titlePaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
    canvas.drawText(
        titleText,
        normalGapSize.toFloat(),
        0f,
        titlePaint
    )

    currentVerticalPosition += qrCodeSize

    val tableTitleTextPaint = Paint()
    tableTitleTextPaint.textSize = contentTextSize.toFloat()
    tableTitleTextPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)

    val tableValueTextPaint = Paint()
    tableValueTextPaint.textSize = contentTextSize.toFloat()

    for (row in data) {
        canvas.drawText(
            row.first,
            normalGapSize.toFloat(),
            currentVerticalPosition.toFloat(),
            tableTitleTextPaint
        )

        val textWidth = tableValueTextPaint.measureText(row.second)
        val secondTextStart = canvas.width - textWidth - normalGapSize
        canvas.drawText(
            row.second,
            secondTextStart,
            currentVerticalPosition.toFloat(),
            tableValueTextPaint
        )
        if (data.indexOf(row) != data.lastIndex) {
            currentVerticalPosition += rowHeight
        }
    }

    pdfDocument.finishPage(page)

    return saveInfoFile(titleText, pdfDocument)
}
The code snippet below creates a PDF document with a QR code on the right, a title to the left of the QR code, and key-value pairs below the QR code based on the data list passed to the function.
Let's look at the most important parts in detail.
The first part of the method reads some resources and calculates the dimensions of the PDF.
val rowHeight = resources.getDimensionPixelSize(R.dimen.pdf_row_height)
val normalGapSize = resources.getDimensionPixelSize(R.dimen.pdf_gap_size)
val qrCodeSize = resources.getDimensionPixelSize(R.dimen.pdf_qrcode_size)

val titleTextSize = resources.getDimensionPixelSize(R.dimen.text_size_little)
val contentTextSize = resources.getDimensionPixelSize(R.dimen.text_size_tiny)

val pdfWidth = resources.getDimensionPixelSize(R.dimen.pdf_width)
val pdfHeight =
    (qrCodeSize + normalGapSize + (normalGapSize + (rowHeight * data.size)) + normalGapSize)
The following code snippet shows how the QR code is drawn. The QR code is generated using the com.journeyapps:zxing-android-embedded library.
val qrCodeBitmap = barcodeEncoder.encodeBitmap(
    qrCodeContent,
    BarcodeFormat.QR_CODE,
    qrCodeSize,
    qrCodeSize,
    mapOf(EncodeHintType.MARGIN to 0),
)
canvas.drawBitmap(
	qrCodeBitmap,
	(canvas.width - qrCodeSize),
	0f,
	null,
)
As shown in the snippet above, you need to specify the x and y coordinates. To move the QR code to the right along the x axis, the width of the QR code bitmap is subtracted from the canvas width. Conversely, we don't want to move the QR code along the y-axis, so we specify 0f, which results in the QR code being placed in the top right-hand corner.
The next step is to write the title:
val titlePaint = Paint()
titlePaint.textSize = titleTextSize.toFloat()
titlePaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
canvas.drawText(
    titleText,
    normalGapSize.toFloat(),
    0f,
    titlePaint,
)
As you can see, the Paint object is used to format the text. No special positioning is applied here, so the title text will appear in the top left corner.
Next, the key-value pairs in the data list are drawn row by row below the QR code.
    currentVerticalPosition += qrCodeSize

    val tableTitleTextPaint = Paint()
    tableTitleTextPaint.textSize = contentTextSize.toFloat()
    tableTitleTextPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)

    val tableValueTextPaint = Paint()
    tableValueTextPaint.textSize = contentTextSize.toFloat()

    for (row in data) {
        canvas.drawText(
            row.first,
            normalGapSize.toFloat(),
            currentVerticalPosition.toFloat(),
            tableTitleTextPaint
        )

        val textWidth = tableValueTextPaint.measureText(row.second)
        val secondTextStart = canvas.width - textWidth - normalGapSize
        canvas.drawText(
            row.second,
            secondTextStart,
            currentVerticalPosition.toFloat(),
            tableValueTextPaint
        )
        if (data.indexOf(row) != data.lastIndex) {
            currentVerticalPosition += rowHeight
        }
    }
Here we start by increasing the current vertical position. This variable keeps track of where we are on the y-axis. Next, we create the paint objects to specify the formatting of the text. Then, by traversing the data list, we plot the key-value pairs as follows: with the key aligned to the left and the value aligned to the right. The right alignment is done similarly to the QR code: we can specify the width of the text by subtracting the width of the canvas from the width of the text.
If you want to centre a piece of text horizontally, you can do this by subtracting the text width from the canvas width and dividing the result by two. This will give you the x coordinate needed to centre it.
Learn how Zebra devices simplify Android app development for specific hardware, including label printing, QR code scanning, Kiosk Mode and offline functionality.
At LogiNet, we assist you with both native and cross-platform mobile application development. Our professional experts are proficient in various technologies: creating native iOS and Android applications using Kotlin and Swift, and cross-platform solutions using Flutter. Our mobile development team also participates in resource outsourcing projects. Learn more about our mobile solutions!

Let's talk about

your project

Drop us a message about your digital product development project and we will get back to you within 2 days.
We'd love to hear the details about your ideas and goals, so that our experts can guide you from the first meeting.
John Radford
Client Services Director UK