An Android Studio Custom Printing Tutorial

As we have seen in the preceding chapters, the Android Printing framework makes it relatively easy to build printing support into applications as long as the content is in the form of an image or HTML markup. More advanced printing requirements can be met by using the custom document printing feature of the Printing framework.

An Overview of Android Custom Document Printing

In simplistic terms, custom document printing uses canvases to represent the pages of the document to be printed. The application draws the content to be printed onto these canvases as shapes, colors, text, and images. The canvases are represented by instances of the Android Canvas class, providing access to a rich selection of drawing options. Once all the pages have been drawn, the document is then printed.

While this sounds simple enough, some steps need to be performed to make this happen, which can be summarized as follows:

  • Implement a custom print adapter sub-classed from the PrintDocumentAdapter class.
  • Obtain a reference to the Print Manager Service.
  • Create an instance of the PdfDocument class to store the document pages.
  • Add pages to the PdfDocument in the form of PdfDocument.Page instances.
  • Obtain references to the Canvas objects associated with the document pages.
  • Draw content onto the canvases.
  • Write the PDF document to a destination output stream provided by the Printing framework.
  • Notify the Printing framework that the document is ready to print.

This chapter will provide an overview of these steps, followed by a detailed tutorial designed to demonstrate the implementation of custom document printing within Android applications.

Custom Print Adapters

The role of the print adapter is to provide the Printing framework with the content to be printed and to ensure that it is formatted correctly for the user’s chosen preferences (considering factors such as paper size and page orientation).

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Much of this work is performed by the print adapters provided as part of the Android Printing framework and designed for these specific printing tasks when printing HTML and images. When printing a web page, for example, a print adapter is created for us when a call is made to the createPrintDocumentAdapter() method of an instance of the WebView class.

In the case of custom document printing, however, it is the responsibility of the application developer to design the print adapter and implement the code to draw and format the content in preparation for printing.

Custom print adapters are created by sub-classing the PrintDocumentAdapter class and overriding a set of callback methods within that class which will be called by the Printing framework at various stages in the print process. These callback methods can be summarized as follows:

· onStart() – This method is called when the printing process begins and is provided so that the application code can perform any necessary tasks to create the print job. Implementation of this method within the PrintDocumentAdapter sub-class is optional.

· onLayout() – This callback method is called after the call to the onStart() method and then again each time the user makes changes to the print settings (such as changing the orientation, paper size, or color settings). This method should adapt the content and layout to accommodate these changes. Once these changes are completed, the method must return the number of pages to be printed. Implementation of the onLayout() method within the PrintDocumentAdapter sub-class is mandatory.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

· onWrite() – This method is called after each call to onLayout() and is responsible for rendering the content on the canvases of the pages to be printed. Amongst other arguments, this method is passed a file descriptor to which the resulting PDF document must be written once rendering is complete. A call is then made to the onWriteFinished() callback method passing through an argument containing information about the page ranges to be printed. Implementation of the onWrite() method within the PrintDocumentAdapter sub-class is mandatory.

· onFinish() – An optional method which, if implemented, is called once by the Printing framework when the printing process is completed, thereby providing the application the opportunity to perform any clean-up operations that may be necessary.

Preparing the Custom Document Printing Project

Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Views Activity template before clicking on the Next button.

Enter CustomPrint into the Name field and specify com.ebookfrenzy.customprint as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin.

Load the activity_main.xml layout file into the Layout Editor tool and, in Design mode, select and delete the “Hello World!” TextView object. Drag and drop a Button view from the Common section of the palette and position it in the center of the layout view. With the Button view selected, change the text property to “Print Document” and extract the string to a new resource. On completion, the user interface layout should match that shown in Figure 82-1:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 82-1

When the button is selected within the application, it will be required to call a method to initiate the document printing process. Remaining within the Attributes tool window, set the onClick property to call a method named printDocument.

Creating the Custom Print Adapter

Most of the work involved in printing a custom document from within an Android application involves the implementation of the custom print adapter. This example will require a print adapter with the onLayout() and onWrite() callback methods implemented. Within the MainActivity.kt file, add the template for this new class so that it reads as follows:

