告别XML布局:用Jetpack Compose优雅集成AndroidPdfViewer

告别XML布局:用Jetpack Compose优雅集成AndroidPdfViewer

【免费下载链接】AndroidPdfViewer Android view for displaying PDFs rendered with PdfiumAndroid 【免费下载链接】AndroidPdfViewer 项目地址: https://gitcode.com/gh_mirrors/an/AndroidPdfViewer

你还在为XML与Compose混合开发烦恼?还在手动封装传统View到Compose中?本文将手把手教你使用Jetpack Compose封装AndroidPdfViewer,实现现代化PDF浏览组件,彻底摆脱XML布局的束缚。

读完本文你将获得:

  • 掌握AndroidView封装传统视图的核心技巧
  • 实现PDF文件的Compose式加载与控制
  • 解决Compose与传统View通信的关键问题
  • 完整的PDF浏览组件代码,可直接集成到项目中

为什么需要Compose适配?

AndroidPdfViewer作为一款成熟的PDF浏览库,采用传统的XML布局和View体系实现。其核心类PDFView提供了丰富的PDF渲染功能,包括缩放、滑动、页面切换等。但随着Jetpack Compose的普及,越来越多的项目开始采用纯Compose开发,这就需要将传统View组件封装到Compose体系中。

传统XML集成方式需要在布局文件中声明PDFView,如:

<com.github.barteksc.pdfviewer.PDFView
    android:id="@+id/pdfView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

而在Compose中,我们可以通过更简洁的方式实现同样的功能,同时获得Compose带来的状态管理、重组优化等优势。

实现Compose封装的核心步骤

1. 添加依赖

首先确保项目中已添加AndroidPdfViewer依赖。在模块级build.gradle中添加:

implementation 'com.github.barteksc:android-pdf-viewer:3.2.0-beta.1'

同时需要添加Compose相关依赖,确保项目支持Jetpack Compose。

2. 创建Compose封装组件

使用AndroidView组件将PDFView封装到Compose中,创建一个可组合函数PdfViewer:

@Composable
fun PdfViewer(
    modifier: Modifier = Modifier,
    fileUri: Uri? = null,
    assetFileName: String? = null,
    onPageChanged: (Int, Int) -> Unit = { _, _ -> },
    onLoadComplete: (Int) -> Unit = {}
) {
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { context ->
            PDFView(context, null).apply {
                backgroundColor = Color.LTGRAY
                // 设置默认的滚动手柄
                scrollHandle = DefaultScrollHandle(context)
            }
        },
        update = { pdfView ->
            when {
                fileUri != null -> pdfView.fromUri(fileUri)
                assetFileName != null -> pdfView.fromAsset(assetFileName)
                else -> return@AndroidView
            }
            .defaultPage(0)
            .onPageChange { page, pageCount -> onPageChanged(page, pageCount) }
            .enableAnnotationRendering(true)
            .onLoad { onLoadComplete(it) }
            .spacing(10)
            .pageFitPolicy(FitPolicy.BOTH)
            .load()
        }
    )
}

这段代码使用AndroidView的factory创建PDFView实例,并在update块中根据提供的URI或资产文件名加载PDF文件。

3. 实现状态管理与控制

为了更好地在Compose中控制PDFView,我们需要创建一个状态管理器类,处理PDF加载、页面切换等操作:

class PdfViewerState(
    private val context: Context
) {
    var currentPage by mutableStateOf(0)
    var pageCount by mutableStateOf(0)
    var isLoading by mutableStateOf(false)
    
    // PDFView实例,用于直接调用其方法
    var pdfView: PDFView? = null
    
    fun loadFromAsset(assetFileName: String) {
        isLoading = true
        pdfView?.fromAsset(assetFileName)
            ?.defaultPage(currentPage)
            ?.onPageChange { page, count ->
                currentPage = page
                pageCount = count
            }
            ?.onLoad { 
                pageCount = it
                isLoading = false
            }
            ?.load()
    }
    
    fun loadFromUri(uri: Uri) {
        isLoading = true
        pdfView?.fromUri(uri)
            ?.defaultPage(currentPage)
            ?.onPageChange { page, count ->
                currentPage = page
                pageCount = count
            }
            ?.onLoad { 
                pageCount = it
                isLoading = false
            }
            ?.load()
    }
    
    fun nextPage() {
        if (currentPage < pageCount - 1) {
            currentPage++
            pdfView?.jumpTo(currentPage)
        }
    }
    
    fun previousPage() {
        if (currentPage > 0) {
            currentPage--
            pdfView?.jumpTo(currentPage)
        }
    }
}

4. 完善Compose组件

将状态管理类集成到PdfViewer组件中,实现完整的封装:

@Composable
fun PdfViewer(
    modifier: Modifier = Modifier,
    state: PdfViewerState,
    fileUri: Uri? = null,
    assetFileName: String? = null
) {
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { context ->
            PDFView(context, null).apply {
                backgroundColor = Color.LTGRAY
                scrollHandle = DefaultScrollHandle(context)
                state.pdfView = this
            }
        },
        update = { pdfView ->
            when {
                fileUri != null -> state.loadFromUri(fileUri)
                assetFileName != null -> state.loadFromAsset(assetFileName)
            }
        }
    )
    
    // 加载状态指示器
    if (state.isLoading) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            CircularProgressIndicator()
        }
    }
    
    // 页面导航控制
    Row(
        modifier = Modifier
            .align(Alignment.BottomCenter)
            .padding(16.dp)
            .background(Color.White, RoundedCornerShape(24.dp))
            .padding(8.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        IconButton(onClick = { state.previousPage() }) {
            Icon(Icons.Default.ArrowBack, contentDescription = "Previous page")
        }
        Text(
            text = "${state.currentPage + 1}/${state.pageCount}",
            modifier = Modifier.padding(horizontal = 16.dp)
        )
        IconButton(onClick = { state.nextPage() }) {
            Icon(Icons.Default.ArrowForward, contentDescription = "Next page")
        }
    }
}

5. 使用封装好的组件

在Activity或其他Composable中使用我们封装好的PdfViewer组件:

class PdfViewerActivity : ComponentActivity() {
    private lateinit var pdfViewerState: PdfViewerState
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pdfViewerState = PdfViewerState(this)
        
        setContent {
            MaterialTheme {
                Scaffold { padding ->
                    PdfViewer(
                        modifier = Modifier.padding(padding),
                        state = pdfViewerState,
                        assetFileName = "sample.pdf"
                    )
                }
            }
        }
    }
}

处理关键问题与优化

1. 权限处理

加载外部存储的PDF文件时,需要处理存储权限。在Compose中可以使用rememberLauncherForActivityResult来请求权限:

val requestPermissionLauncher = rememberLauncherForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
    if (isGranted) {
        // 权限已授予,加载PDF文件
        state.loadFromUri(selectedUri)
    } else {
        // 权限被拒绝,显示提示
        Toast.makeText(context, "需要存储权限才能加载PDF文件", Toast.LENGTH_SHORT).show()
    }
}

// 当需要加载外部PDF时调用
LaunchedEffect(Unit) {
    if (ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.READ_EXTERNAL_STORAGE
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
    } else {
        // 已有权限,直接加载
        state.loadFromUri(selectedUri)
    }
}

2. 状态保存与恢复

为了在配置变化(如屏幕旋转)时保持PDF浏览状态,需要保存当前页码等信息。可以使用rememberSaveable来保存状态:

@Composable
fun rememberPdfViewerState(context: Context): PdfViewerState {
    return rememberSaveable(saver = PdfViewerState.Saver(context)) {
        PdfViewerState(context)
    }
}

class PdfViewerState(
    private val context: Context
) {
    // ... 其他代码 ...
    
    companion object {
        fun Saver(context: Context) = object : Saver<PdfViewerState, Int> {
            override fun restore(value: Int): PdfViewerState {
                return PdfViewerState(context).apply {
                    currentPage = value
                }
            }

            override fun SaverScope.save(value: PdfViewerState): Int? {
                return value.currentPage
            }
        }
    }
}

3. 自定义滚动手柄

AndroidPdfViewer提供了默认的滚动手柄DefaultScrollHandle,我们可以在封装时自定义其样式或位置:

// 在创建PDFView时设置自定义滚动手柄
scrollHandle = DefaultScrollHandle(context, true) // true表示放在左侧

也可以通过修改资源文件来自定义滚动手柄的外观,如default_scroll_handle_right.xml

完整代码示例

下面是完整的Compose封装PDF浏览组件的代码结构:

- PdfViewer.kt             // Compose组件封装
- PdfViewerState.kt        // 状态管理类
- PdfViewerExtensions.kt   // 扩展函数和工具方法

以从资产文件加载PDF为例,完整的使用代码如下:

@Composable
fun AssetPdfViewer(
    modifier: Modifier = Modifier,
    assetFileName: String
) {
    val context = LocalContext.current
    val state = rememberPdfViewerState(context)
    
    PdfViewer(
        modifier = modifier,
        state = state,
        assetFileName = assetFileName
    )
}

// 在Activity中使用
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Scaffold { padding ->
                    AssetPdfViewer(
                        modifier = Modifier.padding(padding),
                        assetFileName = "sample.pdf"
                    )
                }
            }
        }
    }
}

总结与展望

通过本文的方法,我们成功将AndroidPdfViewer封装到Jetpack Compose中,实现了现代化的PDF浏览组件。这种封装方式不仅适用于PDFView,也可推广到其他传统View组件的Compose适配中。

AndroidPdfViewer项目目前正在寻找贡献者,如果您有兴趣改进这个库,可以访问项目README.md了解更多信息。未来,我们期待看到官方直接支持Compose的版本发布,彻底摆脱传统View的封装过程。

掌握Compose封装技巧,不仅能够提升开发效率,还能让我们的应用界面更加现代化、性能更加优化。希望本文的内容能够帮助您更好地在项目中使用Jetpack Compose和AndroidPdfViewer。

如果您觉得本文有帮助,请点赞、收藏、关注三连,下期我们将探讨如何实现PDF文件的在线加载与缓存策略。

【免费下载链接】AndroidPdfViewer Android view for displaying PDFs rendered with PdfiumAndroid 【免费下载链接】AndroidPdfViewer 项目地址: https://gitcode.com/gh_mirrors/an/AndroidPdfViewer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值