چاپ اسناد سفارشی

برای برخی از برنامه‌ها، مانند برنامه‌های طراحی، برنامه‌های صفحه‌آرایی و سایر برنامه‌هایی که بر خروجی گرافیکی تمرکز دارند، ایجاد صفحات چاپی زیبا یک ویژگی کلیدی است. در این مورد، چاپ یک تصویر یا یک سند HTML کافی نیست. خروجی چاپ برای این نوع برنامه ها نیازمند کنترل دقیق هر چیزی است که در یک صفحه قرار می گیرد، از جمله فونت، جریان متن، شکستگی صفحه، سرصفحه، پاورقی و عناصر گرافیکی.

ایجاد خروجی چاپی که کاملاً برای برنامه شما سفارشی شده باشد، به سرمایه گذاری برنامه نویسی بیشتری نسبت به رویکردهای مورد بحث قبلی نیاز دارد. شما باید اجزایی بسازید که با چارچوب چاپ ارتباط برقرار کنند، تنظیمات چاپگر را تنظیم کنند، عناصر صفحه را ترسیم کنند و چاپ را در چندین صفحه مدیریت کنند.

این درس به شما نشان می‌دهد که چگونه با مدیر چاپ ارتباط برقرار می‌کنید، یک آداپتور چاپ ایجاد می‌کنید و محتوایی برای چاپ می‌سازید.

هنگامی که برنامه شما مستقیماً فرآیند چاپ را مدیریت می کند، اولین مرحله پس از دریافت درخواست چاپ از طرف کاربر، اتصال به فریمورک چاپ اندروید و دریافت نمونه ای از کلاس PrintManager است. این کلاس به شما امکان می دهد تا یک کار چاپ را مقداردهی اولیه کنید و چرخه عمر چاپ را شروع کنید. مثال کد زیر نحوه دریافت مدیر چاپ و شروع فرآیند چاپ را نشان می دهد.

کاتلین

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

جاوا

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 می گیرد. می توانید از این پارامتر برای ارائه نکاتی به چارچوب چاپ و گزینه های از پیش تنظیم شده بر اساس چرخه چاپ قبلی استفاده کنید و در نتیجه تجربه کاربر را بهبود ببخشید. همچنین می‌توانید از این پارامتر برای تنظیم گزینه‌هایی استفاده کنید که مناسب‌تر با محتوای در حال چاپ هستند، مانند تنظیم جهت روی افقی هنگام چاپ عکسی که در آن جهت است.

یک آداپتور چاپ با چارچوب چاپ اندروید تعامل دارد و مراحل فرآیند چاپ را انجام می دهد. این فرآیند به کاربران نیاز دارد تا قبل از ایجاد سند برای چاپ، چاپگرها و گزینه های چاپ را انتخاب کنند. این انتخاب‌ها می‌توانند بر خروجی نهایی تأثیر بگذارند زیرا کاربر چاپگرهایی با قابلیت‌های خروجی متفاوت، اندازه‌های مختلف صفحه یا جهت‌گیری‌های مختلف صفحه انتخاب می‌کند. همانطور که این انتخاب ها انجام می شود، چارچوب چاپ از آداپتور شما می خواهد که برای آماده سازی برای خروجی نهایی، یک سند چاپی را طراحی و تولید کند. هنگامی که کاربر روی دکمه چاپ ضربه می زند، چارچوب سند چاپی نهایی را می گیرد و برای خروجی به ارائه دهنده چاپ می دهد. در طول فرآیند چاپ، کاربران می‌توانند عمل چاپ را لغو کنند، بنابراین آداپتور چاپ شما نیز باید به درخواست‌های لغو گوش دهد و به آن واکنش نشان دهد.

کلاس انتزاعی PrintDocumentAdapter برای مدیریت چرخه عمر چاپ طراحی شده است که دارای چهار روش اصلی بازگشت به تماس است. شما باید این روش ها را در آداپتور چاپ خود پیاده سازی کنید تا به درستی با چارچوب چاپ تعامل داشته باشید:

  • onStart() - یک بار در ابتدای فرآیند چاپ فراخوانی می شود. اگر برنامه شما وظایف آماده سازی یکباره ای برای انجام دارد، مانند گرفتن یک عکس فوری از داده هایی که باید چاپ شوند، آنها را در اینجا اجرا کنید. اجرای این روش در آداپتور شما لازم نیست.
  • onLayout() - هر بار که کاربر تنظیمات چاپی را تغییر می‌دهد که بر خروجی تأثیر می‌گذارد، مانند اندازه صفحه یا جهت صفحه متفاوت، فراخوانی می‌شود و به برنامه شما فرصتی می‌دهد تا طرح‌بندی صفحاتی که قرار است چاپ شود را محاسبه کند. حداقل، این روش باید تعداد صفحات مورد انتظار در سند چاپ شده را برگرداند.
  • onWrite() - برای رندر کردن صفحات چاپ شده در فایلی که باید چاپ شود فراخوانی می شود. این متد ممکن است یک یا چند بار پس از هر فراخوانی onLayout() فراخوانی شود.
  • onFinish() - یک بار در پایان فرآیند چاپ فراخوانی می شود. اگر برنامه شما کارهایی برای حذف یکباره دارد، آنها را در اینجا اجرا کنید. اجرای این روش در آداپتور شما لازم نیست.

بخش‌های زیر نحوه پیاده‌سازی روش‌های طرح‌بندی و نوشتن را که برای عملکرد یک آداپتور چاپ ضروری هستند، توضیح می‌دهد.

