Wdrażanie przeglądarki PDF

PdfViewerFragment to wyspecjalizowany Fragment, którego możesz użyć do wyświetlania dokumentów PDF w aplikacji na Androida. PdfViewerFragment upraszcza renderowanie plików PDF, dzięki czemu możesz skupić się na innych aspektach funkcjonalności aplikacji.

Wyniki

Dokument PDF renderowany w aplikacji na Androida za pomocą elementu PdfViewerFragment.
Dokument PDF wyświetlany w aplikacji.

Zgodność wersji

Aby używać PdfViewerFragment, aplikacja musi być kierowana na Androida w wersji S (poziom API 31) lub nowszej oraz na poziom rozszerzenia SDK 13 lub wyższy. Jeśli te wymagania dotyczące zgodności nie zostaną spełnione, biblioteka zgłosi wyjątek UnsupportedOperationException.

Wersję rozszerzenia SDK możesz sprawdzić w czasie działania aplikacji za pomocą modułu SdkExtensions. Dzięki temu możesz warunkowo wczytywać fragment i dokument PDF tylko wtedy, gdy urządzenie spełnia wymagane kryteria.

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

Zależności

Aby zintegrować przeglądarkę PDF z aplikacją, zadeklaruj zależność androidx.pdf w pliku modułu aplikacjibuild.gradle. Biblioteka PDF jest dostępna w repozytorium Google Maven.

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

Funkcje PdfViewerFragment

PdfViewerFragment wyświetla dokumenty PDF w formacie stronicowym, co ułatwia nawigację. Aby zapewnić wydajne wczytywanie, fragment stosuje 2-etapową strategię renderowania, która stopniowo wczytuje wymiary strony.

Aby zoptymalizować wykorzystanie pamięci, PdfViewerFragment renderuje tylko aktualnie widoczne strony i zwalnia mapy bitowe stron, które są poza ekranem. Dodatkowo PdfViewerFragment zawiera pływający przycisk działania (FAB), który obsługuje adnotacje, wysyłając niejawny intent android.intent.action.ANNOTATE zawierający identyfikator URI dokumentu.

Implementacja

Dodanie przeglądarki PDF do aplikacji na Androida to proces wieloetapowy.

Utwórz układ aktywności

Zacznij od zdefiniowania układu XML dla aktywności, która będzie hostować przeglądarkę PDF. Układ powinien zawierać element FrameLayout, który będzie zawierać PdfViewerFragment, oraz przyciski do interakcji z użytkownikiem, np. do wyszukiwania w dokumencie.

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

Skonfiguruj aktywność

Aktywność, która hostuje PdfViewerFragment, musi rozszerzać AppCompatActivity. W metodzie onCreate() aktywności ustaw widok treści na utworzony układ i zainicjuj wszystkie niezbędne elementy interfejsu.

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

Zainicjuj PdfViewerFragment

Utwórz instancję PdfViewerFragment za pomocą menedżera fragmentów uzyskanego z getSupportFragmentManager(). Zanim utworzysz nową instancję fragmentu, sprawdź, czy już istnieje, zwłaszcza podczas zmian konfiguracji.

W przykładzie poniżej funkcja initializePdfViewerFragment() obsługuje tworzenie i zatwierdzanie transakcji fragmentu. Funkcja zastępuje istniejący fragment w kontenerze instancją 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"
    }
}

Rozszerz funkcjonalność PdfViewerFragment

PdfViewerFragment udostępnia funkcje publiczne, które możesz zastąpić, aby rozszerzyć jego możliwości. Utwórz nową klasę, która dziedziczy po PdfViewerFragment. W podklasie zastąp metody takie jak onLoadDocumentSuccess() i onLoadDocumentError(), aby dodać niestandardową logikę, np. rejestrowanie danych.

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

Chociaż PdfViewerFragment nie zawiera wbudowanego menu wyszukiwania, obsługuje pasek wyszukiwania. Widoczność paska wyszukiwania możesz kontrolować za pomocą interfejsu API isTextSearchActive. Aby włączyć wyszukiwanie w dokumentach, ustaw właściwość isTextSearchActive instancji PdfViewerFragment.

Użyj WindowCompat.setDecorFitsSystemWindows(), aby mieć pewność, że WindowInsetsCompat jest prawidłowo przekazywany do widoków treści, co jest niezbędne do prawidłowego pozycjonowania widoku wyszukiwania.

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

Integracja z oknem wyboru plików

Aby umożliwić użytkownikom wybieranie plików PDF z urządzenia, zintegruj PdfViewerFragment z oknem wyboru plików Androida. Najpierw zaktualizuj układ XML aktywności, aby dodać przycisk uruchamiający okno wyboru plików.

<androidx.constraintlayout.widget.ConstraintLayout
    ...
    tools:context=".MainActivity">

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

Następnie w aktywności uruchom okno wyboru plików za pomocą registerForActivityResult(GetContent()). Gdy użytkownik wybierze plik, wywołanie zwrotne udostępni identyfikator URI. Następnie ustaw właściwość documentUri instancji PdfViewerFragment na ten identyfikator URI, aby wczytać i wyświetlić wybrany plik PDF.

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

Dostosowanie interfejsu

Interfejs użytkownika PdfViewerFragment możesz dostosować, zastępując atrybuty XML udostępniane przez bibliotekę. Dzięki temu możesz dostosować wygląd elementów takich jak pasek przewijania i wskaźnik strony, aby pasowały do projektu aplikacji.

Atrybuty, które można dostosować, to:

  • fastScrollVerticalThumbDrawable – ustawia obiekt rysowalny dla uchwytu paska przewijania.
  • fastScrollPageIndicatorBackgroundDrawable – ustawia obiekt rysowalny tła dla wskaźnika strony.
  • fastScrollPageIndicatorMarginEnd – ustawia prawy margines wskaźnika strony. Upewnij się, że wartości marginesów są dodatnie.
  • fastScrollVerticalThumbMarginEnd – ustawia prawy margines uchwytu pionowego paska przewijania. Upewnij się, że wartości marginesów są dodatnie.

Aby zastosować te dostosowania, zdefiniuj styl niestandardowy w zasobach 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>

Następnie przekaż zasób stylu niestandardowego do PdfViewerFragment za pomocą PdfStylingOptions gdy tworzysz instancję fragmentu za pomocą 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.
    }
}

Jeśli masz podklasę PdfViewerFragment, użyj chronionego konstruktora, aby podać opcje stylu. Dzięki temu style niestandardowe zostaną prawidłowo zastosowane do rozszerzonego fragmentu.

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

Pełna implementacja

Poniższy kod zawiera kompletny przykład implementacji PdfViewerFragment w aktywności, w tym inicjowanie, integrację z oknem wyboru plików, funkcję wyszukiwania i dostosowanie interfejsu.

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

Najważniejsze informacje o kodzie

  • Upewnij się, że projekt spełnia minimalne wymagania dotyczące poziomu API i rozszerzenia SDK.
  • Aktywność hostująca PdfViewerFragment musi rozszerzać AppCompatActivity.
  • Możesz rozszerzyć PdfViewerFragment, aby dodać niestandardowe zachowania.
  • Dostosuj interfejs PdfViewerFragment, zastępując atrybuty XML.