Benutzerdefinierte Dokumente drucken

Bei einigen Anwendungen, z. B. Zeichen-Apps, Seitenlayout-Apps und anderen Apps, die sich hauptsächlich auf die grafische Ausgabe konzentrieren, ist das Erstellen ansprechender gedruckter Seiten eine wichtige Funktion. In diesem Fall reicht es nicht aus, ein Bild oder ein HTML-Dokument zu drucken. Bei der Druckausgabe für diese Arten von Anwendungen muss alles, was auf einer Seite angezeigt wird, genau gesteuert werden, einschließlich Schriftarten, Textfluss, Seitenumbrüchen, Kopfzeilen, Fußzeilen und Grafikelementen.

Das Erstellen einer vollständig an Ihre Anwendung angepassten Druckausgabe erfordert mehr Programmierinvestitionen als die zuvor erläuterten Ansätze. Sie müssen Komponenten erstellen, die mit dem Druck-Framework kommunizieren, die Druckereinstellungen anpassen, Seitenelemente zeichnen und den Druck auf mehreren Seiten verwalten.

In dieser Lektion erfahren Sie, wie Sie eine Verbindung zum Druckmanager herstellen, einen Druckadapter erstellen und Inhalte für den Druck erstellen.

Wenn Ihre Anwendung den Druckprozess direkt verwaltet, besteht der erste Schritt nach dem Empfang einer Druckanfrage des Nutzers darin, eine Verbindung zum Android-Druck-Framework herzustellen und eine Instanz der PrintManager-Klasse abzurufen. Mit dieser Klasse können Sie einen Druckauftrag initialisieren und den Drucklebenszyklus starten. Das folgende Codebeispiel zeigt, wie Sie den Druckmanager abrufen und den Druckvorgang starten.

Kotlin

private fun doPrint() {
    activity?.also { context ->
        // Get a PrintManager instance
        val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
        // Set job name, which will be displayed in the print queue
        val jobName = "${context.getString(R.string.app_name)} Document"
        // Start a print job, passing in a PrintDocumentAdapter implementation
        // to handle the generation of a print document
        printManager.print(jobName, MyPrintDocumentAdapter(context), null)
    }
}

Java

private void doPrint() {
    // Get a PrintManager instance
    PrintManager printManager = (PrintManager) getActivity()
            .getSystemService(Context.PRINT_SERVICE);

    // Set job name, which will be displayed in the print queue
    String jobName = getActivity().getString(R.string.app_name) + " Document";

    // Start a print job, passing in a PrintDocumentAdapter implementation
    // to handle the generation of a print document
    printManager.print(jobName, new MyPrintDocumentAdapter(getActivity()),
            null); //
}

Der Beispielcode oben zeigt, wie Sie einen Druckauftrag benennen und eine Instanz der PrintDocumentAdapter-Klasse festlegen, die die Schritte des Drucklebenszyklus verarbeitet. Die Implementierung der Druckerklasse wird im nächsten Abschnitt beschrieben.

Hinweis:Der letzte Parameter in der Methode print() verwendet ein PrintAttributes-Objekt. Mit diesem Parameter können Sie dem Druck-Framework und vordefinierte Optionen basierend auf dem vorherigen Druckzyklus Hinweise geben und so die Nutzerfreundlichkeit verbessern. Du kannst diesen Parameter auch verwenden, um Optionen festzulegen, die sich besser für den gerade gedruckten Inhalt eignen. So lässt sich z. B. die Ausrichtung auf Querformat festlegen, wenn ein Foto mit dieser Ausrichtung gedruckt wird.

