Skip to Content

Chapitre 3.2 — View, Text, Image

Où on en est : le mental model est posé. On détaille maintenant les trois primitives qui composent 80 % de n’importe quel écran — avec leurs props réellement utiles.

⏱️ TL;DRView = conteneur pur (layout, fond, bordures). Text = tout le texte, avec numberOfLines, ellipsizeMode et l’imbrication. Image = source (require local vs URI distant), resizeMode, dimensions obligatoires pour les images distantes. Pour les vraies apps : expo-image, plus performant (cache disque, placeholders, transitions).

🎯 Objectifs

  • Utiliser les props utiles de View, Text et Image (la vraie liste courte).
  • Comprendre require() vs URI et pourquoi les images distantes exigent des dimensions.
  • Choisir entre Image core et expo-image.
  • Construire la première carte de thème de Drill.

View — le conteneur

View n’affiche rien par elle-même : elle organise (flexbox, Partie 4) et décore (fond, bordure, ombre). Ses props courantes :

<View style={styles.card} // le layout et la déco (Partie 4) pointerEvents="none" // laisser passer les touchers (overlays) accessibilityRole="summary" // sémantique pour lecteurs d'écran testID="theme-card" // hook pour les tests E2E (Partie 14) />

C’est volontairement pauvre : une View est un ViewGroup Android nu. Toute la richesse vient du style et de la composition.

Text — plus subtil qu’il n’y paraît

Au-delà de l’imbrication (chapitre 3.1), trois props sauvent des écrans entiers :

// L'intitulé d'une question Drill, borné à 3 lignes <Text numberOfLines={3} ellipsizeMode="tail" style={styles.prompt}> {question.prompt} </Text>
  • numberOfLines : tronque au-delà de N lignes — indispensable dans les cartes et les listes, où un contenu long casserait la hauteur.
  • ellipsizeMode : où placer le « … » (tail par défaut, middle utile pour les chemins).
  • onTextLayout : mesurer le texte réellement rendu (cas avancés).

Deux détails Android à connaître : includeFontPadding: false (style) retire le padding vertical fantôme des polices — utile pour aligner des chiffres de score ; et le font scaling système (l’utilisateur peut grossir sa police : testez avec, ne bloquez allowFontScaling qu’en dernier recours — l’accessibilité prime).

Image — le piège des dimensions

// Locale : require, dimensions lues dans le fichier — rien à préciser <Image source={require('../assets/images/logo.png')} style={{ width: 96, height: 96 }} /> // Distante : URI… et dimensions OBLIGATOIRES, sinon 0×0 ! <Image source={{ uri: 'https://raw.githubusercontent.com/alex/drill-questions/main/badges/php.png' }} style={{ width: 48, height: 48 }} resizeMode="contain" />

Pourquoi cette asymétrie ? Une image require() est connue au build : Metro lit ses dimensions. Une URI ne l’est qu’au chargement — RN ne réserve donc aucun espace par défaut. L’oubli classique : « mon image distante ne s’affiche pas » = pas de width/height.

resizeMode répond à « comment remplir le cadre » : cover (remplit, rogne — défaut), contain (tout visible, letterbox), stretch (déforme — à éviter), center.

💡 Pour un dev Next.jsrequire() d’une image ≈ l’import statique de next/image : connue au build, optimisée, dimensionnée. L’URI ≈ une image distante sans width/height — que Next refuse aussi ! Même problème (réserver l’espace), solutions cousines.

expo-image — le choix des vraies apps

Le composant Image core est basique : cache mémoire sommaire, pas de placeholder, pas de transition. Dès qu’une app charge des images réseau en série (avatars, badges de thèmes), expo-image s’impose :

npx expo install expo-image
import { Image } from 'expo-image' <Image source="https://…/badge-symfony.png" style={{ width: 48, height: 48 }} contentFit="contain" // remplace resizeMode placeholder={{ blurhash }} // flou élégant pendant le chargement transition={200} // fondu à l'arrivée cachePolicy="disk" // cache persistant />

Décision de cours : Drill utilise expo-image partout où une image vient du réseau, et l’Image core pour les assets locaux simples. (Drill reste sobre en images — l’essentiel de son UI est du texte et des composants stylés.)

