طباعة المستندات المخصصة

إنّ إنشاء صفحات مطبوعة جميلة هي ميزة أساسية في بعض التطبيقات، مثل تطبيقات الرسم وتطبيقات تنسيق الصفحات والتطبيقات الأخرى التي تركّز على إخراج الرسومات. وفي هذه الحالة، لا يكفي طباعة صورة أو مستند HTML. فإخراج الطباعة لهذه الأنواع من التطبيقات يتطلب تحكُّمًا دقيقًا في كل ما يدخل في الصفحة، بما في ذلك الخطوط وتدفق النص وفواصل الصفحات والعناوين والتذييلات والعناصر الرسومية.

إنّ إنشاء مخرجات مطبوعة مخصّصة بالكامل لتطبيقك يتطلّب استثمارًا في البرمجة أكبر من الأساليب التي تمت مناقشتها سابقًا. عليك إنشاء مكونات تتصل بإطار عمل الطباعة، وأن تضبط مع إعدادات الطابعة وترسم عناصر الصفحات وتدير الطباعة على صفحات متعددة.

يوضّح هذا الدرس كيفية الاتصال بمدير الطباعة وإنشاء محوّل طباعة وإنشاء محتوى للطباعة.

عندما يدير تطبيقك عملية الطباعة مباشرةً، تكون الخطوة الأولى بعد تلقّي طلب طباعة من المستخدم هي الربط بإطار عمل الطباعة من Android والحصول على مثيل من الفئة PrintManager. تتيح لك هذه الفئة إعداد مهمة الطباعة وبدء دورة حياة الطباعة. يوضّح مثال الرمز التالي طريقة الحصول على مدير الطباعة وبدء عملية الطباعة.

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

يوضّح الرمز المذكور أعلاه كيفية تسمية مهمة طباعة وضبط مثيل للفئة PrintDocumentAdapter التي تعالج خطوات مراحل عملية الطباعة. وستتم مناقشة كيفية استخدام فئة محوّل الطباعة في القسم التالي.

ملاحظة: تستخدم المعلَمة الأخيرة في الطريقة print() كائن PrintAttributes. ويمكنك استخدام هذه المَعلمة لتقديم تلميحات حول إطار عمل الطباعة وخيارات محدَّدة مسبقًا استنادًا إلى دورة الطباعة السابقة، وبالتالي تحسين تجربة المستخدم. يمكنك أيضًا استخدام هذه المَعلمة لضبط الخيارات الأكثر ملاءمة للمحتوى الذي تتم طباعته، مثل ضبط الاتجاه على الوضع الأفقي عند طباعة صورة بهذا الاتجاه.

يتفاعل محوِّل الطباعة مع إطار عمل الطباعة من Android ويعالج خطوات عملية الطباعة. تتطلّب هذه العملية من المستخدمين تحديد الطابعات وخيارات الطباعة قبل إنشاء مستند للطباعة. يمكن أن تؤثّر هذه الاختيارات في الإخراج النهائي عندما يختار المستخدم طابعات ذات إمكانات إخراج مختلفة أو أحجام صفحات مختلفة أو اتجاهات مختلفة للصفحات. عند إجراء هذه الاختيارات، يطلب إطار عمل الطباعة من المحوّل تنسيق وإنشاء مستند طباعة استعدادًا للإخراج النهائي. وبعد أن ينقر المستخدم على زر الطباعة، يأخذ إطار العمل مستند الطباعة النهائي ويمرّره إلى موفّر الطباعة لإخراجه. أثناء عملية الطباعة، يمكن للمستخدمين اختيار إلغاء إجراء الطباعة، بحيث يجب أن ينتظر محوّل الطباعة أيضًا الاستجابة لطلبات الإلغاء.

