Mencetak dokumen kustom

Untuk beberapa aplikasi, seperti aplikasi menggambar, aplikasi tata letak halaman, dan aplikasi lain yang fokus pada output grafis, menciptakan halaman cetak yang indah adalah fitur utamanya. Dalam hal ini, tidak cukup untuk mencetak gambar atau dokumen HTML. Output cetak untuk jenis aplikasi ini memerlukan mengontrol secara akurat semua hal yang masuk ke dalam laman, termasuk {i>font<i}, alur teks, batas laman, {i>header<i}, {i>footer<i}, dan elemen grafis.

Membuat output cetak yang sepenuhnya disesuaikan untuk aplikasi Anda memerlukan lebih banyak lebih banyak daripada pendekatan yang telah dibahas sebelumnya. Anda harus membangun komponen yang berkomunikasi dengan kerangka kerja cetak, menyesuaikan pengaturan {i>printer<i}, menggambar elemen halaman dan mengelola pencetakan di beberapa halaman.

Pelajaran ini menunjukkan kepada Anda cara terhubung dengan pengelola cetak, membuat adaptor cetak, dan membuat konten untuk dicetak.

Saat aplikasi Anda mengelola proses pencetakan secara langsung, langkah pertama setelah menerima cetak dari pengguna adalah terhubung ke framework cetak Android dan mendapatkan instance dari class PrintManager. Class ini memungkinkan Anda melakukan inisialisasi tugas pencetakan dan memulai siklus proses pencetakan. Contoh kode berikut menunjukkan cara mendapatkan pengelola pencetakan 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. Tujuan implementasi class adaptor cetak dibahas di bagian berikutnya.

Catatan: Parameter terakhir di print() mengambil objek PrintAttributes. Anda dapat menggunakan parameter ini untuk memberikan petunjuk ke framework pencetakan dan opsi preset berdasarkan siklus pencetakan sebelumnya, sehingga meningkatkan pengalaman pengguna. Anda juga dapat menggunakan parameter ini untuk mengatur opsi yang lebih sesuai untuk konten yang dicetak, seperti menyetel orientasi ke lanskap saat mencetak foto dengan 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 mempengaruhi {i>output<i} akhir saat pengguna memilih {i>printer<i} dengan kemampuan output yang berbeda, ukuran halaman yang berbeda, atau orientasi halaman yang berbeda. Setelah pilihan ini dibuat, framework cetak akan meminta adaptor untuk menyusun dan membuat cetak dokumen, sebagai persiapan untuk {i>output<i} akhir. Setelah pengguna mengetuk tombol cetak, framework mengambil dokumen cetak akhir dan meneruskannya ke penyedia cetak untuk output. Selama pencetakan , pengguna dapat memilih membatalkan tindakan cetak, sehingga adaptor cetak Anda juga harus memproses dan menanggapi permintaan pembatalan.

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

  • onStart() - Menelepon sekali pada awal proses cetak. Jika aplikasi Anda memiliki tugas persiapan satu kali untuk lakukan, seperti mendapatkan {i>snapshot<i} dari data yang akan dicetak, jalankan di sini. Menerapkan metode ini dalam adaptor Anda tidak diperlukan.
  • onLayout() - Dipanggil setiap kali pengguna mengubah pengaturan cetak yang berdampak pada {i>output<i}, seperti ukuran halaman yang berbeda, atau orientasi halaman, yang memberi aplikasi Anda peluang untuk menghitung tata letak halaman yang akan dicetak. Minimal, metode ini harus menampilkan jumlah halaman yang diharapkan di dokumen yang dicetak.
  • onWrite() - Dipanggil untuk merender hasil cetak halaman menjadi file yang akan dicetak. Metode ini dapat dipanggil satu atau beberapa kali setelah masing-masing Panggilan onLayout().
  • onFinish() - Menelepon sekali di akhir dari proses cetak. Jika aplikasi Anda memiliki tugas pembongkaran satu kali untuk dilakukan, jalankan di sini. Menerapkan metode ini pada adaptor tidak diperlukan.

Bagian berikut ini menjelaskan cara mengimplementasikan tata letak dan metode penulisan, yang yang sangat penting untuk fungsi{i> <i}adaptor cetak.

