Печать нестандартных документов

Для некоторых приложений, таких как приложения для рисования, приложения для верстки страниц и другие приложения, ориентированные на вывод графики, создание красивых печатных страниц является ключевой функцией. В этом случае недостаточно распечатать изображение или HTML-документ. Вывод на печать для приложений такого типа требует точного контроля всего, что находится на странице, включая шрифты, расположение текста, разрывы страниц, верхние и нижние колонтитулы и графические элементы.

Создание вывода на печать, полностью адаптированного к вашему приложению, требует больше инвестиций в программирование, чем ранее обсуждавшиеся подходы. Вы должны создавать компоненты, которые взаимодействуют с платформой печати, настраивать параметры принтера, рисовать элементы страницы и управлять печатью на нескольких страницах.

В этом уроке показано, как подключиться к диспетчеру печати, создать адаптер печати и создать содержимое для печати.

Если ваше приложение управляет процессом печати напрямую, первым шагом после получения запроса на печать от пользователя является подключение к платформе печати Android и получение экземпляра класса PrintManager . Этот класс позволяет инициализировать задание печати и начать жизненный цикл печати. В следующем примере кода показано, как получить диспетчер печати и запустить процесс печати.

Котлин

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

Ява

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

В приведенном выше примере кода показано, как назвать задание на печать и установить экземпляр класса PrintDocumentAdapter , который обрабатывает этапы жизненного цикла печати. Реализация класса адаптера печати обсуждается в следующем разделе.

Примечание. Последний параметр метода print() принимает объект PrintAttributes . Вы можете использовать этот параметр, чтобы предоставить подсказки по структуре печати и предварительно установленным параметрам на основе предыдущего цикла печати, тем самым улучшая взаимодействие с пользователем. Вы также можете использовать этот параметр для установки параметров, которые больше подходят для печатаемого содержимого, например, установка альбомной ориентации при печати фотографии в этой ориентации.

Адаптер печати взаимодействует с платформой печати Android и обрабатывает этапы процесса печати. Этот процесс требует от пользователей выбора принтеров и параметров печати перед созданием документа для печати. Эти параметры могут повлиять на конечный результат, поскольку пользователь выбирает принтеры с различными возможностями вывода, разными размерами страниц или различной ориентацией страниц. После того, как этот выбор сделан, платформа печати просит ваш адаптер разметить и сгенерировать документ для печати для подготовки к окончательному выводу. Как только пользователь нажимает кнопку печати, платформа берет окончательный документ для печати и передает его поставщику печати для вывода. Во время процесса печати пользователи могут отменить действие печати, поэтому ваш адаптер печати также должен прослушивать запросы на отмену и реагировать на них.

Абстрактный класс PrintDocumentAdapter предназначен для управления жизненным циклом печати и имеет четыре основных метода обратного вызова. Вы должны реализовать эти методы в своем адаптере печати, чтобы правильно взаимодействовать с платформой печати:

  • onStart() — вызывается один раз в начале процесса печати. Если вашему приложению необходимо выполнить какие-либо одноразовые подготовительные задачи, например получение моментального снимка данных для печати, выполните их здесь. Реализация этого метода в вашем адаптере не требуется.
  • onLayout() — вызывается каждый раз, когда пользователь меняет параметры печати, которые влияют на вывод, например, другой размер страницы или ориентацию страницы, давая вашему приложению возможность вычислить макет страниц для печати. Как минимум, этот метод должен возвращать ожидаемое количество страниц в печатном документе.
  • onWrite() — вызывается для преобразования напечатанных страниц в файл для печати. Этот метод можно вызывать один или несколько раз после каждого вызова onLayout() .
  • onFinish() — вызывается один раз в конце процесса печати. Если вашему приложению необходимо выполнить какие-либо одноразовые задачи по удалению, выполните их здесь. Реализация этого метода в вашем адаптере не требуется.

В следующих разделах описывается, как реализовать методы макетирования и записи, которые имеют решающее значение для функционирования адаптера печати.

Примечание. Эти методы адаптера вызываются в основном потоке вашего приложения. Если вы ожидаете, что выполнение этих методов в вашей реализации займет значительное время, реализуйте их выполнение в отдельном потоке. Например, вы можете инкапсулировать макет или работу по написанию документа печати в отдельных объектах AsyncTask .

Вычислить информацию о документе для печати

