Renderizando documentos PDF no Android usando PdfRenderer
Conteudo
Introdução
Durante o desenvolvimento, a menos que encontremos um desafio ou problema, a maioria de nós não está ciente de muitas coisas no sistema das quais podemos nos beneficiar. Estamos habituados de tal forma que, a menos que algo seja necessário, não exploramos o material disponível.
Portanto, é sempre um bom hábito explorar alguns recursos que tornam nosso trabalho mais fácil quando chegar a hora e, para isso, precisamos continuar explorando as coisas na plataforma que escolhermos. Uma coisa no Android é lidar com documentos PDF. Nesta postagem, veremos diferentes maneiras de abrir documentos PDF.
Problema
Lidar com documentos PDF é uma das coisas básicas que a maioria de nós não está ciente porque não tivemos a chance de trabalhar nisso. Não é tão fácil na fase inicial do Android abrir PDFs porque não havia renderizadores ou componentes que pudessem lidar com eles. Então começamos a usar navegadores ou WebViews para lidar com PDFs da seguinte forma
private fun loadPDFWebView(pdfDocUrl: String) { webview?.settings?.javaScriptEnabled = true webview?.clearHistory() webview?.loadUrl("https://docs.google.com/gview?embedded=true&url=$pdfDocUrl") }
Mas há um problema de usar isso, não era possível carregar documentos PDF de grande porte. Na minha experiência, digo que mostra um erro ao tentar abrir documentos com mais de 10 MB de tamanho.
Embora existam muitas bibliotecas disponíveis, nem sempre foi uma tarefa fácil personalizar a biblioteca. Em seguida, costumamos disparar a intenção do visualizador de PDF, que leva o usuário a deixar nosso aplicativo e navegar para qualquer outro aplicativo que possa lidar com esse conteúdo PDF.
private fun openDocument(path: Uri) { val pdfIntent = Intent(Intent.ACTION_VIEW) pdfIntent.setDataAndType(path, "application/pdf") pdfIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP pdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) try { mContext.startActivity(pdfIntent) } catch (e: ActivityNotFoundException) { mContext.toast(mContext.getString(R.string.no_app_to_view_pdf)) } }
Esta também não era uma solução preferível.
Solução
A solução para abrir PDF é resolvida com a introdução da classe PdfRenderer no Android-Lollipop (API 21). Vamos explorar como podemos usar isso em nossos aplicativos.
O que é PdfRenderer?
O PdfRenderer nos permite criar um Bitmap a partir de uma página em um documento PDF para que possamos exibi-lo na tela. A classe PdfRenderer não é segura para thread. Se quisermos renderizar um PDF, primeiro precisamos obter um ParcelFileDescriptor do arquivo e, em seguida, criar uma instância do renderizador.
Mais tarde, para cada página que queremos renderizar, abrimos a página, renderizamos e fechamos a página. Depois de terminar a renderização, fechamos o renderizador. Depois que o renderizador é fechado, ele não deve ser mais usado.
Usando este PdfRenderer, somos responsáveis pelo tratamento do fechamento do renderizador e de cada página que abrimos. Aqui, só podemos ter uma página aberta por vez. Antes de fechar, renderizador, precisamos fechar a página aberta no momento. Vejamos passo a passo com um exemplo.
Como usar o PdfRenderer?
Inicialmente, vamos verificar as etapas básicas antes de passar pelo código
Passo 1:
Obtenha um descritor de arquivo procurável em nosso documento pdf:
val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
Passo 2:
Agora, vamos criar a instância PDFRenderer usando o ParcleFileDescriptor obtido acima:
val renderer = PdfRenderer(pfd)
Passo 3:
Vamos criar a instância de Bitmap com as dimensões necessárias:
val bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_4444)
Passo 4:
Não fazer com que a página seja renderizada usando PdfRenderer.Page apenas passando o índice da página
val page = renderer.openPage(pageIndex)
Passo 5:
Por último, renderize a página em bitmap criado na etapa 3:
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
Passo 6:
Feche a página e o renderizador assim que terminar
page.close() renderer.close()
Exemplo
Vamos verificar como abrir o documento pdf no fragmento fornecendo Uri como um argumento para ele. É apenas uma parte do exemplo de armazenamento de documentos do Android.
Nosso exemplo é renderizar um documento que é um bitmap em nosso caso, então precisamos de um ImageView, visto que ele exibe uma página por vez, precisamos de dois botões para mover para as páginas seguintes e anteriores, se disponíveis. Vamos projetar o XML
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="@android:color/white" android:scaleType="fitCenter" android:contentDescription="@null"/> <LinearLayout style="?android:attr/buttonBarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" android:measureWithLargestChild="true" android:orientation="horizontal"> <Button android:id="@+id/previous" style="?android:attr/buttonBarButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/previous" /> <Button android:id="@+id/next" style="?android:attr/buttonBarButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/next" /> </LinearLayout> </LinearLayout>
Agora que terminamos o design, vamos passar para a parte de codificação – nada mais é do que um fragmento que trata de todas as etapas discutidas acima
package com.example.pdf import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap.createBitmap import android.graphics.pdf.PdfRenderer import android.net.Uri import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView import androidx.core.net.toUri import androidx.fragment.app.Fragment import com.example.pdf.R import java.io.FileDescriptor import java.io.IOException class OpenDocumentFragment : Fragment() { private lateinit var pdfRenderer: PdfRenderer private lateinit var currentPage: PdfRenderer.Page private var currentPageNumber: Int = INITIAL_PAGE_INDEX private lateinit var pdfPageView: ImageView private lateinit var previousButton: Button private lateinit var nextButton: Button val pageCount get() = pdfRenderer.pageCount companion object { private const val DOCUMENT_URI_ARGUMENT = "com.example.pdf.opendocument.args.DOCUMENT_URI_ARGUMENT" fun newInstance(documentUri: Uri): OpenDocumentFragment { return OpenDocumentFragment().apply { arguments = Bundle().apply { putString(DOCUMENT_URI_ARGUMENT, documentUri.toString()) } } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_pdf_renderer_basic, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) previous.setOnClickListener { showPage(currentPage.index - 1) } next.setOnClickListener { showPage(currentPage.index + 1) } // If there is a savedInstanceState (screen orientations, etc.), we restore the page index. currentPageNumber = savedInstanceState?.getInt(CURRENT_PAGE_INDEX_KEY, INITIAL_PAGE_INDEX) ?: INITIAL_PAGE_INDEX } override fun onStart() { super.onStart() val documentUri = arguments?.getString(DOCUMENT_URI_ARGUMENT)?.toUri() ?: return try { openRenderer(activity, documentUri) showPage(currentPageNumber) } catch (ioException: IOException) { Log.d(TAG, "Exception opening document", ioException) } } override fun onStop() { super.onStop() try { closeRenderer() } catch (ioException: IOException) { Log.d(TAG, "Exception closing document", ioException) } } override fun onSaveInstanceState(outState: Bundle) { outState.putInt(CURRENT_PAGE_INDEX_KEY, currentPage.index) super.onSaveInstanceState(outState) } /** * Sets up a [PdfRenderer] and related resources. */ @Throws(IOException::class) private fun openRenderer(context: Context?, documentUri: Uri) { if (context == null) return /** * It may be tempting to use `use` here, but [PdfRenderer] expects to take ownership * of the [FileDescriptor], and, if we did use `use`, it would be auto-closed at the * end of the block, preventing us from rendering additional pages. */ val fileDescriptor = context.contentResolver.openFileDescriptor(documentUri, "r") ?: return // This is the PdfRenderer we use to render the PDF. pdfRenderer = PdfRenderer(fileDescriptor) currentPage = pdfRenderer.openPage(currentPageNumber) } /** * Closes the [PdfRenderer] and related resources. * * @throws IOException When the PDF file cannot be closed. */ @Throws(IOException::class) private fun closeRenderer() { currentPage.close() pdfRenderer.close() } /** * Shows the specified page of PDF to the screen. * * The way [PdfRenderer] works is that it allows for "opening" a page with the method * [PdfRenderer.openPage], which takes a (0 based) page number to open. This returns * a [PdfRenderer.Page] object, which represents the content of this page. * * There are two ways to render the content of a [PdfRenderer.Page]. * [PdfRenderer.Page.RENDER_MODE_FOR_PRINT] and [PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY]. * Since we're displaying the data on the screen of the device, we'll use the later. * * @param index The page index. */ private fun showPage(index: Int) { if (index < 0 || index >= pdfRenderer.pageCount) return currentPage.close() currentPage = pdfRenderer.openPage(index) // Important: the destination bitmap must be ARGB (not RGB). val bitmap = createBitmap(currentPage.width, currentPage.height, Bitmap.Config.ARGB_8888) currentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) pdfPageView.setImageBitmap(bitmap) val pageCount = pdfRenderer.pageCount previousButton.isEnabled = (0 != index) nextButton.isEnabled = (index + 1 < pageCount) activity?.title = getString(R.string.app_name_with_index, index + 1, pageCount) } } /** * Key string for saving the state of current page index. */ private const val CURRENT_PAGE_INDEX_KEY = "com.example.pdf.opendocument.state.CURRENT_PAGE_INDEX_KEY" private const val TAG = "OpenDocumentFragment" private const val INITIAL_PAGE_INDEX = 0
Resumo
Agora que você deve ter uma ideia básica de como abrir os documentos PDF. Você pode encontrar o código completo do exemplo de abertura de um documento PDF usando o renderizador de PDF no GitHub.