Drukowanie dokumentów niestandardowych

W przypadku niektórych aplikacji, takich jak aplikacje do rysowania, układy stron i inne aplikacje, których główną funkcją jest grafika, tworzenie atrakcyjnych stron drukowanych jest kluczową funkcją. W takim przypadku nie wystarczy wydrukować obraz lub dokument HTML. Drukowanie w takich aplikacjach wymaga dokładnej kontroli nad wszystkim, co znajduje się na stronie, w tym nad czcionkami, przepływem tekstu, podziałami stron, nagłówkami, stopkami i elementami graficznymi.

Tworzenie materiałów drukowanych, które są w pełni dostosowane do danej aplikacji, wymaga więcej inwestycji w programowanie niż omówione wcześniej metody. Musisz utworzyć komponenty, które komunikują się z platformą drukowania, dostosowują się do ustawień drukarki, rysują elementy stron i zarządzają drukowaniem na wielu stronach.

Z tej lekcji dowiesz się, jak nawiązać połączenie z menedżerem drukowania, utworzyć adapter wydruku i tworzyć treści do drukowania.

Gdy Twoja aplikacja bezpośrednio zarządza procesem drukowania, pierwszym krokiem po otrzymaniu żądania wydruku od użytkownika jest połączenie z platformą Android Print i uzyskanie instancji klasy PrintManager. Ta klasa umożliwia zainicjowanie zadania drukowania i rozpoczęcie cyklu życia drukowania. Poniższy przykładowy kod pokazuje, jak pobrać menedżera drukowania i rozpocząć proces drukowania.

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

Przykładowy kod powyżej pokazuje, jak nazwać zadanie drukowania i ustawić wystąpienie klasy PrintDocumentAdapter, która obsługuje kroki cyklu życia drukowania. Implementacja klasy adaptera wydruku została omówiona w następnej sekcji.

Uwaga: ostatni parametr w metodzie print() pobiera obiekt PrintAttributes. Możesz używać tego parametru, aby podawać wskazówki dotyczące platformy drukowania i gotowych opcji na podstawie poprzedniego cyklu drukowania, aby poprawić wygodę użytkowników. Za pomocą tego parametru możesz też ustawić opcje bardziej odpowiednie do drukowanych treści, np. ustawić orientację poziomą w przypadku drukowania zdjęcia w tej orientacji.

Adapter wydruku współpracuje z platformą drukowania w Androidzie i wykonuje kroki procesu drukowania. Ten proces wymaga, aby przed utworzeniem dokumentu do wydrukowania użytkownicy wybrali drukarki i opcje drukowania. Wybory te mogą mieć wpływ na wynik końcowy, gdy użytkownik wybierze drukarki o różnych możliwościach pracy, różnych rozmiarach stron lub orientacjach stron. Gdy dokonujemy wyboru, platforma drukowania prosi adapter o ułożenie i wygenerowanie dokumentu do wydruku, aby przygotować się do ostatecznego wyniku. Gdy użytkownik kliknie przycisk drukowania, platforma przekazuje ostateczną wersję dokumentu do dostawcy wydruku, aby ją otrzymać. Podczas procesu drukowania użytkownicy mogą anulować działanie drukowania, więc adapter wydruku również musi nasłuchiwać próśb o anulowanie drukowania i na nie zareagować.

Klasa abstrakcyjna PrintDocumentAdapter jest przeznaczona do obsługi cyklu życia drukowania, który ma 4 główne metody wywołania zwrotnego. Aby prawidłowo współdziałać z platformą drukowania, musisz zastosować w adapterze wydruku te metody:

  • onStart() – wywoływane raz na początku procesu drukowania. Jeśli Twoja aplikacja zawiera jednorazowe zadania przygotowawcze, takie jak uzyskanie zrzutu danych do wydrukowania, wykonaj je tutaj. Implementacja tej metody w adapterze nie jest wymagana.
  • onLayout() – wywoływane za każdym razem, gdy użytkownik zmieni ustawienie drukowania, które ma wpływ na dane wyjściowe, na przykład inny rozmiar lub orientacja strony, umożliwiając aplikacji obliczenie układu stron do wydrukowania. Ta metoda musi zwracać co najmniej liczbę oczekiwanych stron w wydrukowanym dokumencie.
  • onWrite() – wywoływane do renderowania drukowanych stron w pliku do wydrukowania. Ta metoda może być wywoływana co najmniej raz po każdym wywołaniu onLayout().
  • onFinish() – wywoływane raz na końcu procesu drukowania. Jeśli Twoja aplikacja ma do wykonania jednorazowe zadania odbudowy, wykonaj je tutaj. Wdrażanie tej metody w adapterze nie jest wymagane.

W kolejnych sekcjach opisano, jak wdrożyć metody układu i zapisu, które mają kluczowe znaczenie dla działania adaptera wydruku.

Uwaga: te metody adaptera są wywoływane w wątku głównym aplikacji. Jeśli spodziewasz się, że wykonanie tych metod w Twojej implementacji zajmie dużo czasu, zaimplementuj je tak, aby zostały wykonane w oddzielnym wątku. Możesz na przykład umieścić układ lub wydrukować dokument w osobnych obiektach AsyncTask.

Informacje o dokumencie Compute Print

