feat: pdf viewer

This commit is contained in:
2026-01-10 17:28:06 +08:00
parent 085af11706
commit f19b0d7c68
8 changed files with 347 additions and 3 deletions

View File

@@ -49,7 +49,8 @@
"Bash(while IFS= read -r file)",
"Bash(do if ! grep -q \"import com.lukouguoji.module_base.ktx.formatDate\" \"$file\")",
"Bash(then sed -i '' '/import dev.utils.common.DateUtils/a\\\\\nimport com.lukouguoji.module_base.ktx.formatDate\n' \"$file\" echo \"Added formatDate import to: $file\" fi done)",
"Bash(identify:*)"
"Bash(identify:*)",
"WebFetch(domain:github.com)"
],
"deny": [],
"ask": []

View File

@@ -138,6 +138,9 @@ dependencies {
api 'io.github.lucksiege:pictureselector:v3.11.2'
api "com.github.bumptech.glide:glide:4.15.1"
// PDF预览库
api 'com.github.barteksc:android-pdf-viewer:2.8.2'
// DevApp - Android 工具类库
api 'io.github.afkt:DevAppX:2.4.0'

View File

@@ -0,0 +1,18 @@
package com.lukouguoji.module_base.cache
import dev.DevUtils
/**
* PDF文件缓存管理
* 负责PDF文件的下载和本地缓存
*/
object PdfCacheModel : FileCacheModel() {
/**
* PDF文件缓存目录
* 路径: /data/data/com.lukouguoji.aerologic/files/cacheFiles/pdf/
*/
override fun folderPath(): String {
return DevUtils.getContext().filesDir.toString() + "/cacheFiles/pdf/"
}
}

View File

@@ -0,0 +1,223 @@
package com.lukouguoji.module_base.ui.page.preview
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener
import com.github.barteksc.pdfviewer.listener.OnPageChangeListener
import com.github.barteksc.pdfviewer.listener.OnErrorListener
import com.lukouguoji.module_base.R
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.cache.PdfCacheModel
import com.lukouguoji.module_base.common.Constant
import com.lukouguoji.module_base.databinding.ActivityPdfPreviewBinding
import com.lukouguoji.module_base.impl.EmptyViewModel
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.util.MediaUtil
import dev.utils.app.BarUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
/**
* PDF预览界面
* 支持在线PDF下载缓存和本地PDF文件预览
*/
class PdfPreviewActivity : BaseBindingActivity<ActivityPdfPreviewBinding, EmptyViewModel>() {
// PDF文件路径 (可能是在线URL或本地路径)
private var pdfPath: String = ""
// 当前页码 (从0开始)
private var currentPage = 0
// 总页数
private var totalPages = 0
override fun layoutId() = R.layout.activity_pdf_preview
override fun viewModelClass() = EmptyViewModel::class.java
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 强制横屏显示 (与项目其他页面保持一致)
resources?.let {
if (it.displayMetrics.widthPixels < it.displayMetrics.heightPixels) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} else {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
}
}
@SuppressLint("SourceLockedOrientationActivity")
override fun initOnCreate(savedInstanceState: Bundle?) {
// 设置沉浸式状态栏
BarUtils.transparentStatusBar(this)
// 获取传递的PDF路径
pdfPath = intent.getStringExtra(Constant.Key.DATA) ?: ""
if (pdfPath.isEmpty()) {
showToast("PDF路径为空")
finish()
return
}
// 设置关闭按钮
binding.ivClose.setOnClickListener {
finish()
}
// 加载PDF文件
loadPdfFile()
}
/**
* 加载PDF文件 (支持在线和本地)
*/
private fun loadPdfFile() {
// val fullUrl = MediaUtil.fillUrl(pdfPath)
val fullUrl = pdfPath
// 判断是否为在线资源
if (isOnlineResource(fullUrl)) {
// 在线PDF: 先下载再加载
downloadAndLoadPdf(fullUrl)
} else {
// 本地PDF: 直接加载
loadLocalPdf(File(pdfPath))
}
}
/**
* 判断是否为在线资源
*/
private fun isOnlineResource(url: String): Boolean {
return url.startsWith("http://") || url.startsWith("https://")
}
/**
* 下载在线PDF并加载
*/
private fun downloadAndLoadPdf(url: String) {
// 显示Loading
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
try {
// 使用PdfCacheModel下载PDF
PdfCacheModel.saveFile(
url = url,
onProgress = { progress ->
// 更新下载进度
lifecycleScope.launch(Dispatchers.Main) {
binding.tvLoadingTip.text = "正在下载PDF... $progress%"
}
},
onSuccess = { file ->
// 下载成功切换到主线程加载PDF
lifecycleScope.launch(Dispatchers.Main) {
hideLoading()
loadLocalPdf(file)
}
},
onError = { errorMsg ->
// 下载失败
lifecycleScope.launch(Dispatchers.Main) {
hideLoading()
showToast("PDF下载失败: $errorMsg")
finish()
}
}
)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
hideLoading()
showToast("PDF下载异常: ${e.message}")
finish()
}
}
}
}
/**
* 加载本地PDF文件
*/
private fun loadLocalPdf(file: File) {
if (!file.exists()) {
showToast("PDF文件不存在")
finish()
return
}
try {
binding.pdfView.fromFile(file)
.defaultPage(0) // 默认显示第一页
.enableSwipe(true) // 启用滑动翻页
.swipeHorizontal(false) // 垂直滑动
.enableDoubletap(true) // 启用双击缩放
.enableAnnotationRendering(false) // 禁用注释渲染(提升性能)
.password(null) // PDF密码(如需要)
.scrollHandle(null) // 禁用默认滚动条
.onLoad(OnLoadCompleteListener { nbPages ->
// PDF加载完成
totalPages = nbPages
updatePageIndicator()
})
.onPageChange(OnPageChangeListener { page, pageCount ->
// 页面切换
currentPage = page
updatePageIndicator()
})
.onError(OnErrorListener { t ->
// 加载错误
showToast("PDF加载失败: ${t.message}")
finish()
})
.load()
} catch (e: Exception) {
showToast("PDF渲染异常: ${e.message}")
finish()
}
}
/**
* 更新页码指示器
*/
private fun updatePageIndicator() {
binding.tvPageIndicator.text = "${currentPage + 1} / $totalPages"
}
/**
* 显示Loading
*/
private fun showLoading() {
binding.loadingLayout.visibility = View.VISIBLE
}
/**
* 隐藏Loading
*/
private fun hideLoading() {
binding.loadingLayout.visibility = View.GONE
}
companion object {
/**
* 启动PDF预览
* @param context 上下文
* @param pdfPath PDF路径 (支持在线URL或本地路径)
*/
@JvmStatic
fun start(context: Context, pdfPath: String) {
val starter = Intent(context, PdfPreviewActivity::class.java)
.putExtra(Constant.Key.DATA, pdfPath)
context.startActivity(starter)
}
}
}

