298 lines
9.2 KiB
Markdown
298 lines
9.2 KiB
Markdown
# 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.
|