Реализовать средство просмотра PDF-файлов,Реализовать средство просмотра PDF-файлов

PdfViewerFragment — это специализированный Fragment , который можно использовать для отображения PDF-документов в приложении Android. PdfViewerFragment упрощает рендеринг PDF-файлов, позволяя вам сосредоточиться на других аспектах функциональности вашего приложения.

Результаты

PDF-документ, визуализированный в приложении Android с помощью PdfViewerFragment.
PDF-документ, отображаемый в приложении.

Совместимость версий

Чтобы использовать PdfViewerFragment , ваше приложение должно быть ориентировано как минимум на Android S (уровень API 31) и уровень расширения SDK 13. Если эти требования совместимости не выполнены, библиотека выдает исключение UnsupportedOperationException .

Проверить версию расширения SDK во время выполнения можно с помощью модуля SdkExtensions . Это позволяет загружать фрагмент и PDF-документ только в том случае, если устройство соответствует необходимым требованиям.

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

Зависимости

Чтобы интегрировать просмотрщик PDF-файлов в своё приложение, объявите зависимость androidx.pdf в файле build.gradle модуля вашего приложения. Библиотека PDF-файлов доступна в репозитории Google Maven.

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

Возможности PdfViewerFragment

PdfViewerFragment представляет PDF-документы в постраничном формате, что упрощает навигацию. Для эффективной загрузки фрагмент использует двухпроходную стратегию рендеринга, которая постепенно загружает страницы по всем размерам.

Для оптимизации использования памяти PdfViewerFragment отображает только видимые в данный момент страницы и освобождает растровые изображения страниц, находящихся за пределами экрана. Кроме того, PdfViewerFragment включает плавающую кнопку действия (FAB), которая поддерживает аннотации, активируя неявное намерение android.intent.action.ANNOTATE , содержащее URI документа.

Выполнение

Добавление средства просмотра PDF-файлов в приложение Android — многоэтапный процесс.

Создайте макет активности

Начните с определения XML-макета для активности, в которой размещается средство просмотра PDF-файлов. Макет должен включать FrameLayout для размещения PdfViewerFragment и кнопок для взаимодействия с пользователем, например, поиска по документу.

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

Настройте активность

Активность, в которой размещается PdfViewerFragment , должна расширять AppCompatActivity . В методе onCreate() активности установите представление содержимого в соответствии с созданным вами макетом и инициализируйте все необходимые элементы пользовательского интерфейса.

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

Инициализировать PdfViewerFragment

Создайте экземпляр PdfViewerFragment используя менеджер фрагментов, полученный из getSupportFragmentManager() . Перед созданием нового фрагмента проверьте, существует ли уже существующий экземпляр, особенно при изменении конфигурации.

В следующем примере функция initializePdfViewerFragment() обрабатывает создание и фиксацию транзакции фрагмента. Функция заменяет существующий фрагмент в контейнере экземпляром вашего 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"
    }
}

Расширить функциональность PdfViewerFragment

PdfViewerFragment предоставляет открытые функции, которые можно переопределить для расширения его возможностей. Создайте новый класс, унаследованный от PdfViewerFragment . В подклассе переопределите методы, такие как onLoadDocumentSuccess() и onLoadDocumentError() чтобы добавить пользовательскую логику, например, регистрацию метрик.

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

Хотя PdfViewerFragment не имеет встроенного меню поиска, он поддерживает панель поиска. Видимость панели поиска управляется API isTextSearchActive . Чтобы включить поиск по документам, необходимо задать свойство isTextSearchActive экземпляра PdfViewerFragment .

Используйте WindowCompat.setDecorFitsSystemWindows() чтобы гарантировать правильную передачу WindowInsetsCompat в представления содержимого, что необходимо для правильного позиционирования представления поиска.

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

Интеграция с выбором файлов

Чтобы пользователи могли выбирать PDF-файлы на своих устройствах, интегрируйте PdfViewerFragment с инструментом выбора файлов Android. Сначала обновите XML-код макета вашей активности, добавив кнопку для запуска инструмента выбора файлов.

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

Затем в вашей активности запустите средство выбора файлов с помощью registerForActivityResult(GetContent()) . Когда пользователь выбирает файл, функция обратного вызова возвращает URI. Затем вы задаёте этот URI свойству documentUri вашего экземпляра PdfViewerFragment для загрузки и отображения выбранного 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"
        // ...
    }
}

Настройте пользовательский интерфейс

Вы можете настроить пользовательский интерфейс PdfViewerFragment , переопределив XML-атрибуты, предоставляемые библиотекой. Это позволяет адаптировать внешний вид таких элементов, как полоса прокрутки и индикатор страницы, к дизайну вашего приложения.

Настраиваемые атрибуты включают в себя:

  • fastScrollVerticalThumbDrawable — Устанавливает отрисовку ползунка полосы прокрутки.
  • fastScrollPageIndicatorBackgroundDrawable — Устанавливает отрисовываемый фон для индикатора страницы.
  • fastScrollPageIndicatorMarginEnd — устанавливает правое поле для индикатора страницы. Убедитесь, что значения полей положительные.
  • fastScrollVerticalThumbMarginEnd — устанавливает правое поле для ползунка вертикальной полосы прокрутки. Убедитесь, что значения поля положительные.

Чтобы применить эти настройки, определите пользовательский стиль в своих 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>

Затем предоставьте ресурс пользовательского стиля для PdfViewerFragment с помощью PdfStylingOptions при создании экземпляра фрагмента с помощью 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.
    }
}

Если вы создали подкласс PdfViewerFragment , используйте защищённый конструктор для настройки стилей. Это гарантирует корректное применение ваших стилей к расширенному фрагменту.

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

Полная реализация

Следующий код представляет собой полный пример того, как реализовать PdfViewerFragment в вашей деятельности, включая инициализацию, интеграцию средства выбора файлов, функцию поиска и настройку пользовательского интерфейса.

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

Ключевые моменты кода

  • Убедитесь, что ваш проект соответствует минимальным требованиям к уровню API и расширению SDK.
  • Активность, размещающая PdfViewerFragment должна расширять AppCompatActivity .
  • Вы можете расширить PdfViewerFragment , добавив пользовательские настройки.
  • Настройте пользовательский интерфейс PdfViewerFragment , переопределив атрибуты XML.