تم تصميم فئة مجرّد PrintDocumentAdapter للتعامل مع مراحل نشاط الطباعة، والتي تتضمن أربع طرق رئيسية لمعاودة الاتصال. يجب تطبيق هذه الطرق في محوّل الطباعة للتفاعل بشكل صحيح مع إطار عمل الطباعة:

  • onStart(): يتم طلب هذه السمة مرة واحدة في بداية عملية الطباعة. إذا كان تطبيقك يتضمّن أي مهام تحضيرية تُستخدم مرة واحدة لتنفيذها، مثل الحصول على لقطة من البيانات المُراد طباعتها، يمكنك تنفيذها هنا. وليس من المطلوب تطبيق هذه الطريقة في المحوِّل.
  • onLayout(): يتم استدعاء هذه الطريقة في كل مرة يغيّر فيها المستخدم أحد إعدادات الطباعة، ما يؤثر في النتيجة، مثل اختلاف حجم الصفحة أو اتجاه الصفحة، ما يمنح تطبيقك فرصة لحساب تنسيق الصفحات التي ستتم طباعتها. وكحد أدنى، يجب أن تعرض هذه الطريقة عدد الصفحات المتوقعة في المستند المطبوع.
  • onWrite(): تتم استدعاء هذه الطريقة لعرض الصفحات المطبوعة في ملف مطلوب طباعته. يمكن طلب هذه الطريقة مرة واحدة أو أكثر بعد كل مكالمة onLayout().
  • onFinish(): يتم طلب هذه السمة مرة واحدة في نهاية عملية الطباعة. إذا كان تطبيقك يتضمن أي مهام نهائية لمرة واحدة يجب تنفيذها، يمكنك تنفيذها هنا. وليس من المطلوب تطبيق هذه الطريقة في المحوِّل.

توضّح الأقسام التالية طريقة تنفيذ التنسيق وأساليب الكتابة، وهما مهمتان لعمل محوّل الطباعة.

ملاحظة: يتم استدعاء طرق المحوِّل هذه في سلسلة التعليمات الرئيسية لتطبيقك. إذا كنت تتوقع أن يستغرق تنفيذ هذه الطرق في عملية التنفيذ وقتًا طويلاً، عليك تنفيذها ضمن سلسلة محادثات منفصلة. على سبيل المثال، يمكنك تغليف التنسيق أو عمل كتابة المستند في كائنات AsyncTask منفصلة.

حساب معلومات مستند الطباعة

في إطار تنفيذ الفئة PrintDocumentAdapter، يجب أن يكون تطبيقك قادرًا على تحديد نوع المستند الذي يتم إنشاؤه واحتساب إجمالي عدد الصفحات المطلوبة لمهمة الطباعة، مع الأخذ في الاعتبار معلومات حول حجم الصفحة المطبوعة. يؤدي تطبيق طريقة onLayout() في المحوِّل إلى إجراء هذه العمليات الحسابية وتوفير معلومات عن الإخراج المتوقّع لمهمة الطباعة في فئة PrintDocumentInfo، بما في ذلك عدد الصفحات ونوع المحتوى. يوضّح مثال الرمز التالي التنفيذ الأساسي لطريقة onLayout() في 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.");
    }
}

يمكن أن يؤدي تنفيذ طريقة onLayout() إلى ثلاث نتائج: الإكمال أو الإلغاء أو الإخفاق في حال تعذّر إكمال حساب التصميم. ويجب الإشارة إلى إحدى هذه النتائج من خلال استدعاء الطريقة المناسبة للكائن PrintDocumentAdapter.LayoutResultCallback.

ملاحظة: تشير المعلَمة المنطقية للطريقة onLayoutFinished() إلى ما إذا كان محتوى التنسيق قد تغيّر منذ آخر طلب أم لا. يؤدي ضبط هذه المَعلمة بشكل صحيح إلى السماح لإطار عمل الطباعة بتجنُّب طلب طريقة onWrite() بدون داعٍ، ما يؤدي بشكل أساسي إلى التخزين المؤقت للمستند المطبوع الذي تمت كتابته سابقًا وتحسين الأداء.

يعتمد عمل onLayout() الرئيسي على احتساب عدد الصفحات المتوقّعة كنتائج استنادًا إلى سمات الطابعة. وتعتمد طريقة احتساب هذا العدد بشكل كبير على طريقة تخطيط تطبيقك للصفحات المخصّصة لطباعة الصفحات. يوضّح مثال الرمز التالي عملية تنفيذ يتم فيها تحديد عدد الصفحات حسب اتجاه الطباعة:

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

كتابة ملف طباعة

