Como imprimir documentos personalizados

Para alguns aplicativos, como apps de desenho, de layout de página e outros com foco em resultados gráficos, criar belas páginas impressas é um recurso fundamental. Nesse caso, não basta imprimir uma imagem ou um documento HTML. A saída de impressão para esses tipos de aplicativos requer um controle preciso de tudo o que entra em uma página, incluindo fontes, fluxo de texto, quebras de página, cabeçalhos, rodapés e elementos gráficos.

Criar uma saída de impressão totalmente personalizada para o aplicativo requer mais investimento em programação do que as abordagens discutidas anteriormente. Você precisa criar componentes que se comuniquem com o framework de impressão, ajustar as configurações da impressora, desenhar elementos da página e gerenciar a impressão em várias páginas.

Esta lição mostra como você se conecta ao gerenciador de impressão, cria um adaptador de impressão e cria conteúdo para impressão.

Quando o app gerencia o processo de impressão diretamente, a primeira etapa após receber uma solicitação de impressão do usuário é se conectar ao framework de impressão do Android e conseguir uma instância da classe PrintManager. Essa classe permite inicializar um trabalho de impressão e iniciar o ciclo de impressão. O exemplo de código a seguir mostra como conseguir o gerenciador de impressão e iniciar o processo de impressão.

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

O exemplo acima demonstra como nomear um trabalho de impressão e definir uma instância da classe PrintDocumentAdapter, que processa as etapas do ciclo de impressão. A implementação da classe do adaptador de impressão é discutida na próxima seção.

Observação:o último parâmetro no método print() usa um objeto PrintAttributes. Você pode usar esse parâmetro para dar dicas para o framework de impressão e opções predefinidas com base no ciclo de impressão anterior, melhorando a experiência do usuário. Você também pode usar esse parâmetro para definir opções mais adequadas ao conteúdo que está sendo impresso, como definir a orientação como paisagem ao imprimir uma foto que esteja nessa orientação.

Um adaptador de impressão interage com o framework de impressão do Android e processa as etapas do processo de impressão. Esse processo exige que os usuários selecionem impressoras e opções de impressão antes de criar um documento para impressão. Essas seleções podem influenciar a saída final, já que o usuário escolhe impressoras com diferentes recursos de saída, tamanhos de página ou orientações de página diferentes. Quando essas seleções são feitas, o framework de impressão pede que o adaptador faça o layout e gere um documento de impressão, em preparação para a saída final. Depois que um usuário toca no botão de impressão, o framework recebe o documento de impressão final e o transmite a um provedor de impressão para saída. Durante o processo de impressão, os usuários podem optar por cancelar a ação de impressão. Portanto, o adaptador de impressão também precisa detectar e reagir a solicitações de cancelamento.

A classe abstrata PrintDocumentAdapter foi projetada para processar o ciclo de impressão, que tem quatro métodos principais de callback. Implemente estes métodos no adaptador de impressão para interagir corretamente com o framework de impressão:

  • onStart(): chamado uma vez no início do processo de impressão. Se o aplicativo tiver tarefas de preparação únicas para executar, como receber um snapshot dos dados a serem impressos, execute-as aqui. Não é necessário implementar esse método no seu adaptador.
  • onLayout(): chamado sempre que um usuário altera uma configuração de impressão que afeta a saída, como um tamanho ou orientação de página diferente, oferecendo ao seu aplicativo a oportunidade de calcular o layout das páginas a serem impressas. Esse método precisa retornar, no mínimo, quantas páginas são esperadas no documento impresso.
  • onWrite(): chamado para renderizar as páginas impressas em um arquivo a ser impresso. Esse método pode ser chamado uma ou mais vezes após cada chamada de onLayout().
  • onFinish(): chamado uma vez no final do processo de impressão. Se o aplicativo tiver tarefas de desmontagem única para realizar, execute-as aqui. A implementação deste método no adaptador não é necessária.

As seções a seguir descrevem como implementar os métodos de layout e gravação, que são essenciais para o funcionamento de um adaptador de impressão.

Observação: esses métodos do adaptador são chamados na linha de execução principal do app. Se você espera que a execução desses métodos na sua implementação leve um tempo significativo, implemente-os em uma linha de execução separada. Por exemplo, você pode encapsular o trabalho de layout ou impressão de documentos em objetos AsyncTask separados.

Calcular informações de documentos de impressão

