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.
Collegati al gestore di 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.
Crea un adattatore di stampa
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 aonLayout()
.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.