Triển khai trình xem PDF

PdfViewerFragment là một Fragment chuyên biệt mà bạn có thể dùng để hiển thị tài liệu PDF trong ứng dụng Android. PdfViewerFragment giúp đơn giản hoá quá trình kết xuất PDF, cho phép bạn tập trung vào các khía cạnh khác của chức năng ứng dụng.

Kết quả

Một tài liệu PDF được kết xuất trong ứng dụng Android bằng PdfViewerFragment.
Tài liệu PDF xuất hiện trong một ứng dụng.

Khả năng tương thích giữa các phiên bản

Để sử dụng PdfViewerFragment, ứng dụng của bạn phải nhắm đến tối thiểu Android S (API cấp 31) và cấp độ tiện ích SDK 13. Nếu không đáp ứng các yêu cầu về khả năng tương thích này, thư viện sẽ gửi một UnsupportedOperationException.

Bạn có thể kiểm tra phiên bản tiện ích SDK trong thời gian chạy bằng cách sử dụng mô-đun SdkExtensions. Điều này cho phép bạn tải đoạn mã và tài liệu PDF có điều kiện chỉ khi thiết bị đáp ứng các yêu cầu cần thiết.

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

Phần phụ thuộc

Để kết hợp trình xem PDF vào ứng dụng, hãy khai báo phần phụ thuộc androidx.pdf trong tệp build.gradle mô-đun của ứng dụng. Bạn có thể truy cập vào thư viện PDF trong kho lưu trữ Google Maven.

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

Tính năng PdfViewerFragment

PdfViewerFragment trình bày tài liệu PDF ở định dạng phân trang, giúp bạn dễ dàng điều hướng. Để tải hiệu quả, đoạn mã này sử dụng chiến lược kết xuất hai lượt, tải dần các phương diện trang.

Để tối ưu hoá mức sử dụng bộ nhớ, PdfViewerFragment chỉ kết xuất các trang hiện đang hiển thị và giải phóng các bitmap cho những trang nằm ngoài màn hình. Ngoài ra, PdfViewerFragment còn có một nút hành động nổi (FAB) hỗ trợ chú thích bằng cách kích hoạt một ý định android.intent.action.ANNOTATE ngầm chứa URI tài liệu.

Triển khai

Thêm trình xem PDF vào ứng dụng Android là một quy trình gồm nhiều bước.

Tạo bố cục hoạt động

Bắt đầu bằng cách xác định bố cục XML cho hoạt động lưu trữ trình xem PDF. Bố cục phải có một FrameLayout để chứa PdfViewerFragment và các nút cho hoạt động tương tác của người dùng, chẳng hạn như tìm kiếm trong tài liệu.

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

Thiết lập hoạt động

Hoạt động lưu trữ PdfViewerFragment phải mở rộng AppCompatActivity. Trong phương thức onCreate() của hoạt động, hãy đặt khung hiển thị nội dung thành bố cục mà bạn đã tạo và khởi chạy mọi phần tử giao diện người dùng cần thiết.

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

Khởi chạy PdfViewerFragment

Tạo một thực thể của PdfViewerFragment bằng trình quản lý mảnh nhận được từ getSupportFragmentManager(). Kiểm tra xem một thực thể của mảnh đã tồn tại hay chưa trước khi tạo một thực thể mới, đặc biệt là trong quá trình thay đổi cấu hình.

Trong ví dụ sau, hàm initializePdfViewerFragment() xử lý việc tạo và cam kết giao dịch đối với mảnh. Hàm này sẽ thay thế một mảnh hiện có trong vùng chứa bằng một thực thể của 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"
    }
}

Mở rộng chức năng PdfViewerFragment

PdfViewerFragment cung cấp các hàm công khai mà bạn có thể ghi đè để mở rộng các chức năng của hàm này. Tạo một lớp mới kế thừa từ PdfViewerFragment. Trong lớp con, hãy ghi đè các phương thức như onLoadDocumentSuccess()onLoadDocumentError() để thêm logic tuỳ chỉnh, chẳng hạn như ghi nhật ký chỉ số.

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

