添加搜索文字功能(基本完成),还可优化。
This commit is contained in:
parent
8e4c25d54f
commit
ed7430e7d2
@ -3,13 +3,19 @@ package com.all.pdfreader.pro.app.ui.act
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.all.pdfreader.pro.app.R
|
||||
import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding
|
||||
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.ui.dialog.BookmarksDialogFragment
|
||||
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.view.CustomScrollHandle
|
||||
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.PDFHighlighter
|
||||
import com.all.pdfreader.pro.app.util.PDFSearchManager
|
||||
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
|
||||
import com.all.pdfreader.pro.app.viewmodel.observeEvent
|
||||
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.gyf.immersionbar.BarHide
|
||||
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.withContext
|
||||
import java.io.File
|
||||
|
||||
class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeListener,
|
||||
@ -51,6 +67,10 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
private val repository = getRepository()
|
||||
private var isFullScreen = false
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -58,6 +78,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
setContentView(binding.root)
|
||||
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
|
||||
.navigationBarColor(R.color.white).init()
|
||||
setupDoubleBackExit()
|
||||
initObserve()
|
||||
val filePath = intent.getStringExtra(EXTRA_PDF_HASH)
|
||||
?: throw IllegalArgumentException("PDF file hash is required")
|
||||
@ -165,7 +186,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
|
||||
private fun setupOnClick() {
|
||||
binding.backBtn.setOnClickListener {
|
||||
finish()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
binding.eyeCareOverlayBtn.setOnClickListener {
|
||||
appStore.isEyeCareMode = !appStore.isEyeCareMode
|
||||
@ -188,6 +209,55 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
binding.moreBtn.setOnClickListener {
|
||||
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() {
|
||||
@ -199,6 +269,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
finish()
|
||||
return
|
||||
}
|
||||
setupPDFSearchManager()//初始化搜索文本需要的类
|
||||
if (pdfDocument.isPassword) {
|
||||
showPasswordDialog(file)
|
||||
} else {
|
||||
@ -239,10 +310,22 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
lastReadPage = page, readingProgress = (page.toFloat() / pageCount.toFloat()) * 100
|
||||
)
|
||||
saveReadingProgress()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
val doc = searchManager.getCachedDoc() ?: return
|
||||
// 取消上一次检查任务
|
||||
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() {
|
||||
@ -276,9 +359,7 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
onTap(this@PdfViewActivity) // 单击回调
|
||||
onPageChange(this@PdfViewActivity) // 页面改变回调
|
||||
scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示
|
||||
onDraw { canvas, pageWidth, pageHeight, displayedPage ->
|
||||
|
||||
}
|
||||
onDraw(highlighter)
|
||||
if (appStore.isPageFling) {
|
||||
pageSnap(true)//页面可在逐页中,可居中展示,非逐页模式,滑动停止后可自动定格在居中位置。
|
||||
pageFling(true)//逐页
|
||||
@ -299,6 +380,9 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
}
|
||||
|
||||
private fun toggleFullScreen() {
|
||||
if (showSearchTextView) {//如果是搜索文本时,不全屏
|
||||
return
|
||||
}
|
||||
isFullScreen = !isFullScreen
|
||||
updateStatusAndNavigationLayout(isFullScreen)
|
||||
if (isFullScreen) {
|
||||
@ -337,4 +421,129 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
|
||||
.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()
|
||||
}
|
||||
|
||||
}
|
||||
@ -100,6 +100,15 @@ object AppUtils {
|
||||
}, 200)
|
||||
}
|
||||
|
||||
fun EditText.hideKeyboard() {
|
||||
clearFocus()
|
||||
postDelayed({
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||
}, 200)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享文件(自动识别 MIME 类型)
|
||||
* @param context 上下文
|
||||
|
||||
@ -3,6 +3,7 @@ package com.all.pdfreader.pro.app.util
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import com.all.pdfreader.pro.app.R
|
||||
import com.github.barteksc.pdfviewer.PDFView
|
||||
import com.github.barteksc.pdfviewer.listener.OnDrawListener
|
||||
|
||||
@ -11,6 +12,13 @@ class PDFHighlighter(
|
||||
private val searchManager: PDFSearchManager
|
||||
) : OnDrawListener {
|
||||
|
||||
enum class ClickType {
|
||||
NEXT,
|
||||
PREVIOUS
|
||||
}
|
||||
|
||||
private var type = ClickType.NEXT
|
||||
|
||||
private val highlightPaint = Paint().apply {
|
||||
color = 0x80FFD700.toInt() // 半透明黄色
|
||||
style = Paint.Style.FILL
|
||||
@ -22,13 +30,34 @@ class PDFHighlighter(
|
||||
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 currentSelectedIndex: Int = -1
|
||||
|
||||
/**
|
||||
* 设置当前页面的高亮
|
||||
*/
|
||||
fun setCurrentPageHighlights(page: Int) {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -37,18 +66,78 @@ class PDFHighlighter(
|
||||
*/
|
||||
fun clearCurrentHighlights() {
|
||||
currentPageHighlights = emptyList()
|
||||
currentSelectedIndex = -1
|
||||
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) {
|
||||
searchManager.currentHighlightPage = displayedPage
|
||||
setCurrentPageHighlights(displayedPage + 1) // PDFView 页面从0开始,我们的搜索从1开始
|
||||
// setCurrentPageHighlights(displayedPage + 1)
|
||||
}
|
||||
|
||||
// 绘制高亮区域
|
||||
currentPageHighlights.forEach { position ->
|
||||
currentPageHighlights.forEachIndexed { index, position ->
|
||||
try {
|
||||
val rect = position.getRelativeRect()
|
||||
val absoluteRect = RectF(
|
||||
@ -58,7 +147,6 @@ class PDFHighlighter(
|
||||
rect.bottom * pageHeight
|
||||
)
|
||||
|
||||
// 可选:添加小的边距让高亮更明显
|
||||
val margin = 1f
|
||||
val paddedRect = RectF(
|
||||
absoluteRect.left - margin,
|
||||
@ -67,15 +155,16 @@ class PDFHighlighter(
|
||||
absoluteRect.bottom + margin
|
||||
)
|
||||
|
||||
// 绘制半透明高亮
|
||||
canvas.drawRect(paddedRect, highlightPaint)
|
||||
|
||||
// 绘制边框
|
||||
canvas.drawRect(paddedRect, highlightStrokePaint)
|
||||
|
||||
if (index == currentSelectedIndex) {
|
||||
canvas.drawRect(paddedRect, selectedPaint)
|
||||
canvas.drawRect(paddedRect, selectedStrokePaint)
|
||||
} else {
|
||||
canvas.drawRect(paddedRect, highlightPaint)
|
||||
canvas.drawRect(paddedRect, highlightStrokePaint)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
positions: List<TextPositionInfo>,
|
||||
searchText: String
|
||||
): List<TextPositionInfo> {
|
||||
val matches = mutableListOf<TextPositionInfo>()
|
||||
val fullText = positions.joinToString("") { it.text }.toLowerCase()
|
||||
val chars = searchText.lowercase().toCharArray()
|
||||
|
||||
var index = 0
|
||||
while (index < fullText.length) {
|
||||
val matchIndex = fullText.indexOf(searchText, index)
|
||||
if (matchIndex == -1) break
|
||||
// 按纵向 y、横向 x 排序,保证字符视觉顺序
|
||||
val sortedPositions = positions.sortedWith(compareBy({ it.y }, { it.x }))
|
||||
|
||||
// 精确查找匹配的文本段在positions中的范围
|
||||
var currentLength = 0
|
||||
val matchedPositions = mutableListOf<TextPositionInfo>()
|
||||
// 按行拆分字符,避免高亮跨行
|
||||
val lines = mutableListOf<MutableList<TextPositionInfo>>()
|
||||
var currentLine = mutableListOf<TextPositionInfo>()
|
||||
var lastY = sortedPositions.firstOrNull()?.y ?: 0f
|
||||
val lineThreshold = 2f // y 差值小于阈值则视为同一行,可根据字体大小微调
|
||||
|
||||
for (pos in positions) {
|
||||
val positionStart = currentLength
|
||||
val positionEnd = currentLength + pos.text.length
|
||||
for (pos in sortedPositions) {
|
||||
if (currentLine.isNotEmpty() && kotlin.math.abs(pos.y - lastY) > lineThreshold) {
|
||||
// y 坐标变化超过阈值,说明换行
|
||||
lines.add(currentLine)
|
||||
currentLine = mutableListOf()
|
||||
}
|
||||
currentLine.add(pos)
|
||||
lastY = pos.y
|
||||
}
|
||||
if (currentLine.isNotEmpty()) lines.add(currentLine)
|
||||
|
||||
// 精确检查:这个位置是否在当前匹配范围内
|
||||
if (positionStart < matchIndex + searchText.length &&
|
||||
positionEnd > matchIndex
|
||||
) {
|
||||
matchedPositions.add(pos)
|
||||
// 对每一行独立匹配搜索文本
|
||||
for (line in lines) {
|
||||
var i = 0
|
||||
while (i <= line.size - chars.size) {
|
||||
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 (currentLength >= matchIndex + searchText.length) break
|
||||
if (matched) {
|
||||
// 合并匹配字符为矩形区域
|
||||
matches.add(mergePositions(matchedPositions, searchText))
|
||||
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
|
||||
}
|
||||
|
||||
@ -297,10 +322,6 @@ class PDFSearchManager(private val pdfView: PDFView) {
|
||||
|
||||
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++
|
||||
|
||||
9
app/src/main/res/drawable/arrow_next_icon.xml
Normal file
9
app/src/main/res/drawable/arrow_next_icon.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/arrow_previous_icon.xml
Normal file
9
app/src/main/res/drawable/arrow_previous_icon.xml
Normal 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>
|
||||
@ -2,6 +2,6 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<stroke android:color="@color/line_color"
|
||||
android:width="0.5dp"/>
|
||||
android:width="1dp"/>
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
@ -42,47 +42,87 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/searchTextLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dr_item_img_frame"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/searchEdit"
|
||||
style="@style/TextViewFont_PopRegular"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@null"
|
||||
android:hint="@string/search_hint"
|
||||
android:textSize="14sp" />
|
||||
android:background="@drawable/dr_item_img_frame"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/searchIv"
|
||||
<RelativeLayout
|
||||
android:layout_width="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
|
||||
android:id="@+id/deleteIv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/delete_cha_icon"
|
||||
android:visibility="gone" />
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/arrow_previous_icon" />
|
||||
|
||||
</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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -109,7 +149,7 @@
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/dr_click_effect_oval_transparent"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
android:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
|
||||
@ -163,4 +163,6 @@
|
||||
<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="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>
|
||||
Loading…
Reference in New Issue
Block a user