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