Implementar um visualizador de PDF

PdfViewerFragment é um Fragment especializado que você pode usar para mostrar documentos PDF no seu aplicativo Android. O PdfViewerFragment simplifica a renderização de PDF, permitindo que você se concentre em outros aspectos da funcionalidade do app.

Resultados

Um documento PDF renderizado em um aplicativo Android usando PdfViewerFragment.
Documento PDF exibido em um app.

Compatibilidade de versões

Para usar PdfViewerFragment, seu aplicativo precisa ser direcionado a um mínimo de Android S (nível 31 da API) e nível 13 da extensão do SDK. Se esses requisitos de compatibilidade não forem atendidos, a biblioteca vai gerar um UnsupportedOperationException.

É possível verificar a versão da extensão do SDK no momento da execução usando o módulo SdkExtensions. Isso permite carregar condicionalmente o fragmento e o documento PDF somente se o dispositivo atender aos requisitos necessários.

if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
    // Load the fragment and document.
}

Dependências

Para incorporar o visualizador de PDF ao seu aplicativo, declare a dependência androidx.pdf no arquivo build.gradle do módulo do app. A biblioteca PDF está acessível no repositório Maven do Google.

dependencies {
    val pdfVersion = "1.0.0-alpha0X"
    implementation("androidx.pdf:pdf:pdf-viewer-fragment:$pdfVersion")
}

Recursos PdfViewerFragment

O PdfViewerFragment apresenta documentos PDF em formato paginado, facilitando a navegação. Para um carregamento eficiente, o fragmento usa uma estratégia de renderização de duas passagens que carrega progressivamente as dimensões da página.

Para otimizar o uso da memória, o PdfViewerFragment renderiza apenas as páginas visíveis no momento e libera os bitmaps das páginas que estão fora da tela. Além disso, PdfViewerFragment inclui um botão de ação flutuante (FAB) que oferece suporte a anotações ao acionar uma intent android.intent.action.ANNOTATE implícita que contém o URI do documento.

Implementação

Adicionar um visualizador de PDF ao seu aplicativo Android é um processo de várias etapas.

Criar o layout da atividade

Comece definindo o XML de layout da atividade que hospeda o visualizador de PDF. O layout precisa incluir um FrameLayout para conter o PdfViewerFragment e botões para interações do usuário, como pesquisar no documento.

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/pdf_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fragment_container_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/search_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/search_string"
        app:strokeWidth="1dp"
        android:layout_marginStart="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Configurar a atividade

A atividade que hospeda PdfViewerFragment precisa estender AppCompatActivity. No método onCreate() da atividade, defina a visualização de conteúdo como o layout criado e inicialize os elementos de interface necessários.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val getContentButton: MaterialButton = findViewById(R.id.launch_button)
        val searchButton: MaterialButton = findViewById(R.id.search_button)
    }
}

Inicializar o PdfViewerFragment

Crie uma instância de PdfViewerFragment usando um gerenciador de fragmentos obtido de getSupportFragmentManager(). Verifique se uma instância do fragmento já existe antes de criar uma nova, principalmente durante mudanças de configuração.

No exemplo a seguir, a função initializePdfViewerFragment() processa a criação e a confirmação da transação de fragmento. A função substitui um fragmento existente em um contêiner por uma instância do seu PdfViewerFragment.

class MainActivity : AppCompatActivity() {
    @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
    private var pdfViewerFragment: PdfViewerFragment? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        if (pdfViewerFragment == null) {
            pdfViewerFragment =
                supportFragmentManager
                    .findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG) as PdfViewerFragment?
        }

    }

    // Used to instantiate and commit the fragment.
    @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
    private fun initializePdfViewerFragment() {
        // This condition can be skipped if you want to create a new fragment every time.
        if (pdfViewerFragment == null) {
            val fragmentManager: FragmentManager = supportFragmentManager

          // Fragment initialization.
          pdfViewerFragment = PdfViewerFragmentExtended()
          val transaction: FragmentTransaction = fragmentManager.beginTransaction()

          // Replace an existing fragment in a container with an instance of a new fragment.
          transaction.replace(
              R.id.fragment,4_container_view,
              pdfViewerFragment!!,
              PDF_VIEWER_FRAGMENT_TAG
          )
          transaction.commitAllowingStateLoss()
          fragmentManager.executePendingTransactions()
        }
    }

    companion object {
        private const val MIME_TYPE_PDF = "application/pdf"
        private const val PDF_VIEWER_FRAGMENT_TAG = "pdf_viewer_fragment_tag"
    }
}

