Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Mencetak dokumen kustom

Untuk beberapa aplikasi, seperti aplikasi menggambar, aplikasi tata letak halaman, dan aplikasi lainnya yang berfokus pada output grafis, membuat halaman tercetak indah merupakan fitur utamanya. Dalam hal ini, bukan hanya sekadar mencetak gambar atau dokumen HTML. Output cetak untuk jenis-jenis aplikasi ini membutuhkan kontrol yang akurat untuk semua elemen yang ada dalam halaman, termasuk font, alur teks, batas halaman, header, footer, dan elemen grafis.

Membuat hasil cetak yang sepenuhnya disesuaikan untuk aplikasi Anda perlu lebih banyak investasi pemrograman daripada pendekatan yang sudah dibahas sebelumnya. Anda harus membuat komponen yang berkomunikasi dengan framework cetak, menyesuaikan 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 untuk menginisialisasi pekerjaan cetak 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 pada bagian selanjutnya.

Catatan: Parameter terakhir dalam metode print() memerlukan objek PrintAttributes. Parameter ini bisa Anda gunakan untuk memberikan petunjuk tentang 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 dengan orientasi itu.

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 karena pengguna memilih printer dengan beragam kemampuan output, ukuran halaman, atau orientasi halaman yang berbeda. Saat pilihan ini dibuat, framework cetak akan meminta adaptor untuk mengatur tata letak dan menghasilkan dokumen cetak, sebagai persiapan untuk output akhir. Setelah tombol cetak diketuk, framework mengambil dokumen cetak akhir dan meneruskannya ke penyedia cetak untuk output. Selama proses pencetakan, pengguna dapat memilih untuk membatalkan tindakan cetak, sehingga adaptor cetak juga harus memproses dan menanggapi permintaan pembatalan.

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

  • onStart() - Dipanggil sekali pada awal proses cetak. Jika aplikasi perlu melakukan tugas persiapan satu kali, seperti mengambil ringkasan data yang akan dicetak, jalankan di sini. Menerapkan metode ini pada adaptor Anda tidak diperlukan.
  • onLayout() - Dipanggil setiap kali pengguna mengubah setelan cetak yang berdampak pada output, seperti ukuran atau orientasi halaman yang berbeda, sehingga memberi aplikasi Anda peluang untuk menghitung tata letak halaman yang akan dicetak. Minimal, 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 sekali pada akhir proses cetak. Jika aplikasi memiliki tugas pembersihan satu kali, jalankan di sini. Menerapkan metode ini pada adaptor tidak diperlukan.

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

Catatan: Metode adaptor ini dipanggil di thread utama aplikasi Anda. Jika eksekusi metode ini dalam implementasi diperkirakan memerlukan waktu yang lama, implementasikan metode tersebut untuk dieksekusi di 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 pekerjaan cetak, berdasarkan informasi mengenai ukuran halaman yang dicetak. Implementasi metode onLayout() di adaptor membuat perhitungan ini dan memberikan informasi tentang output yang diharapkan dari pekerjaan cetak dalam 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 di mana perhitungan tata letak tidak dapat diselesaikan. Anda harus menunjukkan salah satu dari hasil ini dengan memanggil metode objek PrintDocumentAdapter.LayoutResultCallback yang tepat.

Catatan: Parameter boolean pada metode onLayoutFinished() menunjukkan apakah konten tata letak benar-benar berubah setelah 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 di mana 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 terhadap metode ini harus merender setiap halaman konten yang diminta menjadi file dokumen PDF multi-halaman. Setelah proses ini selesai, panggil metode onWriteFinished() dari objek callback.

Catatan: Framework cetak Android dapat memanggil metode onWrite() satu atau beberapa kali untuk setiap panggilan ke onLayout(). Dengan alasan ini, maka penting untuk menyetel parameter boolean pada metode onLayoutFinished() menjadi 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 setelah 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 dibahas pada bagian selanjutnya.

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 tersebut dengan memanggil metode objek PrintDocumentAdapter.WriteResultCallback yang tepat.

Catatan: Merender dokumen untuk dicetak bisa menjadi operasi intensif resource. Sebagai upaya menghindari pemblokiran thread antarmuka pengguna utama pada aplikasi, Anda harus pertimbangkan melakukan operasi menulis dan merender halaman pada thread terpisah, misalnya pada AsyncTask. Guna mengetahui informasi selengkapnya 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 keperluan ini, Anda bisa menggunakan library pembuatan PDF apa pun. Tutorial ini menunjukkan cara menggunakan class PrintedPdfDocument guna menghasilkan 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 poin, yaitu 1/72 inci. Pastikan menggunakan unit ukuran ini untuk menentukan ukuran elemen pada halaman. Untuk penentuan posisi elemen yang digambar, sistem koordinat dimulai dengan 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 lembar kertas fisik. Pastikan Anda memperhitungkan tepi halaman yang tidak dapat dicetak saat membuat dokumen cetak dengan class ini.