Mencetak dokumen kustom

Untuk beberapa aplikasi, seperti aplikasi menggambar, aplikasi tata letak halaman, dan aplikasi lain yang berfokus pada output grafis, membuat halaman cetak yang indah adalah fitur utamanya. Dalam hal ini, mencetak gambar atau dokumen HTML tidaklah cukup. Output cetak untuk jenis aplikasi ini memerlukan kontrol yang akurat atas semua elemen yang ada di halaman, termasuk font, alur teks, batas halaman, header, footer, dan elemen grafis.

Membuat output cetak yang sepenuhnya disesuaikan untuk aplikasi Anda memerlukan lebih banyak investasi pemrograman daripada pendekatan yang telah dibahas sebelumnya. Anda harus mem-build komponen yang berkomunikasi dengan framework cetak, menyesuaikan dengan setelan printer, menggambar elemen halaman, dan mengelola pencetakan pada banyak halaman.

Tutorial ini menunjukkan cara terhubung dengan pengelola cetak, membuat adaptor cetak, dan membuat konten untuk dicetak.

Jika aplikasi Anda mengelola proses pencetakan secara langsung, langkah pertama setelah menerima permintaan cetak dari pengguna adalah menghubungkan ke framework cetak Android dan mendapatkan instance class PrintManager. Class ini memungkinkan Anda melakukan inisialisasi tugas pencetakan dan memulai siklus proses pencetakan. Contoh kode berikut menunjukkan cara mendapatkan pengelola cetak dan memulai proses pencetakan.

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); //
}

Kode contoh di atas menunjukkan cara memberi nama pekerjaan cetak dan menetapkan instance class PrintDocumentAdapter yang menangani langkah-langkah siklus proses pencetakan. Implementasi class adaptor cetak dibahas di bagian berikutnya.

Catatan: Parameter terakhir dalam metode print() mengambil objek PrintAttributes. Anda dapat menggunakan parameter ini untuk memberikan petunjuk bagi framework pencetakan dan opsi yang telah ditetapkan berdasarkan siklus pencetakan sebelumnya, sehingga meningkatkan pengalaman pengguna. Anda juga dapat menggunakan parameter ini untuk menyetel opsi yang lebih sesuai dengan konten yang akan dicetak, seperti menyetel orientasi ke lanskap saat mencetak foto dalam orientasi tersebut.

Adaptor cetak berinteraksi dengan framework cetak Android dan menangani langkah-langkah proses pencetakan. Proses ini mengharuskan pengguna memilih printer dan opsi cetak sebelum membuat dokumen untuk dicetak. Pilihan ini dapat memengaruhi output akhir saat pengguna memilih printer dengan beragam kemampuan output, ukuran halaman, atau orientasi halaman yang berbeda. Saat pilihan ini dibuat, framework cetak akan meminta adaptor untuk menata letak dan menghasilkan dokumen cetak, sebagai persiapan untuk output akhir. Setelah pengguna mengetuk tombol cetak, framework akan mengambil dokumen cetak akhir dan meneruskannya ke penyedia cetak untuk output. Selama proses pencetakan, pengguna dapat memilih untuk membatalkan tindakan cetak, sehingga adaptor cetak Anda juga harus memproses dan merespons permintaan pembatalan.

Class abstrak PrintDocumentAdapter dirancang untuk menangani siklus proses pencetakan dengan empat metode callback utama. Anda harus mengimplementasikan metode ini dalam adaptor cetak agar dapat berinteraksi secara benar dengan framework cetak:

  • onStart() - Dipanggil satu kali pada awal proses cetak. Jika aplikasi Anda memiliki tugas persiapan satu kali, seperti mengambil snapshot data yang akan dicetak, jalankan di sini. Menerapkan metode ini pada adaptor Anda tidak diperlukan.
  • onLayout() - Dipanggil setiap kali pengguna mengubah setelan cetak yang memengaruhi output, seperti ukuran halaman atau orientasi halaman yang berbeda, sehingga memberi aplikasi Anda kesempatan untuk menghitung tata letak halaman yang akan dicetak. Setidaknya, metode ini harus menampilkan jumlah halaman dokumen cetak yang diharapkan.
  • onWrite() - Dipanggil untuk merender halaman yang dicetak ke file yang akan dicetak. Metode ini dapat dipanggil satu atau beberapa kali setelah setiap panggilan onLayout().
  • onFinish() - Dipanggil satu kali pada akhir proses cetak. Jika aplikasi Anda memiliki tugas pembersihan satu kali, jalankan di sini. Menerapkan metode ini pada adaptor tidak diperlukan.

Bagian berikut menjelaskan cara menerapkan tata letak dan metode penulisan, yang sangat penting untuk fungsi adaptor cetak.

Catatan: Metode adaptor ini dipanggil di thread utama aplikasi Anda. Jika eksekusi metode ini dalam implementasi Anda memerlukan waktu yang cukup lama, implementasikan metode tersebut untuk dieksekusi dalam thread terpisah. Misalnya, Anda dapat merangkum tata letak atau mencetak pekerjaan menulis dokumen dalam objek AsyncTask terpisah.

Menghitung info dokumen cetak