Ein Druckeradapter interagiert mit dem Android-Druck-Framework und führt die Schritte des Druckvorgangs aus. Bei diesem Vorgang müssen Nutzer Drucker und Druckoptionen auswählen, bevor sie ein Dokument zum Drucken erstellen. Diese Auswahl kann die endgültige Ausgabe beeinflussen, wenn der Nutzer Drucker mit unterschiedlichen Ausgabefunktionen, Seitengrößen oder Seitenausrichtungen auswählt. Während Sie diese Auswahl treffen, fordert das Druck-Framework Ihren Adapter auf, das Layout zu erstellen und ein Druckdokument zur Vorbereitung der endgültigen Ausgabe zu generieren. Sobald ein Nutzer auf die Druckschaltfläche tippt, übergibt das Framework das endgültige Druckdokument zur Ausgabe an einen Druckanbieter. Während des Druckvorgangs können Nutzer die Druckaktion abbrechen. Daher muss auch der Druckadapter auf Abbruchanfragen warten und darauf reagieren.

Die abstrakte Klasse PrintDocumentAdapter wurde für den Drucklebenszyklus entwickelt. Es gibt vier Haupt-Callback-Methoden. Sie müssen diese Methoden in Ihrem Druckeradapter implementieren, damit eine ordnungsgemäße Interaktion mit dem Druck-Framework möglich ist:

  • onStart(): Wird zu Beginn des Druckvorgangs einmal aufgerufen. Wenn für Ihre Anwendung einmalige Vorbereitungsaufgaben erforderlich sind, z. B. das Abrufen eines Snapshots der zu druckenden Daten, führen Sie diese hier aus. Die Implementierung dieser Methode in Ihrem Adapter ist nicht erforderlich.
  • onLayout() – Wird jedes Mal aufgerufen, wenn ein Nutzer eine Druckeinstellung ändert, die sich auf die Ausgabe auswirkt, z. B. bei einer anderen Seitengröße oder Seitenausrichtung. So kann deine Anwendung das Layout der zu druckenden Seiten berechnen. Diese Methode muss mindestens zurückgeben, wie viele Seiten im gedruckten Dokument erwartet werden.
  • onWrite() – Zum Rendern gedruckter Seiten in eine zu druckende Datei wird aufgerufen. Diese Methode kann nach jedem onLayout()-Aufruf einmal oder mehrmals aufgerufen werden.
  • onFinish(): Wird einmal am Ende des Druckvorgangs aufgerufen. Wenn für Ihre Anwendung einmalige Teardown-Aufgaben auszuführen sind, führen Sie diese hier aus. Die Implementierung dieser Methode in Ihrem Adapter ist nicht erforderlich.

In den folgenden Abschnitten wird beschrieben, wie die Layout- und Schreibmethoden implementiert werden, die für das Funktionieren eines Druckadapters entscheidend sind.

Hinweis:Diese Adaptermethoden werden im Hauptthread Ihrer Anwendung aufgerufen. Wenn Sie davon ausgehen, dass die Ausführung dieser Methoden in Ihrer Implementierung viel Zeit in Anspruch nimmt, implementieren Sie sie so, dass sie in einem separaten Thread ausgeführt werden. Du kannst beispielsweise das Layout oder die Schreibarbeit für Druckdokumente in separate AsyncTask-Objekte kapseln.

Druckdokumentinformationen berechnen

Innerhalb einer Implementierung der Klasse PrintDocumentAdapter muss deine Anwendung den Dokumenttyp angeben und die Gesamtzahl der Seiten für den Druckauftrag anhand der Informationen zur gedruckten Seitengröße berechnen können. Durch die Implementierung der Methode onLayout() im Adapter werden diese Berechnungen durchgeführt und Informationen zur erwarteten Ausgabe des Druckauftrags in einer PrintDocumentInfo-Klasse bereitgestellt, einschließlich der Anzahl der Seiten und des Inhaltstyps. Das folgende Codebeispiel zeigt eine grundlegende Implementierung der Methode onLayout() für eine PrintDocumentAdapter:

Kotlin

