Implementare un visualizzatore PDF

PdfViewerFragment è un Fragment specializzato che puoi utilizzare per visualizzare documenti PDF all'interno della tua applicazione Android. PdfViewerFragment semplifica il rendering dei PDF, consentendoti di concentrarti su altri aspetti della funzionalità della tua app.

Risultati

Un documento PDF sottoposto a rendering all'interno di un'applicazione Android utilizzando PdfViewerFragment.
Documento PDF visualizzato in un'app.

Compatibilità delle versioni

Per utilizzare PdfViewerFragment, la tua applicazione deve avere come target almeno Android S (livello API 31) e il livello di estensione SDK 13. Se questi requisiti di compatibilità non vengono soddisfatti, la libreria genera un UnsupportedOperationException.

Puoi controllare la versione dell'estensione SDK in fase di runtime utilizzando il modulo SdkExtensions. In questo modo, puoi caricare in modo condizionale il frammento e il documento PDF solo se il dispositivo soddisfa i requisiti necessari.

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

Dipendenze

Per incorporare il visualizzatore PDF nella tua applicazione, dichiara la dipendenza androidx.pdf nel file build.gradle del modulo dell'app. La libreria PDF è accessibile dal repository Maven di Google.

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

PdfViewerFragment funzionalità

PdfViewerFragment presenta i documenti PDF in formato impaginato, rendendoli facili da esplorare. Per un caricamento efficiente, il frammento utilizza una strategia di rendering a due passaggi che carica progressivamente le dimensioni della pagina.

Per ottimizzare l'utilizzo della memoria, PdfViewerFragment esegue il rendering solo delle pagine attualmente visibili e rilascia le bitmap per le pagine fuori dallo schermo. Inoltre, PdfViewerFragment include un Floating Action Button (FAB) che supporta le annotazioni attivando un intent implicito android.intent.action.ANNOTATE contenente l'URI del documento.

Implementazione

L'aggiunta di un visualizzatore PDF all'applicazione Android è un processo in più passaggi.

Creare il layout dell'attività

Inizia definendo il layout XML per l'attività che ospita il visualizzatore PDF. Il layout deve includere un FrameLayout per contenere i pulsanti PdfViewerFragment e per le interazioni degli utenti, ad esempio la ricerca all'interno del 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>

Configurare l'attività

L'attività che ospita PdfViewerFragment deve estendere AppCompatActivity. Nel metodo onCreate() dell'attività, imposta la visualizzazione dei contenuti sul layout che hai creato e inizializza gli elementi dell'interfaccia utente necessari.

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

Inizializza PdfViewerFragment

Crea un'istanza di PdfViewerFragment utilizzando un fragment manager ottenuto da getSupportFragmentManager(). Prima di crearne uno nuovo, controlla se esiste già un'istanza del frammento, soprattutto durante le modifiche alla configurazione.

Nell'esempio seguente, la funzione initializePdfViewerFragment() gestisce la creazione e l'impegno della transazione di frammento. La funzione sostituisce un frammento esistente in un contenitore con un'istanza del tuo 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"
    }
}

Estendere la funzionalità di PdfViewerFragment

PdfViewerFragment espone funzioni pubbliche che puoi sostituire per estenderne le funzionalità. Crea una nuova classe che eredita da PdfViewerFragment. Nella sottoclasse, esegui l'override di metodi come onLoadDocumentSuccess() e onLoadDocumentError() per aggiungere logica personalizzata, ad esempio la registrazione delle metriche.

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

Anche se PdfViewerFragment non include un menu di ricerca integrato, supporta una barra di ricerca. Controlli la visibilità della barra di ricerca utilizzando l'API isTextSearchActive. Per attivare la ricerca di documenti, imposta la proprietà isTextSearchActive dell'istanza PdfViewerFragment.

Utilizza WindowCompat.setDecorFitsSystemWindows() per assicurarti che WindowInsetsCompat venga passato correttamente alle visualizzazioni dei contenuti, il che è necessario per il corretto posizionamento della visualizzazione di ricerca.

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

Integrare il selettore file

Per consentire agli utenti di selezionare file PDF dal proprio dispositivo, integra PdfViewerFragment con il selettore di file Android. Per prima cosa, aggiorna il layout XML dell'attività in modo da includere un pulsante che avvii il selettore di file.

<...>
    <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>

Successivamente, nell'attività, avvia il selettore di file utilizzando registerForActivityResult(GetContent()). Quando l'utente seleziona un file, il callback fornisce un URI. A questo punto, imposta la proprietà documentUri dell'istanza PdfViewerFragment con questo URI per caricare e visualizzare il PDF selezionato.

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

Personalizzare l'interfaccia utente

Puoi personalizzare l'interfaccia utente di PdfViewerFragment sostituendo gli attributi XML esposti dalla libreria. In questo modo puoi personalizzare l'aspetto di elementi come la barra di scorrimento e l'indicatore di pagina in modo che corrispondano al design della tua app.

Gli attributi personalizzabili includono:

  • fastScrollVerticalThumbDrawable: imposta il drawable per il cursore della barra di scorrimento.
  • fastScrollPageIndicatorBackgroundDrawable: imposta la risorsa disegnabile di sfondo per l'indicatore di pagina.
  • fastScrollPageIndicatorMarginEnd: imposta il margine destro per l'indicatore di pagina. Assicurati che i valori del margine siano positivi.
  • fastScrollVerticalThumbMarginEnd: imposta il margine destro del cursore della barra di scorrimento verticale. Assicurati che i valori del margine siano positivi.

Per applicare queste personalizzazioni, definisci uno stile personalizzato nelle risorse 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>

Poi, fornisci la risorsa di stile personalizzato a PdfViewerFragment utilizzando PdfStylingOptions quando crei un'istanza del frammento con 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 hai creato una sottoclasse di PdfViewerFragment, utilizza il costruttore protetto per fornire le opzioni di stile. In questo modo, gli stili personalizzati vengono applicati correttamente allo snippet esteso.

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

Implementazione completa

Il seguente codice fornisce un esempio completo di come implementare PdfViewerFragment nella tua attività, inclusi l'inizializzazione, l'integrazione del selettore di file, la funzionalità di ricerca e la personalizzazione dell'UI.

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

Punti chiave del codice

  • Assicurati che il tuo progetto soddisfi i requisiti minimi relativi al livello API e all'estensione SDK.
  • L'attività di hosting PdfViewerFragment deve estendere AppCompatActivity.
  • Puoi estendere PdfViewerFragment per aggiungere comportamenti personalizzati.
  • Personalizza la UI di PdfViewerFragment sostituendo gli attributi XML.