Personnalisation du thème¶
Vue d'ensemble¶
La personnalisation de thème vous permet de créer une expérience de navigation intérieure conforme à votre identité visuelle. MINE prend en charge la personnalisation complète des couleurs, de la typographie, des composants UI, des icônes et des styles de mise en page, avec support des modes clair/sombre.
Capacités de personnalisation
- 🎨 Contrôle total des couleurs : personnalisez chaque élément visuel
- 🌓 Mode clair/sombre : détection auto du thème système
- 🔤 Typographie : polices, tailles et styles sur-mesure
- 🎯 Thématisation des composants : marqueurs, chemins, POI, UI
- 🖼️ Icônes custom : remplacez les icônes par vos assets
- 📱 Responsive : thèmes adaptatifs selon les écrans
- 💾 Persistance : sauvegarde/restauration des préférences
Couleurs par défaut¶
Palette par défaut optimisée pour la lisibilité :
| Composant | Couleur | Hex | Aperçu |
|---|---|---|---|
| Sol | Rose clair | #FDE4E4 |
|
| Chemins | Rose clair | #FDE4E4 |
|
| Murs | Gris | #888383 |
|
| POI | Gris clair | #B9B2B2 |
|
| POI sélectionné | Bleu acier | #779CB7 |
|
| Chemin de nav | Bleu | #2196F3 |
|
| Position utilisateur | Orange | #FF5722 |
| Composant | Couleur | Hex | Aperçu |
|---|---|---|---|
| Sol | Gris foncé | #282828 |
|
| Chemins | Gris moyen | #575757 |
|
| Murs | Gris foncé | #282828 |
|
| POI | Charbon | #3F3F3F |
|
| POI sélectionné | Vert forêt | #2D7C32 |
|
| Chemin de nav | Bleu clair | #64B5F6 |
|
| Position utilisateur | Orange vif | #FF6E40 |
Utilisation du thème par défaut
Le thème par défaut est appliqué si aucun mapColorTheme n'est fourni.
Implémentation de base¶
Thème par défaut¶
sceneView.initialize(
context = this,
mapUrl = "maps/venue.json"
// mapColorTheme optionnel
)
Créer un thème personnalisé¶
@Composable
fun createCustomTheme(): MapColorTheme {
val isDarkTheme = isSystemInDarkTheme()
return if (isDarkTheme) {
MapColorTheme(
ground = Color.parseColor("#1A1A1A"),
pathways = Color.parseColor("#404040"),
walls = Color.parseColor("#2A2A2A"),
poi = Color.parseColor("#505050"),
selectedPoi = Color.parseColor("#4CAF50"),
navigationPath = Color.parseColor("#64B5F6"),
userLocation = Color.parseColor("#FF6E40"),
obstacles = Color.parseColor("#E53935"),
textPrimary = Color.parseColor("#FFFFFF"),
textSecondary = Color.parseColor("#B0B0B0"),
background = Color.parseColor("#121212"),
surface = Color.parseColor("#1E1E1E")
)
} else {
MapColorTheme(
ground = Color.parseColor("#F5F5F5"),
pathways = Color.parseColor("#E0E0E0"),
walls = Color.parseColor("#9E9E9E"),
poi = Color.parseColor("#BDBDBD"),
selectedPoi = Color.parseColor("#2196F3"),
navigationPath = Color.parseColor("#1976D2"),
userLocation = Color.parseColor("#FF5722"),
obstacles = Color.parseColor("#D32F2F"),
textPrimary = Color.parseColor("#212121"),
textSecondary = Color.parseColor("#757575"),
background = Color.parseColor("#FFFFFF"),
surface = Color.parseColor("#FAFAFA")
)
}
}
Personnalisation avancée¶
Configuration complète¶
data class MapColorTheme(
val ground: Int,
val pathways: Int,
val walls: Int,
val doors: Int = Color.parseColor("#8D6E63"),
val windows: Int = Color.parseColor("#B0BEC5"),
val obstacles: Int = Color.parseColor("#E53935"),
val poi: Int,
val selectedPoi: Int,
val poiBorder: Int = Color.WHITE,
val poiText: Int = Color.BLACK,
val navigationPath: Int,
val alternativePath: Int = Color.parseColor("#9E9E9E"),
val pathOutline: Int = Color.WHITE,
val userLocation: Int,
val userLocationAccuracyCircle: Int = Color.parseColor("#4D2196F3"),
val destinationMarker: Int = Color.parseColor("#F44336"),
val textPrimary: Int,
val textSecondary: Int,
val background: Int,
val surface: Int,
val primaryAccent: Int = Color.parseColor("#2196F3"),
val secondaryAccent: Int = Color.parseColor("#4CAF50"),
val errorColor: Int = Color.parseColor("#F44336"),
val warningColor: Int = Color.parseColor("#FF9800"),
val elevator: Int = Color.parseColor("#9C27B0"),
val stairs: Int = Color.parseColor("#FF9800"),
val escalator: Int = Color.parseColor("#00BCD4"),
val ramp: Int = Color.parseColor("#8BC34A"),
val shadowColor: Int = Color.parseColor("#33000000"),
val highlightColor: Int = Color.parseColor("#FFEB3B"),
val disabledColor: Int = Color.parseColor("#BDBDBD")
)
Thèmes de marque¶
object BrandThemes {
fun corporateBlue(isDark: Boolean = false): MapColorTheme { /* variantes clair/sombre */ }
fun modernGreen(isDark: Boolean = false): MapColorTheme { /* variantes clair/sombre */ }
fun healthcare(isDark: Boolean = false): MapColorTheme { /* variantes clair/sombre */ }
fun retail(isDark: Boolean = false): MapColorTheme { /* variantes clair/sombre */ }
}
sceneView.setTheme(BrandThemes.corporateBlue(isSystemInDarkTheme()))
Contraste
Assurez un contraste suffisant (WCAG 2.1 AA) et testez en clair/sombre.
Typographie¶
data class MapTypography(
val headingFont: Typeface = Typeface.DEFAULT_BOLD,
val bodyFont: Typeface = Typeface.DEFAULT,
val monoFont: Typeface = Typeface.MONOSPACE,
val headingLarge: Float = 24f,
val headingMedium: Float = 20f,
val headingSmall: Float = 18f,
val bodyLarge: Float = 16f,
val bodyMedium: Float = 14f,
val bodySmall: Float = 12f,
val caption: Float = 10f,
val poiLabelSize: Float = 14f,
val instructionTextSize: Float = 18f,
val distanceTextSize: Float = 16f,
val floorLabelSize: Float = 20f,
val letterSpacing: Float = 0f,
val lineHeight: Float = 1.5f
)
val customTypography = MapTypography(
headingFont = ResourcesCompat.getFont(context, R.font.roboto_bold)!!,
bodyFont = ResourcesCompat.getFont(context, R.font.roboto_regular)!!,
headingLarge = 28f,
bodyMedium = 16f
)
sceneView.setTypography(customTypography)
Style des composants¶
Marqueurs POI¶
data class POIMarkerStyle(
val shape: MarkerShape = MarkerShape.CIRCLE,
val size: Float = 24f,
val borderWidth: Float = 2f,
val borderColor: Int = Color.WHITE,
val shadowEnabled: Boolean = true,
val shadowRadius: Float = 4f,
val icon: Drawable? = null,
val iconSize: Float = 16f,
val labelEnabled: Boolean = true,
val labelPosition: LabelPosition = LabelPosition.BOTTOM,
val animationEnabled: Boolean = true
)
Chemin de navigation¶
data class NavigationPathStyle(
val width: Float = 4f,
val color: Int = Color.parseColor("#2196F3"),
val outlineWidth: Float = 2f,
val outlineColor: Int = Color.WHITE,
val dashPattern: FloatArray? = null,
val animated: Boolean = true,
val animationSpeed: Float = 1.0f,
val gradientEnabled: Boolean = false,
val gradientColors: IntArray? = null,
val capStyle: Paint.Cap = Paint.Cap.ROUND,
val joinStyle: Paint.Join = Paint.Join.ROUND
)
Position utilisateur¶
data class UserLocationStyle(
val markerSize: Float = 20f,
val markerColor: Int = Color.parseColor("#2196F3"),
val pulseEnabled: Boolean = true,
val pulseColor: Int = Color.parseColor("#4D2196F3"),
val pulseRadius: Float = 40f,
val headingIndicatorEnabled: Boolean = true,
val headingIndicatorLength: Float = 30f,
val headingIndicatorColor: Int = Color.parseColor("#2196F3"),
val accuracyCircleEnabled: Boolean = true,
val accuracyCircleColor: Int = Color.parseColor("#332196F3")
)
Bascule dynamique des thèmes¶
class ThemeManager(private val sceneView: MineSceneView) {
private var currentTheme: ThemeMode = ThemeMode.SYSTEM
enum class ThemeMode { LIGHT, DARK, SYSTEM }
fun setTheme(mode: ThemeMode) { currentTheme = mode; applyTheme() }
fun toggleTheme() { currentTheme = when (currentTheme) { ThemeMode.LIGHT -> ThemeMode.DARK; ThemeMode.DARK -> ThemeMode.LIGHT; ThemeMode.SYSTEM -> if (isSystemInDarkMode()) ThemeMode.LIGHT else ThemeMode.DARK }; applyTheme() }
private fun applyTheme() { /* applique createLightTheme/createDarkTheme ou système */ }
}
Persistance du thème¶
class ThemePreferences(context: Context) {
private val prefs = context.getSharedPreferences("mine_theme", Context.MODE_PRIVATE)
fun saveTheme(theme: MapColorTheme, name: String) { /* stocker les couleurs */ }
fun loadTheme(name: String): MapColorTheme? { /* charger si présent */ }
fun saveThemeMode(mode: ThemeManager.ThemeMode) { prefs.edit { putString("theme_mode", mode.name) } }
fun loadThemeMode(): ThemeManager.ThemeMode = ThemeManager.ThemeMode.valueOf(prefs.getString("theme_mode", ThemeManager.ThemeMode.SYSTEM.name)!!)
}
Bonnes pratiques¶
Recommandé
- Assurer le contraste (≥ 4.5:1), tester clair/sombre
- Nomer les couleurs par rôle (primary, error...) plutôt que par teinte
- Animer les changements de thème (ex: 300 ms)
À éviter
- Contraste insuffisant
- Couleurs en dur partout (préférer la config thème)
- Oublier les polices de repli
- Ne pas tester sur différentes tailles d'écran
Outils palette¶
| Outil | Usage | Lien |
|---|---|---|
| Coolors | Générer des palettes | coolors.co |
| Adobe Color | Créer des schémas | color.adobe.com |
| Material Design | Outil Material | material.io/color |
| Contrast Checker | Contraste WCAG | webaim.org/contrast |
| Paletton | Designer de palettes | paletton.com |
Accessibilité¶
object AccessibilityValidator {
fun calculateContrastRatio(foreground: Int, background: Int): Double { /* luminance et ratio */ }
fun validateThemeAccessibility(theme: MapColorTheme): List<String> { /* vérif texte, POI, chemin */ }
}
Dépannage¶
- Thème non appliqué : définir le thème avant le chargement de carte ou recharger après changement
- Texte illisible : ajouter un contour (stroke) ou fond contrasté
- Couleurs incohérentes selon appareil : éviter noirs/blancs purs, tester plusieurs devices