Imprimer des documents personnalisés

Pour certaines applications, telles que les applications de dessin, de mise en page et d'autres applications axées sur la sortie graphique, la création de superbes pages imprimées est une fonctionnalité essentielle. Dans ce cas, il ne suffit pas d'imprimer une image ou un document HTML. Pour ces types d'applications, la sortie d'impression nécessite un contrôle précis de tout ce qui compose une page, y compris les polices, le flux de texte, les sauts de page, les en-têtes, les pieds de page et les éléments graphiques.

La création d'une sortie d'impression entièrement personnalisée pour votre application nécessite plus d'investissement en programmation que les approches décrites précédemment. Vous devez créer des composants qui communiquent avec le framework d'impression, ajuster les paramètres de l'imprimante, dessiner des éléments de page et gérer l'impression sur plusieurs pages.

Cette leçon vous explique comment vous connecter au gestionnaire d'impression, créer un adaptateur d'impression et créer du contenu pour l'impression.

Lorsque votre application gère directement le processus d'impression, la première étape après avoir reçu une requête d'impression de la part de l'utilisateur consiste à se connecter au framework d'impression Android et à obtenir une instance de la classe PrintManager. Cette classe vous permet d'initialiser une tâche d'impression et de commencer le cycle de vie d'une impression. L'exemple de code suivant montre comment obtenir le gestionnaire d'impression et lancer le processus d'impression.

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

L'exemple de code ci-dessus montre comment nommer une tâche d'impression et définir une instance de la classe PrintDocumentAdapter qui gère les étapes du cycle d'impression. L'implémentation de la classe d'adaptateur d'impression est abordée dans la section suivante.

Remarque:Le dernier paramètre de la méthode print() utilise un objet PrintAttributes. Vous pouvez utiliser ce paramètre pour fournir des indications sur le framework d'impression et les options prédéfinies en fonction du cycle d'impression précédent, améliorant ainsi l'expérience utilisateur. Vous pouvez également utiliser ce paramètre pour définir des options plus adaptées au contenu à imprimer, telles que la définition de l'orientation en mode paysage lors de l'impression d'une photo dans cette orientation.

Un adaptateur d'impression interagit avec le framework d'impression Android et gère les étapes du processus d'impression. Ce processus oblige les utilisateurs à sélectionner des imprimantes et des options d'impression avant de créer un document pour impression. Ces sélections peuvent influencer la sortie finale, car l'utilisateur choisit des imprimantes offrant différentes capacités de sortie, différentes tailles de page ou différentes orientations de page. Au fur et à mesure que ces sélections sont effectuées, le framework d'impression demande à votre adaptateur de mettre en page et de générer un document imprimé en vue de la sortie finale. Lorsqu'un utilisateur appuie sur le bouton d'impression, le framework prend le document d'impression final et le transmet à un fournisseur de service d'impression. Au cours du processus d'impression, les utilisateurs peuvent choisir d'annuler l'action d'impression. Votre adaptateur d'impression doit donc également écouter les demandes d'annulation et y réagir.

La classe abstraite PrintDocumentAdapter est conçue pour gérer le cycle de vie d'impression, qui comporte quatre méthodes de rappel principales. Vous devez implémenter les méthodes suivantes dans votre adaptateur d'impression pour interagir correctement avec le framework d'impression:

  • onStart() : appelé une fois au début du processus d'impression. Si votre application doit effectuer des tâches de préparation ponctuelles, telles que l'obtention d'un instantané des données à imprimer, exécutez-les ici. Il n'est pas nécessaire d'implémenter cette méthode dans votre adaptateur.
  • onLayout() : Appelé chaque fois qu'un utilisateur modifie un paramètre d'impression qui affecte la sortie, par exemple une taille ou une orientation de page différente, ce qui permet à votre application de calculer la mise en page des pages à imprimer. Cette méthode doit au minimum renvoyer le nombre de pages attendues dans le document imprimé.
  • onWrite() : appelé pour afficher les pages imprimées dans un fichier à imprimer. Cette méthode peut être appelée une ou plusieurs fois après chaque appel onLayout().
  • onFinish() : appelé une fois à la fin du processus d'impression. Si votre application doit effectuer des tâches de suppression ponctuelles, exécutez-les ici. Il n'est pas nécessaire d'implémenter cette méthode dans votre adaptateur.

Les sections suivantes décrivent comment implémenter les méthodes de mise en page et d'écriture, qui sont essentielles au fonctionnement d'un adaptateur d'impression.

Remarque:Ces méthodes d'adaptateur sont appelées sur le thread principal de votre application. Si vous pensez que l'exécution de ces méthodes dans votre implémentation prendra beaucoup de temps, implémentez-les pour qu'elles s'exécutent dans un thread distinct. Par exemple, vous pouvez encapsuler la mise en page ou imprimer le travail d'écriture d'un document dans des objets AsyncTask distincts.

Calculer les informations sur l'impression du document

