添加搜索文字测试代码,后续看是否使用。(未完成)

This commit is contained in:
ocean 2025-10-23 17:16:28 +08:00
parent d9dfa75b9b
commit 8e4c25d54f
10 changed files with 890 additions and 24 deletions

View File

@ -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"

View File

@ -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()
}
}

View File

@ -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)//逐页

View File

@ -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
}

View File

@ -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 ->

View File

@ -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()
}
}
}
}

View File

@ -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()
}

View File

@ -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>

View 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>

View 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>