Dalam implementasi class PrintDocumentAdapter, aplikasi Anda harus dapat menentukan jenis dokumen yang dibuatnya dan menghitung jumlah total halaman untuk tugas pencetakan, berdasarkan informasi tentang ukuran halaman yang dicetak. Implementasi metode onLayout() di adaptor membuat perhitungan ini dan memberikan informasi tentang output yang diharapkan dari tugas pencetakan di class PrintDocumentInfo, termasuk jumlah halaman dan jenis konten. Contoh kode berikut menunjukkan implementasi dasar metode onLayout() untuk 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.");
    }
}

Eksekusi metode onLayout() dapat memiliki tiga hasil: penyelesaian, pembatalan, atau kegagalan dalam kasus ketika penghitungan tata letak tidak dapat diselesaikan. Anda harus menunjukkan salah satu dari hasil ini dengan memanggil metode objek PrintDocumentAdapter.LayoutResultCallback yang sesuai.

Catatan: Parameter boolean pada metode onLayoutFinished() menunjukkan apakah konten tata letak benar-benar berubah sejak permintaan terakhir. Menyetel parameter ini dengan benar memungkinkan framework cetak menghindari pemanggilan metode onWrite() yang tidak perlu, sehingga hanya perlu meng-cache dokumen cetak yang ditulis sebelumnya dan meningkatkan performa.

Pekerjaan utama onLayout() adalah menghitung jumlah halaman yang diharapkan sebagai output berdasarkan atribut printer. Cara Anda menghitung jumlah ini sangat bergantung pada cara aplikasi mengatur tata letak halaman untuk dicetak. Contoh kode berikut menunjukkan implementasi dengan jumlah halaman ditentukan oleh orientasi cetak:

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);
}

Menulis file dokumen cetak

Jika tiba saatnya menulis output cetak untuk suatu file, framework cetak Android akan memanggil metode onWrite() dari class PrintDocumentAdapter aplikasi Anda. Parameter metode ini menentukan halaman mana yang harus ditulis dan file output yang akan digunakan. Implementasi Anda atas metode ini kemudian harus merender setiap halaman konten yang diminta menjadi file dokumen PDF multi-halaman. Setelah proses ini selesai, panggil metode onWriteFinished() objek callback.

Catatan: Framework cetak Android dapat memanggil metode onWrite() satu atau beberapa kali untuk setiap panggilan ke onLayout(). Oleh karena itu, penting untuk menyetel parameter boolean pada metode onLayoutFinished() ke false jika tata letak konten cetak tidak berubah, demi menghindari penulisan ulang dokumen cetak yang tidak perlu.

Catatan: Parameter boolean pada metode onLayoutFinished() menunjukkan apakah konten tata letak benar-benar berubah sejak permintaan terakhir. Menyetel parameter ini dengan benar memungkinkan framework cetak menghindari pemanggilan metode onLayout() yang tidak perlu, sehingga hanya perlu meng-cache dokumen cetak yang ditulis sebelumnya dan meningkatkan performa.

Contoh berikut menunjukkan mekanisme dasar dari proses ini menggunakan class PrintedPdfDocument untuk membuat file 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);

    ...
}

Contoh ini mendelegasikan rendering konten halaman PDF ke metode drawPage(), yang akan dibahas di bagian berikutnya.

Seperti tata letak, eksekusi metode onWrite() dapat memiliki tiga hasil: penyelesaian, pembatalan, atau kegagalan dalam kasus di mana konten tidak dapat ditulis. Anda harus menunjukkan salah satu dari hasil ini dengan memanggil metode objek PrintDocumentAdapter.WriteResultCallback yang sesuai.

Catatan: Merender dokumen untuk dicetak dapat menjadi operasi intensif resource. Untuk menghindari pemblokiran thread antarmuka pengguna utama pada aplikasi, sebaiknya pertimbangkan untuk melakukan operasi penulisan dan rendering halaman pada thread terpisah, misalnya di AsyncTask. Untuk mengetahui informasi selengkapnya tentang cara menangani thread eksekusi seperti tugas asinkron, lihat Proses dan Thread.

Menggambar konten halaman PDF

Saat aplikasi Anda mencetak, aplikasi tersebut harus menghasilkan dokumen PDF dan meneruskannya ke framework cetak Android untuk dicetak. Untuk tujuan ini, Anda dapat menggunakan library pembuatan PDF apa pun. Tutorial ini menunjukkan cara menggunakan class PrintedPdfDocument untuk membuat halaman PDF dari konten Anda.

Class PrintedPdfDocument menggunakan objek Canvas untuk menggambar elemen pada halaman PDF, mirip dengan menggambar pada tata letak aktivitas. Anda dapat menggambar elemen pada halaman yang dicetak menggunakan metode gambar Canvas. Kode contoh berikut menunjukkan cara menggambar beberapa elemen sederhana pada halaman dokumen PDF menggunakan metode ini:

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);
}

Saat menggunakan Canvas untuk menggambar pada halaman PDF, elemen ditentukan dalam titik, yaitu 1/72 inci. Pastikan Anda menggunakan satuan ukuran ini untuk menentukan ukuran elemen pada halaman. Untuk penempatan elemen yang digambar, sistem koordinat dimulai dari 0,0 untuk pojok kiri atas halaman.

Tips: Meskipun objek Canvas memungkinkan Anda menempatkan elemen cetak di tepi dokumen PDF, banyak printer yang tidak dapat mencetak hingga tepi kertas fisik. Pastikan Anda memperhitungkan tepi halaman yang tidak dapat dicetak saat membuat dokumen cetak dengan class ini.