chore: initial release
This commit is contained in:
278
lib/core/services/animation_service.dart
Normal file
278
lib/core/services/animation_service.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../shared/theme/theme_extensions.dart';
|
||||
|
||||
/// Service for managing animations with performance optimization and accessibility
|
||||
class AnimationService {
|
||||
/// Get optimized animation duration based on context and settings
|
||||
static Duration getOptimizedDuration(
|
||||
BuildContext context,
|
||||
Duration defaultDuration, {
|
||||
bool respectReducedMotion = true,
|
||||
}) {
|
||||
if (respectReducedMotion && MediaQuery.of(context).disableAnimations) {
|
||||
return Duration.zero;
|
||||
}
|
||||
|
||||
// Optimize for 60fps - keep animations under 300ms for snappy feel
|
||||
final optimizedDuration = Duration(
|
||||
milliseconds: (defaultDuration.inMilliseconds * 0.8).round().clamp(
|
||||
100,
|
||||
300,
|
||||
),
|
||||
);
|
||||
|
||||
return optimizedDuration;
|
||||
}
|
||||
|
||||
/// Get optimized curve for smooth 60fps animations
|
||||
static Curve getOptimizedCurve({Curve defaultCurve = Curves.easeInOut}) {
|
||||
// Use curves that are optimized for mobile performance
|
||||
final curveType = defaultCurve.runtimeType.toString();
|
||||
|
||||
// Replace performance-heavy curves with lighter alternatives
|
||||
if (curveType.contains('Bounce')) {
|
||||
return Curves.easeInOutQuart; // Replace heavy bounce with smooth curve
|
||||
} else if (curveType.contains('Elastic')) {
|
||||
return Curves.easeInOutBack; // Lighter alternative to elastic
|
||||
} else if (defaultCurve == Curves.easeInOut) {
|
||||
return Curves.easeInOutCubic; // Better performance than default
|
||||
}
|
||||
|
||||
return defaultCurve;
|
||||
}
|
||||
|
||||
/// Create performant fade transition
|
||||
static Widget createOptimizedFadeTransition({
|
||||
required Widget child,
|
||||
required Animation<double> animation,
|
||||
Duration? duration,
|
||||
}) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
}
|
||||
|
||||
/// Create performant slide transition
|
||||
static Widget createOptimizedSlideTransition({
|
||||
required Widget child,
|
||||
required Animation<Offset> animation,
|
||||
Duration? duration,
|
||||
}) {
|
||||
return SlideTransition(position: animation, child: child);
|
||||
}
|
||||
|
||||
/// Create performant scale transition
|
||||
static Widget createOptimizedScaleTransition({
|
||||
required Widget child,
|
||||
required Animation<double> animation,
|
||||
Duration? duration,
|
||||
}) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
}
|
||||
|
||||
/// Create optimized page transition
|
||||
static PageRouteBuilder createOptimizedPageRoute({
|
||||
required Widget page,
|
||||
Duration? transitionDuration,
|
||||
PageTransitionType type = PageTransitionType.slide,
|
||||
}) {
|
||||
return PageRouteBuilder(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => page,
|
||||
transitionDuration:
|
||||
transitionDuration ?? const Duration(milliseconds: 250),
|
||||
reverseTransitionDuration:
|
||||
transitionDuration ?? const Duration(milliseconds: 200),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
final optimizedCurve = getOptimizedCurve();
|
||||
final curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: optimizedCurve,
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case PageTransitionType.fade:
|
||||
return FadeTransition(opacity: curvedAnimation, child: child);
|
||||
case PageTransitionType.slide:
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(curvedAnimation),
|
||||
child: child,
|
||||
);
|
||||
case PageTransitionType.scale:
|
||||
return ScaleTransition(
|
||||
scale: Tween<double>(
|
||||
begin: 0.8,
|
||||
end: 1.0,
|
||||
).animate(curvedAnimation),
|
||||
child: FadeTransition(opacity: curvedAnimation, child: child),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Create staggered animation for lists
|
||||
static Widget createStaggeredListAnimation({
|
||||
required Widget child,
|
||||
required int index,
|
||||
Duration? delay,
|
||||
Duration? duration,
|
||||
}) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: duration ?? const Duration(milliseconds: 200),
|
||||
curve: getOptimizedCurve(),
|
||||
builder: (context, value, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - value)),
|
||||
child: Opacity(opacity: value, child: child),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create performant shimmer animation
|
||||
static Widget createOptimizedShimmer({
|
||||
required Widget child,
|
||||
Duration? duration,
|
||||
Color? baseColor,
|
||||
Color? highlightColor,
|
||||
}) {
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: duration ?? const Duration(milliseconds: 1500),
|
||||
curve: Curves.linear,
|
||||
builder: (context, value, child) {
|
||||
return ShaderMask(
|
||||
shaderCallback: (bounds) {
|
||||
return LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
baseColor ?? context.conduitTheme.shimmerBase,
|
||||
highlightColor ?? context.conduitTheme.shimmerHighlight,
|
||||
baseColor ?? context.conduitTheme.shimmerBase,
|
||||
],
|
||||
stops: [0.0, value, 1.0],
|
||||
).createShader(bounds);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create optimized rotation animation
|
||||
static Widget createOptimizedRotation({
|
||||
required Widget child,
|
||||
required Animation<double> animation,
|
||||
double turns = 1.0,
|
||||
}) {
|
||||
return RotationTransition(
|
||||
turns: Tween<double>(begin: 0, end: turns).animate(animation),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check if device can handle complex animations
|
||||
static bool canHandleComplexAnimations(BuildContext context) {
|
||||
// Simple heuristic based on screen density and platform
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final totalPixels = screenSize.width * screenSize.height * devicePixelRatio;
|
||||
|
||||
// If total pixels exceed 4M, assume it's a high-end device
|
||||
return totalPixels > 4000000;
|
||||
}
|
||||
|
||||
/// Create adaptive animation based on device capability
|
||||
static Widget createAdaptiveAnimation({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
required Widget Function(Widget) complexAnimation,
|
||||
required Widget Function(Widget) simpleAnimation,
|
||||
}) {
|
||||
if (canHandleComplexAnimations(context) &&
|
||||
!MediaQuery.of(context).disableAnimations) {
|
||||
return complexAnimation(child);
|
||||
} else {
|
||||
return simpleAnimation(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum for page transition types
|
||||
enum PageTransitionType { fade, slide, scale }
|
||||
|
||||
/// Provider for reduced motion preference
|
||||
final reducedMotionProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
/// Provider for animation performance settings
|
||||
final animationPerformanceProvider = StateProvider<AnimationPerformance>((ref) {
|
||||
return AnimationPerformance.adaptive;
|
||||
});
|
||||
|
||||
/// Animation performance levels
|
||||
enum AnimationPerformance {
|
||||
high, // All animations enabled
|
||||
adaptive, // Adaptive based on device
|
||||
reduced, // Simplified animations
|
||||
minimal, // Essential animations only
|
||||
}
|
||||
|
||||
/// Provider for managing animation settings
|
||||
final animationSettingsProvider =
|
||||
StateNotifierProvider<AnimationSettingsNotifier, AnimationSettings>(
|
||||
(ref) => AnimationSettingsNotifier(),
|
||||
);
|
||||
|
||||
class AnimationSettings {
|
||||
final bool reduceMotion;
|
||||
final AnimationPerformance performance;
|
||||
final double animationSpeed;
|
||||
|
||||
const AnimationSettings({
|
||||
this.reduceMotion = false,
|
||||
this.performance = AnimationPerformance.adaptive,
|
||||
this.animationSpeed = 1.0,
|
||||
});
|
||||
|
||||
AnimationSettings copyWith({
|
||||
bool? reduceMotion,
|
||||
AnimationPerformance? performance,
|
||||
double? animationSpeed,
|
||||
}) {
|
||||
return AnimationSettings(
|
||||
reduceMotion: reduceMotion ?? this.reduceMotion,
|
||||
performance: performance ?? this.performance,
|
||||
animationSpeed: animationSpeed ?? this.animationSpeed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationSettingsNotifier extends StateNotifier<AnimationSettings> {
|
||||
AnimationSettingsNotifier() : super(const AnimationSettings());
|
||||
|
||||
void setReduceMotion(bool reduce) {
|
||||
state = state.copyWith(reduceMotion: reduce);
|
||||
}
|
||||
|
||||
void setPerformance(AnimationPerformance performance) {
|
||||
state = state.copyWith(performance: performance);
|
||||
}
|
||||
|
||||
void setAnimationSpeed(double speed) {
|
||||
state = state.copyWith(animationSpeed: speed.clamp(0.5, 2.0));
|
||||
}
|
||||
|
||||
Duration adjustDuration(Duration baseDuration) {
|
||||
if (state.reduceMotion) return Duration.zero;
|
||||
|
||||
final adjustedMs = (baseDuration.inMilliseconds / state.animationSpeed)
|
||||
.round();
|
||||
return Duration(milliseconds: adjustedMs);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user