添加搜索文字测试代码,后续看是否使用。(未完成)
This commit is contained in:
parent
d9dfa75b9b
commit
8e4c25d54f
@ -105,13 +105,18 @@
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.act.PdfToImageActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.act.PdfTextSearchTestActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@ -0,0 +1,226 @@
|
||||
package com.all.pdfreader.pro.app.ui.act
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.all.pdfreader.pro.app.databinding.ActivityPdfViewerTestBinding
|
||||
import com.all.pdfreader.pro.app.ui.adapter.SearchResultsAdapter
|
||||
import com.all.pdfreader.pro.app.util.PDFHighlighter
|
||||
import com.all.pdfreader.pro.app.util.PDFSearchManager
|
||||
import com.github.barteksc.pdfviewer.listener.OnPageChangeListener
|
||||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class PdfTextSearchTestActivity : BaseActivity() {
|
||||
override val TAG: String = "PdfTextSearchTestActivity"
|
||||
private lateinit var binding: ActivityPdfViewerTestBinding
|
||||
private lateinit var searchManager: PDFSearchManager
|
||||
private lateinit var highlighter: PDFHighlighter
|
||||
|
||||
private var pdfFile: File? = null
|
||||
private val searchResults = mutableListOf<PDFSearchManager.TextSearchResult>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -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)//逐页
|
||||
|
||||
@ -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<PDFSearchManager.TextSearchResult>,
|
||||
private val onItemClick: (PDFSearchManager.TextSearchResult) -> Unit
|
||||
) : RecyclerView.Adapter<SearchResultsAdapter.ViewHolder>() {
|
||||
|
||||
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
|
||||
}
|
||||
@ -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 ->
|
||||
|
||||
@ -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<PDFSearchManager.TextPositionInfo> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<TextSearchResult> = 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<TextPositionInfo>
|
||||
)
|
||||
|
||||
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<TextSearchResult> {
|
||||
Log.d("ocean", "searchText pdfFile->$pdfFile")
|
||||
Log.d("ocean", "searchText key->$key")
|
||||
Log.d("ocean", "searchText targetPage->$targetPage")
|
||||
val results = mutableListOf<TextSearchResult>()
|
||||
|
||||
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<TextPositionInfo>()
|
||||
val searchTextLower = searchText.toLowerCase()
|
||||
val pageWidth = page.mediaBox.width
|
||||
val pageHeight = page.mediaBox.height
|
||||
|
||||
val textStripper = object : PDFTextStripper() {
|
||||
private val currentPagePositions = mutableListOf<TextPositionInfo>()
|
||||
|
||||
override fun writeString(text: String, textPositions: List<TextPosition>) {
|
||||
// 收集文本位置信息
|
||||
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<TextPositionInfo>,
|
||||
searchText: String
|
||||
): List<TextPositionInfo> {
|
||||
val matches = mutableListOf<TextPositionInfo>()
|
||||
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<TextPositionInfo>()
|
||||
|
||||
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<TextPositionInfo>,
|
||||
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<TextPositionInfo> {
|
||||
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()
|
||||
}
|
||||
@ -39,33 +39,99 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextViewFont_PopMedium"
|
||||
android:layout_width="0dp"
|
||||
<LinearLayout
|
||||
android:id="@+id/searchTextLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dr_item_img_frame"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/searchEdit"
|
||||
style="@style/TextViewFont_PopRegular"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@null"
|
||||
android:hint="@string/search_hint"
|
||||
android:textSize="14sp" />
|
||||
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/searchIv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/search" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/deleteIv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/delete_cha_icon"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/moreBtn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/dr_click_effect_oval_transparent"
|
||||
android:gravity="center">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/more" />
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextViewFont_PopMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/searchTextBtn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/dr_click_effect_oval_transparent"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/search" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/moreBtn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/dr_click_effect_oval_transparent"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/more" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
74
app/src/main/res/layout/activity_pdf_viewer_test.xml
Normal file
74
app/src/main/res/layout/activity_pdf_viewer_test.xml
Normal file
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#f5f5f5"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/searchEditText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="输入搜索关键词"
|
||||
android:imeOptions="actionSearch"
|
||||
android:singleLine="true" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/searchButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="搜索" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/clearButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="清除" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debugTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- PDF显示区域 -->
|
||||
<com.github.barteksc.pdfviewer.PDFView
|
||||
android:id="@+id/pdfView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="3" />
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/searchResultsRecyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="#ffffff" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
34
app/src/main/res/layout/item_search_result.xml
Normal file
34
app/src/main/res/layout/item_search_result.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pageText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#333333" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/matchCount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#666666"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/previewText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#888888"
|
||||
android:layout_marginTop="4dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
Loading…
Reference in New Issue
Block a user