Dans une implémentation de la classe PrintDocumentAdapter, votre application doit pouvoir spécifier le type de document qu'elle crée et calculer le nombre total de pages pour la tâche d'impression, en fonction des informations sur le format des pages imprimées. L'implémentation de la méthode onLayout() dans l'adaptateur effectue ces calculs et fournit des informations sur le résultat attendu de la tâche d'impression dans une classe PrintDocumentInfo, y compris le nombre de pages et le type de contenu. L'exemple de code suivant illustre une implémentation de base de la méthode onLayout() pour un 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.");
    }
}

L'exécution de la méthode onLayout() peut avoir trois résultats: achèvement, annulation ou échec lorsque le calcul de la mise en page ne peut pas être effectué. Vous devez indiquer l'un de ces résultats en appelant la méthode appropriée de l'objet PrintDocumentAdapter.LayoutResultCallback.

Remarque:Le paramètre booléen de la méthode onLayoutFinished() indique si le contenu de la mise en page a effectivement été modifié depuis la dernière requête. La définition correcte de ce paramètre permet au framework d'impression d'éviter d'appeler inutilement la méthode onWrite(), ce qui met en cache le document imprimé précédemment et améliore les performances.

La tâche principale de onLayout() consiste à calculer le nombre de pages attendues en tant que sortie en fonction des attributs de l'imprimante. La manière dont vous calculez ce nombre dépend fortement de la mise en page de votre application pour l'impression. L'exemple de code suivant montre une implémentation dans laquelle le nombre de pages est déterminé par l'orientation de l'impression:

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

Écrire un fichier d'impression

Au moment d'écrire la sortie d'impression dans un fichier, le framework d'impression Android appelle la méthode onWrite() de la classe PrintDocumentAdapter de votre application. Les paramètres de la méthode spécifient les pages à écrire et le fichier de sortie à utiliser. La mise en œuvre de cette méthode doit ensuite afficher chaque page de contenu demandée dans un fichier PDF de plusieurs pages. Une fois ce processus terminé, vous appelez la méthode onWriteFinished() de l'objet de rappel.

Remarque:Le framework d'impression Android peut appeler la méthode onWrite() une ou plusieurs fois pour chaque appel à onLayout(). Pour cette raison, il est important de définir le paramètre booléen de la méthode onLayoutFinished() sur false lorsque la mise en page du contenu imprimé n'a pas changé, afin d'éviter toute réécriture inutile du document imprimé.

Remarque:Le paramètre booléen de la méthode onLayoutFinished() indique si le contenu de la mise en page a effectivement été modifié depuis la dernière requête. La définition correcte de ce paramètre permet au framework d'impression d'éviter d'appeler inutilement la méthode onLayout(), ce qui met en cache le document imprimé précédemment et améliore les performances.

L'exemple suivant illustre le mécanisme de base de ce processus en utilisant la classe PrintedPdfDocument pour créer un fichier PDF:

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

    ...
}

Cet exemple délègue le rendu du contenu de la page PDF à la méthode drawPage(), qui est abordée dans la section suivante.

Comme pour la mise en page, l'exécution de la méthode onWrite() peut avoir trois résultats: achèvement, annulation ou échec lorsque le contenu ne peut pas être écrit. Vous devez indiquer l'un de ces résultats en appelant la méthode appropriée de l'objet PrintDocumentAdapter.WriteResultCallback.

Remarque:L'affichage d'un document pour impression peut prendre beaucoup de ressources. Pour éviter de bloquer le thread d'interface utilisateur principal de votre application, vous devez envisager d'effectuer les opérations d'affichage et d'écriture de la page sur un thread distinct, par exemple dans un AsyncTask. Pour en savoir plus sur l'utilisation de threads d'exécution tels que les tâches asynchrones, consultez la section Processus et threads.

Dessiner le contenu de la page d'un PDF

Lors de l'impression de votre application, elle doit générer un document PDF et le transmettre à Android Print Framework pour l'impression. Pour ce faire, vous pouvez utiliser n'importe quelle bibliothèque de génération de PDF. Cette leçon explique comment utiliser la classe PrintedPdfDocument pour générer des pages PDF à partir de votre contenu.

La classe PrintedPdfDocument utilise un objet Canvas pour dessiner des éléments sur une page PDF, comme pour le dessin sur la mise en page d'une activité. Vous pouvez dessiner des éléments sur la page imprimée à l'aide des méthodes de dessin Canvas. L'exemple de code suivant montre comment dessiner des éléments simples sur une page de document PDF à l'aide des méthodes suivantes:

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

Lorsque vous utilisez Canvas pour dessiner sur une page PDF, les éléments sont spécifiés en points, soit 1/72 de pouce. Veillez à utiliser cette unité de mesure pour spécifier la taille des éléments sur la page. Pour le positionnement des éléments dessinés, le système de coordonnées commence à 0,0 pour l'angle supérieur gauche de la page.

Conseil:Bien que l'objet Canvas vous permette de placer des éléments d'impression sur le bord d'un document PDF, de nombreuses imprimantes ne sont pas en mesure d'imprimer sur le bord d'une feuille de papier physique. Veillez à tenir compte des bords non imprimables de la page lorsque vous créez un document imprimé avec cette classe.