實作 PDF 檢視器

PdfViewerFragment 是專用的 Fragment,可用於在 Android 應用程式中顯示 PDF 文件。PdfViewerFragment 可簡化 PDF 轉譯作業,讓您專注於應用程式功能的其他層面。

結果

使用 PdfViewerFragment 在 Android 應用程式中轉譯的 PDF 文件。
應用程式中顯示的 PDF 文件。

版本相容性

如要使用 PdfViewerFragment,應用程式必須指定至少 Android S (API 級別 31) 和 SDK 擴充功能級別 13。如未滿足這些相容性要求,程式庫會擲回 UnsupportedOperationException

您可以使用 SdkExtensions 模組,在執行階段檢查 SDK 擴充功能版本。這樣一來,只有在裝置符合必要條件時,您才能有條件地載入片段和 PDF 文件。

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

依附元件

如要將 PDF 檢視器併入應用程式,請在應用程式模組的 build.gradle 檔案中宣告 androidx.pdf 依附元件。您可以透過 Google Maven 存放區存取 PDF 程式庫。

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

PdfViewerFragment 功能

PdfViewerFragment 會以分頁格式呈現 PDF 文件,方便瀏覽。為有效載入,這個片段採用兩階段的算繪策略,逐步載入網頁維度。

為最佳化記憶體用量,PdfViewerFragment 只會算繪目前顯示的頁面,並釋出螢幕外頁面的點陣圖。此外,PdfViewerFragment 還包含浮動動作按鈕 (FAB),可透過觸發包含文件 URI 的隱含 android.intent.action.ANNOTATE 意圖,支援註解功能。

實作

在 Android 應用程式中加入 PDF 檢視器需要多個步驟。

建立活動版面配置

首先,請為代管 PDF 檢視器的活動定義版面配置 XML。版面配置應包含 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

使用從 getSupportFragmentManager() 取得的片段管理工具,建立 PdfViewerFragment 的例項。建立新的片段執行個體前,請先檢查是否已存在片段執行個體,尤其是在設定變更期間。

在下列範例中,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 不提供內建搜尋選單,但支援搜尋列。您可以使用 isTextSearchActive API 控制搜尋列的顯示設定。如要啟用文件搜尋功能,請設定 PdfViewerFragment 執行個體的 isTextSearchActive 屬性。

使用 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 檔案,請整合 Android 檔案挑選器與 PdfViewerFragment。首先,請更新活動的版面配置 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 設定 PdfViewerFragment 執行個體的 documentUri 屬性,載入並顯示所選 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"
        // ...
    }
}

自訂 UI

您可以覆寫程式庫公開的 XML 屬性,自訂 PdfViewerFragment 的使用者介面。您可以自訂捲軸和網頁指標等元素的樣式,配合應用程式設計。

可自訂的屬性包括:

  • 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.newInstance(stylingOptions) 建立片段執行個體時,請使用 PdfStylingOptions,將自訂樣式資源提供給 PdfViewerFragment

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,包括初始化、檔案挑選器整合、搜尋功能和 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"
    }
}

程式碼重點

  • 確認專案符合最低 API 級別和 SDK 擴充功能需求。
  • 主辦 PdfViewerFragment 的活動必須擴充 AppCompatActivity
  • 您可以擴充 PdfViewerFragment,新增自訂行為。
  • 如要自訂 PdfViewerFragment 的 UI,請覆寫 XML 屬性。