1.添加搜索功能

2.优化主要adapter的展示,使用PdfDiffCallback来只刷新有改变的item
This commit is contained in:
ocean 2025-09-24 17:19:35 +08:00
parent e8594793cd
commit f0fb627774
13 changed files with 320 additions and 25 deletions

View File

@ -79,6 +79,12 @@
android:label="@string/app_name"
android:screenOrientation="portrait" />
<activity
android:name=".ui.act.SearchActivity"
android:exported="true"
android:label="@string/app_name"
android:screenOrientation="portrait" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -23,7 +23,7 @@ interface PdfDocumentDao {
@Query("SELECT * FROM pdf_documents WHERE isFavorite = 1 ORDER BY addedToFavoriteTime DESC")
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>>
@Query("SELECT * FROM pdf_documents WHERE fileName LIKE '%' || :query || '%' OR metadataTitle LIKE '%' || :query || '%'")
@Query("""SELECT * FROM pdf_documents WHERE :query != '' AND TRIM(:query) != '' AND LOWER(fileName) LIKE '%' || LOWER(:query) || '%' """)
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>>
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")

View File

@ -39,6 +39,12 @@ class PdfRepository private constructor(context: Context) {
pdfDao.getRecentlyOpenedDocuments()
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>> = pdfDao.getFavoriteDocuments()
/**
* 1.用户输入 " " 不会返回任何数据
* 2.用户输入 " my file " 会去掉两边空格正确匹配包含 my file 的文件
* 3.用户输入 "my" 正常模糊匹配
*/
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>> =
pdfDao.searchDocuments(query)

View File

@ -161,7 +161,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
}
binding.searchBtn.setClickWithAnimation {
startActivity(SearchActivity.createIntent(this))
}
binding.sortingBtn.setClickWithAnimation {

View File

@ -0,0 +1,105 @@
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.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivitySearchPddBinding
import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter
import com.all.pdfreader.pro.app.ui.dialog.ListMoreDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class SearchActivity : BaseActivity() {
override val TAG: String = "SearchActivity"
companion object {
const val FRAG_TAG = "SearchActivity"
fun createIntent(context: Context): Intent {
return Intent(context, SearchActivity::class.java)
}
}
private lateinit var binding: ActivitySearchPddBinding
private lateinit var adapter: PdfAdapter
private val pdfRepository = getRepository()
private var searchJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchPddBinding.inflate(layoutInflater)
setContentView(binding.root)
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.bg_color).init()
initView()
setupClick()
}
private fun setupClick() {
binding.backBtn.setOnClickListener {
finish()
}
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
// 取消之前的任务,防止重复 collect
searchJob?.cancel()
searchJob = lifecycleScope.launch {
delay(150)//防止用户飞快打字
if (query.isEmpty()) {
adapter.updateData(emptyList())
binding.noFilesLayout.visibility = View.VISIBLE
return@launch
}
pdfRepository.searchDocuments(query).collectLatest { list ->
if (list.isNotEmpty()) {
adapter.updateData(list)
adapter.highlightItems(query.trim()) // payload 高亮
binding.noFilesLayout.visibility = View.GONE
} else {
adapter.updateData(emptyList())
binding.noFilesLayout.visibility = View.VISIBLE
}
}
}
}
})
binding.deleteIv.setOnClickListener {
binding.searchEdit.apply {
setText("")
clearComposingText()
setSelection(0)
}
}
}
private fun initView() {
binding.searchEdit.showKeyboard()
adapter = PdfAdapter(onItemClick = { pdf ->
val intent = PdfViewActivity.createIntent(this, pdf.filePath)
startActivity(intent)
}, onMoreClick = { pdf ->
ListMoreDialogFragment(pdf.filePath).show(supportFragmentManager, FRAG_TAG)
})
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
}
}

View File

