Search RecyclerView
Implementación de funcionalidad de búsqueda en RecyclerView con filtros en tiempo real y animaciones suaves
Descripción del Proyecto
Este proyecto demuestra la implementación de funcionalidad de búsqueda avanzada en RecyclerView con filtros en tiempo real. Incluye características como búsqueda por múltiples campos, filtros dinámicos, animaciones suaves y optimizaciones de rendimiento para listas grandes.
La implementación utiliza CardView para un diseño moderno, DataClass para modelos de datos eficientes, y algoritmos de filtrado optimizados que mantienen la fluidez de la interfaz incluso con datasets grandes.
Capturas de Pantalla
Vista inicial con todos los elementos
Búsqueda en tiempo real
Resultados filtrados
Tecnologías Utilizadas
Search Algorithm
Búsqueda eficiente en tiempo real
RecyclerView
Lista optimizada y flexible
CardView
Diseño Material Design
DataClass
Modelos de datos Kotlin
Filter Animations
Animaciones de filtrado
Kotlin
Lenguaje moderno y conciso
Características Principales
Búsqueda en Tiempo Real
Los resultados se actualizan instantáneamente mientras el usuario escribe, sin lag perceptible.
Búsqueda Multi-campo
Busca en múltiples campos simultáneamente: título, descripción, categoría, etc.
Búsqueda Inteligente
Búsqueda insensible a mayúsculas, acentos y con soporte para búsqueda parcial.
Animaciones Suaves
Transiciones fluidas cuando los elementos aparecen o desaparecen del filtro.
Alto Rendimiento
Optimizado para manejar listas con miles de elementos sin impacto en performance.
Highlighting
Resaltado automático de términos de búsqueda en los resultados mostrados.
Implementación Técnica
Adapter con Filtrado
class SearchableRecyclerAdapter(
private val originalList: List<SearchableItem>
) : RecyclerView.Adapter<SearchableRecyclerAdapter.ViewHolder>(), Filterable {
private var filteredList = originalList.toMutableList()
private var currentQuery = ""
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val query = constraint?.toString()?.lowercase()?.trim() ?: ""
currentQuery = query
val filtered = if (query.isEmpty()) {
originalList
} else {
originalList.filter { item ->
item.isMatchingQuery(query)
}
}
return FilterResults().apply {
values = filtered
count = filtered.size
}
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
val newList = results?.values as? List<SearchableItem> ?: emptyList()
updateList(newList)
}
}
}
private fun updateList(newList: List<SearchableItem>) {
val diffCallback = SearchItemDiffCallback(filteredList, newList)
val diffResult = DiffUtil.calculateDiff(diffCallback)
filteredList.clear()
filteredList.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
}
Modelo de Datos Searchable
data class SearchableItem(
val id: String,
val title: String,
val description: String,
val category: String,
val tags: List<String> = emptyList(),
val imageUrl: String? = null
) {
fun isMatchingQuery(query: String): Boolean {
val normalizedQuery = query.lowercase().trim()
return title.lowercase().contains(normalizedQuery) ||
description.lowercase().contains(normalizedQuery) ||
category.lowercase().contains(normalizedQuery) ||
tags.any { tag -> tag.lowercase().contains(normalizedQuery) }
}
fun getHighlightedTitle(query: String): SpannableString {
return highlightText(title, query)
}
fun getHighlightedDescription(query: String): SpannableString {
return highlightText(description, query)
}
private fun highlightText(text: String, query: String): SpannableString {
val spannable = SpannableString(text)
if (query.isNotEmpty()) {
val startIndex = text.lowercase().indexOf(query.lowercase())
if (startIndex >= 0) {
spannable.setSpan(
BackgroundColorSpan(Color.YELLOW),
startIndex,
startIndex + query.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
return spannable
}
}
ViewHolder con Highlighting
class ViewHolder(private val binding: ItemSearchableBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: SearchableItem, query: String) {
binding.apply {
// Configurar CardView
cardView.apply {
radius = 12f
cardElevation = 4f
setCardBackgroundColor(
ContextCompat.getColor(context, R.color.surface)
)
}
// Texto con highlighting
if (query.isNotEmpty()) {
textTitle.text = item.getHighlightedTitle(query)
textDescription.text = item.getHighlightedDescription(query)
} else {
textTitle.text = item.title
textDescription.text = item.description
}
textCategory.text = item.category
// Cargar imagen si existe
item.imageUrl?.let { url ->
Glide.with(imageView.context)
.load(url)
.placeholder(R.drawable.placeholder)
.into(imageView)
}
// Tags
chipGroup.removeAllViews()
item.tags.forEach { tag ->
val chip = Chip(chipGroup.context).apply {
text = tag
isClickable = false
chipBackgroundColor = ColorStateList.valueOf(
ContextCompat.getColor(context, R.color.primary_light)
)
}
chipGroup.addView(chip)
}
// Click listener
root.setOnClickListener {
// Manejar click en item
}
}
}
}
Activity con SearchView
class SearchActivity : AppCompatActivity() {
private lateinit var binding: ActivitySearchBinding
private lateinit var adapter: SearchableRecyclerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root)
setupRecyclerView()
setupSearchView()
}
private fun setupRecyclerView() {
adapter = SearchableRecyclerAdapter(generateSampleData())
binding.recyclerView.apply {
adapter = this@SearchActivity.adapter
layoutManager = LinearLayoutManager(this@SearchActivity)
// Añadir animaciones personalizadas
itemAnimator = DefaultItemAnimator().apply {
addDuration = 300
removeDuration = 300
changeDuration = 300
}
// Decoración entre items
addItemDecoration(
DividerItemDecoration(this@SearchActivity, DividerItemDecoration.VERTICAL)
)
}
}
private fun setupSearchView() {
binding.searchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
adapter.filter.filter(newText)
return true
}
})
// Configurar placeholder
queryHint = "Buscar elementos..."
// Focus automático
requestFocus()
}
}
}
Optimizaciones de Rendimiento
- DiffUtil: Cálculo eficiente de diferencias para animaciones suaves
- Debouncing: Evita filtros excesivos durante escritura rápida
- Background Threading: Filtrado en hilo de fondo para UI fluida
- View Recycling: Reutilización optimizada de ViewHolders
- Memory Management: Gestión cuidadosa de listas grandes
- Lazy Loading: Carga diferida de contenido no visible