La première carte de Drill

Assemblons les trois piliers — la carte d’un thème, qu’on retrouvera dans la liste des thèmes :

// components/ThemeCard.tsx — v1 (Pressable au chapitre suivant, style réel en Partie 4) import { StyleSheet, Text, View } from 'react-native' type Props = { title: string // « Symfony » questionCount: number emoji: string // « 🎼 » — pas d'images à télécharger pour la v1 } export function ThemeCard({ title, questionCount, emoji }: Props) { return ( <View style={styles.card}> <Text style={styles.emoji}>{emoji}</Text> <View style={styles.body}> <Text style={styles.title} numberOfLines={1}>{title}</Text> <Text style={styles.meta}>{questionCount} questions</Text> </View> </View> ) } const styles = StyleSheet.create({ card: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#101b21', borderRadius: 14, padding: 16, }, emoji: { fontSize: 32 }, body: { flex: 1 }, title: { color: '#e2e8f0', fontSize: 18, fontWeight: '700' }, meta: { color: '#7a8b94', fontSize: 13 }, })

📱 Essaie sur ton téléphone — Importez ThemeCard dans app/index.tsx et affichez-en deux ou trois (Symfony, PHP, Next.js) sous le titre. Jouez avec numberOfLines en donnant un titre très long. C’est la graine de l’écran Thèmes (Partie 5).

✏️ Exercices

1. Pourquoi ce code n’affiche-t-il rien, et quelles sont les DEUX corrections possibles ?

<Image source={{ uri: 'https://picsum.photos/200' }} />

✅ Solution

Une image distante n’a aucune dimension intrinsèque connue avant chargement → rendu 0×0. Corrections : (1) donner style={{ width: 200, height: 200 }} ; (2) la faire remplir un parent dimensionné (style={{ flex: 1 }} dans une View de taille connue, ou width: '100%' + aspectRatio: 1).

2. Ajoutez à ThemeCard un badge « NOUVEAU » qui ne s’affiche que si une prop isNew est vraie — sans tomber dans le piège du &&.

✅ Solution

type Props = { /* … */ isNew?: boolean } {isNew === true && ( <View style={styles.badge}> <Text style={styles.badgeText}>NOUVEAU</Text> </View> )}

isNew est optionnel (undefined possible) : isNew === true (ou !!isNew) garantit un booléen à gauche du &&. Le texte du badge vit dans un Text, jamais nu.

3. Un titre de thème « Programmation orientée objet avancée en PHP 8.5 » casse la mise en page de la carte. Réparez sans changer la taille de police.

✅ Solution

Déjà fait dans notre version : numberOfLines={1} sur le titre (+ ellipsizeMode par défaut tail) → « Programmation orientée ob… ». La View body en flex: 1 est l’autre moitié de la solution : elle borne la largeur disponible du texte au lieu de laisser la ligne pousser la carte.

🧠 Quiz

1. Pourquoi les images require() n’exigent-elles pas de dimensions ?

Réponse

Elles sont résolues au build par Metro, qui embarque leurs dimensions intrinsèques. Les URI distantes ne sont connues qu’au chargement → il faut réserver l’espace soi-même.

2. Quelle prop borne un Text à 2 lignes avec « … » ?

Réponse

numberOfLines={2} (avec ellipsizeMode, tail par défaut, pour la position des points de suspension).

3. Citez trois avantages d’expo-image sur l’Image core.

Réponse

Cache disque persistant (cachePolicy), placeholders élégants (blurhash), transitions de chargement, meilleures perfs de décodage — pour toute image réseau.

4. Différence entre cover et contain ?

Réponse

cover remplit tout le cadre quitte à rogner l’image ; contain montre TOUTE l’image quitte à laisser des bandes vides. (stretch déforme — à éviter.)

5. À quoi sert testID ?

Réponse

À identifier un élément pour les tests (RN Testing Library, Maestro E2E — Partie 14), sans dépendre du texte affiché.


👉 Chapitre suivant : 3.3 — Pressable & le tactile — le doigt n’est pas une souris.