override fun onLayout(
        oldAttributes: PrintAttributes?,
        newAttributes: PrintAttributes,
        cancellationSignal: CancellationSignal?,
        callback: LayoutResultCallback,
        extras: Bundle?
) {
    // Create a new PdfDocument with the requested page attributes
    pdfDocument = PrintedPdfDocument(activity, newAttributes)

    // Respond to cancellation request
    if (cancellationSignal?.isCanceled == true) {
        callback.onLayoutCancelled()
        return
    }

    // Compute the expected number of printed pages
    val pages = computePageCount(newAttributes)

    if (pages > 0) {
        // Return print information to print framework
        PrintDocumentInfo.Builder("print_output.pdf")
                .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                .setPageCount(pages)
                .build()
                .also { info ->
                    // Content layout reflow is complete
                    callback.onLayoutFinished(info, true)
                }
    } else {
        // Otherwise report an error to the print framework
        callback.onLayoutFailed("Page count calculation failed.")
    }
}

Java

@Override
public void onLayout(PrintAttributes oldAttributes,
                     PrintAttributes newAttributes,
                     CancellationSignal cancellationSignal,
                     LayoutResultCallback callback,
                     Bundle metadata) {
    // Create a new PdfDocument with the requested page attributes
    pdfDocument = new PrintedPdfDocument(getActivity(), newAttributes);

    // Respond to cancellation request
    if (cancellationSignal.isCanceled() ) {
        callback.onLayoutCancelled();
        return;
    }

    // Compute the expected number of printed pages
    int pages = computePageCount(newAttributes);

    if (pages > 0) {
        // Return print information to print framework
        PrintDocumentInfo info = new PrintDocumentInfo
                .Builder("print_output.pdf")
                .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                .setPageCount(pages)
                .build();
        // Content layout reflow is complete
        callback.onLayoutFinished(info, true);
    } else {
        // Otherwise report an error to the print framework
        callback.onLayoutFailed("Page count calculation failed.");
    }
}

Die Ausführung der Methode onLayout() kann drei Ergebnisse haben: Abschluss, Abbruch oder Fehler, falls die Berechnung des Layouts nicht abgeschlossen werden kann. Sie müssen eines dieser Ergebnisse angeben, indem Sie die entsprechende Methode des PrintDocumentAdapter.LayoutResultCallback-Objekts aufrufen.

Hinweis:Der boolesche Parameter der onLayoutFinished()-Methode gibt an, ob sich der Layoutinhalt seit der letzten Anfrage geändert hat. Wenn dieser Parameter richtig konfiguriert ist, kann das Druck-Framework das unnötige Aufrufen der onWrite()-Methode vermeiden, wodurch das zuvor geschriebene Druckdokument im Cache gespeichert und die Leistung verbessert wird.

Die Hauptfunktion von onLayout() besteht darin, die Anzahl der Seiten zu berechnen, die basierend auf den Attributen des Druckers als Ausgabe erwartet werden. Wie du diese Zahl berechnest, hängt stark davon ab, wie das Layout der zu druckenden Seiten in deiner Anwendung erfolgt. Das folgende Codebeispiel zeigt eine Implementierung, bei der die Anzahl der Seiten durch die Druckausrichtung bestimmt wird:

Kotlin

private fun computePageCount(printAttributes: PrintAttributes): Int {
    var itemsPerPage = 4 // default item count for portrait mode

    val pageSize = printAttributes.mediaSize
    if (!pageSize.isPortrait) {
        // Six items per page in landscape orientation
        itemsPerPage = 6
    }

    // Determine number of print items
    val printItemCount: Int = getPrintItemCount()

    return Math.ceil((printItemCount / itemsPerPage.toDouble())).toInt()
}

Java

private int computePageCount(PrintAttributes printAttributes) {
    int itemsPerPage = 4; // default item count for portrait mode

    MediaSize pageSize = printAttributes.getMediaSize();
    if (!pageSize.isPortrait()) {
        // Six items per page in landscape orientation
        itemsPerPage = 6;
    }

    // Determine number of print items
    int printItemCount = getPrintItemCount();

    return (int) Math.ceil(printItemCount / itemsPerPage);
}

Eine Druckdokumentdatei schreiben

