Для некоторых приложений, таких как приложения для рисования, приложения для верстки страниц и другие приложения, ориентированные на вывод графики, создание красивых печатных страниц является ключевой функцией. В этом случае недостаточно распечатать изображение или 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-документа, многие принтеры не могут печатать по краю физического листа бумаги. Убедитесь, что вы учитываете непечатаемые края страницы при создании документа для печати с помощью этого класса.