Como imprimir documentos personalizados

Para alguns aplicativos, como aplicativos de desenho, aplicativos de layout de página e outros aplicativos que se concentram em saída gráfica, criar belas páginas impressas é um recurso fundamental. Nesse caso, não basta para imprimir uma imagem ou um documento HTML. O resultado de impressão desses tipos de aplicativos exige controle preciso de tudo o que há 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 seu aplicativo requer mais de programação do que as abordagens discutidas anteriormente. Você precisa criar componentes que comunicar-se com a estrutura 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 com o gerenciador de impressão, criar um adaptador de impressão e criar conteúdo para impressão.

Quando o aplicativo 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 obter uma instância da classe PrintManager. Esta classe permite inicializar um trabalho de impressão e iniciar o ciclo de impressão. O exemplo de código a seguir mostra como acessar 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 A implementação da classe do adaptador de impressão será discutida na próxima seção.

Observação:o último parâmetro na print() usa um objeto PrintAttributes. É possível usar esse parâmetro para fornecer dicas sobre 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 que sejam mais apropriado para o conteúdo sendo impresso, por exemplo, definindo a orientação como paisagem ao imprimir uma foto nessa orientação.

Um adaptador de impressão interage com o framework de impressão do Android e processa as etapas da processo de impressão. Esse processo exige que os usuários selecionem as impressoras e opções de impressão antes de criar um documento para impressão. Essas seleções podem influenciar o resultado final à medida que o usuário escolhe impressoras com diferentes recursos de saída, tamanhos de página ou orientações de página diferentes. À medida que essas seleções são feitas, o framework de impressão pede ao adaptador que disponha e gere uma imprimir documento, em preparação para o resultado final. Quando 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 para fins de saída. Durante a impressão os usuários podem optar por cancelar a ação de impressão, de modo que o adaptador de impressão também deve ouvir e reagir a solicitações de cancelamento.

A classe abstrata PrintDocumentAdapter foi projetada para processar o de impressão, que tem quatro métodos principais de callback. Você precisa implementar esses métodos no adaptador de impressão para interagir corretamente com o framework de impressão:

  • onStart() (ligação feita uma vez no início do processo de impressão. Caso sua inscrição tenha tarefas de preparação únicas para executar, como conseguir um snapshot dos dados a serem impressos, execute-os aqui. Implementação esse método no adaptador não é necessário.
  • onLayout(): chamado sempre que um o usuário altera uma configuração de impressão que afeta o resultado, como outro tamanho de página, ou orientação de página, permitindo que seu aplicativo calcule o layout da páginas a serem impressas. No mínimo, esse método precisa retornar quantas páginas são esperadas no documento impresso.
  • onWrite(): chamado para renderizar na impressão páginas em um arquivo a ser impresso. Esse método pode ser chamado uma ou mais vezes após cada onLayout().
  • onFinish() (chamada uma vez no final) do processo de impressão. Se seu aplicativo tiver alguma tarefa única de eliminação para executar, executá-las 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 do 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 em sua implementação tome uma quantidade significativa de implemente-os para que sejam executados em uma linha de execução separada. Por exemplo, é possível encapsular 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, a classe aplicativo deve ser capaz de especificar o tipo de documento que está criando e calcular o total número de páginas para o trabalho de impressão, com base nas informações sobre o tamanho da página impressa. A implementação do método onLayout() em o 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 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 têm três resultados: conclusão, cancelamento ou falha caso o cálculo do não pode ser concluído. Você deve indicar um desses resultados chamando o método do objeto PrintDocumentAdapter.LayoutResultCallback.

Observação: o parâmetro booleano da O método onLayoutFinished() indica se o conteúdo do layout foi realmente alterado 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(), armazenando em cache o documento impresso previamente gravado, o que melhora o desempenho.

O principal trabalho do onLayout() é calcular o número de páginas esperadas como saída, considerando os atributos da impressora. A forma como você calcula esse número depende muito de como seu aplicativo organiza 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 é determinada 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 devem ser gravado e o arquivo de saída a ser usado. A implementação desse método deve então renderizar cada página solicitada de conteúdo em um arquivo de documento PDF com várias páginas. Quando esse processo for 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 para onLayout(). Por isso, é importante definir o parâmetro booleano Método onLayoutFinished() para false quando o layout do conteúdo de impressão não muda. para evitar regravações desnecessárias nos documentos impressos.

Observação: o parâmetro booleano da O método onLayoutFinished() indica se o conteúdo do layout foi realmente alterado 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(), armazenando em cache o documento impresso previamente gravado, o que melhora 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);

    ...
}

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

Assim como no layout, a execução de onWrite() pode ter três resultados: conclusão, cancelamento ou falha caso o não é possível gravar o conteúdo. Você deve 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. Em para evitar o bloqueio da linha de execução principal da interface do usuário do aplicativo, considere executando as operações de renderização e gravação da página em uma linha de execução separada, por exemplo em um AsyncTask. Para mais informações sobre como trabalhar com linhas de execução como tarefas assíncronas, consulte Processos e Threads.

Desenhar conteúdo de página PDF

Quando seu aplicativo for impresso, ele deverá gerar um documento PDF e passá-lo para o framework de impressão do Android para impressão. Você pode usar qualquer biblioteca de geração de PDF para do propósito da tecnologia. 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 Canvas. para desenhar elementos em uma página PDF, semelhante ao desenho em um layout de atividade. Você pode desenhar elementos na página impressa usando os métodos de desenho Canvas. O seguinte exemplo de código demonstra como desenhar alguns elementos simples em uma página de documento PDF usando estes 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 Canvas para desenhar em uma página PDF, os elementos são especificados em de ponta, que é 1/72 de polegada. Use essa unidade de medida para especificar o tamanho de elementos da 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 colocar a impressão elementos na borda de um documento PDF, muitas impressoras não conseguem uma folha de papel física. Considere as bordas da página que não podem ser impressas ao a criar um documento de impressão com esta classe.