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 đồ 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.

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 đó.

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ọi onLayout().
  • 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.