In tài liệu tuỳ chỉnh

Đố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 đầu ra đồ hoạ, việc tạo các trang in đẹp mắt là một tính năng chính. Trong trường hợp này, việc in hình ảnh hoặc tài liệu HTML là chưa đủ. Kết quả in cho những loại ứng dụng này yêu cầu quyền kiểm soát chính xác mọi nội dung trên một trang, bao gồm cả phông chữ, luồng văn bản, ngắt trang, đầu trang, chân trang và thành phần đồ hoạ.

Việc tạo bản in được tuỳ chỉnh hoàn toàn cho ứng dụng của bạn yêu cầu đầu tư lập trình nhiều hơn so với các phương pháp đã thảo luận trước đó. Bạn phải tạo các thành phần giao tiếp với khung in, điều chỉnh chế độ cài đặt máy in, vẽ các phần tử 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ý máy in, tạo bộ điều hợp in và tạo nội dung để 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 từ 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 thấy cách tải trình quản lý in và bắt đầu quá 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à thiết lập một thực thể của lớp PrintDocumentAdapter giúp xử lý các bước trong vòng đời in. Việc 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 phương thức print() sẽ lấy đố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 đó, nhờ đó cải thiện trải nghiệm người dùng. Bạn cũng có thể dùng tham số này để đặt các tuỳ chọn phù hợp hơn với nội dung được in, chẳng hạn như đặt hướng thành hướng ngang khi in ảnh theo hướng đó.

Bộ điều hợp in sẽ 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à tuỳ chọn in trước khi tạo tài liệu để in. Những 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 khác nhau, kích thước trang khác nhau hoặc hướng trang khác nhau. Khi những lựa chọn này được đưa ra, khung in sẽ yêu cầu bộ chuyển đổi bố trí và tạo tài liệu in để chuẩn bị cho bản đầu ra cuối cùng. Sau khi người dùng nhấn vào nút in, khung sẽ lấy tài liệu in cuối cùng rồi chuyển tài liệu đó đến một nhà cung cấp dịch vụ in để xuất. Trong quá trình in, người dùng có thể chọn huỷ hành động in, vì vậy, bộ chuyển đổi máy 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ộ chuyển đổi in để tương tác đúng cách với khung in:

  • onStart() – Được gọi một lần vào đầu quá trình in. Nếu ứng dụng của bạn cần thực hiện bất kỳ nhiệm vụ chuẩn bị một lần nào, chẳng hạn như nhận thông tin tổng quan nhanh về dữ liệu sẽ được in, hãy thực thi các nhiệm vụ đó tại đây. Bạn không cần phải triển khai phương thức này trong bộ chuyển đổi.
  • onLayout() – Được gọi mỗi khi người dùng thay đổi một chế độ cài đặt in ảnh hưởng đến kết quả, chẳng hạn như một kích thước trang hoặc hướng trang khác, nhờ đó ứng dụng có cơ hội tính toán bố cục của các trang cần 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 được in.
  • onWrite() – Được gọi để hiển thị các trang đã 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ệnh gọi onLayout().
  • onFinish() – Được gọi một lần vào cuối quá trình in. Nếu ứng dụng của bạn có tác vụ chia nhỏ một lần cần thực hiện, hãy thực hiện các tác vụ đó tại đây. Bạn không cần 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à phương thức ghi. Đây là những yếu 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 thực thi các phương thức này trong quá trình triển khai sẽ mất một khoảng thời gian đáng kể, hãy triển khai các phương thức này để thực thi trong một luồng riêng. Ví dụ: bạn có thể đóng gói bố cục hoặc tác vụ in tài liệu 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 của bạn phải có khả năng chỉ định loại tài liệu mà ứng dụng đó đang tạo và tính toán tổng 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 trình chuyển đổi sẽ thực hiện các phép tính 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à 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.");
    }
}

Quá trình thực thi phương thức onLayout() có thể có 3 kết quả: hoàn thành, huỷ hoặc không thành công trong trường hợp không thể hoàn tất việc tính toán 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 phương thức thích hợp của đối tượng PrintDocumentAdapter.LayoutResultCallback.

Lưu ý: Tham số boolean của phương thức onLayoutFinished() cho biết liệu nội dung bố cục có thực sự thay đổi kể từ yêu cầu gần đây nhất hay khô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 đã viết 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 dự kiến sẽ được cung cấp dưới dạng kết quả đầu ra dựa trên các thuộc tính của máy in. Cách bạn tính số liệu này phụ thuộc nhiều vào cách ứng dụng của bạn bố trí các trang để in. Ví dụ về mã sau đây cho thấy cách triển khai mà số lượng 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 một tệp in tài liệu

Khi đến lúc ghi đầu ra in vào 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 tham số của phương thức chỉ định những trang cần được ghi và tệp đầu ra sẽ được sử dụng. Sau đó, khi triển khai phương thức này, bạn phải hiển thị từng trang nội dung được yêu cầu trong một tệp tài liệu PDF nhiều trang. Khi quá trình này hoàn tất, bạn sẽ 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 lệnh gọi đến onLayout(). Vì lý do này, bạn cần phải đặt tham số boolean của phương thức onLayoutFinished() thành false khi bố cục nội dung in không thay đổi, để tránh ghi 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 liệu nội dung bố cục có thực sự thay đổi kể từ yêu cầu gần đây nhất hay khô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 đã viết 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 phương thức drawPage(). Điều này sẽ được thảo luận trong phần tiếp theo.

Tương tự như bố cục, việc thực thi phương thức onWrite() có thể có 3 kết quả: hoàn tất, huỷ hoặc không thành công trong trường hợp không thể viết 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 phương thức thích hợp của đối tượng PrintDocumentAdapter.WriteResultCallback.

Lưu ý: Việc hiển thị tài liệu để in có thể là một thao tác tốn nhiều tài nguyên. Để 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 các thao tác ghi và kết xuất trang trên một luồng riêng biệt, chẳng hạn như trong AsyncTask. Để biết thêm thông tin về cách xử lý các luồng thực thi, chẳng hạn như các tác vụ không đồng bộ, hãy xem bài viết Quy trình và luồng.

Vẽ nội dung trang PDF

Khi in, ứng dụng của bạn phải tạo một tài liệu PDF và truyền tài liệu đó đến khung in của Android để in. Bạn có thể sử dụng bất kỳ thư viện tạo tệp PDF nào cho mục đích này. 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 đối tượng Canvas để vẽ các thành phần trên trang PDF, tương tự như việc vẽ trên bố cục hoạt động. Bạn có thể vẽ các thành phần trên trang đã in bằng các phương thức vẽ Canvas. Mã ví dụ sau đây minh hoạ cách vẽ một số thành phần đơn giản trên trang tài liệu PDF bằng các phương thức sau:

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 một trang PDF, các phần tử được chỉ định bằng các điểm (1/72 inch). Hãy nhớ sử dụng đơn vị đo lường này để chỉ định kích thước của các phần tử trên trang. Để định vị các phần tử được vẽ, hệ thống 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 đặt các phần tử in ở cạnh của tài liệu PDF, nhưng nhiều máy in không thể in ra cạnh của một mảnh giấy thực. Đảm bảo bạn có tính đến các cạnh không in được của trang khi tạo tài liệu in bằng lớp này.