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:label="@string/app_name"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name=".ui.act.SearchActivity"
android:exported="true"
android:label="@string/app_name"
android:screenOrientation="portrait" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.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") @Query("SELECT * FROM pdf_documents WHERE isFavorite = 1 ORDER BY addedToFavoriteTime DESC")
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>> 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>> fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>>
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC") @Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")

View File

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

View File

@ -161,7 +161,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
} }
binding.searchBtn.setClickWithAnimation { binding.searchBtn.setClickWithAnimation {
startActivity(SearchActivity.createIntent(this))
} }
binding.sortingBtn.setClickWithAnimation { 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView 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.databinding.AdapterPdfItemBinding
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity 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.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.toFormatFileSize
import com.all.pdfreader.pro.app.util.FileUtils.toSlashDate 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.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
class PdfAdapter( class PdfAdapter(
private var pdfList: MutableList<PdfDocumentEntity>,
private val onItemClick: (PdfDocumentEntity) -> Unit, private val onItemClick: (PdfDocumentEntity) -> Unit,
private val onMoreClick: (PdfDocumentEntity) -> Unit private val onMoreClick: (PdfDocumentEntity) -> Unit
) : RecyclerView.Adapter<PdfAdapter.PdfViewHolder>() { ) : ListAdapter<PdfDocumentEntity, PdfAdapter.PdfViewHolder>(PdfDiffCallback()) {
inner class PdfViewHolder(val binding: AdapterPdfItemBinding) : inner class PdfViewHolder(val binding: AdapterPdfItemBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfViewHolder {
val binding = val binding = AdapterPdfItemBinding.inflate(
AdapterPdfItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) LayoutInflater.from(parent.context), parent, false
)
return PdfViewHolder(binding) return PdfViewHolder(binding)
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: PdfViewHolder, position: Int) { override fun onBindViewHolder(holder: PdfViewHolder, position: Int) {
val item = pdfList[position] bindItem(holder, getItem(position), null)
holder.binding.tvFileName.text = item.fileName }
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.tvFileSize.text = item.fileSize.toFormatFileSize()
holder.binding.tvFileDate.text = item.lastModified.toSlashDate() holder.binding.tvFileDate.text = item.lastModified.toSlashDate()
if (item.isPassword) { if (item.isPassword) {
holder.binding.lockLayout.visibility = View.VISIBLE holder.binding.lockLayout.visibility = View.VISIBLE
holder.binding.tvFileImg.visibility = View.GONE holder.binding.tvFileImg.visibility = View.GONE
} else { } else {
holder.binding.lockLayout.visibility = View.GONE holder.binding.lockLayout.visibility = View.GONE
holder.binding.tvFileImg.visibility = View.VISIBLE holder.binding.tvFileImg.visibility = View.VISIBLE
Glide.with(holder.binding.root).load(item.thumbnailPath) Glide.with(context)
.transform(CenterCrop(), RoundedCorners(8.dpToPx(holder.binding.root.context))) .load(item.thumbnailPath)
.transform(CenterCrop(), RoundedCorners(8.dpToPx(context)))
.into(holder.binding.tvFileImg) .into(holder.binding.tvFileImg)
} }
holder.binding.root.setOnClickListener { holder.binding.root.setOnClickListener { onItemClick(item) }
onItemClick(item) holder.binding.moreBtn.setOnClickListener { onMoreClick(item) }
}
holder.binding.moreBtn.setOnClickListener {
onMoreClick(item)
}
} }
override fun getItemCount(): Int = pdfList.size
@SuppressLint("NotifyDataSetChanged")
fun updateData(newList: List<PdfDocumentEntity>) { fun updateData(newList: List<PdfDocumentEntity>) {
pdfList.clear() submitList(newList.toList())
pdfList.addAll(newList) }
notifyDataSetChanged()
/**
* 搜索场景使用只刷新高亮不刷新整个列表
*/
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() { private fun initView() {
adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf -> adapter = PdfAdapter(onItemClick = { pdf ->
val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath) val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
startActivity(intent) startActivity(intent)
}, onMoreClick = { pdf -> }, onMoreClick = { pdf ->

View File

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

View File

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

View File

@ -9,6 +9,9 @@ import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.print.PrintAttributes import android.print.PrintAttributes
import android.print.PrintManager import android.print.PrintManager
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
@ -28,6 +31,7 @@ import com.tom_roush.pdfbox.pdmodel.common.PDPageLabelRange
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.regex.Pattern
object AppUtils { object AppUtils {
@ -237,4 +241,24 @@ object AppUtils {
null 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="file_created_success">Your file has been successfully created</string>
<string name="please_select_page">Please select at least one page</string> <string name="please_select_page">Please select at least one page</string>
<string name="splitting">Splitting…</string> <string name="splitting">Splitting…</string>
<string name="search_hint">Search……</string>
</resources> </resources>