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