Mặc dù PdfViewerFragment không có trình đơn tìm kiếm tích hợp sẵn, nhưng ứng dụng này có hỗ trợ thanh tìm kiếm. Bạn kiểm soát chế độ hiển thị của thanh tìm kiếm bằng API isTextSearchActive. Để bật tính năng tìm kiếm tài liệu, bạn hãy đặt thuộc tính isTextSearchActive của phiên bản PdfViewerFragment.

Sử dụng WindowCompat.setDecorFitsSystemWindows() để đảm bảo WindowInsetsCompat được truyền chính xác đến các khung hiển thị nội dung. Điều này là cần thiết để định vị chính xác khung hiển thị tìm kiếm.

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

Tích hợp với bộ chọn tệp

Để cho phép người dùng chọn tệp PDF trên thiết bị của họ, hãy tích hợp PdfViewerFragment với trình chọn tệp Android. Trước tiên, hãy cập nhật XML bố cục của hoạt động để thêm một nút khởi chạy trình chọn tệp.

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

Tiếp theo, trong hoạt động của bạn, hãy khởi chạy công cụ chọn tệp bằng cách sử dụng registerForActivityResult(GetContent()). Khi người dùng chọn một tệp, lệnh gọi lại sẽ cung cấp một URI. Sau đó, bạn đặt thuộc tính documentUri của phiên bản PdfViewerFragment bằng URI này để tải và hiển thị tệp PDF đã chọn.

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

Tuỳ chỉnh giao diện người dùng

Bạn có thể tuỳ chỉnh giao diện người dùng của PdfViewerFragment bằng cách ghi đè các thuộc tính XML mà thư viện này hiển thị. Điều này cho phép bạn điều chỉnh giao diện của các phần tử như thanh cuộn và chỉ báo trang cho phù hợp với thiết kế của ứng dụng.

Các thuộc tính có thể tuỳ chỉnh bao gồm:

  • fastScrollVerticalThumbDrawable – Đặt đối tượng có thể vẽ cho ngón tay cái của thanh cuộn.
  • fastScrollPageIndicatorBackgroundDrawable – Đặt đối tượng vẽ nền cho chỉ báo trang.
  • fastScrollPageIndicatorMarginEnd – Đặt lề phải cho chỉ báo trang. Đảm bảo giá trị lề là số dương.
  • fastScrollVerticalThumbMarginEnd – Đặt lề phải cho nút kéo thanh cuộn dọc. Đảm bảo giá trị lề là số dương.

Để áp dụng các chế độ tuỳ chỉnh này, hãy xác định một kiểu tuỳ chỉnh trong tài nguyên 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>

Sau đó, hãy cung cấp tài nguyên kiểu tuỳ chỉnh cho PdfViewerFragment bằng cách sử dụng PdfStylingOptions khi bạn tạo một thực thể của mảnh bằng 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.
    }
}

Nếu bạn đã tạo lớp con PdfViewerFragment, hãy dùng hàm khởi tạo được bảo vệ để cung cấp các lựa chọn tạo kiểu. Điều này đảm bảo rằng các kiểu tuỳ chỉnh của bạn được áp dụng chính xác cho mảnh mở rộng.

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

Triển khai đầy đủ

Đoạn mã sau đây cung cấp một ví dụ hoàn chỉnh về cách triển khai PdfViewerFragment trong hoạt động của bạn, bao gồm cả quá trình khởi tạo, tích hợp trình chọn tệp, chức năng tìm kiếm và tuỳ chỉnh giao diện người dùng.

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

Các điểm chính về mã

  • Đảm bảo dự án của bạn đáp ứng các yêu cầu tối thiểu về cấp độ API và tiện ích SDK.
  • Hoạt động lưu trữ PdfViewerFragment phải mở rộng AppCompatActivity.
  • Bạn có thể mở rộng PdfViewerFragment để thêm các hành vi tuỳ chỉnh.
  • Tuỳ chỉnh giao diện người dùng của PdfViewerFragment bằng cách ghi đè các thuộc tính XML.