عندما يحين وقت كتابة إخراج الطباعة إلى ملف، يطلب إطار عمل الطباعة في Android طريقة onWrite() من فئة PrintDocumentAdapter لتطبيقك. تحدِّد مَعلمات الطريقة الصفحات التي يجب كتابتها وملف الإخراج المطلوب استخدامه. يجب أن يؤدي تنفيذ هذه الطريقة إلى عرض كل صفحة محتوى مطلوبة في ملف PDF متعدِّد الصفحات. عند اكتمال هذه العملية، يتم استدعاء الطريقة onWriteFinished() لكائن استدعاء الدالة.

ملاحظة: قد يطلب إطار عمل الطباعة في Android طريقة onWrite() مرة واحدة أو أكثر لكل اتصال برقم onLayout(). لهذا السبب، من المهم ضبط المَعلمة المنطقية للطريقة onLayoutFinished() على false في حال عدم تغيير تنسيق محتوى الطباعة، وذلك لتجنُّب عمليات إعادة كتابة مستند الطباعة غير الضروري.

ملاحظة: تشير المعلَمة المنطقية للطريقة onLayoutFinished() إلى ما إذا كان محتوى التنسيق قد تغيّر منذ آخر طلب أم لا. يؤدي ضبط هذه المَعلمة بشكل صحيح إلى السماح لإطار عمل الطباعة بتجنُّب طلب طريقة onLayout() بدون داعٍ، ما يؤدي بشكل أساسي إلى التخزين المؤقت للمستند المطبوع الذي تمت كتابته سابقًا وتحسين الأداء.

يوضّح النموذج التالي الآليات الأساسية لهذه العملية باستخدام الفئة PrintedPdfDocument لإنشاء ملف 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);

    ...
}

يستخدم هذا النموذج الذي ينفّذه المستخدم طريقة عرض محتوى صفحة PDF وفقًا لطريقة drawPage()، الموضَّحة في القسم التالي.

كما هي الحال في التصميم، يمكن أن يؤدي تنفيذ طريقة onWrite() إلى ثلاث نتائج: الإكمال أو الإلغاء أو الإخفاق في حالة عدم إمكانية كتابة المحتوى. ويجب الإشارة إلى إحدى هذه النتائج من خلال استدعاء الطريقة المناسبة للكائن PrintDocumentAdapter.WriteResultCallback.

ملاحظة: قد يؤدي عرض مستند لطباعته إلى استغراق عملية مكثفة للموارد. لتجنّب حظر سلسلة محادثات واجهة المستخدم الرئيسية في تطبيقك، ننصحك بإجراء عمليات عرض الصفحة وكتابتها في سلسلة محادثات منفصلة، مثل AsyncTask. لمزيد من المعلومات عن التعامل مع سلاسل التنفيذ مثل المهام غير المتزامنة، راجِع العمليات وسلاسل المحادثات.

رسم محتوى صفحة PDF

عند طباعة الطلب، يجب أن ينشئ التطبيق مستند PDF ويمرره إلى إطار عمل الطباعة في Android. يمكنك استخدام أي مكتبة من مكتبات إنشاء ملفات PDF لهذا الغرض. يوضّح هذا الدرس طريقة استخدام الصف PrintedPdfDocument لإنشاء صفحات PDF من المحتوى الخاص بك.

وتستخدم الفئة PrintedPdfDocument الكائن Canvas لرسم عناصر على صفحة PDF، على غرار الرسم على تنسيق نشاط. يمكنك رسم العناصر على الصفحة المطبوعة باستخدام طرق الرسم Canvas. يوضّح المثال التالي الرمز البرمجي طريقة رسم بعض العناصر البسيطة على صفحة مستند PDF باستخدام الطرق التالية:

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

عند استخدام Canvas للرسم على صفحة PDF، يتم تحديد العناصر بنقاط، أي 1/72 بوصة. احرص على استخدام وحدة القياس هذه لتحديد حجم العناصر على الصفحة. لتحديد موضع العناصر المرسومة، يبدأ نظام الإحداثيات عند 0,0 في أعلى يمين الصفحة.

ملاحظة: يسمح لك الكائن Canvas بوضع عناصر الطباعة على حافة مستند PDF، إلا أنّه يتعذّر على العديد من الطابعات الطباعة على حافة قطعة ورقية. احرص على مراعاة الحواف غير القابلة للطباعة في الصفحة عند إنشاء مستند مطبوع باستخدام هذا الصف.