Em uma implementação da classe PrintDocumentAdapter, o aplicativo precisa ser capaz de especificar o tipo de documento que está criando e calcular o número total de páginas para o trabalho de impressão, de acordo com as informações sobre o tamanho da página impressa. A implementação do método onLayout() no adaptador faz esses cálculos e fornece informações sobre a saída esperada do trabalho de impressão em uma classe PrintDocumentInfo, incluindo o número de páginas e o tipo de conteúdo. O exemplo a seguir mostra uma implementação básica do método onLayout() para um 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.");
    }
}

A execução do método onLayout() pode ter três resultados: conclusão, cancelamento ou falha caso o cálculo do layout não possa ser concluído. É preciso indicar um desses resultados chamando o método apropriado do objeto PrintDocumentAdapter.LayoutResultCallback.

Observação:o parâmetro booleano do método onLayoutFinished() indica se o conteúdo do layout mudou ou não desde a última solicitação. Definir esse parâmetro corretamente permite que o framework de impressão evite chamar desnecessariamente o método onWrite(), essencialmente armazenando em cache o documento de impressão gravado anteriormente, melhorando o desempenho.

O principal trabalho do onLayout() é calcular o número de páginas que são esperadas como saída, considerando os atributos da impressora. A forma como esse número é calculado depende muito de como o aplicativo dispõe as páginas para impressão. O exemplo de código a seguir mostra uma implementação em que o número de páginas é determinado pela orientação da impressão:

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

Gravar um arquivo de documento de impressão

Quando for necessário gravar uma saída de impressão em um arquivo, o framework de impressão do Android chamará o método onWrite() da classe PrintDocumentAdapter do seu aplicativo. Os parâmetros do método especificam quais páginas precisam ser gravadas e o arquivo de saída a ser usado. Sua implementação desse método precisa renderizar cada página de conteúdo solicitada em um arquivo de documento PDF de várias páginas. Quando esse processo estiver concluído, chame o método onWriteFinished() do objeto de callback.

Observação:o framework de impressão do Android pode chamar o método onWrite() uma ou mais vezes a cada chamada para onLayout(). Por esse motivo, é importante definir o parâmetro booleano do método onLayoutFinished() como false quando o layout do conteúdo de impressão não foi modificado para evitar regravações desnecessárias do documento de impressão.

Observação:o parâmetro booleano do método onLayoutFinished() indica se o conteúdo do layout mudou ou não desde a última solicitação. Definir esse parâmetro corretamente permite que o framework de impressão evite chamar desnecessariamente o método onLayout(), essencialmente armazenando em cache o documento de impressão gravado anteriormente, melhorando o desempenho.

A amostra a seguir demonstra a mecânica básica desse processo usando a classe PrintedPdfDocument para criar um arquivo 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);

    ...
}

Esse exemplo delega a renderização do conteúdo da página PDF ao método drawPage(), que será discutido na próxima seção.

Assim como no layout, a execução do método onWrite() pode ter três resultados: conclusão, cancelamento ou falha caso o conteúdo não possa ser gravado. É preciso indicar um desses resultados chamando o método apropriado do objeto PrintDocumentAdapter.WriteResultCallback.

Observação: a renderização de um documento para impressão pode ser uma operação que consome muitos recursos. Para evitar o bloqueio da linha de execução principal da interface do usuário do seu aplicativo, considere executar as operações de renderização e gravação da página em uma linha de execução separada, por exemplo, em uma AsyncTask. Para mais informações sobre como trabalhar com linhas de execução como tarefas assíncronas, consulte Processos e linhas de execução.

Desenhar conteúdo de página PDF

Quando o aplicativo é impresso, ele precisa gerar um documento PDF e transmiti-lo ao framework de impressão do Android para impressão. É possível usar qualquer biblioteca de geração de PDF para essa finalidade. Esta lição mostra como usar a classe PrintedPdfDocument para gerar páginas PDF a partir do seu conteúdo.

A classe PrintedPdfDocument usa um objeto Canvas para desenhar elementos em uma página PDF, de forma semelhante ao desenho em um layout de atividade. Você pode desenhar elementos na página impressa usando os métodos de desenho Canvas. O código de exemplo a seguir demonstra como desenhar alguns elementos simples em uma página de documentos PDF usando esses métodos:

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

Ao usar o Canvas para desenhar em uma página PDF, os elementos são especificados em pontos, o que equivale a 1/72 de polegada. Use essa unidade de medida para especificar o tamanho dos elementos na página. Para o posicionamento de elementos desenhados, o sistema de coordenadas começa em 0,0 no canto superior esquerdo da página.

Dica:embora o objeto Canvas permita posicionar elementos de impressão na borda de um documento PDF, não é possível, para muitas impressoras, imprimir na borda de uma folha de papel física. Considere as bordas não imprimíveis da página ao criar um documento de impressão com essa classe.