Filtrage de contenu en temps réel
L'intégration d'un composant SearchView avec un RecyclerView permet de proposer une interface fluide où les résultats s'affinent à mesure que l'utilisateur saisit du texte. Le cœur de cette fonctionnalité repose sur l'écouteur OnQueryTextListener, qui intercepte chaque modification de la chaîne de caractères saisie.
Deux approches peuvent être envisagées pour la correspondance textuelle :
- L'appproche par Regex : Elle consiste à transformer la saisie en un motif flexible. Par exemple, transformer "ABC" en un pattern permettant de trouver ces lettres dans l'ordre, même si d'autres caractères s'intercalent.
- L'approche par itération manuelle : On parcourt les caractères pour vérifier leur présence dans la source de données, offrant un contrôle granulaire sur la logique de filtrage.
Modèle de données
Pour illustrer ce mécanisme, nous utilisons une classe de modèle simple représentant un article ou un produit.
public class ProductModel {
private String label;
private String details;
private int thumbnail;
public ProductModel(String label, String details, int thumbnail) {
this.label = label;
this.details = details;
this.thumbnail = thumbnail;
}
public String getLabel() { return label; }
public String getDetails() { return details; }
public int getThumbnail() { return thumbnail; }
}
Logique de filtrage par Expressions Régulières
La méthode suivante génère dynamiquement une expression régulière à partir de la saisie utilisateur pour effectuer une recherche "souple".
private boolean performRegexMatch(String userInput, String targetText) {
if (userInput.isEmpty()) return true;
// Construction d'un pattern : chaque caractère est suivi de n'importe quel symbole
StringBuilder patternBuilder = new StringBuilder();
for (char c : userInput.toCharArray()) {
patternBuilder.append(Pattern.quote(String.valueOf(c))).append(".*");
}
Pattern pattern = Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE);
return pattern.matcher(targetText).find();
}
Configuration de l'Activity
L'Activity gère la liste principale et la liste filtrée. À chaque changement dans la SearchView, la liste filtrée est vidée, recalculée, puis notifiée à l'adaptateur.
public class SearchActivity extends AppCompatActivity {
private List<ProductModel> masterList = new ArrayList<>();
private List<ProductModel> filteredList = new ArrayList<>();
private ProductAdapter adapter;
private RecyclerView recyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initMockData();
SearchView searchView = findViewById(R.id.search_bar);
recyclerView = findViewById(R.id.product_list);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new ProductAdapter(masterList);
recyclerView.setAdapter(adapter);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) { return false; }
@Override
public boolean onQueryTextChange(String newText) {
filterContent(newText);
return true;
}
});
}
private void filterContent(String query) {
filteredList.clear();
for (ProductModel item : masterList) {
if (performRegexMatch(query, item.getLabel())) {
filteredList.add(item);
}
}
// Mise à jour de l'adapter avec les nouveaux résultats
adapter.updateData(new ArrayList<>(filteredList));
}
private void initMockData() {
masterList.add(new ProductModel("Raquette Alpha", "Technologie carbone haute performance", R.drawable.img1));
masterList.add(new ProductModel("Grip Elite", "Confort et absorption maximale", R.drawable.img2));
// ... ajouter d'autres données
}
}
Gestionnaire d'affichage (Adapter) et Compteur de clics
L'daaptateur ne se contente pas d'afficher les données, il gère également une interaction simple : un compteur de clics (simulant un système de "likes") via un TextView.
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
private List<ProductModel> dataList;
private SparseIntArray clickCounters = new SparseIntArray();
public ProductAdapter(List<ProductModel> dataList) {
this.dataList = dataList;
}
public void updateData(List<ProductModel> newList) {
this.dataList = newList;
notifyDataSetChanged();
}
@NonNull
@Override
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product_card, parent, false);
return new ProductViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
ProductModel item = dataList.get(position);
holder.title.setText(item.getLabel());
holder.desc.setText(item.getDetails());
holder.img.setImageResource(item.getThumbnail());
int currentCount = clickCounters.get(item.hashCode(), 0);
holder.counter.setText(String.valueOf(currentCount));
holder.btnAction.setOnClickListener(v -> {
int newCount = clickCounters.get(item.hashCode(), 0) + 1;
clickCounters.put(item.hashCode(), newCount);
holder.counter.setText(String.valueOf(newCount));
});
}
@Override
public int getItemCount() {
return dataList.size();
}
static class ProductViewHolder extends RecyclerView.ViewHolder {
ImageView img;
TextView title, desc, counter;
View btnAction;
public ProductViewHolder(@NonNull View itemView) {
super(itemView);
img = itemView.findViewById(R.id.img_thumb);
title = itemView.findViewById(R.id.txt_title);
desc = itemView.findViewById(R.id.txt_desc);
counter = itemView.findViewById(R.id.txt_counter);
btnAction = itemView.findViewById(R.id.layout_action);
}
}
}
Structure XML de l'interface
Le layout principal utilise un LinearLayout pour superposer la barre de recherche et la liste.
<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.SearchView
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:queryHint="Rechercher un produit..."
android:background="@android:color/white" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/product_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Chaque élément de la liste est encapsulé dans un CardView pour un rendu visuel moderne.
<!-- item_product_card.xml -->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/img_thumb"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="12dp">
<TextView
android:id="@+id/txt_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="18sp" />
<TextView
android:id="@+id/txt_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2" />
<LinearLayout
android:id="@+id/layout_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
android:clickable="true"
android:focusable="true">
<TextView
android:id="@+id/txt_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>