添加搜索文字功能(基本完成),还可优化。

This commit is contained in:
ocean 2025-10-27 10:17:33 +08:00
parent 8e4c25d54f
commit ed7430e7d2
9 changed files with 469 additions and 81 deletions

View File

@ -3,13 +3,19 @@ package com.all.pdfreader.pro.app.ui.act
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding
import com.all.pdfreader.pro.app.model.FileActionEvent import com.all.pdfreader.pro.app.model.FileActionEvent
import com.all.pdfreader.pro.app.model.PdfPickerSource
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.ui.dialog.BookmarksDialogFragment import com.all.pdfreader.pro.app.ui.dialog.BookmarksDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.ListMoreDialogFragment import com.all.pdfreader.pro.app.ui.dialog.ListMoreDialogFragment
@ -17,7 +23,11 @@ import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordProtectionDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.ViewModelDialogFragment import com.all.pdfreader.pro.app.ui.dialog.ViewModelDialogFragment
import com.all.pdfreader.pro.app.ui.view.CustomScrollHandle import com.all.pdfreader.pro.app.ui.view.CustomScrollHandle
import com.all.pdfreader.pro.app.util.AppUtils import com.all.pdfreader.pro.app.util.AppUtils
import com.all.pdfreader.pro.app.util.AppUtils.hideKeyboard
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
import com.all.pdfreader.pro.app.util.FileUtils import com.all.pdfreader.pro.app.util.FileUtils
import com.all.pdfreader.pro.app.util.PDFHighlighter
import com.all.pdfreader.pro.app.util.PDFSearchManager
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import com.all.pdfreader.pro.app.viewmodel.observeEvent import com.all.pdfreader.pro.app.viewmodel.observeEvent
import com.github.barteksc.pdfviewer.listener.OnErrorListener import com.github.barteksc.pdfviewer.listener.OnErrorListener
@ -26,7 +36,13 @@ import com.github.barteksc.pdfviewer.listener.OnPageChangeListener
import com.github.barteksc.pdfviewer.listener.OnTapListener import com.github.barteksc.pdfviewer.listener.OnTapListener
import com.gyf.immersionbar.BarHide import com.gyf.immersionbar.BarHide
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
import com.tom_roush.pdfbox.pdmodel.PDDocument
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeListener, class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeListener,
@ -51,6 +67,10 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
private val repository = getRepository() private val repository = getRepository()
private var isFullScreen = false private var isFullScreen = false
private var lastLoadedFilePath: String? = null private var lastLoadedFilePath: String? = null
private var showSearchTextView = false
private lateinit var searchManager: PDFSearchManager
private lateinit var highlighter: PDFHighlighter
private var pageCheckJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -58,6 +78,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
setContentView(binding.root) setContentView(binding.root)
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.white).init() .navigationBarColor(R.color.white).init()
setupDoubleBackExit()
initObserve() initObserve()
val filePath = intent.getStringExtra(EXTRA_PDF_HASH) val filePath = intent.getStringExtra(EXTRA_PDF_HASH)
?: throw IllegalArgumentException("PDF file hash is required") ?: throw IllegalArgumentException("PDF file hash is required")
@ -165,7 +186,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
private fun setupOnClick() { private fun setupOnClick() {
binding.backBtn.setOnClickListener { binding.backBtn.setOnClickListener {
finish() onBackPressedDispatcher.onBackPressed()
} }
binding.eyeCareOverlayBtn.setOnClickListener { binding.eyeCareOverlayBtn.setOnClickListener {
appStore.isEyeCareMode = !appStore.isEyeCareMode appStore.isEyeCareMode = !appStore.isEyeCareMode
@ -188,6 +209,55 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
binding.moreBtn.setOnClickListener { binding.moreBtn.setOnClickListener {
ListMoreDialogFragment(pdfDocument.filePath).show(supportFragmentManager, FRAG_TAG) ListMoreDialogFragment(pdfDocument.filePath).show(supportFragmentManager, FRAG_TAG)
} }
binding.searchTextBtn.setOnClickListener {
showSearchTextView = true
updateSearchState(true)
binding.selectNextBtn.visibility = View.GONE
binding.selectPreviousBtn.visibility = View.GONE
}
binding.searchEdit.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val query = s?.toString().orEmpty()
binding.deleteIv.visibility = if (query.isEmpty()) View.GONE else View.VISIBLE
binding.searchIv.visibility = if (query.isEmpty()) View.VISIBLE else View.GONE
}
})
binding.searchEdit.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val searchText = binding.searchEdit.text.toString().trim()
if (searchText.isNotEmpty()) {
performSearch(searchText)
binding.searchEdit.hideKeyboard() // 收起键盘
} else {
showToast(getString(R.string.enter_search_key))
}
true
} else {
false
}
}
binding.deleteIv.setOnClickListener {
binding.searchEdit.apply {
setText("")
clearComposingText()
setSelection(0)
}
clearHighlights()
binding.selectNextBtn.visibility = View.GONE
binding.selectPreviousBtn.visibility = View.GONE
}
binding.selectNextBtn.setOnClickListener {
highlighter.selectNext { id ->
showToast(getString(id))
}
}
binding.selectPreviousBtn.setOnClickListener {
highlighter.selectPrevious { id ->
showToast(getString(id))
}
}
} }
private fun loadPdf() { private fun loadPdf() {
@ -199,6 +269,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
finish() finish()
return return
} }
setupPDFSearchManager()//初始化搜索文本需要的类
if (pdfDocument.isPassword) { if (pdfDocument.isPassword) {
showPasswordDialog(file) showPasswordDialog(file)
} else { } else {
@ -239,10 +310,22 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
lastReadPage = page, readingProgress = (page.toFloat() / pageCount.toFloat()) * 100 lastReadPage = page, readingProgress = (page.toFloat() / pageCount.toFloat()) * 100
) )
saveReadingProgress() saveReadingProgress()
}
override fun onDestroy() { val doc = searchManager.getCachedDoc() ?: return
super.onDestroy() // 取消上一次检查任务
pageCheckJob?.cancel()
// 启动新的防抖任务
pageCheckJob = lifecycleScope.launch(Dispatchers.IO) {
delay(120) // 防抖 120ms
val hasText = checkPageHasText(doc, page)
if (hasText && searchManager.hasSearched && !searchManager.currentSearchText.isNullOrEmpty()) {
searchTextIng(
File(pdfDocument.filePath),
searchManager.currentSearchText ?: "",
binding.pdfview.currentPage
)
}
}
} }
private fun saveReadingProgress() { private fun saveReadingProgress() {
@ -276,9 +359,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
onTap(this@PdfViewActivity) // 单击回调 onTap(this@PdfViewActivity) // 单击回调
onPageChange(this@PdfViewActivity) // 页面改变回调 onPageChange(this@PdfViewActivity) // 页面改变回调
scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示 scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示
onDraw { canvas, pageWidth, pageHeight, displayedPage -> onDraw(highlighter)
}
if (appStore.isPageFling) { if (appStore.isPageFling) {
pageSnap(true)//页面可在逐页中,可居中展示,非逐页模式,滑动停止后可自动定格在居中位置。 pageSnap(true)//页面可在逐页中,可居中展示,非逐页模式,滑动停止后可自动定格在居中位置。
pageFling(true)//逐页 pageFling(true)//逐页
@ -299,6 +380,9 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
} }
private fun toggleFullScreen() { private fun toggleFullScreen() {
if (showSearchTextView) {//如果是搜索文本时,不全屏
return
}
isFullScreen = !isFullScreen isFullScreen = !isFullScreen
updateStatusAndNavigationLayout(isFullScreen) updateStatusAndNavigationLayout(isFullScreen)
if (isFullScreen) { if (isFullScreen) {
@ -337,4 +421,129 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(navColor).init() .navigationBarColor(navColor).init()
} }
private fun setupDoubleBackExit() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (showSearchTextView) {//如果当前是搜索,则退出搜索
showSearchTextView = false
updateSearchState(false)
clearHighlights()
binding.searchEdit.apply {
setText("")
clearComposingText()
setSelection(0)
}
} else {
isEnabled = false // 解除拦截
onBackPressedDispatcher.onBackPressed() // 调用系统默认返回逻辑
}
}
})
}
private fun updateSearchState(b: Boolean) {
if (b) {
binding.searchTextLayout.visibility = View.VISIBLE
binding.navigationLayout.visibility = View.GONE
binding.searchEdit.showKeyboard()
} else {
binding.searchTextLayout.visibility = View.GONE
binding.navigationLayout.visibility = View.VISIBLE
binding.searchEdit.hideKeyboard()
}
}
private fun setupPDFSearchManager() {
searchManager = PDFSearchManager(binding.pdfview)
highlighter = PDFHighlighter(binding.pdfview, searchManager)
initSearchDocument()
}
private fun initSearchDocument() {
val file = File(pdfDocument.filePath)
lifecycleScope.launch(Dispatchers.IO) {
searchManager.getDocument(file, onLoaded = { doc ->
lifecycleScope.launch(Dispatchers.IO) {
checkPageHasText(doc, binding.pdfview.currentPage)
}
}, onError = { e ->
runOnUiThread {
binding.searchTextBtn.visibility = View.GONE
binding.searchTextBtn.isEnabled = false
}
})
}
}
/**
* 异步检查指定页是否有文字并更新按钮
*/
private suspend fun checkPageHasText(doc: PDDocument, pageIndex: Int): Boolean {
val hasText = searchManager.pageHasText(doc, pageIndex, minChars = 10) // 可调整阈值
withContext(Dispatchers.Main) {
if (hasText) {
binding.searchTextBtn.visibility = View.VISIBLE
binding.searchTextBtn.isEnabled = true
} else {
binding.searchTextBtn.visibility = View.GONE
binding.searchTextBtn.isEnabled = false
}
}
return hasText
}
private fun performSearch(searchText: String) {
val file = File(pdfDocument.filePath)
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) {
if (!results.isEmpty()) {
binding.selectNextBtn.visibility = View.VISIBLE
binding.selectPreviousBtn.visibility = View.VISIBLE
results.firstOrNull()?.let { firstResult ->
onSearchResultClicked(firstResult)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
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
}
private fun clearHighlights() {
searchManager.clearHighlights()
highlighter.clearCurrentHighlights()
}
override fun onDestroy() {
super.onDestroy()
searchManager.closeCachedDocument()
}
} }

