Como imprimir documentos personalizados

Para alguns aplicativos, como apps de desenho, de layout de página e outros com foco em resultados gráficos, a criação de páginas impressas bonitas é um recurso fundamental. Nesse caso, imprimir uma imagem ou um documento HTML não é suficiente . A saída de impressão para esse tipo de aplicativos requer um controle preciso de tudo o que faz parte de 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 seu aplicativo requer mais investimento em programação do que as abordagens discutidas anteriormente. É necessário 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 se conectar ao gerenciador de impressão, criar um adaptador de impressão e criar 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 da 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 código de 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 fornecer dicas para o framework de impressão e opções pré-definidas 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 de 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 as impressoras e as 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 ou orientações de página. À medida que essas seleções são feitas, o framework de impressão solicita que o adaptador faça um 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 transmite o documento de impressão final para um provedor de impressão. Durante o processo de impressão, os usuários podem optar por cancelar a ação de impressão. Por esse motivo, o adaptador de impressão também precisa ouvir e reagir a solicitações de cancelamento.

A classe abstrata PrintDocumentAdapter foi projetada para processar o ciclo de impressão, que possui quatro métodos principais de callback. É necessário implementar estes métodos no adaptador de impressão para a interação adequada com o framework de impressão:

  • onStart(): chamado uma vez no início do processo de impressão. Se seu aplicativo tiver tarefas de preparação única para executar, como fazer um snapshot dos dados a serem impressos, execute tais tarefas aqui. A implementação deste método no adaptador não é necessária.
  • onLayout(): chamado sempre que o usuário altera uma configuração de impressão que afeta a saída, como tamanho ou orientação da página diferentes, o que possibilita ao app calcular o layout das páginas a serem impressas. Este 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. Este método pode ser chamado uma ou mais vezes após cada chamada do onLayout().
  • onFinish(): chamado uma vez no final do processo de impressão. Se seu aplicativo tiver tarefas de desmontagem única para executar, execute tais tarefas 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, 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. Implemente esses métodos em uma linha de execução separada se for esperado que a execução deles na sua implementação leve muito tempo. 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, é necessário que seu app possa especificar o tipo de documento sendo criado e calcular o número total de páginas para o trabalho de impressão, considerando 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 de código 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 apresentar três resultados: conclusão, cancelamento ou falha caso não seja possível concluir o cálculo do layout. É necessário 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 houve alteração do conteúdo do layout desde a última solicitação. Definir esse parâmetro corretamente evita que o framework de impressão chame desnecessariamente o método onWrite(). Essencialmente, o framework armazena em cache o documento de impressão previamente gravado, o que melhora 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 distribui 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. Então, a implementação desse método precisa renderizar cada página solicitada de conteúdo em um arquivo de documento PDF com 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 para cada chamada do 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 alterado, para evitar regravações desnecessárias do documento de impressão.

Observação: o parâmetro booleano do método onLayoutFinished() indica se houve alteração do conteúdo do layout desde a última solicitação. Definir esse parâmetro corretamente evita que o framework de impressão chame desnecessariamente o método onLayout(). Essencialmente, o framework armazena em cache o documento de impressão previamente gravado, o que melhora o desempenho.

O exemplo 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. É necessário 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 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 ver 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

Ao imprimir, é necessário que o aplicativo gere um documento PDF e transmita-o para o framework de impressão do Android para imprimir. Você pode usar qualquer biblioteca de geração de PDF para isso. Essa 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 do Canvas. O código de exemplo a seguir demonstra como desenhar alguns elementos simples em uma página de documento 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 da página em que não é possível imprimir ao criar um documento de impressão com essa classe.