Fluid Design Research.
This commit is contained in:
297
docs/fluid-design.md
Normal file
297
docs/fluid-design.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# 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-подход:** мягкий меш-градиент с 2–3 цветовыми пятнами.
|
||||
|
||||
```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 |
|
||||
|
||||
Пункты 1–2 — изменения в одну строку с максимальным эффектом.
|
||||
Пункты 3–5 — создание `GlassCard` и замена `Card` на него в 2–3 местах.
|
||||
Пункты 6–8 — более глубокие изменения навигации и transition.
|
||||
Reference in New Issue
Block a user