Estender a funcionalidade PdfViewerFragment

O PdfViewerFragment expõe funções públicas que podem ser substituídas para estender os recursos dele. Crie uma classe que herde de PdfViewerFragment. Na sua subclasse, substitua métodos como onLoadDocumentSuccess() e onLoadDocumentError() para adicionar lógica personalizada, como métricas de registro.

@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
class PdfViewerFragmentExtended : PdfViewerFragment() {
          private val someLogger : SomeLogger = // ... used to log metrics

          override fun onLoadDocumentSuccess() {
                someLogger.log(/** log document success */)
          }

          override fun onLoadDocumentError(error: Throwable) {
                someLogger.log(/** log document error */, error)
          }
}

Embora o PdfViewerFragment não inclua um menu de pesquisa integrado, ele é compatível com uma barra de pesquisa. Você controla a visibilidade da barra de pesquisa usando a API isTextSearchActive. Para ativar a pesquisa de documentos, defina a propriedade isTextSearchActive da instância PdfViewerFragment.

Use WindowCompat.setDecorFitsSystemWindows() para garantir que WindowInsetsCompat seja transmitido corretamente às visualizações de conteúdo, o que é necessário para o posicionamento adequado da visualização de pesquisa.

class MainActivity : AppCompatActivity() {
    @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        searchButton.setOnClickListener {
            pdfViewerFragment?.isTextSearchActive =
                pdfViewerFragment?.isTextSearchActive == false
        }

        // Ensure WindowInsetsCompat are passed to content views without being
        // consumed by the decor view. These insets are used to calculate the
        // position of the search view.
        WindowCompat.setDecorFitsSystemWindows(window, false)
    }
}

Integrar com o seletor de arquivos

Para permitir que os usuários selecionem arquivos PDF no dispositivo, integre PdfViewerFragment ao seletor de arquivos do Android. Primeiro, atualize o XML de layout da atividade para incluir um botão que inicia o seletor de arquivos.

<...>
    <FrameLayout
        ...
        app:layout_constraintBottom_toTopOf="@+id/launch_button"/>
    // Adding a button to open file picker.
    <com.google.android.material.button.MaterialButton
        android:id="@+id/launch_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/launch_string"
        app:strokeWidth="1dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/search_button"/>

    <com.google.android.material.button.MaterialButton
        ...
        app:layout_constraintStart_toEndOf="@id/launch_button" />

</androidx.constraintlayout.widget.ConstraintLayout>

Em seguida, na sua atividade, inicie o seletor de arquivos usando registerForActivityResult(GetContent()). Quando o usuário seleciona um arquivo, o callback fornece um URI. Em seguida, defina a propriedade documentUri da sua instância PdfViewerFragment com esse URI para carregar e mostrar o PDF selecionado.

class MainActivity : AppCompatActivity() {
    // ...

    private var filePicker: ActivityResultLauncher<String> =
        registerForActivityResult(GetContent()) { uri: Uri? ->
            uri?.let {
                initializePdfViewerFragment()
                pdfViewerFragment?.documentUri = uri
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        getContentButton.setOnClickListener { filePicker.launch(MIME_TYPE_PDF) }
    }

    private fun initializePdfViewerFragment() {
        // ...
    }

    companion object {
        private const val MIME_TYPE_PDF = "application/pdf"
        // ...
    }
}

Personalizar a interface

É possível personalizar a interface do usuário de PdfViewerFragment substituindo atributos XML que a biblioteca expõe. Isso permite personalizar a aparência de elementos como a barra de rolagem e o indicador de página para corresponder ao design do app.

Os atributos personalizáveis incluem:

  • fastScrollVerticalThumbDrawable: define o drawable para o marcador da barra de rolagem.
  • fastScrollPageIndicatorBackgroundDrawable: define o elemento desenhável de plano de fundo para o indicador de página.
  • fastScrollPageIndicatorMarginEnd: define a margem direita do indicador de página. Verifique se os valores de margem são positivos.
  • fastScrollVerticalThumbMarginEnd: define a margem direita da alça da barra de rolagem vertical. Verifique se os valores de margem são positivos.

Para aplicar essas personalizações, defina um estilo personalizado nos seus recursos XML.

<resources>
    <style name="pdfContainerStyle">
        <item name="fastScrollVerticalThumbDrawable">@drawable/custom_thumb_drawable</item>
        <item name="fastScrollPageIndicatorBackgroundDrawable">@drawable/custom_page_indicator_background</item>
        <item name="fastScrollVerticalThumbMarginEnd">8dp</item>
    </style>
</resources>

Em seguida, forneça o recurso de estilo personalizado a PdfViewerFragment usando PdfStylingOptions ao criar uma instância do fragmento com PdfViewerFragment.newInstance(stylingOptions).

private fun initializePdfViewerFragment() {
    // This condition can be skipped if you want to create a new fragment every time.
    if (pdfViewerFragment == null) {
      val fragmentManager: FragmentManager = supportFragmentManager

      // Create styling options.
      val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)

      // Fragment initialization.
      pdfViewerFragment = PdfViewerFragment.newInstance(stylingOptions)

      // Execute fragment transaction.
    }
}

Se você criou uma subclasse de PdfViewerFragment, use o construtor protegido para fornecer as opções de estilo. Isso garante que seus estilos personalizados sejam aplicados corretamente ao fragmento estendido.

class StyledPdfViewerFragment: PdfViewerFragment {

    constructor() : super()

    private constructor(pdfStylingOptions: PdfStylingOptions) : super(pdfStylingOptions)

    companion object {
        fun newInstance(): StyledPdfViewerFragment {
            val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)
            return StyledPdfViewerFragment(stylingOptions)
        }
    }
}

Implementação completa

O código a seguir oferece um exemplo completo de como implementar PdfViewerFragment na sua atividade, incluindo inicialização, integração do seletor de arquivos, funcionalidade de pesquisa e personalização da interface.

class MainActivity : AppCompatActivity() {

    private var pdfViewerFragment: PdfViewerFragment? = null
    private var filePicker: ActivityResultLauncher<String> =
        registerForActivityResult(GetContent()) { uri: Uri? ->
            uri?.let {
                initializePdfViewerFragment()
                pdfViewerFragment?.documentUri = uri
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (pdfViewerFragment == null) {
            pdfViewerFragment =
                supportFragmentManager
                   .findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG) as PdfViewerFragment?
        }

        val getContentButton: MaterialButton = findViewById(R.id.launch_button)
        val searchButton: MaterialButton = findViewById(R.id.search_button)

        getContentButton.setOnClickListener { filePicker.launch(MIME_TYPE_PDF) }
        searchButton.setOnClickListener {
            pdfViewerFragment?.isTextSearchActive = pdfViewerFragment?.isTextSearchActive == false
        }
    }

    private fun initializePdfViewerFragment() {
        // This condition can be skipped if you want to create a new fragment every time.
        if (pdfViewerFragment == null) {
            val fragmentManager: FragmentManager = supportFragmentManager

          // Create styling options.
          // val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)

          // Fragment initialization.
          // For customization:
          // pdfViewerFragment = PdfViewerFragment.newInstance(stylingOptions)
          pdfViewerFragment = PdfViewerFragmentExtended()
          val transaction: FragmentTransaction = fragmentManager.beginTransaction()

          // Replace an existing fragment in a container with an instance of a new fragment.
          transaction.replace(
              R.id.fragment_container_view,
              pdfViewerFragment!!,
              PDF_VIEWER_FRAGMENT_TAG
          )
          transaction.commitAllowingStateLoss()
          fragmentManager.executePendingTransactions()
        }
    }

    companion object {
        private const val MIME_TYPE_PDF = "application/pdf"
        private const val PDF_VIEWER_FRAGMENT_TAG = "pdf_viewer_fragment_tag"
    }
}

Pontos principais sobre o código

  • Verifique se o projeto atende aos requisitos mínimos de nível da API e extensão do SDK.
  • A atividade que hospeda PdfViewerFragment precisa estender AppCompatActivity.
  • Você pode estender PdfViewerFragment para adicionar comportamentos personalizados.
  • Personalize a interface do PdfViewerFragment substituindo atributos XML.