View File

@@ -27,6 +27,13 @@
android:configChanges="orientation|keyboardHidden"
android:exported="true"/>
<!-- PDF预览Activity -->
<activity
android:name=".ui.page.preview.PdfPreviewActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:screenOrientation="userLandscape" />
<!-- 屏幕适配 -->
<meta-data
android:name="design_width_in_dp"

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.lukouguoji.module_base.impl.EmptyViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<!-- PDF视图 -->
<com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Loading蒙层 (下载时显示) -->
<RelativeLayout
android:id="@+id/loadingLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_tran50"
android:visibility="gone">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@color/black_tran70"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/tvLoadingTip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="正在加载PDF..."
android:textColor="@color/white"
android:textSize="14sp" />
</LinearLayout>
</RelativeLayout>
<!-- 页码指示器 -->
<TextView
android:id="@+id/tvPageIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="30dp"
android:background="@color/black_tran50"
android:paddingLeft="15dp"
android:paddingTop="5dp"
android:paddingRight="15dp"
android:paddingBottom="5dp"
android:text="1 / 10"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 关闭按钮 -->
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ivClose"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentRight="true"
android:layout_marginTop="60dp"
android:layout_marginRight="30dp"
android:background="@color/black_tran30"
android:padding="4dp"
android:src="@drawable/img_close" />
</RelativeLayout>
</layout>

View File

@@ -41,4 +41,9 @@
<color name="bottom_tool_tips_text_color">#797979</color>
<!-- 半透明黑色 (用于PDF预览) -->
<color name="black_tran30">#4D000000</color> <!-- 30%透明度 -->
<color name="black_tran50">#80000000</color> <!-- 50%透明度 -->
<color name="black_tran70">#B3000000</color> <!-- 70%透明度 -->
</resources>

View File

@@ -17,6 +17,7 @@ import com.lukouguoji.module_base.ktx.showConfirmDialog
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.ktx.toRequestBody
import com.lukouguoji.module_base.ui.page.preview.PreviewActivity
import com.lukouguoji.module_base.ui.page.preview.PdfPreviewActivity
import com.lukouguoji.module_base.util.MediaUtil
import kotlinx.coroutines.launch
@@ -154,9 +155,9 @@ class GjcInspectionDetailsViewModel : BaseViewModel() {
)
PreviewActivity.start(getTopActivity(), listOf(fileBean))
}
// PDF格式提示暂不支持
// PDF格式使用PdfPreviewActivity预览
attach.name.endsWith(".pdf", true) -> {
showToast("PDF文件预览功能开发中")
PdfPreviewActivity.start(getTopActivity(), attach.path)
}
// 其他格式
else -> {