Catatan: Metode adaptor ini dipanggil di thread utama aplikasi Anda. Jika Anda mengharapkan eksekusi metode ini dalam implementasi memerlukan banyak waktu waktu, menerapkannya untuk dieksekusi dalam thread terpisah. Misalnya, Anda dapat melakukan enkapsulasi menata letak atau mencetak tugas penulisan dokumen dalam objek AsyncTask terpisah.

Menghitung info dokumen cetak

Dalam implementasi class PrintDocumentAdapter, metode aplikasi harus dapat menentukan jenis dokumen yang dibuatnya dan menghitung jumlah total jumlah halaman untuk tugas pencetakan, berdasarkan informasi tentang ukuran halaman yang dicetak. Implementasi metode onLayout() di adaptor membuat perhitungan ini dan memberikan informasi tentang {i>output<i} yang diharapkan dari cetak 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 di mana perhitungan tata letak tidak dapat diselesaikan. Anda harus menunjukkan salah satu dari hasil ini dengan memanggil objek PrintDocumentAdapter.LayoutResultCallback.

Catatan: Parameter boolean Metode onLayoutFinished() menunjukkan apakah konten tata letak benar-benar telah berubah atau tidak sejak permintaan terakhir. Menyetel parameter ini dengan benar memungkinkan framework cetak menghindari memanggil metode onWrite() yang tidak perlu, melakukan {i>caching<i} dokumen cetak yang ditulis sebelumnya dan meningkatkan kinerja.

Pekerjaan utama onLayout() adalah menghitung jumlah halaman yang diharapkan sebagai {i>output<i} berdasarkan atribut {i>printer<i}. Cara Anda menghitung jumlah ini sangat bergantung pada cara aplikasi mengatur tata letak laman untuk pencetakan. Contoh kode berikut menunjukkan penerapan 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 menetapkan halaman mana yang harus ditulis dan file {i>output<i} yang akan digunakan. Implementasi Anda terhadap metode ini kemudian harus merender setiap halaman konten yang diminta ke file dokumen PDF multi-halaman. Ketika proses ini selesai, Anda memanggil metode onWriteFinished() dari objek callback.

Catatan: Framework cetak Android dapat memanggil metode onWrite() satu atau beberapa kali untuk setiap panggilan ke onLayout(). Untuk alasan ini, penting untuk mengatur parameter boolean onLayoutFinished() ke false jika tata letak konten cetak tidak berubah, untuk menghindari penulisan ulang dokumen cetak yang tidak perlu.

Catatan: Parameter boolean Metode onLayoutFinished() menunjukkan apakah konten tata letak benar-benar telah berubah atau tidak sejak permintaan terakhir. Menyetel parameter ini dengan benar memungkinkan framework cetak menghindari memanggil metode onLayout() yang tidak perlu, melakukan {i>caching<i} dokumen cetak yang ditulis sebelumnya dan meningkatkan kinerja.

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 drawPage() , yang akan dibahas di bagian berikutnya.

Seperti halnya tata letak, eksekusi 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 yang sesuai dari objek PrintDocumentAdapter.WriteResultCallback.

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

Menggambar konten halaman PDF

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

Class PrintedPdfDocument menggunakan Canvas untuk menggambar elemen pada halaman PDF, mirip dengan menggambar di tata letak aktivitas. Anda bisa menggambar pada halaman yang dicetak menggunakan metode gambar Canvas. Hal berikut kode contoh menunjukkan cara menggambar beberapa elemen sederhana pada halaman dokumen PDF menggunakan metode:

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 Anda menggunakan satuan ukuran ini untuk menentukan ukuran elemen pada halaman. Untuk penentuan posisi elemen yang digambar, sistem koordinat dimulai dari 0,0 di sudut kiri atas halaman.

Tips: Meskipun objek Canvas memungkinkan Anda melakukan pencetakan elemen di tepi dokumen PDF, banyak {i>printer<i} tidak dapat mencetak ke tepi dokumen sepotong kertas fisik. Pastikan bahwa Anda memperhitungkan tepi laman yang tidak dapat dicetak saat Anda akan membuat dokumen cetak dengan class ini.