diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 48ca91a..4073b57 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -105,13 +105,18 @@
android:label="@string/app_name"
android:screenOrientation="portrait" />
-
+
+
()
+ private lateinit var adapter: SearchResultsAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityPdfViewerTestBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ val pdfPath = intent.getStringExtra("pdf_path") ?: ""
+ if (pdfPath.isEmpty()) {
+ finish()
+ return
+ }
+ pdfFile = File(pdfPath)
+ initViews()
+ setupPDFViewer()
+ loadPDF()
+ }
+
+ private fun initViews() {
+ adapter = SearchResultsAdapter(searchResults) { result ->
+ onSearchResultClicked(result, true)
+ }
+ binding.searchResultsRecyclerView.adapter = adapter
+ binding.searchResultsRecyclerView.layoutManager = LinearLayoutManager(this)
+
+ binding.searchButton.alpha = 0.5f
+ binding.searchButton.isEnabled = false
+ binding.searchButton.setOnClickListener {
+ performSearch()
+ }
+ binding.clearButton.setOnClickListener {
+ clearHighlights()
+ }
+ }
+
+ private fun setupPDFViewer() {
+ searchManager = PDFSearchManager(binding.pdfView)
+ highlighter = PDFHighlighter(binding.pdfView, searchManager)
+
+ initSearchDocument()
+ }
+
+ private fun initSearchDocument() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ searchManager.getDocument(
+ pdfFile!!,
+ onLoaded = { doc ->
+ lifecycleScope.launch(Dispatchers.IO) {
+ checkPageHasText(doc, binding.pdfView.currentPage)
+ }
+ },
+ onError = { e ->
+ runOnUiThread {
+ binding.searchButton.alpha = 0.5f
+ binding.searchButton.isEnabled = false
+ }
+ }
+ )
+ }
+ }
+
+
+ /**
+ * 异步检查指定页是否有文字并更新按钮
+ */
+ private suspend fun checkPageHasText(doc: PDDocument, pageIndex: Int): Boolean {
+ val hasText = searchManager.pageHasText(doc, pageIndex, minChars = 8) // 可调整阈值
+ withContext(Dispatchers.Main) {
+ if (hasText) {
+ binding.searchButton.alpha = 1f
+ binding.searchButton.isEnabled = true
+ } else {
+ binding.searchButton.alpha = 0.5f
+ binding.searchButton.isEnabled = false
+ }
+ }
+ return hasText
+ }
+
+ private var pageCheckJob: Job? = null
+
+ private fun loadPDF() {
+ binding.pdfView.fromFile(pdfFile)
+ .enableSwipe(true)
+ .swipeHorizontal(true)
+ .enableDoubletap(true)
+ .defaultPage(0)
+ .onDraw(highlighter) // 设置绘制监听器
+ .enableAnnotationRendering(true)
+ .password(null)
+ .scrollHandle(null)
+ .swipeHorizontal(false)
+ .enableAntialiasing(true)
+ .onPageChange(object : OnPageChangeListener {
+ override fun onPageChanged(page: Int, pageCount: Int) {
+ logDebug("page->$page")
+ logDebug("pageCount->$pageCount")
+ val doc = searchManager.getCachedDoc()
+ if (doc != null) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ pageCheckJob?.cancel()
+ pageCheckJob = lifecycleScope.launch(Dispatchers.IO) {
+ delay(50) // 防抖
+ val hasText = checkPageHasText(doc, page)
+ //确定这页有文字,是否进行搜索过,当前搜索的文字
+ if (hasText && searchManager.hasSearched && searchManager.currentSearchText != null) {
+ searchTextIng(
+ pdfFile!!,
+ searchManager.currentSearchText ?: "",
+ binding.pdfView.currentPage
+ )
+ }
+ }
+ }
+ }
+
+ }
+ })
+ .spacing(0)
+ .load()
+ }
+
+ private fun performSearch() {
+ val searchText = binding.searchEditText.text.toString().trim()
+ if (searchText.isEmpty()) {
+ Toast.makeText(this, "请输入搜索关键词", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val file = pdfFile ?: return
+
+ binding.progressBar.visibility = View.VISIBLE
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ searchTextIng(file, searchText, binding.pdfView.currentPage)
+ }
+ }
+
+ private fun searchTextIng(pdfFile: File, key: String, targetPage: Int? = null) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ val results = searchManager.searchText(pdfFile, key, targetPage)
+
+ withContext(Dispatchers.Main) {
+ binding.progressBar.visibility = View.GONE
+ searchResults.clear()
+ searchResults.addAll(results)
+ adapter.notifyDataSetChanged()
+
+ if (!results.isEmpty()) {
+ results.firstOrNull()?.let { firstResult ->
+ onSearchResultClicked(firstResult)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ binding.progressBar.visibility = View.GONE
+ }
+ }
+ }
+ }
+
+ private fun onSearchResultClicked(
+ result: PDFSearchManager.TextSearchResult,
+ isJumpTo: Boolean = false
+ ) {
+ if (isJumpTo) {
+ // 跳转到对应页面 (PDFView 页面索引从0开始)
+ binding.pdfView.jumpTo(result.pageNumber - 1)
+ }
+
+ // 设置高亮
+ highlighter.setCurrentPageHighlights(result.pageNumber)
+
+ // 标记已搜索高亮
+ searchManager.currentSearchText = result.text
+ searchManager.hasSearched = true
+
+ // 可选:滚动到搜索结果列表中的对应项
+ val index = searchResults.indexOf(result)
+ if (index != -1) {
+ binding.searchResultsRecyclerView.scrollToPosition(index)
+ }
+ }
+
+ private fun clearHighlights() {
+ searchManager.clearHighlights()
+ highlighter.clearCurrentHighlights()
+ searchResults.clear()
+ adapter.notifyDataSetChanged()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ searchManager.closeCachedDocument()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt
index 3c100c0..c74a303 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt
@@ -276,7 +276,9 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
onTap(this@PdfViewActivity) // 单击回调
onPageChange(this@PdfViewActivity) // 页面改变回调
scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示
+ onDraw { canvas, pageWidth, pageHeight, displayedPage ->
+ }
if (appStore.isPageFling) {
pageSnap(true)//页面可在逐页中,可居中展示,非逐页模式,滑动停止后可自动定格在居中位置。
pageFling(true)//逐页
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/SearchResultsAdapter.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/SearchResultsAdapter.kt
new file mode 100644
index 0000000..ef9bfd5
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/SearchResultsAdapter.kt
@@ -0,0 +1,41 @@
+package com.all.pdfreader.pro.app.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.all.pdfreader.pro.app.R
+import com.all.pdfreader.pro.app.util.PDFSearchManager
+
+class SearchResultsAdapter(
+ private val results: List,
+ private val onItemClick: (PDFSearchManager.TextSearchResult) -> Unit
+) : RecyclerView.Adapter() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val pageText: TextView = view.findViewById(R.id.pageText)
+ val matchCount: TextView = view.findViewById(R.id.matchCount)
+ val previewText: TextView = view.findViewById(R.id.previewText)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_search_result, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val result = results[position]
+
+ holder.pageText.text = "第 ${result.pageNumber} 页"
+ holder.matchCount.text = "${result.positions.size} 处匹配"
+ holder.previewText.text = "搜索: \"${result.text}\""
+
+ holder.itemView.setOnClickListener {
+ onItemClick(result)
+ }
+ }
+
+ override fun getItemCount() = results.size
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt
index c2a618c..6b38f44 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt
@@ -1,13 +1,12 @@
package com.all.pdfreader.pro.app.ui.fragment
import android.content.Context
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.databinding.FragmentHomeBinding
import com.all.pdfreader.pro.app.model.FragmentType
@@ -15,6 +14,7 @@ import com.all.pdfreader.pro.app.model.SortConfig
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.ui.act.MainActivity
+import com.all.pdfreader.pro.app.ui.act.PdfTextSearchTestActivity
import com.all.pdfreader.pro.app.ui.act.PdfViewActivity
import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter
import com.all.pdfreader.pro.app.ui.dialog.ListMoreDialogFragment
@@ -53,6 +53,11 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf ->
val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
startActivity(intent)
+
+// val intent = Intent(context, PdfTextSearchTestActivity::class.java).apply {
+// putExtra("pdf_path", pdf.filePath)
+// }
+// startActivity(intent)
}, onMoreClick = { pdf ->
ListMoreDialogFragment(pdf.filePath).show(parentFragmentManager, FRAG_TAG)
}, onLongClick = { pdf ->
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PDFHighlighter.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PDFHighlighter.kt
new file mode 100644
index 0000000..901cd92
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PDFHighlighter.kt
@@ -0,0 +1,81 @@
+package com.all.pdfreader.pro.app.util
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RectF
+import com.github.barteksc.pdfviewer.PDFView
+import com.github.barteksc.pdfviewer.listener.OnDrawListener
+
+class PDFHighlighter(
+ private val pdfView: PDFView,
+ private val searchManager: PDFSearchManager
+) : OnDrawListener {
+
+ private val highlightPaint = Paint().apply {
+ color = 0x80FFD700.toInt() // 半透明黄色
+ style = Paint.Style.FILL
+ }
+
+ private val highlightStrokePaint = Paint().apply {
+ color = 0xFFFFA500.toInt() // 橙色边框
+ style = Paint.Style.STROKE
+ strokeWidth = 2f
+ }
+
+ private var currentPageHighlights: List = emptyList()
+
+ /**
+ * 设置当前页面的高亮
+ */
+ fun setCurrentPageHighlights(page: Int) {
+ currentPageHighlights = searchManager.getHighlightsForPage(page)
+ pdfView.invalidate()
+ }
+
+ /**
+ * 清除当前高亮
+ */
+ fun clearCurrentHighlights() {
+ currentPageHighlights = emptyList()
+ pdfView.invalidate()
+ }
+
+ override fun onLayerDrawn(canvas: Canvas, pageWidth: Float, pageHeight: Float, displayedPage: Int) {
+ // 只绘制当前显示页面的高亮
+ if (displayedPage != searchManager.currentHighlightPage) {
+ searchManager.currentHighlightPage = displayedPage
+ setCurrentPageHighlights(displayedPage + 1) // PDFView 页面从0开始,我们的搜索从1开始
+ }
+
+ // 绘制高亮区域
+ currentPageHighlights.forEach { position ->
+ try {
+ val rect = position.getRelativeRect()
+ val absoluteRect = RectF(
+ rect.left * pageWidth,
+ rect.top * pageHeight,
+ rect.right * pageWidth,
+ rect.bottom * pageHeight
+ )
+
+ // 可选:添加小的边距让高亮更明显
+ val margin = 1f
+ val paddedRect = RectF(
+ absoluteRect.left - margin,
+ absoluteRect.top - margin,
+ absoluteRect.right + margin,
+ absoluteRect.bottom + margin
+ )
+
+ // 绘制半透明高亮
+ canvas.drawRect(paddedRect, highlightPaint)
+
+ // 绘制边框
+ canvas.drawRect(paddedRect, highlightStrokePaint)
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PDFSearchManager.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PDFSearchManager.kt
new file mode 100644
index 0000000..962f41e
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PDFSearchManager.kt
@@ -0,0 +1,332 @@
+package com.all.pdfreader.pro.app.util
+
+import android.util.Log
+import com.github.barteksc.pdfviewer.PDFView
+import com.tom_roush.pdfbox.pdmodel.PDDocument
+import com.tom_roush.pdfbox.pdmodel.PDPage
+import com.tom_roush.pdfbox.text.PDFTextStripper
+import com.tom_roush.pdfbox.text.TextPosition
+import java.io.File
+
+class PDFSearchManager(private val pdfView: PDFView) {
+
+ private var currentSearchResults: List = emptyList()
+ var currentHighlightPage = -1
+
+ private var cachedDoc: PDDocument? = null // 缓存已打开的文档
+ private var cachedPath: String? = null // 记录缓存对应的文件路径
+
+ var currentSearchText: String? = null//记录当前的搜索文字
+ var hasSearched: Boolean = false//记录当前是否进行过搜索
+
+ data class TextSearchResult(
+ val pageNumber: Int,
+ val text: String,
+ val positions: List
+ )
+
+ data class TextPositionInfo(
+ val text: String, // 当前字符或文本片段
+ val x: Float, // 字符左下角的 X 坐标(PDF页面坐标系)
+ val y: Float, // 字符基线(baseline)在页面上的 Y 坐标(PDF页面坐标系)
+ val width: Float, // 字符宽度
+ val height: Float, // 字符高度
+ val pageWidth: Float, // 当前 PDF 页宽
+ val pageHeight: Float // 当前 PDF 页高
+ ) {
+ /**
+ * 将字符位置信息转换为 PDFView 可用的相对矩形 (RectF)
+ *
+ * @return RectF 返回一个相对页面比例的矩形(左上角和右下角的值在 0~1 范围)
+ *
+ * 说明:
+ * - PDFBox 中 y 是字符基线 (baseline),不是顶部,所以顶部需要计算为 y - height。(如果不这样那选中的位子会在文字的正下方)
+ * - RectF 的 left/top/right/bottom 坐标是相对于页面尺寸的比例。
+ * - 添加了 padding,使高亮框稍微大于文字,避免高亮框太紧。
+ */
+ fun getRelativeRect(): android.graphics.RectF {
+ // padding:用于微调高亮框大小,按页面比例计算
+ val padding = 2f / pageHeight
+
+ // 计算相对页面坐标
+ val left = x / pageWidth - padding // 左边界
+ val top = (y - height) / pageHeight - padding // 顶部边界(基线减去字符高度,再减去 margin)
+ val right = (x + width) / pageWidth + padding // 右边界
+ val bottom = y / pageHeight + padding // 底部边界(基线加上 margin)
+
+ // 返回相对页面坐标的矩形
+ return android.graphics.RectF(left, top, right, bottom)
+ }
+ }
+
+ /**
+ * 获取 PDDocument,若已缓存则直接返回,否则加载并缓存。
+ * @param file 要加载的 PDF 文件
+ * @param onLoaded 加载成功时回调(带 PDDocument 参数)
+ * @param onError 加载失败时回调(可选)
+ */
+ fun getDocument(
+ file: File,
+ onLoaded: ((PDDocument) -> Unit)? = null,
+ onError: ((Exception) -> Unit)? = null
+ ): PDDocument {
+ if (cachedDoc == null || cachedPath != file.absolutePath) {
+ try {
+ // 如果是新的文件,则关闭旧文档
+ cachedDoc?.close()
+ cachedDoc = PDDocument.load(file)
+ cachedPath = file.absolutePath
+
+ // 通知加载成功
+ onLoaded?.invoke(cachedDoc!!)
+ } catch (e: Exception) {
+ onError?.invoke(e)
+ throw e
+ }
+ } else {
+ // 已缓存文档,仍可调用回调
+ onLoaded?.invoke(cachedDoc!!)
+ }
+ return cachedDoc!!
+ }
+
+ fun getCachedDoc(): PDDocument? {
+ return cachedDoc
+ }
+
+ /**
+ * 释放 PDF 资源。
+ */
+ fun closeCachedDocument() {
+ try {
+ cachedDoc?.close()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ cachedDoc = null
+ cachedPath = null
+ }
+ }
+
+ /**
+ * 主搜索方法
+ * @param pdfFile 要搜索的 PDF 文件
+ * @param key 搜索关键词
+ * @param targetPage 可选参数:指定页码(从 0 开始)。如果为 null 则搜索整本。
+ */
+ fun searchText(pdfFile: File, key: String, targetPage: Int? = null): List {
+ Log.d("ocean", "searchText pdfFile->$pdfFile")
+ Log.d("ocean", "searchText key->$key")
+ Log.d("ocean", "searchText targetPage->$targetPage")
+ val results = mutableListOf()
+
+ try {
+ val doc = getDocument(pdfFile) // 这里不再每次重新 load
+
+ val startPage = targetPage ?: 0
+ val endPage = targetPage ?: (doc.numberOfPages - 1)
+
+ for (pageIndex in startPage..endPage) {
+ val page = doc.getPage(pageIndex)
+ val pageResults = searchInPage(doc, pageIndex, key, page)
+ results.add(pageResults)
+ }
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ currentSearchResults = results
+ return results
+ }
+
+ private fun searchInPage(
+ document: PDDocument,
+ pageIndex: Int,
+ searchText: String,
+ page: PDPage
+ ): TextSearchResult {
+ val positions = mutableListOf()
+ val searchTextLower = searchText.toLowerCase()
+ val pageWidth = page.mediaBox.width
+ val pageHeight = page.mediaBox.height
+
+ val textStripper = object : PDFTextStripper() {
+ private val currentPagePositions = mutableListOf()
+
+ override fun writeString(text: String, textPositions: List) {
+ // 收集文本位置信息
+ for (tp in textPositions) {
+ val positionInfo = TextPositionInfo(
+ text = tp.unicode,
+ x = tp.xDirAdj,
+ y = tp.yDirAdj,
+ width = tp.widthDirAdj,
+ height = tp.heightDir,
+ pageWidth = pageWidth,
+ pageHeight = pageHeight
+ )
+ currentPagePositions.add(positionInfo)
+ }
+ super.writeString(text, textPositions)
+ }
+
+ override fun endPage(page: PDPage?) {
+ // 在页面结束时进行搜索匹配
+ val matchedPositions = findTextMatches(currentPagePositions, searchTextLower)
+ if (matchedPositions.isNotEmpty()) {
+ positions.addAll(matchedPositions)
+ }
+ currentPagePositions.clear()
+ super.endPage(page)
+ }
+ }
+
+ textStripper.startPage = pageIndex + 1
+ textStripper.endPage = pageIndex + 1
+
+ try {
+ textStripper.getText(document)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ return TextSearchResult(
+ pageNumber = pageIndex + 1,
+ text = searchText,
+ positions = positions
+ )
+ }
+
+ private fun findTextMatches(
+ positions: List,
+ searchText: String
+ ): List {
+ val matches = mutableListOf()
+ val fullText = positions.joinToString("") { it.text }.toLowerCase()
+
+ var index = 0
+ while (index < fullText.length) {
+ val matchIndex = fullText.indexOf(searchText, index)
+ if (matchIndex == -1) break
+
+ // 精确查找匹配的文本段在positions中的范围
+ var currentLength = 0
+ val matchedPositions = mutableListOf()
+
+ for (pos in positions) {
+ val positionStart = currentLength
+ val positionEnd = currentLength + pos.text.length
+
+ // 精确检查:这个位置是否在当前匹配范围内
+ if (positionStart < matchIndex + searchText.length &&
+ positionEnd > matchIndex
+ ) {
+ matchedPositions.add(pos)
+ }
+
+ currentLength += pos.text.length
+
+ // 如果已经超过当前匹配范围,立即停止
+ if (currentLength >= matchIndex + searchText.length) break
+ }
+
+ // 计算合并后的矩形区域
+ if (matchedPositions.isNotEmpty()) {
+ val mergedPosition = mergePositions(matchedPositions, searchText)
+ matches.add(mergedPosition)
+ }
+
+ index = matchIndex + searchText.length
+ }
+
+ Log.d("PDF_DEBUG", "找到 ${matches.size} 个独立匹配")
+ return matches
+ }
+
+ private fun mergePositions(
+ positions: List,
+ originalText: String
+ ): TextPositionInfo {
+ val first = positions.first()
+ val minX = positions.minByOrNull { it.x }?.x ?: first.x
+ val maxX = positions.maxByOrNull { it.x + it.width }?.let { it.x + it.width }
+ ?: (first.x + first.width)
+ val minY = positions.minByOrNull { it.y }?.y ?: first.y
+ val maxY = positions.maxByOrNull { it.y + it.height }?.let { it.y + it.height }
+ ?: (first.y + first.height)
+
+ return TextPositionInfo(
+ text = originalText,
+ x = minX,
+ y = minY,
+ width = maxX - minX,
+ height = maxY - minY,
+ pageWidth = first.pageWidth,
+ pageHeight = first.pageHeight
+ )
+ }
+
+ /**
+ * 获取当前页面的高亮区域
+ */
+ fun getHighlightsForPage(page: Int): List {
+ return currentSearchResults
+ .find { it.pageNumber == page }
+ ?.positions ?: emptyList()
+ }
+
+ /**
+ * 清除所有高亮
+ */
+ fun clearHighlights() {
+ currentSearchResults = emptyList()
+ pdfView.invalidate()
+ currentSearchText = null
+ hasSearched = false
+ }
+
+ /**
+ * 判断当前页是否有可搜索文字
+ * @param doc 当前 PDF 文档
+ * @param pageIndex 当前页索引
+ * @param minChars 阈值:认为一页有文字的最少字符数
+ */
+ fun pageHasText(doc: PDDocument, pageIndex: Int, minChars: Int = 10): Boolean {
+ var charCount = 0
+
+ val stripper = object : PDFTextStripper() {
+ override fun processTextPosition(text: TextPosition) {
+ Log.d(
+ "ocean",
+ "processTextPosition text->$text width->${text.width}height->${text.height}"
+ )
+ //太小的文字当没有
+ if (text.width > 1 && text.height > 2) {
+ charCount++
+ }
+ // 如果超过阈值,直接停止
+ if (charCount >= minChars) {
+ throw StopParsingException()
+ }
+ }
+ }
+
+ return try {
+ stripper.startPage = pageIndex + 1
+ stripper.endPage = pageIndex + 1
+ stripper.getText(doc)
+ // 解析完,如果字符数达不到阈值就返回 false
+ charCount >= minChars
+ } catch (e: StopParsingException) {
+ // 遇到足够字符,认为有文字
+ true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+
+
+ private class StopParsingException : RuntimeException()
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_pdf_view.xml b/app/src/main/res/layout/activity_pdf_view.xml
index c6afa4e..767f606 100644
--- a/app/src/main/res/layout/activity_pdf_view.xml
+++ b/app/src/main/res/layout/activity_pdf_view.xml
@@ -39,33 +39,99 @@
-
+ android:visibility="gone">
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_pdf_viewer_test.xml b/app/src/main/res/layout/activity_pdf_viewer_test.xml
new file mode 100644
index 0000000..e4fd264
--- /dev/null
+++ b/app/src/main/res/layout/activity_pdf_viewer_test.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml
new file mode 100644
index 0000000..f470d5d
--- /dev/null
+++ b/app/src/main/res/layout/item_search_result.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file