Stampa di documenti personalizzati

Per alcune applicazioni, come per il disegno o di layout di pagina, e altre app incentrate sull'output grafico, la creazione di pagine stampate di qualità è una funzionalità chiave. In questo caso, non è sufficiente stampare un'immagine o un documento HTML. L'output di stampa per questi tipi di applicazioni richiede un controllo preciso di tutto ciò che viene inserito in una pagina, inclusi caratteri, flusso del testo, interruzioni di pagina, intestazioni, piè di pagina ed elementi grafici.

La creazione di un output di stampa completamente personalizzato per la tua applicazione richiede un investimento di programmazione maggiore rispetto agli approcci discussi in precedenza. Devi creare componenti che comunicano con il framework di stampa, adattarsi alle impostazioni della stampante, disegnare elementi di pagina e gestire la stampa su più pagine.

Questa lezione spiega come connetterti al gestore di stampa, creare un adattatore di stampa e creare contenuti per la stampa.

Se l'applicazione gestisce direttamente il processo di stampa, il primo passaggio dopo aver ricevuto una richiesta di stampa dall'utente consiste nel connettersi al framework di stampa Android e ottenere un'istanza della classe PrintManager. Questa classe consente di inizializzare un processo di stampa e iniziare il ciclo di vita di stampa. Il codice di esempio riportato di seguito mostra come ottenere il gestore di stampa e avviare il processo di stampa.

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

Il codice di esempio riportato sopra mostra come assegnare un nome a un processo di stampa e impostare un'istanza della classe PrintDocumentAdapter che gestisce i passaggi del ciclo di vita di stampa. L'implementazione della classe dell'adattatore di stampa è discussa nella prossima sezione.

Nota: l'ultimo parametro del metodo print() accetta un oggetto PrintAttributes. Puoi utilizzare questo parametro per fornire suggerimenti al framework di stampa e opzioni preimpostate in base al ciclo di stampa precedente, migliorando così l'esperienza utente. Puoi utilizzare questo parametro anche per impostare le opzioni più appropriate per i contenuti da stampare, ad esempio impostare l'orientamento su orizzontale per la stampa di una foto con quell'orientamento.

Un adattatore di stampa interagisce con il framework di stampa Android e gestisce i passaggi del processo di stampa. Questo processo richiede agli utenti di selezionare le stampanti e le opzioni di stampa prima di creare un documento da stampare. Queste selezioni possono influenzare l'output finale poiché l'utente sceglie stampanti con capacità di output diverse, dimensioni di pagina diverse o orientamenti di pagina diversi. Man mano che vengono effettuate queste selezioni, il framework di stampa chiede all'adattatore di impaginare e generare un documento di stampa, in preparazione dell'output finale. Una volta che un utente tocca il pulsante di stampa, il framework recupera il documento di stampa finale e lo passa a un fornitore di stampa per l'output. Durante il processo di stampa, gli utenti possono scegliere di annullare l'azione di stampa, pertanto l'adattatore di stampa deve anche rilevare e reagire a una richiesta di annullamento.

La classe astratta PrintDocumentAdapter è progettata per gestire il ciclo di vita della stampa, che prevede quattro metodi principali di callback. Per interagire correttamente con il framework di stampa, devi implementare questi metodi nell'adattatore di stampa:

  • onStart(): richiamato una volta all'inizio del processo di stampa. Se l'applicazione ha attività di preparazione una tantum da eseguire, ad esempio ottenere uno snapshot dei dati da stampare, eseguile qui. L'implementazione di questo metodo nell'adattatore non è necessaria.
  • onLayout(): richiamato ogni volta che un utente modifica un'impostazione di stampa che influisce sull'output, ad esempio un formato di pagina diverso o un orientamento della pagina diverso, dando all'applicazione l'opportunità di calcolare il layout delle pagine da stampare. Come minimo, questo metodo deve restituire il numero di pagine previste nel documento stampato.
  • onWrite(): viene chiamato per visualizzare le pagine stampate in un file da stampare. Questo metodo può essere chiamato una o più volte dopo ogni chiamata a onLayout().
  • onFinish(): chiamata una volta al termine del processo di stampa. Se l'applicazione ha attività di eliminazione una tantum da eseguire, eseguile qui. Non è necessario implementare questo metodo nell'adattatore.

Le seguenti sezioni descrivono come implementare i metodi di layout e scrittura, fondamentali per il funzionamento di un adattatore di stampa.

Nota: questi metodi dell'adattatore sono chiamati nel thread principale dell'applicazione. Se prevedi che l'esecuzione di questi metodi nell'implementazione richieda molto tempo, implementali in modo che vengano eseguiti in un thread separato. Ad esempio, puoi incapsulare il layout o stampare il lavoro di scrittura di documenti in oggetti AsyncTask separati.

Informazioni sui documenti di Compute Print

