Files
food-ai/docs/fluid-design.md
2026-03-28 00:04:01 +02:00

298 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Apple Fluid Design — примеры изменений
## Контекст
Текущий UI уже iOS-вдохновлённый (белые карточки на сером фоне, цвет `#007AFF`, flat Material 3).
Задача — поднять его до уровня **Apple Fluid** (visionOS/iOS 16+ эстетика):
стеклянные поверхности, мягкие градиентные фоны, пружинные анимации, размытые навбары.
---
## Ключевые файлы для изменений
- `client/lib/core/theme/app_colors.dart` — цветовая система
- `client/lib/core/theme/app_theme.dart` — Material theme
- `client/lib/features/home/home_screen.dart` — главный экран
- `client/lib/app.dart` — корень приложения
---
## 1. Градиентный фон вместо плоского `#F2F2F7`
**Текущее состояние:** `scaffoldBackgroundColor: AppColors.background` — серый `#F2F2F7`.
**Fluid-подход:** мягкий меш-градиент с 23 цветовыми пятнами.
```dart
// Новый виджет-обёртка — заменяет Scaffold background
class FluidBackground extends StatelessWidget {
final Widget child;
const FluidBackground({required this.child, super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFEEF4FF), // холодный голубой
Color(0xFFF5F0FF), // лавандовый
Color(0xFFE8F8F2), // мятный
],
stops: [0.0, 0.55, 1.0],
),
),
child: child,
);
}
}
```
**Применение в `home_screen.dart`:** обернуть `Scaffold` в `FluidBackground`.
---
## 2. Стеклянные карточки (Glassmorphism)
**Текущее состояние:** `Card` с `elevation: 0` и белым фоном.
**Fluid-подход:** `BackdropFilter` + `ImageFilter.blur` + полупрозрачный контейнер.
```dart
import 'dart:ui';
class GlassCard extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
final double borderRadius;
const GlassCard({
required this.child,
this.padding = const EdgeInsets.all(16),
this.borderRadius = 20,
super.key,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.72),
borderRadius: BorderRadius.circular(borderRadius),
border: Border.all(
color: Colors.white.withValues(alpha: 0.6),
width: 1.0,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF007AFF).withValues(alpha: 0.06),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
padding: padding,
child: child,
),
),
);
}
}
```
**Применение:** заменить `Card(...)` на `GlassCard(child: ...)` для `_CaloriesCard`, `_MacrosRow`.
---
## 3. Frosted-glass навигационная панель
**Текущее состояние:** белый `BottomNavigationBar` с `elevation: 0`.
**Fluid-подход:** размытая нижняя панель, как iOS Home indicator area.
```dart
class FluidBottomNavBar extends StatelessWidget {
final int currentIndex;
final ValueChanged<int> onTap;
const FluidBottomNavBar({
required this.currentIndex,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.80),
border: Border(
top: BorderSide(
color: Colors.white.withValues(alpha: 0.5),
width: 0.5,
),
),
),
child: BottomNavigationBar(
currentIndex: currentIndex,
onTap: onTap,
backgroundColor: Colors.transparent,
elevation: 0,
// ... items
),
),
),
);
}
}
```
---
## 4. Rounded шрифт — DM Sans / Plus Jakarta Sans
**Текущее состояние:** `GoogleFonts.roboto()` — нейтральный, не «тёплый».
**Fluid-подход:** `GoogleFonts.dmSans()` или `GoogleFonts.plusJakartaSans()` — округлые буквы, ближе к SF Pro Rounded.
```dart
// app_theme.dart
fontFamily: GoogleFonts.dmSans().fontFamily,
```
Изменение в одну строку — затрагивает весь UI сразу.
---
## 5. Цветные «blob»-тени на карточках вместо нейтральных
**Текущее состояние:** тени отсутствуют (`elevation: 0`).
**Fluid-подход:** мягкая цветная тень `#007AFF` с малой opacity.
```dart
// Вместо CardTheme используем обёртку с boxShadow
BoxDecoration fluidCardDecoration = BoxDecoration(
color: Colors.white.withValues(alpha: 0.80),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color(0xFF007AFF).withValues(alpha: 0.08),
blurRadius: 32,
spreadRadius: 0,
offset: const Offset(0, 12),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
);
```
---
## 6. Пружинные анимации (Spring Physics)
**Текущее состояние:** стандартный `AnimatedContainer` с `Duration(milliseconds: 150)`.
**Fluid-подход:** `SpringDescription` с bounce, как в UIKit.
```dart
// Пример для DateSelector — при выборе даты
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.8, end: 1.0),
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutBack, // ← даёт эффект bounce
builder: (context, scaleValue, child) =>
Transform.scale(scale: scaleValue, child: child),
child: selectedDatePill,
)
```
---
## 7. Frosted Bottom Sheet
**Текущее состояние:** белый bottom sheet с `borderRadius: 12`.
**Fluid-подход:** стеклянный sheet с blur из контента под ним.
```dart
// В showModalBottomSheet:
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent, // ← ключевое
builder: (sheetContext) => ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.88),
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
border: Border(
top: BorderSide(
color: Colors.white.withValues(alpha: 0.7),
width: 1,
),
),
),
child: FoodSearchSheet(...),
),
),
),
);
```
---
## 8. Скругления: 12 → 20pt
**Текущее состояние:** `BorderRadius.circular(12)` повсюду.
**Fluid-подход:** `20pt` для карточек, `28pt` для bottom sheets, `14pt` для полей ввода.
```dart
// app_theme.dart
cardTheme: CardThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // было 12
),
),
bottomSheetTheme: const BottomSheetThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)), // было 12
),
),
```
---
## Итог: приоритет изменений
| Приоритет | Изменение | Файл | Эффект |
|-----------|-----------|------|--------|
| 1 | Шрифт DM Sans | `app_theme.dart` | Весь UI сразу |
| 2 | Скругления 20/28pt | `app_theme.dart` | Весь UI сразу |
| 3 | Градиентный фон | `home_screen.dart` + новый виджет | Главный экран |
| 4 | `GlassCard` | новый файл `glass_card.dart` | Карточки калорий/макро |
| 5 | Цветные тени | точечно в карточках | Глубина |
| 6 | Frosted навбар | `app.dart` | Всё приложение |
| 7 | Frosted bottom sheet | `food_search_sheet.dart` | Поиск еды |
| 8 | Spring-анимации | `home_screen.dart` | DateSelector |
Пункты 12 — изменения в одну строку с максимальным эффектом.
Пункты 35 — создание `GlassCard` и замена `Card` на него в 23 местах.
Пункты 68 — более глубокие изменения навигации и transition.