Wenn es an der Zeit ist, die Druckausgabe in eine Datei zu schreiben, ruft das Android-Druck-Framework die Methode onWrite() der Klasse PrintDocumentAdapter Ihrer Anwendung auf. Die Parameter der Methode geben an, welche Seiten geschrieben und welche Ausgabedatei verwendet werden soll. Ihre Implementierung dieser Methode muss dann jede angeforderte Seite mit Inhalten in eine mehrseitige PDF-Dokumentdatei rendern. Nach Abschluss dieses Vorgangs rufen Sie die Methode onWriteFinished() des Callback-Objekts auf.

Hinweis:Das Android Print-Framework kann die Methode onWrite() für jeden Aufruf von onLayout() einmal oder mehrmals aufrufen. Aus diesem Grund ist es wichtig, den booleschen Parameter der Methode onLayoutFinished() auf false zu setzen, wenn sich das Layout der Druckinhalte nicht geändert hat, um unnötiges Neuschreiben des Druckdokuments zu vermeiden.

Hinweis:Der boolesche Parameter der onLayoutFinished()-Methode gibt an, ob sich der Layoutinhalt seit der letzten Anfrage geändert hat. Wenn dieser Parameter richtig konfiguriert ist, kann das Druck-Framework das unnötige Aufrufen der onLayout()-Methode vermeiden, wodurch das zuvor geschriebene Druckdokument im Cache gespeichert und die Leistung verbessert wird.

Im folgenden Beispiel werden die grundlegenden Mechanismen dieses Prozesses unter Verwendung der PrintedPdfDocument-Klasse zum Erstellen einer PDF-Datei veranschaulicht:

Kotlin

override fun onWrite(
        pageRanges: Array<out PageRange>,
        destination: ParcelFileDescriptor,
        cancellationSignal: CancellationSignal?,
        callback: WriteResultCallback
) {
    // Iterate over each page of the document,
    // check if it's in the output range.
    for (i in 0 until totalPages) {
        // Check to see if this page is in the output range.
        if (containsPage(pageRanges, i)) {
            // If so, add it to writtenPagesArray. writtenPagesArray.size()
            // is used to compute the next output page index.
            writtenPagesArray.append(writtenPagesArray.size(), i)
            pdfDocument?.startPage(i)?.also { page ->

                // check for cancellation
                if (cancellationSignal?.isCanceled == true) {
                    callback.onWriteCancelled()
                    pdfDocument?.close()
                    pdfDocument = null
                    return
                }

                // Draw page content for printing
                drawPage(page)

                // Rendering is complete, so page can be finalized.
                pdfDocument?.finishPage(page)
            }
        }
    }

    // Write PDF document to file
    try {
        pdfDocument?.writeTo(FileOutputStream(destination.fileDescriptor))
    } catch (e: IOException) {
        callback.onWriteFailed(e.toString())
        return
    } finally {
        pdfDocument?.close()
        pdfDocument = null
    }
    val writtenPages = computeWrittenPages()
    // Signal the print framework the document is complete
    callback.onWriteFinished(writtenPages)

    ...
}

Java

@Override
public void onWrite(final PageRange[] pageRanges,
                    final ParcelFileDescriptor destination,
                    final CancellationSignal cancellationSignal,
                    final WriteResultCallback callback) {
    // Iterate over each page of the document,
    // check if it's in the output range.
    for (int i = 0; i < totalPages; i++) {
        // Check to see if this page is in the output range.
        if (containsPage(pageRanges, i)) {
            // If so, add it to writtenPagesArray. writtenPagesArray.size()
            // is used to compute the next output page index.
            writtenPagesArray.append(writtenPagesArray.size(), i);
            PdfDocument.Page page = pdfDocument.startPage(i);

            // check for cancellation
            if (cancellationSignal.isCanceled()) {
                callback.onWriteCancelled();
                pdfDocument.close();
                pdfDocument = null;
                return;
            }

            // Draw page content for printing
            drawPage(page);

            // Rendering is complete, so page can be finalized.
            pdfDocument.finishPage(page);
        }
    }

    // Write PDF document to file
    try {
        pdfDocument.writeTo(new FileOutputStream(
                destination.getFileDescriptor()));
    } catch (IOException e) {
        callback.onWriteFailed(e.toString());
        return;
    } finally {
        pdfDocument.close();
        pdfDocument = null;
    }
    PageRange[] writtenPages = computeWrittenPages();
    // Signal the print framework the document is complete
    callback.onWriteFinished(writtenPages);

    ...
}