@ -4,62 +4,88 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.AdapterPdfItemBinding
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.util.AppUtils.dpToPx
import com.all.pdfreader.pro.app.util.AppUtils.toHighlightedSpannable
import com.all.pdfreader.pro.app.util.FileUtils.toFormatFileSize
import com.all.pdfreader.pro.app.util.FileUtils.toSlashDate
import com.all.pdfreader.pro.app.util.PdfDiffCallback
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
class PdfAdapter(
private var pdfList: MutableList<PdfDocumentEntity>,
private val onItemClick: (PdfDocumentEntity) -> Unit,
private val onMoreClick: (PdfDocumentEntity) -> Unit
) : RecyclerView.Adapter<PdfAdapter.PdfViewHolder>() {
) : ListAdapter<PdfDocumentEntity, PdfAdapter.PdfViewHolder>(PdfDiffCallback()) {
inner class PdfViewHolder(val binding: AdapterPdfItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfViewHolder {
val binding =
AdapterPdfItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val binding = AdapterPdfItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return PdfViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: PdfViewHolder, position: Int) {
val item = pdfList[position]
holder.binding.tvFileName.text = item.fileName
bindItem(holder, getItem(position), null)
}
override fun onBindViewHolder(holder: PdfViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
val payload = payloads[0] as? String
bindItem(holder, getItem(position), payload)
} else {
super.onBindViewHolder(holder, position, payloads)
}
}
private fun bindItem(holder: PdfViewHolder, item: PdfDocumentEntity, highlightKeyword: String?) {
val context = holder.binding.root.context
// 文件名高亮,如果 highlightKeyword 为 null 或空字符串,就显示普通文本
holder.binding.tvFileName.text = if (highlightKeyword.isNullOrBlank()) {
item.fileName
} else {
item.fileName.toHighlightedSpannable(highlightKeyword, holder.binding.root.context.getColor(R.color.icon_sel_on_color))
}
holder.binding.tvFileSize.text = item.fileSize.toFormatFileSize()
holder.binding.tvFileDate.text = item.lastModified.toSlashDate()
if (item.isPassword) {
holder.binding.lockLayout.visibility = View.VISIBLE
holder.binding.tvFileImg.visibility = View.GONE
} else {
holder.binding.lockLayout.visibility = View.GONE
holder.binding.tvFileImg.visibility = View.VISIBLE
Glide.with(holder.binding.root).load(item.thumbnailPath)
.transform(CenterCrop(), RoundedCorners(8.dpToPx(holder.binding.root.context)))
Glide.with(context)
.load(item.thumbnailPath)
.transform(CenterCrop(), RoundedCorners(8.dpToPx(context)))
.into(holder.binding.tvFileImg)
}
holder.binding.root.setOnClickListener {
onItemClick(item)
}
holder.binding.moreBtn.setOnClickListener {
onMoreClick(item)
}
holder.binding.root.setOnClickListener { onItemClick(item) }
holder.binding.moreBtn.setOnClickListener { onMoreClick(item) }
}
override fun getItemCount(): Int = pdfList.size
@SuppressLint("NotifyDataSetChanged")
fun updateData(newList: List<PdfDocumentEntity>) {
pdfList.clear()
pdfList.addAll(newList)
notifyDataSetChanged()
submitList(newList.toList())
}
/**
* 搜索场景使用只刷新高亮不刷新整个列表
*/
fun highlightItems(keyword: String) {
for (i in 0 until itemCount) {
notifyItemChanged(i, keyword)
}
}
}

View File

@ -40,7 +40,7 @@ class FavoriteFrag : BaseFrag() {
}
private fun initView() {
adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf ->
adapter = PdfAdapter(onItemClick = { pdf ->
val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
startActivity(intent)
}, onMoreClick = { pdf ->

View File

@ -42,7 +42,7 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
}
private fun initView() {
adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf ->
adapter = PdfAdapter(onItemClick = { pdf ->
val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
startActivity(intent)
}, onMoreClick = { pdf ->

View File

@ -39,7 +39,7 @@ class RecentlyFrag : BaseFrag() {
}
private fun initView() {
adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf ->
adapter = PdfAdapter(onItemClick = { pdf ->
val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
startActivity(intent)
}, onMoreClick = { pdf ->

View File

@ -9,6 +9,9 @@ import android.net.Uri
import android.os.ParcelFileDescriptor
import android.print.PrintAttributes
import android.print.PrintManager
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
@ -28,6 +31,7 @@ import com.tom_roush.pdfbox.pdmodel.common.PDPageLabelRange
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.regex.Pattern
object AppUtils {
@ -237,4 +241,24 @@ object AppUtils {
null
}
}
/**
* 高亮文字主要是搜索方面
*/
fun String.toHighlightedSpannable(keyword: String, highlightColor: Int): SpannableString {
val spannable = SpannableString(this)
if (keyword.isBlank()) return spannable
val regex = Regex(Pattern.quote(keyword), RegexOption.IGNORE_CASE)
regex.findAll(this).forEach { matchResult ->
spannable.setSpan(
ForegroundColorSpan(highlightColor),
matchResult.range.first,
matchResult.range.last + 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
return spannable
}
}

View File

@ -0,0 +1,20 @@
package com.all.pdfreader.pro.app.util
import androidx.recyclerview.widget.DiffUtil
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
/**
* 用于PdfDocumentEntity数据类的PdfDiff
*/
class PdfDiffCallback : DiffUtil.ItemCallback<PdfDocumentEntity>() {
override fun areItemsTheSame(oldItem: PdfDocumentEntity, newItem: PdfDocumentEntity): Boolean {
return oldItem.filePath == newItem.filePath
}
override fun areContentsTheSame(
oldItem: PdfDocumentEntity,
newItem: PdfDocumentEntity
): Boolean {
return oldItem == newItem
}
}

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/backBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/back_black" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
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: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>
<LinearLayout
android:id="@+id/noFilesLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/img_no_files_yet" />
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_files_yet"
android:textColor="#B6BFCC"
android:textSize="20sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="32dp" />
</LinearLayout>

View File

@ -135,4 +135,5 @@
<string name="file_created_success">Your file has been successfully created</string>
<string name="please_select_page">Please select at least one page</string>
<string name="splitting">Splitting…</string>
<string name="search_hint">Search……</string>
</resources>