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

9.2 KiB
Raw Blame History

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 цветовыми пятнами.

// Новый виджет-обёртка — заменяет 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 + полупрозрачный контейнер.

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.

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.

// app_theme.dart
fontFamily: GoogleFonts.dmSans().fontFamily,

Изменение в одну строку — затрагивает весь UI сразу.


5. Цветные «blob»-тени на карточках вместо нейтральных

Текущее состояние: тени отсутствуют (elevation: 0).

Fluid-подход: мягкая цветная тень #007AFF с малой opacity.

// Вместо 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.

// Пример для 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 из контента под ним.

// В 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 для полей ввода.

// 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.