W ramach implementacji klasy PrintDocumentAdapter aplikacja musi mieć możliwość określenia typu tworzonego dokumentu oraz obliczenia łącznej liczby stron na potrzeby zadania drukowania z uwzględnieniem informacji o rozmiarze drukowanej strony. Implementacja metody onLayout() w adapterze przeprowadza te obliczenia i dostarcza informacji o oczekiwanych wynikach zadania drukowania w klasie PrintDocumentInfo, w tym o liczbie stron i typie treści. Poniższy przykładowy kod przedstawia podstawową implementację metody onLayout() w obrębie typu 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.");
    }
}

Jeśli nie można ukończyć obliczenia układu, wykonanie metody onLayout() może mieć 3 wyniki: zakończenie, anulowanie lub niepowodzenie. Musisz wskazać jeden z tych wyników, wywołując odpowiednią metodę obiektu PrintDocumentAdapter.LayoutResultCallback.

Uwaga: parametr logiczny metody onLayoutFinished() wskazuje, czy treść układu rzeczywiście się zmieniła od ostatniego żądania. Prawidłowe ustawienie tego parametru pozwala platformie drukowania uniknąć niepotrzebnego wywoływania metody onWrite(), zasadniczo buforowania wcześniej napisanego dokumentu wydruku i zwiększenia wydajności.

Podstawowym zajęciem w onLayout() jest obliczanie liczby stron, których oczekiwany wygląd zależy od atrybutów drukarki. Sposób obliczania tej liczby zależy w dużym stopniu od tego, jak aplikacja rozplanuje strony do drukowania. Poniższy przykładowy kod przedstawia implementację, w której liczba stron jest określana na podstawie orientacji wydruku:

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

Zapisywanie pliku drukowanego dokumentu

Gdy nadejdzie czas na zapisanie danych wyjściowych wydruku do pliku, platforma drukowania Androida wywołuje metodę onWrite() klasy PrintDocumentAdapter aplikacji. Parametry metody określają, które strony powinny zostać zapisane, i który ma zostać użyty w pliku wyjściowym. Implementacja tej metody musi następnie wyrenderować każdą żądaną stronę treści w postaci wielostronicowego pliku PDF. Po zakończeniu tego procesu będziesz wywoływać metodę onWriteFinished() obiektu wywołania zwrotnego.

Uwaga: platforma Android Print może wywoływać metodę onWrite() co najmniej raz przy każdym wywołaniu onLayout(). Dlatego ważne jest, aby ustawić parametr logiczny metody onLayoutFinished() na false, gdy układ treści drukowanej nie uległ zmianie, aby uniknąć niepotrzebnego przepisywania dokumentu w wersji drukowanej.

Uwaga: parametr logiczny metody onLayoutFinished() wskazuje, czy treść układu rzeczywiście się zmieniła od ostatniego żądania. Prawidłowe ustawienie tego parametru pozwala platformie drukowania uniknąć niepotrzebnego wywoływania metody onLayout(), zasadniczo buforowania wcześniej napisanego dokumentu wydruku i zwiększenia wydajności.

Poniższy przykład przedstawia podstawową mechanikę tego procesu przy użyciu klasy PrintedPdfDocument do utworzenia pliku 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);

    ...
}

Ten przykład deleguje renderowanie zawartości strony PDF do metody drawPage(), która została omówiona w następnej sekcji.

Podobnie jak w przypadku układu, gdy nie można zapisać treści, wykonanie metody onWrite() może mieć 3 następujące wyniki: zakończenie, anulowanie lub niepowodzenie. Musisz wskazać jeden z tych wyników, wywołując odpowiednią metodę obiektu PrintDocumentAdapter.WriteResultCallback.

Uwaga: renderowanie dokumentu do wydrukowania może wymagać znacznych zasobów. Aby uniknąć zablokowania głównego wątku interfejsu aplikacji, przeprowadź operacje renderowania i pisania strony w osobnym wątku, np. w interfejsie AsyncTask. Więcej informacji o pracy z wątkami wykonywania, takimi jak zadania asynchroniczne, znajdziesz w artykule Procesy i wątki.

Zawartość strony z rysunkiem w pliku PDF

Gdy aplikacja zostanie wydrukowana, musi wygenerować dokument PDF i przekazać go do platformy drukowania w Androidzie w celu drukowania. W tym celu możesz użyć dowolnej biblioteki do generowania plików PDF. Z tej lekcji dowiesz się, jak używać zajęć PrintedPdfDocument do generowania stron w formacie PDF na podstawie swoich treści.

Klasa PrintedPdfDocument używa obiektu Canvas do rysowania elementów na stronie pliku PDF, podobnie jak w przypadku układu aktywności. Możesz rysować elementy na wydrukowanej stronie za pomocą metod rysowania Canvas. Poniższy przykładowy kod pokazuje, jak rysować proste elementy na stronie dokumentu PDF przy użyciu tych metod:

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

Jeśli używasz elementu Canvas do rysowania na stronie PDF, elementy są określone w punktach, co wynosi 1/72 cala. Pamiętaj, by używać tej jednostki miary do określania rozmiaru elementów na stronie. Do ustalania pozycji rysowanych elementów układ współrzędnych zaczyna się od 0,0 w lewym górnym rogu strony.

Wskazówka: chociaż obiekt Canvas umożliwia umieszczanie elementów wydruku na krawędzi dokumentu PDF, wiele drukarek nie daje się wydrukować do krawędzi papieru. Podczas tworzenia dokumentu do wydruku z użyciem tej klasy weź pod uwagę krawędzie strony, których nie można wydrukować.