توجه: این متدهای آداپتور در رشته اصلی برنامه شما فراخوانی می شوند. اگر انتظار دارید که اجرای این روش ها در پیاده سازی شما زمان قابل توجهی داشته باشد، آنها را برای اجرا در یک رشته مجزا پیاده سازی کنید. برای مثال، می‌توانید طرح‌بندی را کپسوله کنید یا کار نوشتن سند را در اشیاء جداگانه AsyncTask چاپ کنید.

اطلاعات سند چاپی را محاسبه کنید

در یک پیاده‌سازی از کلاس PrintDocumentAdapter ، برنامه شما باید بتواند نوع سندی را که ایجاد می‌کند مشخص کند و با توجه به اطلاعات اندازه صفحه چاپ شده، تعداد کل صفحات را برای کار چاپ محاسبه کند. پیاده سازی متد onLayout() در آداپتور این محاسبات را انجام می دهد و اطلاعاتی در مورد خروجی مورد انتظار کار چاپ در یک کلاس PrintDocumentInfo ، از جمله تعداد صفحات و نوع محتوا، ارائه می دهد. مثال کد زیر یک پیاده سازی اساسی از متد onLayout() برای PrintDocumentAdapter نشان می دهد:

کاتلین

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.")
    }
}

جاوا

@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() محاسبه تعداد صفحاتی است که با توجه به ویژگی های چاپگر به عنوان خروجی انتظار می رود. نحوه محاسبه این عدد به شدت به نحوه چیدمان صفحات برای چاپ توسط برنامه شما بستگی دارد. مثال کد زیر پیاده سازی را نشان می دهد که در آن تعداد صفحات با جهت چاپ تعیین می شود:

کاتلین

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

جاوا

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

یک فایل سند چاپی بنویسید

هنگامی که زمان نوشتن خروجی چاپ در یک فایل فرا می رسد، چارچوب چاپ اندروید متد onWrite() کلاس PrintDocumentAdapter برنامه شما را فراخوانی می کند. پارامترهای متد مشخص می کند که کدام صفحات باید نوشته شوند و فایل خروجی مورد استفاده قرار گیرد. سپس اجرای این روش شما باید هر صفحه از محتوای درخواستی را به یک فایل سند PDF چند صفحه ای تبدیل کند. هنگامی که این فرآیند کامل شد، متد onWriteFinished() شیء برگشت را فراخوانی می کنید.

توجه: چارچوب چاپ Android ممکن است متد onWrite() یک یا چند بار برای هر تماس با onLayout() فراخوانی کند. به همین دلیل، تنظیم پارامتر بولی متد onLayoutFinished() روی false زمانی که طرح محتوای چاپی تغییر نکرده است، مهم است تا از نوشتن مجدد غیرضروری سند چاپی جلوگیری شود.

توجه: پارامتر بولی متد onLayoutFinished() نشان می دهد که آیا محتوای طرح بندی واقعاً از آخرین درخواست تغییر کرده است یا خیر. تنظیم صحیح این پارامتر به چارچوب چاپ اجازه می دهد تا از فراخوانی غیرضروری متد onLayout() جلوگیری کند، اساساً سند چاپی قبلاً نوشته شده را در حافظه پنهان نگه می دارد و عملکرد را بهبود می بخشد.

نمونه زیر مکانیک اصلی این فرآیند را با استفاده از کلاس PrintedPdfDocument برای ایجاد یک فایل PDF نشان می دهد:

کاتلین

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)

    ...
}

جاوا

@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() واگذار می کند که در بخش بعدی مورد بحث قرار می گیرد.

همانند layout، اجرای متد onWrite() می‌تواند سه نتیجه داشته باشد: تکمیل، لغو یا شکست در مواردی که محتوا نمی‌تواند نوشته شود. شما باید یکی از این نتایج را با فراخوانی متد مناسب شی PrintDocumentAdapter.WriteResultCallback نشان دهید.

توجه: رندر کردن یک سند برای چاپ می تواند عملیاتی با منابع فشرده باشد. برای جلوگیری از مسدود کردن رشته رابط کاربری اصلی برنامه خود، باید عملیات رندر و نوشتن صفحه را در یک رشته جداگانه انجام دهید، به عنوان مثال در AsyncTask . برای اطلاعات بیشتر در مورد کار با رشته های اجرایی مانند وظایف ناهمزمان، به فرآیندها و موضوعات مراجعه کنید.

ترسیم محتوای صفحه PDF

هنگامی که برنامه شما چاپ می شود، برنامه شما باید یک سند PDF تولید کند و آن را برای چاپ به چارچوب چاپ اندروید ارسال کند. برای این منظور می توانید از هر کتابخانه تولید PDF استفاده کنید. این درس نشان می دهد که چگونه از کلاس PrintedPdfDocument برای تولید صفحات PDF از محتوای خود استفاده کنید.

کلاس PrintedPdfDocument از یک شی Canvas برای ترسیم عناصر در صفحه PDF استفاده می کند، شبیه به طراحی روی یک طرح بندی فعالیت. با استفاده از روش های ترسیم Canvas می توانید عناصر را در صفحه چاپ شده ترسیم کنید. کد مثال زیر نحوه ترسیم برخی از عناصر ساده در صفحه سند PDF را با استفاده از این روش ها نشان می دهد:

کاتلین

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

جاوا

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 قرار دهید، بسیاری از چاپگرها قادر به چاپ روی لبه یک تکه کاغذ فیزیکی نیستند. هنگام ساختن یک سند چاپی با این کلاس، مطمئن شوید که لبه های غیرقابل چاپ صفحه را در نظر بگیرید.