All'interno di un'implementazione della classe PrintDocumentAdapter, l'applicazione deve essere in grado di specificare il tipo di documento che sta creando e di calcolare il numero totale di pagine per il processo di stampa, in base alle informazioni sulle dimensioni delle pagine stampate. L'implementazione del metodo onLayout() nell'adattatore esegue questi calcoli e fornisce informazioni sull'output previsto del processo di stampa in una classe PrintDocumentInfo, incluso il numero di pagine e il tipo di contenuti. Il seguente esempio di codice mostra un'implementazione di base del metodo onLayout() per 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'esecuzione del metodo onLayout() può avere tre risultati: completamento, annullamento o errore nel caso in cui non sia possibile completare il calcolo del layout. Devi indicare uno di questi risultati chiamando il metodo appropriato dell'oggetto PrintDocumentAdapter.LayoutResultCallback.

Nota: il parametro booleano del metodo onLayoutFinished() indica se i contenuti del layout sono effettivamente cambiati o meno dall'ultima richiesta. L'impostazione corretta di questo parametro consente al framework di stampa di evitare chiamate inutilmente al metodo onWrite(), sostanzialmente memorizzando nella cache il documento di stampa scritto in precedenza e migliorando le prestazioni.

Il lavoro principale di onLayout() è calcolare il numero di pagine previste come output in base agli attributi della stampante. Il modo in cui viene calcolato questo numero dipende molto da come l'applicazione dispone il layout delle pagine da stampare. Il seguente esempio di codice mostra un'implementazione in cui il numero di pagine è determinato dall'orientamento di stampa:

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

Scrivere un file di documento di stampa

Quando è il momento di scrivere l'output di stampa su un file, il framework di stampa di Android chiama il metodo onWrite() della classe PrintDocumentAdapter della tua applicazione. I parametri del metodo specificano quali pagine devono essere scritte e il file di output da utilizzare. L'implementazione di questo metodo deve quindi eseguire il rendering di ogni pagina di contenuti richiesta in un file di documento PDF di più pagine. Al termine della procedura, chiami il metodo onWriteFinished() dell'oggetto callback.

Nota: il framework di stampa di Android potrebbe chiamare il metodo onWrite() una o più volte per ogni chiamata a onLayout(). Per questo motivo, è importante impostare il parametro booleano del metodo onLayoutFinished() su false quando il layout dei contenuti di stampa non è cambiato, per evitare riscritture non necessarie del documento stampato.

Nota: il parametro booleano del metodo onLayoutFinished() indica se i contenuti del layout sono effettivamente cambiati o meno dall'ultima richiesta. L'impostazione corretta di questo parametro consente al framework di stampa di evitare chiamate inutilmente al metodo onLayout(), sostanzialmente memorizzando nella cache il documento di stampa scritto in precedenza e migliorando le prestazioni.

Il seguente esempio illustra i meccanismi di base di questo processo utilizzando la classe PrintedPdfDocument per creare un file 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);

    ...
}

Questo esempio delega il rendering dei contenuti delle pagine PDF al metodo drawPage(), di cui parleremo nella prossima sezione.

Come per il layout, l'esecuzione del metodo onWrite() può avere tre risultati: completamento, annullamento o errore nel caso in cui i contenuti non possano essere scritti. Devi indicare uno di questi risultati chiamando il metodo appropriato dell'oggetto PrintDocumentAdapter.WriteResultCallback.

Nota:il rendering di un documento per la stampa può richiedere molte risorse. Per evitare di bloccare il thread dell'interfaccia utente principale dell'applicazione, ti consigliamo di eseguire le operazioni di scrittura e rendering della pagina in un thread separato, ad esempio in un AsyncTask. Per ulteriori informazioni sull'utilizzo di thread di esecuzione come attività asincrone, consulta Processi e thread.

Disegnare i contenuti di una pagina in formato PDF

Quando viene stampata, l'applicazione deve generare un documento PDF e passarlo al framework di stampa Android per la stampa. A questo scopo, puoi utilizzare qualsiasi libreria di generazione di PDF. Questa lezione spiega come utilizzare la classe PrintedPdfDocument per generare pagine PDF dai tuoi contenuti.

La classe PrintedPdfDocument utilizza un oggetto Canvas per disegnare elementi su una pagina PDF, in modo simile a disegnare nel layout di un'attività. Puoi disegnare elementi sulla pagina stampata utilizzando i metodi di disegno Canvas. Il codice di esempio riportato di seguito mostra come disegnare alcuni semplici elementi sulla pagina di un documento PDF utilizzando questi metodi:

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

Quando utilizzi Canvas per disegnare su una pagina PDF, gli elementi sono specificati in punti, ovvero 1/72 di pollice. Assicurati di utilizzare questa unità di misura per specificare le dimensioni degli elementi sulla pagina. Per il posizionamento degli elementi disegnati, il sistema di coordinate inizia da 0,0 per l'angolo in alto a sinistra della pagina.

Suggerimento: l'oggetto Canvas consente di posizionare elementi di stampa sul bordo di un documento PDF, ma molte stampanti non sono in grado di stampare sul bordo di un foglio di carta fisico. Assicurati di prendere in considerazione i bordi non stampabili della pagina quando crei un documento stampato con questa classe.