package com.ebookfrenzy.customprint
 
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.print.PageRange
import android.print.PrintAttributes
import android.print.PrintDocumentAdapter
import android.os.CancellationSignal
import android.content.Context
 
class MainActivity : AppCompatActivity() {
 
    inner class MyPrintDocumentAdapter(private var context: Context) 
                                             : PrintDocumentAdapter() {
 
        override fun onLayout(oldAttributes: PrintAttributes,
                              newAttributes: PrintAttributes,
                              cancellationSignal: CancellationSignal?,
                              callback: LayoutResultCallback?,
                              metadata: Bundle?) {
        }
 
        override fun onWrite(pageRanges: Array<out PageRange>?,
                             destination: ParcelFileDescriptor?,
                             cancellationSignal: CancellationSignal?,
                             callback: WriteResultCallback?) {
 
        }
    }
.
.
}Code language: Kotlin (kotlin)

As the new class currently stands, it contains a constructor method to be called when a new class instance is created. The constructor takes the context of the calling activity as an argument, which is then stored so that it can be referenced later in the two callback methods.

With the class outline established, the next step is implementing the two callback methods, beginning with onLayout().

Implementing the onLayout() Callback Method

Remaining within the MainActivity.kt file, begin by adding some import directives that will be required by the code in the onLayout() method:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

package com.ebookfrenzy.customprint
.
.
import android.print.PrintDocumentInfo
import android.print.pdf.PrintedPdfDocument
import android.graphics.pdf.PdfDocument
 
class MainActivity : AppCompatActivity() {
.
.
}Code language: Kotlin (kotlin)

Next, modify the MyPrintDocumentAdapter class to declare variables to be used within the onLayout() method:

inner class MyPrintDocumentAdapter(private var context: Context) : 
                                    PrintDocumentAdapter() {
    private var pageHeight: Int = 0
    private var pageWidth: Int = 0
    private var myPdfDocument: PdfDocument? = null
    private var totalpages = 4
.
.
}Code language: Kotlin (kotlin)

Note that for this example, a four-page document will be printed. In more complex situations, the application will most likely need to dynamically calculate the number of pages to be printed based on the quantity and layout of the content in relation to the user’s paper size and page orientation selections.

With the variables declared, implement the onLayout() method as outlined in the following code listing:

override fun onLayout(oldAttributes: PrintAttributes?,
                      newAttributes: PrintAttributes?,
                      cancellationSignal: CancellationSignal?,
                      callback: LayoutResultCallback?,
                      metadata: Bundle?) {
 
    myPdfDocument = PrintedPdfDocument(context, newAttributes)
 
    val height = newAttributes.mediaSize?.heightMils
    val width = newAttributes.mediaSize?.heightMils
 
    height?.let {
        pageHeight = it / 1000 * 72
    }
 
    width?.let {
        pageWidth = it / 1000 * 72
    }
 
    cancellationSignal?.let {
        if (it.isCanceled) {
            callback?.onLayoutCancelled()
            return
        }
    }
 
    if (totalpages > 0) {
        val builder =
                PrintDocumentInfo.Builder("print_output.pdf").setContentType(
                        PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                        .setPageCount(totalpages)
 
        val info = builder.build()
        callback?.onLayoutFinished(info, true)
    } else {
        callback?.onLayoutFailed("Page count is zero.")
    }
}Code language: Kotlin (kotlin)

This method performs quite a few tasks, each requiring some detailed explanation.

To begin with, a new PDF document is created as a PdfDocument class instance. One of the arguments passed into the onLayout() method when the Printing framework calls it is an object of type PrintAttributes containing details about the paper size, resolution, and color settings the user selects for the print output. These settings are used when creating the PDF document, along with the context of the activity previously stored for us by our constructor method:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

myPdfDocument = PrintedPdfDocument(context, newAttributes)Code language: Kotlin (kotlin)

The method then uses the PrintAttributes object to extract the height and width values for the document pages. These dimensions are stored in the object as thousandths of an inch. Since the methods that will use these values later in this example work in units of 1/72 of an inch, these numbers are converted before they are stored:

val height = newAttributes?.mediaSize?.heightMils
val width = newAttributes?.mediaSize?.heightMils
 
height?.let {
    pageHeight = it / 1000 * 72
}
 
width?.let {
    pageWidth = it / 1000 * 72
}Code language: Kotlin (kotlin)

Although this example does not make use of the user’s color selection, this property can be obtained via a call to the getColorMode() method of the PrintAttributes object, which will return a value of either COLOR_MODE_COLOR or COLOR_MODE_MONOCHROME.

When the onLayout() method is called, it is passed an object of type LayoutResultCallback. This object provides a way for the method to communicate status information back to the Printing framework via a set of methods. For example, the onLayout() method will be called if the user cancels the print process. The fact that the process has been canceled is indicated via a setting within the CancellationSignal argument. If a cancellation is detected, the onLayout() method must call the onLayoutCancelled() method of the LayoutResultCallback object to notify the Print framework that the cancellation request was received and that the layout task has been canceled:

cancellationSignal?.let {
    if (it.isCanceled) {
        callback?.onLayoutCancelled()
        return
    }
}Code language: Kotlin (kotlin)

When the layout work is complete, the method is required to call the onLayoutFinished() method of the LayoutResultCallback object, passing through two arguments. The first argument is a PrintDocumentInfo object containing information about the document to be printed. This information consists of the name of the PDF document, the type of content (in this case, a document rather than an image), and the page count. The second argument is a Boolean value indicating whether or not the layout has changed since the last call made to the onLayout() method:

if (totalpages > 0) {
    val builder = PrintDocumentInfo.Builder("print_output.pdf").setContentType(
            PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
            .setPageCount(totalpages)
 
    val info = builder.build()
    callback?.onLayoutFinished(info, true)
} else {
    callback?.onLayoutFailed("Page count is zero.")
}Code language: Kotlin (kotlin)

If the page count is zero, the code reports this failure to the Printing framework via a call to the onLayoutFailed() method of the LayoutResultCallback object.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

The call to the onLayoutFinished() method notifies the Printing framework that the layout work is complete, triggering a call to the onWrite() method.

Implementing the onWrite() Callback Method

The onWrite() callback method is responsible for rendering the pages of the document and then notifying the Printing framework that the document is ready to be printed. When completed, the onWrite() method reads as follows:

package com.ebookfrenzy.customprint
 
import java.io.FileOutputStream
import java.io.IOException
.
.
override fun onWrite(pageRanges: Array<out PageRange>?,
                     destination: ParcelFileDescriptor,
                     cancellationSignal: android.os.CancellationSignal?,
                     callback: WriteResultCallback?) {
 
    for (i in 0 until totalpages) {
        if (pageInRange(pageRanges, i)) {
            val newPage = PdfDocument.PageInfo.Builder(pageWidth,
                    pageHeight, i).create()
 
            val page = myPdfDocument?.startPage(newPage)
 
            cancellationSignal?.let {
                if (it.isCanceled) {
                    callback?.onWriteCancelled()
                    myPdfDocument?.close()
                    myPdfDocument = null
                    return
                }
            }
            page?.let {
                drawPage(it, i)
            }
            myPdfDocument?.finishPage(page)
        }
    }
 
    try {
        myPdfDocument?.writeTo(FileOutputStream(
                destination?.fileDescriptor))
    } catch (e: IOException) {
        callback?.onWriteFailed(e.toString())
        return
    } finally {
        myPdfDocument?.close()
        myPdfDocument = null
    }
 
    callback?.onWriteFinished(pageRanges)
}Code language: Kotlin (kotlin)

The onWrite() method starts by looping through each page in the document. However, it is important to consider that the user may have requested that only some of the pages that make up the document be printed. The Printing framework user interface panel provides the option to specify specific pages or ranges of pages to be printed. Figure 82-2, for example, shows the print panel configured to print pages 1-4, page 9, and pages 1113 of a document.

Figure 82-2

When writing the pages to the PDF document, the onWrite() method must take steps to ensure that only those pages specified by the user are printed. To make this possible, the Printing framework passes through as an argument an array of PageRange objects indicating the ranges of pages to be printed. In the above onWrite() implementation, the pageInRange() method is called for each page to verify that the page is within the specified ranges. The code for the pageInRange() method will be implemented later in this chapter.

for (i in 0 until totalpages) {
    if (pageInRange(pageRanges, i)) {Code language: Kotlin (kotlin)

For each page that is within any specified ranges, a new PdfDocument.Page object is created. When creating a new page, the height and width values previously stored by the onLayout() method are passed through as arguments so that the page size matches the print options selected by the user:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

val newPage = PageInfo.Builder(pageWidth, pageHeight, i).create()
 
val page = myPdfDocument?.startPage(newPage)Code language: Kotlin (kotlin)

As with the onLayout() method, the onWrite() method is required to respond to cancellation requests. In this case, the code notifies the Printing framework that the cancellation has been performed before closing and dereferencing the myPdfDocument variable:

cancellationSignal?.let {
    if (it.isCanceled) {
        callback?.onWriteCancelled()
        myPdfDocument?.close()
        myPdfDocument = null
        return
    }
}Code language: Kotlin (kotlin)

As long as the print process has not been canceled, the method calls a method to draw the content on the current page before calling the finishedPage() method on the myPdfDocument object.

page?.let {
    drawPage(it, i)
}
myPdfDocument?.finishPage(page)Code language: Kotlin (kotlin)

The drawPage() method is responsible for drawing the content onto the page and will be implemented once the onWrite() method is complete.

When the required number of pages have been added to the PDF document, the document is then written to the destination stream using the file descriptor, which is passed through as an argument to the onWrite() method. If, for any reason, the write operation fails, the method notifies the framework by calling the onWriteFailed() method of the WriteResultCallback object (also passed as an argument to the onWrite() method).

try {
    myPdfDocument?.writeTo(FileOutputStream(
            destination?.fileDescriptor))
} catch (e: IOException) {
    callback?.onWriteFailed(e.toString())
    return
} finally {
    myPdfDocument?.close()
    myPdfDocument = null
}Code language: Kotlin (kotlin)

Finally, the onWriteFinish() method of the WriteResultsCallback object is called to notify the Printing framework that the document is ready to be printed.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Checking a Page is in Range

As previously outlined, when the onWrite() method is called, it is passed an array of PageRange objects indicating the ranges of pages within the document to be printed. The PageRange class is designed to store the start and end pages of a page range which, in turn, may be accessed via the getStart() and getEnd() methods of the class.

When the onWrite() method was implemented in the previous section, a call was made to a method named pageInRange(), which takes as arguments an array of PageRange objects and a page number. The role of the pageInRange() method is to identify whether the specified page number is within the ranges specified and may be implemented within the MyPrintDocumentAdapter class in the MainActivity.kt class as follows:

inner class MyPrintDocumentAdapter(private var context: Context) : 
                                    PrintDocumentAdapter() {
.
.
   private fun pageInRange(pageRanges: Array<out PageRange>?, page: Int): 
        Boolean {
        pageRanges?.let {
            for (i in it.indices) {
                if (page >= it[i].start && page <= it[i].end)
                    return true
            }
        }
        return false
    }
.
.
}Code language: Kotlin (kotlin)

Drawing the Content on the Page Canvas

We have now reached the point where some code needs to be written to draw the content on the pages so that they are ready for printing. The content that gets drawn is completely application specific and limited only by what can be achieved using the Android Canvas class. In this example, however, some simple text and graphics will be drawn on the canvas.

The onWrite() method has been designed to call a method named drawPage() which takes as arguments the PdfDocument.Page object representing the current page, and an integer, representing the page number. Within the MainActivity.kt file, this method should now be implemented as follows:

package com.ebookfrenzy.customprint
.
.
import android.graphics.Color
import android.graphics.Paint
 
class MainActivity : AppCompatActivity() {
.
.
   inner class MyPrintDocumentAdapter(private var context: Context) : 
                                    PrintDocumentAdapter() {
 
        private fun drawPage(page: PdfDocument.Page,
                             pagenumber: Int) {
            var pagenum = pagenumber
            val canvas = page.canvas
 
            pagenum++ // Make sure page numbers start at 1
 
            val titleBaseLine = 72
            val leftMargin = 54
 
            val paint = Paint()
            paint.color = Color.BLACK
            paint.textSize = 40f
            canvas.drawText(
                    "Test Print Document Page " + pagenum,
                    leftMargin.toFloat(),
                    titleBaseLine.toFloat(),
                    paint)
 
            paint.textSize = 14f
            canvas.drawText("This is some test content to verify that custom document printing works", leftMargin.toFloat(), (titleBaseLine + 35).toFloat(), paint)
 
            if (pagenum % 2 == 0)
                paint.color = Color.RED
            else
                paint.color = Color.GREEN
 
            val pageInfo = page.info
 
            canvas.drawCircle((pageInfo.pageWidth / 2).toFloat(),
                    (pageInfo.pageHeight / 2).toFloat(),
                    150f,
                    paint)
        }
.
.
}Code language: Kotlin (kotlin)

Page numbering within the code starts at 0. Since documents traditionally start at page 1, the method begins by incrementing the stored page number. A reference to the Canvas object associated with the page is then obtained, and some margin and baseline values are declared:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

var pagenum = pagenumber
val canvas = page.canvas
 
pagenum++ // Make sure page numbers start at 1
 
val titleBaseLine = 72
val leftMargin = 54Code language: Kotlin (kotlin)

Next, the code creates Paint and Color objects to be used for drawing, sets a text size, and draws the page title text, including the current page number:

val paint = Paint()
paint.color = Color.BLACK
paint.textSize = 40f
canvas.drawText(
        "Test Print Document Page " + pagenum,
        leftMargin.toFloat(),
        titleBaseLine.toFloat(),
        paint)Code language: Kotlin (kotlin)

The text size is then reduced, and some body text is drawn beneath the title:

paint.textSize = 14f
canvas.drawText("This is some test content to verify that custom document printing works", leftMargin.toFloat(), (titleBaseLine + 35).toFloat(), paint)Code language: Kotlin (kotlin)

The last task performed by this method involves drawing a circle (red on even-numbered pages and green on odd). Having ascertained whether the page is odd or even, the method obtains the height and width of the page before using this information to position the circle in the center of the page:

if (pagenum % 2 == 0)
    paint.color = Color.RED
else
    paint.color = Color.GREEN
 
val pageInfo = page.info
 
canvas.drawCircle((pageInfo.pageWidth / 2).toFloat(),
        (pageInfo.pageHeight / 2).toFloat(),
        150f, paint)Code language: Kotlin (kotlin)

Having drawn on the canvas, the method returns control to the onWrite() method. With the completion of the drawPage() method, the MyPrintDocumentAdapter class is now finished.

Starting the Print Job

When the user touches the “Print Document” button, the printDocument() onClick event handler method will be called. All that now remains before testing can commence, therefore, is to add this method to the MainActivity.kt file, taking particular care to ensure that it is placed outside of the MyPrintDocumentAdapter class:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

package com.ebookfrenzy.customprint
.
.
import android.print.PrintManager
import android.view.View
 
class MainActivity : AppCompatActivity() {
 
    fun printDocument(view: View) {
        val printManager = this
                .getSystemService(Context.PRINT_SERVICE) as PrintManager
 
        val jobName = this.getString(R.string.app_name) + " Document"
 
        printManager.print(jobName, MyPrintDocumentAdapter(this), null)
    }
.
.
}Code language: Kotlin (kotlin)

This method obtains a reference to the Print Manager service running on the device before creating a new String object to serve as the job name for the print task. Finally, the print() method of the Print Manager is called to start the print job, passing through the job name and an instance of our custom print document adapter class.

Testing the Application

Compile and run the application on an Android device or emulator. When the application has loaded, touch the “Print Document” button to initiate the print job and select a suitable target for the output (the Save to PDF option is useful for avoiding wasting paper and printer ink).

Check the printed output, which should consist of 4 pages, including text and graphics. Figure 82-3, for example, shows the four pages of the document viewed as a PDF file ready to be saved on the device.

Experiment with other print configuration options, such as changing the paper size, orientation, and page settings within the print panel. The printed output should reflect each setting change, indicating that the custom print document adapter functions correctly.

Figure 82-3

Summary

Although more complex to implement than the Android Printing framework HTML and image printing options, custom document printing provides considerable flexibility in printing complex content within an Android application. Most of the work in implementing custom document printing involves the creation of a custom Print Adapter class that not only draws the content on the document pages but also responds correctly as the user changes print settings, such as the page size and range of pages to be printed.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print