맞춤 문서 인쇄

일부 애플리케이션(예: 그리기 앱, 페이지 레이아웃 앱, 그래픽 출력에 중점을 둔 앱)의 경우 아름다운 인쇄된 페이지를 만드는 것이 핵심 기능입니다. 이 경우 이미지나 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 클래스의 인쇄 작업 예상 출력에 관한 정보를 제공합니다. 다음 코드 예에서는 PrintDocumentAdapteronLayout() 메서드에 관한 기본 구현을 보여줍니다.

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 인쇄 프레임워크는 애플리케이션 PrintDocumentAdapter 클래스의 onWrite() 메서드를 호출합니다. 메서드의 매개변수는 써야 하는 페이지와 사용할 출력 파일을 지정합니다. 그런 다음 이 메서드의 구현에서는 요청된 각 콘텐츠 페이지를 여러 페이지 PDF 문서 파일로 렌더링해야 합니다. 이 프로세스가 완료되면 콜백 객체의 onWriteFinished() 메서드를 호출합니다.

참고: Android 인쇄 프레임워크는 onLayout()를 호출할 때마다 onWrite() 메서드를 한 번 이상 호출할 수 있습니다. 이러한 이유로 인쇄 콘텐츠 레이아웃이 변경되지 않았을 때 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분의 1인 포인트로 지정됩니다. 페이지의 요소의 크기를 지정할 때 이 측정 단위를 사용해야 합니다. 그려진 요소를 배치하기 위해 좌표계는 페이지 왼쪽 상단 모서리의 0,0에서 시작합니다.

도움말: Canvas 객체를 사용하면 인쇄 요소를 PDF 문서의 가장자리에 배치할 수 있지만 대부분의 프린터는 실제 용지의 가장자리에 인쇄할 수 없습니다. 이 클래스로 인쇄 문서를 빌드할 때 페이지의 인쇄할 수 없는 가장자리를 고려해야 합니다.