View File

@ -100,6 +100,15 @@ object AppUtils {
}, 200) }, 200)
} }
fun EditText.hideKeyboard() {
clearFocus()
postDelayed({
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(windowToken, 0)
}, 200)
}
/** /**
* 分享文件自动识别 MIME 类型 * 分享文件自动识别 MIME 类型
* @param context 上下文 * @param context 上下文

View File

@ -3,6 +3,7 @@ package com.all.pdfreader.pro.app.util
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import com.all.pdfreader.pro.app.R
import com.github.barteksc.pdfviewer.PDFView import com.github.barteksc.pdfviewer.PDFView
import com.github.barteksc.pdfviewer.listener.OnDrawListener import com.github.barteksc.pdfviewer.listener.OnDrawListener
@ -11,6 +12,13 @@ class PDFHighlighter(
private val searchManager: PDFSearchManager private val searchManager: PDFSearchManager
) : OnDrawListener { ) : OnDrawListener {
enum class ClickType {
NEXT,
PREVIOUS
}
private var type = ClickType.NEXT
private val highlightPaint = Paint().apply { private val highlightPaint = Paint().apply {
color = 0x80FFD700.toInt() // 半透明黄色 color = 0x80FFD700.toInt() // 半透明黄色
style = Paint.Style.FILL style = Paint.Style.FILL
@ -22,13 +30,34 @@ class PDFHighlighter(
strokeWidth = 2f strokeWidth = 2f
} }
private val selectedPaint = Paint().apply {
color = 0x80FF4500.toInt() // 当前选中高亮,橙红色半透明
style = Paint.Style.FILL
}
private val selectedStrokePaint = Paint().apply {
color = 0xFFFF6347.toInt() // 当前选中边框,亮橙红
style = Paint.Style.STROKE
strokeWidth = 2f
}
private var currentPageHighlights: List<PDFSearchManager.TextPositionInfo> = emptyList() private var currentPageHighlights: List<PDFSearchManager.TextPositionInfo> = emptyList()
private var currentSelectedIndex: Int = -1
/** /**
* 设置当前页面的高亮 * 设置当前页面的高亮
*/ */
fun setCurrentPageHighlights(page: Int) { fun setCurrentPageHighlights(page: Int) {
currentPageHighlights = searchManager.getHighlightsForPage(page) currentPageHighlights = searchManager.getHighlightsForPage(page)
currentSelectedIndex = when (type) {
ClickType.NEXT -> {
if (currentPageHighlights.isNotEmpty()) 0 else -1
}
ClickType.PREVIOUS -> {
if (currentPageHighlights.isNotEmpty()) currentPageHighlights.size - 1 else -1
}
}
pdfView.invalidate() pdfView.invalidate()
} }
@ -37,18 +66,78 @@ class PDFHighlighter(
*/ */
fun clearCurrentHighlights() { fun clearCurrentHighlights() {
currentPageHighlights = emptyList() currentPageHighlights = emptyList()
currentSelectedIndex = -1
pdfView.invalidate() pdfView.invalidate()
} }
override fun onLayerDrawn(canvas: Canvas, pageWidth: Float, pageHeight: Float, displayedPage: Int) { /**
// 只绘制当前显示页面的高亮 * 选中下一条高亮
*/
fun selectNext(rid: ((Int) -> Unit)? = null) {
type = ClickType.NEXT
if (currentPageHighlights.isEmpty()) {//当前页没有对应的文字,直接跳转下一页
val nextPage = searchManager.currentHighlightPage + 1
pdfView.jumpTo(nextPage, true)
return
}
if (currentSelectedIndex < currentPageHighlights.size - 1) {
currentSelectedIndex++
//下一页+,需要刷新
pdfView.invalidate()
} else {
// 当前页最后一条,尝试跳到下一页
val nextPage = searchManager.currentHighlightPage + 1
if (nextPage < pdfView.pageCount) {
pdfView.jumpTo(nextPage, true)
currentSelectedIndex = 0
} else {
rid?.invoke(R.string.no_more_results)
}
}
}
/**
* 选中上一条高亮
*/
fun selectPrevious(rid: ((Int) -> Unit)? = null) {
type = ClickType.PREVIOUS
if (currentPageHighlights.isEmpty()) { // 当前页没有对应的文字,直接跳转上一页
val prevPage = searchManager.currentHighlightPage - 1
if (prevPage >= 0) {
pdfView.jumpTo(prevPage, true)
} else {
rid?.invoke(R.string.no_more_results)
}
return
}
if (currentSelectedIndex > 0) {
currentSelectedIndex--
pdfView.invalidate()
} else {
val prevPage = searchManager.currentHighlightPage - 1
if (prevPage >= 0) {
pdfView.jumpTo(prevPage, true)
currentPageHighlights = emptyList()
currentSelectedIndex = -1
} else {
rid?.invoke(R.string.no_more_results)
}
}
}
override fun onLayerDrawn(
canvas: Canvas,
pageWidth: Float,
pageHeight: Float,
displayedPage: Int
) {
if (displayedPage != searchManager.currentHighlightPage) { if (displayedPage != searchManager.currentHighlightPage) {
searchManager.currentHighlightPage = displayedPage searchManager.currentHighlightPage = displayedPage
setCurrentPageHighlights(displayedPage + 1) // PDFView 页面从0开始我们的搜索从1开始 // setCurrentPageHighlights(displayedPage + 1)
} }
// 绘制高亮区域 currentPageHighlights.forEachIndexed { index, position ->
currentPageHighlights.forEach { position ->
try { try {
val rect = position.getRelativeRect() val rect = position.getRelativeRect()
val absoluteRect = RectF( val absoluteRect = RectF(
@ -58,7 +147,6 @@ class PDFHighlighter(
rect.bottom * pageHeight rect.bottom * pageHeight
) )
// 可选:添加小的边距让高亮更明显
val margin = 1f val margin = 1f
val paddedRect = RectF( val paddedRect = RectF(
absoluteRect.left - margin, absoluteRect.left - margin,
@ -67,12 +155,13 @@ class PDFHighlighter(
absoluteRect.bottom + margin absoluteRect.bottom + margin
) )
// 绘制半透明高亮 if (index == currentSelectedIndex) {
canvas.drawRect(paddedRect, highlightPaint) canvas.drawRect(paddedRect, selectedPaint)
canvas.drawRect(paddedRect, selectedStrokePaint)
// 绘制边框 } else {
canvas.drawRect(paddedRect, highlightStrokePaint) canvas.drawRect(paddedRect, highlightPaint)
canvas.drawRect(paddedRect, highlightStrokePaint)
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View File

@ -198,49 +198,74 @@ class PDFSearchManager(private val pdfView: PDFView) {
) )
} }
/**
* 在一页的 TextPositionInfo 列表中搜索指定文本并返回每个匹配的高亮区域
*
* 逻辑说明
* 1. 按视觉顺序纵向 y, 横向 x排序字符
* 2. 根据 y 坐标将字符分组为同一行避免跨行高亮
* 3. 对每一行独立进行逐字符匹配
* - 清理不可打印字符零宽空格控制字符等
* - 如果匹配成功则将匹配字符合并为一个矩形区域
*
* @param positions 当前页的所有字符位置信息TextPositionInfo
* @param searchText 要搜索的文本不区分大小写
* @return 匹配的高亮区域列表每个 TextPositionInfo 对象表示一段匹配文字
*/
private fun findTextMatches( private fun findTextMatches(
positions: List<TextPositionInfo>, positions: List<TextPositionInfo>,
searchText: String searchText: String
): List<TextPositionInfo> { ): List<TextPositionInfo> {
val matches = mutableListOf<TextPositionInfo>() val matches = mutableListOf<TextPositionInfo>()
val fullText = positions.joinToString("") { it.text }.toLowerCase() val chars = searchText.lowercase().toCharArray()
var index = 0 // 按纵向 y、横向 x 排序,保证字符视觉顺序
while (index < fullText.length) { val sortedPositions = positions.sortedWith(compareBy({ it.y }, { it.x }))
val matchIndex = fullText.indexOf(searchText, index)
if (matchIndex == -1) break
// 精确查找匹配的文本段在positions中的范围 // 按行拆分字符,避免高亮跨行
var currentLength = 0 val lines = mutableListOf<MutableList<TextPositionInfo>>()
val matchedPositions = mutableListOf<TextPositionInfo>() var currentLine = mutableListOf<TextPositionInfo>()
var lastY = sortedPositions.firstOrNull()?.y ?: 0f
val lineThreshold = 2f // y 差值小于阈值则视为同一行,可根据字体大小微调
for (pos in positions) { for (pos in sortedPositions) {
val positionStart = currentLength if (currentLine.isNotEmpty() && kotlin.math.abs(pos.y - lastY) > lineThreshold) {
val positionEnd = currentLength + pos.text.length // y 坐标变化超过阈值,说明换行
lines.add(currentLine)
currentLine = mutableListOf()
}
currentLine.add(pos)
lastY = pos.y
}
if (currentLine.isNotEmpty()) lines.add(currentLine)
// 精确检查:这个位置是否在当前匹配范围内 // 对每一行独立匹配搜索文本
if (positionStart < matchIndex + searchText.length && for (line in lines) {
positionEnd > matchIndex var i = 0
) { while (i <= line.size - chars.size) {
matchedPositions.add(pos) var matched = true
val matchedPositions = mutableListOf<TextPositionInfo>()
for (j in chars.indices) {
// 清理不可打印字符(零宽空格、控制字符等)
val posChar = line[i + j].text.replace(Regex("\\p{C}"), "").lowercase()
if (posChar != chars[j].toString()) {
matched = false
break
}
matchedPositions.add(line[i + j])
} }
currentLength += pos.text.length if (matched) {
// 合并匹配字符为矩形区域
// 如果已经超过当前匹配范围,立即停止 matches.add(mergePositions(matchedPositions, searchText))
if (currentLength >= matchIndex + searchText.length) break i += chars.size
} else {
i++
}
} }
// 计算合并后的矩形区域
if (matchedPositions.isNotEmpty()) {
val mergedPosition = mergePositions(matchedPositions, searchText)
matches.add(mergedPosition)
}
index = matchIndex + searchText.length
} }
Log.d("PDF_DEBUG", "找到 ${matches.size} 个独立匹配")
return matches return matches
} }
@ -297,10 +322,6 @@ class PDFSearchManager(private val pdfView: PDFView) {
val stripper = object : PDFTextStripper() { val stripper = object : PDFTextStripper() {
override fun processTextPosition(text: TextPosition) { override fun processTextPosition(text: TextPosition) {
Log.d(
"ocean",
"processTextPosition text->$text width->${text.width}height->${text.height}"
)
//太小的文字当没有 //太小的文字当没有
if (text.width > 1 && text.height > 2) { if (text.width > 1 && text.height > 2) {
charCount++ charCount++

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M333.1,126.5l-0.7,0.7c-12.3,12.3 -12.3,32.4 0,44.7l339.9,339.9 -340.1,340.1c-12.5,12.5 -12.5,32.9 0,45.4s32.9,12.5 45.4,0L740,535s0.1,-0.1 0.2,-0.1l0.7,-0.7c12.3,-12.3 12.3,-32.4 0,-44.7l-363,-363c-12.4,-12.3 -32.5,-12.3 -44.8,0z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M690.8,126.5l0.7,0.7c12.3,12.3 12.3,32.4 0,44.7l-340,340L691.6,852c12.5,12.5 12.5,32.9 0,45.4s-32.9,12.5 -45.4,0L283.9,535s-0.1,-0.1 -0.2,-0.1l-0.7,-0.7c-12.3,-12.3 -12.3,-32.4 0,-44.7l363,-363c12.4,-12.3 32.5,-12.3 44.8,0z"
android:fillColor="#000000"/>
</vector>

View File

@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<stroke android:color="@color/line_color" <stroke android:color="@color/line_color"
android:width="0.5dp"/> android:width="1dp"/>
<corners android:radius="8dp" /> <corners android:radius="8dp" />
</shape> </shape>

View File

@ -42,47 +42,87 @@
<LinearLayout <LinearLayout
android:id="@+id/searchTextLayout" android:id="@+id/searchTextLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginEnd="16dp"
android:background="@drawable/dr_item_img_frame"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"> android:visibility="gone">
<com.google.android.material.textfield.TextInputEditText <LinearLayout
android:id="@+id/searchEdit"
style="@style/TextViewFont_PopRegular"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_marginEnd="16dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@null" android:background="@drawable/dr_item_img_frame"
android:hint="@string/search_hint" android:gravity="center_vertical">
android:textSize="14sp" />
<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:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:textSize="14sp" />
<RelativeLayout <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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/search" /> 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/selectPreviousBtn"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/dr_click_effect_oval_transparent"
android:gravity="center">
<ImageView <ImageView
android:id="@+id/deleteIv" android:layout_width="24dp"
android:layout_width="wrap_content" android:layout_height="24dp"
android:layout_height="wrap_content" android:src="@drawable/arrow_previous_icon" />
android:src="@drawable/delete_cha_icon"
android:visibility="gone" /> </LinearLayout>
<LinearLayout
android:id="@+id/selectNextBtn"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/dr_click_effect_oval_transparent"
android:layout_marginEnd="16dp"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/arrow_next_icon" />
</LinearLayout>
</RelativeLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -109,7 +149,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:background="@drawable/dr_click_effect_oval_transparent" android:background="@drawable/dr_click_effect_oval_transparent"
android:gravity="center" android:gravity="center"
android:visibility="gone"> android:visibility="visible">
<ImageView <ImageView
android:layout_width="24dp" android:layout_width="24dp"

View File

@ -163,4 +163,6 @@
<string name="no_app_to_open_image">No app found to open image</string> <string name="no_app_to_open_image">No app found to open image</string>
<string name="failed_to_open_image">Failed to open image</string> <string name="failed_to_open_image">Failed to open image</string>
<string name="press_again_to_exit">Press again to exit</string> <string name="press_again_to_exit">Press again to exit</string>
<string name="enter_search_key">Please enter the search keyword</string>
<string name="no_more_results">No more results</string>
</resources> </resources>