В рамках реализации класса PrintDocumentAdapter ваше приложение должно иметь возможность указывать тип создаваемого документа и рассчитывать общее количество страниц для задания на печать, учитывая информацию о размере напечатанной страницы. Реализация метода onLayout() в адаптере выполняет эти вычисления и предоставляет информацию об ожидаемом результате задания печати в классе PrintDocumentInfo , включая количество страниц и тип контента. В следующем примере кода показана базовая реализация метода onLayout() для PrintDocumentAdapter :

Котлин

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.")
    }
}

Ява

@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.");
    }
}

Выполнение метода onLayout() может иметь три результата: завершение, отмену или сбой в случае, когда расчет макета не может быть завершен. Вы должны указать один из этих результатов, вызвав соответствующий метод объекта PrintDocumentAdapter.LayoutResultCallback .

Примечание. Логический параметр метода onLayoutFinished() указывает, действительно ли содержимое макета изменилось с момента последнего запроса. Правильная установка этого параметра позволяет платформе печати избежать ненужного вызова метода onWrite() , по существу кэшируя ранее записанный документ печати и повышая производительность.

Основная работа onLayout() — вычисление количества страниц, которые ожидаются на выходе с учетом атрибутов принтера. Способ расчета этого числа во многом зависит от того, как ваше приложение размещает страницы для печати. В следующем примере кода показана реализация, в которой количество страниц определяется ориентацией печати:

Котлин

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

Ява

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

Напишите файл документа для печати

Когда приходит время записать вывод печати в файл, платформа печати Android вызывает метод onWrite() класса PrintDocumentAdapter вашего приложения. Параметры метода определяют, какие страницы следует записать и какой выходной файл будет использоваться. Ваша реализация этого метода должна затем преобразовать каждую запрошенную страницу содержимого в многостраничный файл PDF-документа. Когда этот процесс завершится, вы вызываете метод onWriteFinished() объекта обратного вызова.

Примечание. Платформа печати Android может вызывать метод onWrite() один или несколько раз для каждого вызова onLayout() . По этой причине важно установить для логического параметра метода onLayoutFinished() значение false если макет содержимого печати не изменился, чтобы избежать ненужной перезаписи документа печати.

Примечание. Логический параметр метода onLayoutFinished() указывает, действительно ли содержимое макета изменилось с момента последнего запроса. Правильная установка этого параметра позволяет платформе печати избежать ненужного вызова метода onLayout() , по существу кэшируя ранее записанный документ печати и повышая производительность.

В следующем примере демонстрируется базовая механика этого процесса с использованием класса PrintedPdfDocument для создания файла PDF:

Котлин

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)

    ...
}

Ява

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

    ...
}

В этом примере рендеринг содержимого PDF-страницы делегируется методу drawPage() , который обсуждается в следующем разделе.

Как и в случае с макетом, выполнение метода onWrite() может иметь три результата: завершение, отмену или сбой в случае, когда содержимое не может быть записано. Вы должны указать один из этих результатов, вызвав соответствующий метод объекта PrintDocumentAdapter.WriteResultCallback .

Примечание. Подготовка документа к печати может оказаться ресурсоемкой операцией. Чтобы избежать блокировки основного потока пользовательского интерфейса вашего приложения, вам следует рассмотреть возможность выполнения операций рендеринга и записи страниц в отдельном потоке, например, в AsyncTask . Дополнительные сведения о работе с потоками выполнения, такими как асинхронные задачи, см. в разделе Процессы и потоки .

Рисование содержимого PDF-страницы

Когда ваше приложение печатает, оно должно создать PDF-документ и передать его в платформу печати Android для печати. Для этой цели вы можете использовать любую библиотеку создания PDF-файлов. В этом уроке показано, как использовать класс PrintedPdfDocument для создания страниц PDF из вашего контента.

Класс PrintedPdfDocument использует объект Canvas для рисования элементов на странице PDF, аналогично рисованию в макете действия. Вы можете рисовать элементы на печатной странице, используя методы рисования Canvas . В следующем примере кода показано, как нарисовать несколько простых элементов на странице PDF-документа с помощью этих методов:

Котлин

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

Ява

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

При использовании Canvas для рисования на странице PDF элементы указываются в пунктах, что составляет 1/72 дюйма. Обязательно используйте эту единицу измерения для указания размера элементов на странице. Для позиционирования нарисованных элементов система координат начинается с 0,0 для верхнего левого угла страницы.

Совет: Хотя объект Canvas позволяет размещать элементы печати по краю PDF-документа, многие принтеры не могут печатать по краю физического листа бумаги. Убедитесь, что вы учитываете непечатаемые края страницы при создании документа для печати с помощью этого класса.