Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

맞춤 문서 인쇄

일부 애플리케이션(예: 그리기 앱이나 페이지 레이아웃 앱, 그래픽 출력에 중점을 둔 앱)에서는 아름답게 인쇄된 페이지를 만드는 것이 핵심 기능입니다. 이 경우 이미지나 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)
        }
    }
    

자바

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

자바

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

자바

    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)

        ...
    }
    

자바

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

자바

    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 문서 가장자리에 배치할 수 있지만 많은 프린터가 실제 용지의 가장자리에 인쇄할 수 없습니다. 이 클래스로 인쇄 문서를 빌드할 때 페이지의 인쇄할 수 없는 가장자리를 고려하세요.