Печать специализированных документов

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

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

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

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

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

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

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

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

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

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

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

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

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

В реализации класса PrintDocumentAdapter приложение должно указывать тип создаваемого документа и рассчитывать общее количество страниц для задания печати на основе информации о размере печатной страницы. Реализация метода onLayout() в адаптере выполняет эти вычисления и предоставляет информацию об ожидаемом выводе задания на печать в классе PrintDocumentInfo, включая количество страниц и тип контента. В примере кода ниже показана базовая реализация метода onLayout() для 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.");
    }
}

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

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

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

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

Запись файла документа для печати

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

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

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

В примере ниже показаны основные механизмы этого процесса с использованием класса PrintedPdfDocument для создания 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);

    ...
}

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

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

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

Рисование контента страницы PDF

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

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

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

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

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