Đối với một số ứng dụng, chẳng hạn như ứng dụng vẽ, ứng dụng bố cục trang và các ứng dụng khác tập trung vào đồ hoạ, thì việc tạo ra những trang in đẹp là một tính năng quan trọng. Trong trường hợp này thì như vậy là chưa đủ để in hình ảnh hoặc tài liệu HTML. Bạn cần có đầu ra bản in cho những loại ứng dụng này kiểm soát chính xác mọi nội dung trên một trang, bao gồm phông chữ, dòng văn bản, ngắt trang, đầu trang, chân trang và các thành phần đồ hoạ.
Việc tạo bản in được tuỳ chỉnh hoàn toàn cho ứng dụng sẽ đòi hỏi nhiều hơn hơn các phương pháp đã thảo luận trước đây. Bạn phải tạo các thành phần giao tiếp với khung in, điều chỉnh theo chế độ cài đặt máy in, vẽ các thành phần trang và quản lý việc in trên nhiều trang.
Bài học này sẽ hướng dẫn bạn cách kết nối với trình quản lý in, tạo bộ điều hợp in và tạo nội dung để in.
Kết nối với trình quản lý in
Khi ứng dụng của bạn trực tiếp quản lý quy trình in, bước đầu tiên sau khi nhận được
yêu cầu in của người dùng là để kết nối với khung in của Android và lấy một thực thể
của lớp PrintManager
. Lớp này cho phép bạn khởi chạy một lệnh in
và bắt đầu vòng đời in. Ví dụ về mã sau đây cho biết cách tải trình quản lý in
và bắt đầu quy trình in.
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); // }
Mã ví dụ ở trên minh hoạ cách đặt tên cho lệnh in và đặt một thực thể của lớp PrintDocumentAdapter
để xử lý các bước trong vòng đời in. Chiến lược phát hành đĩa đơn
cách triển khai lớp bộ chuyển đổi in sẽ được thảo luận trong phần tiếp theo.
Lưu ý: Tham số cuối cùng trong print()
sẽ nhận đối tượng PrintAttributes
. Bạn có thể sử dụng tham số này để
đưa ra gợi ý cho khung in và các tuỳ chọn đặt trước dựa trên chu kỳ in trước đó,
từ đó cải thiện trải nghiệm người dùng. Bạn cũng có thể sử dụng tham số này để đặt các tuỳ chọn
phù hợp hơn với nội dung đang được in, chẳng hạn như đặt hướng thành ngang
khi in ảnh theo hướng đó.
Tạo bộ điều hợp in
Bộ điều hợp in tương tác với khung in của Android và xử lý các bước của quá trình in. Quá trình này yêu cầu người dùng chọn máy in và tùy chọn in trước khi tạo tài liệu để in. Các lựa chọn này có thể ảnh hưởng đến kết quả cuối cùng khi người dùng chọn máy in có khả năng xuất dữ liệu khác nhau, kích thước trang khác nhau hoặc hướng trang khác nhau. Khi các lựa chọn này được thực hiện, khung in sẽ yêu cầu bộ chuyển đổi bố trí và tạo một in tài liệu để chuẩn bị cho đầu ra cuối cùng. Khi người dùng nhấn vào nút in, khung này lấy tài liệu in cuối cùng và chuyển cho một nhà cung cấp dịch vụ in để xuất ra. Trong khi in của quy trình này, người dùng có thể chọn huỷ thao tác in, vì vậy bộ điều hợp in của bạn cũng phải theo dõi và phản hồi các yêu cầu huỷ.
Lớp trừu tượng PrintDocumentAdapter
được thiết kế để xử lý
vòng đời in, có 4 phương thức gọi lại chính. Bạn phải triển khai các phương thức này
trong bộ điều hợp in của bạn để tương tác đúng cách với khung in:
onStart()
– Được gọi một lần vào đầu quy trình in. Nếu ứng dụng của bạn có bất kỳ nhiệm vụ chuẩn bị một lần nào để thực hiện, chẳng hạn như lấy ảnh chụp nhanh về dữ liệu cần in, hãy thực thi chúng tại đây. Triển khai phương thức này trong bộ chuyển đổi của bạn là không bắt buộc.onLayout()
– Được gọi mỗi khi có người dùng thay đổi cài đặt in ảnh hưởng đến kết quả, chẳng hạn như kích thước trang khác, hoặc hướng trang, giúp ứng dụng của bạn có cơ hội tính toán bố cục của trang được in. Ở mức tối thiểu, phương thức này phải trả về số lượng trang dự kiến trong tài liệu đã in.onWrite()
– Được gọi để kết xuất hình ảnh đã in vào một tệp cần in. Phương thức này có thể được gọi một hoặc nhiều lần sau mỗi lần Cuộc gọionLayout()
.onFinish()
– Được gọi một lần vào cuối của quy trình in. Nếu ứng dụng của bạn có bất kỳ tác vụ chia nhỏ một lần nào cần thực hiện, và thực thi chúng tại đây. Bạn không bắt buộc phải triển khai phương thức này trong bộ chuyển đổi.
Các phần sau đây mô tả cách triển khai bố cục và các phương thức ghi, rất quan trọng đối với hoạt động của bộ điều hợp in.
Lưu ý: Các phương thức trình chuyển đổi này được gọi trên luồng chính của ứng dụng. Nếu
bạn dự kiến việc triển khai các phương pháp này trong quá trình triển khai sẽ mất một lượng đáng kể
thời gian, hãy triển khai chúng để thực thi trong một luồng riêng. Ví dụ: bạn có thể gói
bố cục hoặc in tài liệu viết trong các đối tượng AsyncTask
riêng biệt.
Tính toán thông tin tài liệu in
Trong quá trình triển khai lớp PrintDocumentAdapter
,
ứng dụng phải có khả năng chỉ định loại tài liệu mà ứng dụng đang tạo và tính tổng số tiền
số trang cho lệnh in, dựa trên thông tin về kích thước trang được in.
Việc triển khai phương thức onLayout()
trong
bộ chuyển đổi thực hiện các tính toán này và cung cấp thông tin về kết quả dự kiến của
lệnh in trong một lớp PrintDocumentInfo
, bao gồm cả số trang và số trang
loại nội dung. Ví dụ về mã sau đây cho thấy cách triển khai cơ bản của phương thức onLayout()
cho 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."); } }
Việc thực thi phương thức onLayout()
có thể
có ba kết quả: hoàn thành, huỷ hoặc không thành công trong trường hợp tính toán
không thể hoàn tất bố cục. Bạn phải chỉ ra một trong các kết quả này bằng cách gọi lệnh gọi hàm
của đối tượng PrintDocumentAdapter.LayoutResultCallback
.
Lưu ý: Tham số boolean của
Phương thức onLayoutFinished()
cho biết nội dung bố cục có thực sự thay đổi hay không
kể từ yêu cầu cuối cùng. Việc đặt tham số này đúng cách sẽ cho phép khung in tránh
gọi phương thức onWrite()
một cách không cần thiết,
về cơ bản là lưu tài liệu in trước đó vào bộ nhớ đệm và cải thiện hiệu suất.
Công việc chính của onLayout()
là
tính toán số lượng trang cần là trang đầu ra dựa vào các thuộc tính của máy in.
Cách bạn tính con số này phụ thuộc nhiều vào cách đơn đăng ký của bạn bố trí các trang cho
in. Ví dụ về mã sau đây cho thấy cách triển khai trong đó số trang
được xác định theo hướng in:
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); }
Viết tệp tài liệu in
Khi đến lúc ghi kết quả in vào một tệp, khung in của Android sẽ gọi phương thức onWrite()
của lớp PrintDocumentAdapter
trong ứng dụng. Các thông số của phương thức chỉ định trang nào
và tệp đầu ra sẽ được sử dụng. Sau đó, việc triển khai phương thức này phải hiển thị từng
trang nội dung được yêu cầu sang một tệp tài liệu PDF nhiều trang. Khi quá trình này hoàn tất, bạn
gọi phương thức onWriteFinished()
của đối tượng gọi lại.
Lưu ý: Khung in của Android có thể gọi phương thức onWrite()
một hoặc nhiều lần cho mỗi
gọi đến onLayout()
. Vì lý do này,
bạn cần đặt tham số boolean cho
Phương thức onLayoutFinished()
thành false
khi bố cục nội dung in không thay đổi,
để tránh việc viết lại tài liệu in một cách không cần thiết.
Lưu ý: Tham số boolean của
Phương thức onLayoutFinished()
cho biết nội dung bố cục có thực sự thay đổi hay không
kể từ yêu cầu cuối cùng. Việc đặt tham số này đúng cách sẽ cho phép khung in tránh
gọi phương thức onLayout()
một cách không cần thiết,
về cơ bản là lưu tài liệu in trước đó vào bộ nhớ đệm và cải thiện hiệu suất.
Mẫu sau đây minh hoạ cơ chế cơ bản của quy trình này bằng cách sử dụng lớp PrintedPdfDocument
để tạo tệp 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); ... }
Mẫu này uỷ quyền kết xuất nội dung trang PDF cho drawPage()
mà chúng tôi sẽ thảo luận trong phần tiếp theo.
Tương tự như với bố cục, quá trình thực thi onWrite()
phương thức có thể có ba kết quả: hoàn thành, huỷ hoặc không thành công trong trường hợp
không thể ghi nội dung. Bạn phải chỉ ra một trong các kết quả này bằng cách gọi hàm
phương thức thích hợp của đối tượng PrintDocumentAdapter.WriteResultCallback
.
Lưu ý: Kết xuất tài liệu để in có thể là một thao tác tốn nhiều tài nguyên. Trong
để tránh chặn luồng giao diện người dùng chính của ứng dụng, bạn nên cân nhắc
thực hiện thao tác kết xuất và ghi trang trên một chuỗi riêng biệt, ví dụ:
trong AsyncTask
.
Để biết thêm thông tin về cách xử lý các luồng thực thi như các tác vụ không đồng bộ,
xem Quy trình
và Threads (Luồng).
Vẽ nội dung trang PDF
Khi ứng dụng của bạn in, ứng dụng của bạn phải tạo tài liệu PDF và chuyển tài liệu đó đến
khung in của Android để in. Bạn có thể dùng bất kỳ thư viện tạo tệp PDF nào để tạo tệp PDF này
mục đích. Bài học này sẽ hướng dẫn cách sử dụng lớp PrintedPdfDocument
để tạo các trang PDF từ nội dung của bạn.
Lớp PrintedPdfDocument
sử dụng Canvas
để vẽ các phần tử trên một trang PDF, tương tự như vẽ trên bố cục hoạt động. Bạn có thể vẽ
các phần tử trên trang được in bằng phương thức vẽ Canvas
. Nội dung sau đây
mã ví dụ minh hoạ cách vẽ một số phần tử đơn giản trên một trang tài liệu PDF bằng cách sử dụng
phương thức:
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); }
Khi sử dụng Canvas
để vẽ trên trang PDF, các phần tử được chỉ định trong
điểm, tức là 1/72 inch. Đảm bảo rằng bạn sử dụng đơn vị đo lường này để chỉ định kích thước
phần tử trên trang. Để định vị các phần tử được vẽ, hệ toạ độ bắt đầu từ 0,0
cho góc trên cùng bên trái của trang.
Lưu ý: Mặc dù đối tượng Canvas
cho phép bạn in
nhiều phần tử trên cạnh của tài liệu PDF, nhiều máy in không thể in tới cạnh của
mảnh giấy vật lý. Đảm bảo rằng bạn tính đến cả những cạnh không in được của trang khi
bạn tạo tài liệu in bằng lớp này.