PdfViewerFragment 是一种专用 Fragment,可用于在 Android 应用中显示 PDF 文档。PdfViewerFragment 简化了 PDF 渲染,让您可以专注于应用功能的其他方面。
结果
版本兼容性
如需使用 PdfViewerFragment,您的应用必须以 Android S(API 级别 31)和 SDK 扩展级别 13 为最低目标平台。如果不满足这些兼容性要求,该库会抛出 UnsupportedOperationException。
您可以在运行时使用 SdkExtensions 模块检查 SDK 扩展版本。这样一来,您就可以仅在设备满足必要要求时有条件地加载 fragment 和 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 文档,方便您浏览。为了实现高效加载,该 fragment 采用双遍渲染策略,可逐步加载页面维度。
为了优化内存使用情况,PdfViewerFragment 仅渲染当前可见的页面,并释放屏幕外页面的位图。此外,PdfViewerFragment 还包含一个悬浮操作按钮 (FAB),该按钮通过触发包含文档 URI 的隐式 android.intent.action.ANNOTATE intent 来支持注释。
实现
向 Android 应用添加 PDF 查看器是一个多步骤的过程。
创建 activity 布局
首先,为托管 PDF 查看器的 activity 定义布局 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 的 activity 必须扩展 AppCompatActivity。在 activity 的 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() 获取的 fragment 管理器创建 PdfViewerFragment 的实例。在创建新的 fragment 实例之前,请检查该 fragment 的实例是否已存在,尤其是在配置更改期间。
在以下示例中,initializePdfViewerFragment() 函数负责处理 fragment 事务的创建和提交。该函数会将容器中现有的 fragment 替换为 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 文件,请将 PdfViewerFragment 与 Android 文件选择器集成。首先,更新 activity 的布局 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>
接下来,在您的 activity 中,使用 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"
// ...
}
}
自定义界面
您可以替换库公开的 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) 的 fragment 实例时,使用 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 进行子类化,请使用受保护的构造函数来提供样式选项。这样可确保您的自定义样式正确应用于扩展 fragment。
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)
}
}
}
完成实现
以下代码提供了一个完整示例,展示了如何在 activity 中实现 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的 activity 必须扩展AppCompatActivity。 - 您可以扩展
PdfViewerFragment以添加自定义行为。 - 通过替换 XML 属性来自定义
PdfViewerFragment的界面。