In diesem Beispiel wird das Rendern von PDF-Seiteninhalten an die Methode drawPage() delegiert, die im nächsten Abschnitt erläutert wird.

Wie beim Layout kann die Ausführung der Methode onWrite() drei Ergebnisse haben: Abschluss, Abbruch oder Fehler, falls der Inhalt nicht geschrieben werden kann. Sie müssen eines dieser Ergebnisse angeben, indem Sie die entsprechende Methode des PrintDocumentAdapter.WriteResultCallback-Objekts aufrufen.

Hinweis: Das Rendern eines Dokuments für den Druck kann ein ressourcenintensiver Vorgang sein. Damit der Hauptthread der Benutzeroberfläche Ihrer Anwendung nicht blockiert wird, sollten Sie die Seiten-Rendering- und Schreibvorgänge in einem separaten Thread ausführen, z. B. in einem AsyncTask. Weitere Informationen zum Arbeiten mit Ausführungsthreads wie asynchrone Aufgaben finden Sie unter Prozesse und Threads.

Zeichnen von PDF-Seiteninhalt

Wenn deine App gedruckt wird, muss deine App ein PDF-Dokument generieren und zum Drucken an das Android Print-Framework übergeben. Für diesen Zweck können Sie eine beliebige PDF-Generierungsbibliothek verwenden. In dieser Lektion erfahren Sie, wie Sie mit der Klasse PrintedPdfDocument aus Ihren Inhalten PDF-Seiten generieren können.

Die Klasse PrintedPdfDocument verwendet ein Canvas-Objekt, um Elemente auf einer PDF-Seite zu zeichnen, ähnlich wie beim Zeichnen in einem Aktivitätslayout. Mit den Zeichenmethoden Canvas können Sie Elemente auf der gedruckten Seite zeichnen. Der folgende Beispielcode zeigt, wie Sie mit diesen Methoden einige einfache Elemente auf einer PDF-Dokumentseite zeichnen:

Kotlin

private fun drawPage(page: PdfDocument.Page) {
    page.canvas.apply {

        // units are in points (1/72 of an inch)
        val titleBaseLine = 72f
        val leftMargin = 54f

        val paint = Paint()
        paint.color = Color.BLACK
        paint.textSize = 36f
        drawText("Test Title", leftMargin, titleBaseLine, paint)

        paint.textSize = 11f
        drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint)

        paint.color = Color.BLUE
        drawRect(100f, 100f, 172f, 172f, paint)
    }
}

Java

private void drawPage(PdfDocument.Page page) {
    Canvas canvas = page.getCanvas();

    // units are in points (1/72 of an inch)
    int titleBaseLine = 72;
    int leftMargin = 54;

    Paint paint = new Paint();
    paint.setColor(Color.BLACK);
    paint.setTextSize(36);
    canvas.drawText("Test Title", leftMargin, titleBaseLine, paint);

    paint.setTextSize(11);
    canvas.drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint);

    paint.setColor(Color.BLUE);
    canvas.drawRect(100, 100, 172, 172, paint);
}

Wenn Sie mit Canvas auf einer PDF-Seite zeichnen, werden die Elemente in Punkten angegeben. Das entspricht 1/72 Zoll. Achte darauf, diese Maßeinheit zu verwenden, um die Größe der Elemente auf der Seite anzugeben. Zur Positionierung gezeichneter Elemente beginnt das Koordinatensystem für die obere linke Ecke der Seite bei 0,0.

Tipp:Mit dem Canvas-Objekt können Sie Druckelemente am Rand eines PDF-Dokuments platzieren, bei vielen Druckern ist das jedoch nicht möglich. Achte darauf, dass du die nicht druckbaren Ränder der Seite berücksichtigst, wenn du mit dieser Klasse ein Druckdokument erstellst.