הדפסת מסמכים מותאמים אישית

לאפליקציות מסוימות, כמו אפליקציות לשרטוט, אפליקציות לפריסת דפים ואפליקציות אחרות שמתמקדות פלט גרפי, יצירת דפים מודפסים יפהפיים היא תכונה חשובה. במקרה הזה, לא מספיק כדי להדפיס תמונה או מסמך 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() ה-method לוקחת אובייקט PrintAttributes. אפשר להשתמש בפרמטר הזה כדי לספק רמזים למסגרת ההדפסה ואפשרויות מוגדרות מראש בהתאם למחזור ההדפסה הקודם, וכך לשפר את חוויית המשתמש. תוכלו להשתמש בפרמטר הזה גם כדי להגדיר אפשרויות התאמה טובה יותר לתוכן המודפס, למשל הגדרת כיוון לרוחב כשמדפיסים תמונה בכיוון הזה.

מתאם הדפסה יוצר אינטראקציה עם מסגרת ההדפסה של Android ומטפל בשלבים תהליך ההדפסה. התהליך הזה מחייב את המשתמשים לבחור מדפסות ואפשרויות הדפסה לפני היצירה מסמך להדפסה. הבחירות האלה יכולות להשפיע על הפלט הסופי במהלך בחירת המשתמש מדפסות עם יכולות פלט שונות, דפים בגדלים שונים או בכיוונים שונים של דפים. כשבוחרים באפשרויות האלה, מסגרת ההדפסה מבקשת מהמתאם לצאת וליצור כהכנה לקבלת הפלט הסופי. כשמשתמש מקיש על לחצן ההדפסה, המסגרת לוקחת את המסמך הסופי המודפס ומעבירה אותו לספק ההדפסה לצורך פלט. במהלך ההדפסה המשתמשים יכולים לבחור לבטל את פעולת ההדפסה, כך שמתאם ההדפסה חייב גם ולהגיב לבקשות ביטול.

המחלקה המופשטת PrintDocumentAdapter תוכננה לטפל מחזור החיים של הדפסה, שכולל ארבע שיטות עיקריות של קריאה חוזרת. צריך להטמיע את השיטות האלה במתאם ההדפסה כדי לקיים אינטראקציה תקינה עם מסגרת ההדפסה:

  • onStart() - התקשרת פעם אחת ב תחילתו של תהליך ההדפסה. אם יש באפליקציה משימות הכנה חד-פעמיות, מבצעים, כמו קבלת תמונת מצב של הנתונים שרוצים להדפיס, מפעילים אותם כאן. הטמעה השיטה הזו במתאם לא נדרשת.
  • onLayout() - התקשרות בכל פעם משתמש משנה הגדרת הדפסה שמשפיעה על הפלט, למשל גודל דף שונה, או בכיוון הדף, באופן שמאפשר לאפליקציה לחשב את הפריסה דפים שיודפסו. לכל הפחות, השיטה הזו חייבת להחזיר את מספר הדפים הצפויים במסמך המודפס.
  • onWrite() – בוצעה שיחה כדי לעבד את ההדפסה לדפים בקובץ שרוצים להדפיס. ניתן לקרוא לשיטה הזו פעם אחת או יותר אחרי כל שיחת onLayout().
  • onFinish() – שיחה אחת בסוף בתהליך ההדפסה. אם יש באפליקציה משימות ניתוח חד-פעמיות שצריך לבצע, לבצע אותן כאן. אין צורך להטמיע את השיטה הזו במתאם.

בקטעים הבאים מוסבר איך להטמיע את שיטות הפריסה והכתיבה. קריטי לתפקוד של מתאם הדפסה.

הערה: שיטות המתאמים האלה מופעלות ב-thread הראשי של האפליקציה. אם המיקום אתם מצפים שהביצוע של השיטות האלה בהטמעה שלכם ידרוש זמן, להטמיע אותן כדי לבצע אותן בשרשור נפרד. לדוגמה, אפשר להקיף את פריסה או הדפסה של מסמכים באובייקטים נפרדים מסוג AsyncTask.

פרטי מסמך להדפסת נתונים

בתוך הטמעה של הכיתה PrintDocumentAdapter, היישום חייב להיות מסוגל לציין את סוג המסמך שהוא יוצר ולחשב את הסכום הכולל מספר הדפים למשימת הדפסה, בהינתן מידע על גודל הדף שהודפס. ההטמעה של השיטה onLayout() ב והמתאם מבצע את החישובים האלה ומספק מידע על הפלט הצפוי משימת הדפסה במחלקה PrintDocumentInfo, כולל מספר העמודים סוג התוכן. הקוד לדוגמה הבא מציג הטמעה בסיסית של ה-method 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 מרובה דפים. כשהתהליך הזה יסתיים, קוראים ל-method onWriteFinished() של אובייקט הקריאה החוזרת.

הערה: יכול להיות שמסגרת Android להדפסה ב-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 המתאים.

הערה: עיבוד מסמך להדפסה יכול להיות פעולה שצורכת משאבים רבים. לחשבון כדי למנוע את חסימת ה-thread הראשי של ממשק המשתמש של האפליקציה, כדאי לשקול לבצע פעולות עיבוד וכתיבה בשרשור בשרשור נפרד, בAsyncTask. למידע נוסף על עבודה עם שרשורי ביצוע כמו משימות אסינכרוניות: למידע נוסף, ניתן לעיין בקטע תהליכים ו-Threads.

ציור תוכן של דף 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, בהרבה מדפסות לא ניתן להדפיס פיסת נייר פיזית. חשוב לשים לב לקצוות של הדף שלא ניתנים להדפסה תבנה מסמך לדפוס באמצעות הכיתה הזו.