feat(accessibility): add reduceMotion scaling and minimize animations; apply RepaintBoundary to heavy widgets

This commit is contained in:
JiWoong Sul
2025-09-08 14:30:28 +09:00
parent 10491af55b
commit eb6691ce6a
11 changed files with 478 additions and 331 deletions

View File

@@ -10,6 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../utils/reduce_motion.dart';
// 파비콘 캐시 관리 클래스
class FaviconCache {
@@ -190,12 +191,15 @@ class _WebsiteIconState extends State<WebsiteIcon>
// 애니메이션 컨트롤러 초기화
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
duration: ReduceMotion.platform()
? const Duration(milliseconds: 0)
: const Duration(milliseconds: 300),
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.08).animate(
CurvedAnimation(
parent: _animationController, curve: Curves.easeOutCubic));
_scaleAnimation =
Tween<double>(begin: 1.0, end: ReduceMotion.platform() ? 1.0 : 1.08)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeOutCubic));
// 초기 _previousServiceKey 설정
_previousServiceKey = _serviceKey;
@@ -548,11 +552,14 @@ class _WebsiteIconState extends State<WebsiteIcon>
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
return RepaintBoundary(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
final scale =
ReduceMotion.isEnabled(context) ? 1.0 : _scaleAnimation.value;
return Transform.scale(
scale: _scaleAnimation.value,
scale: scale,
child: child,
);
},
@@ -578,12 +585,25 @@ class _WebsiteIconState extends State<WebsiteIcon>
),
child: _buildIconContent(),
),
);
));
}
Widget _buildIconContent() {
// 로딩 중 표시
if (_isLoading) {
if (ReduceMotion.isEnabled(context)) {
return Container(
key: ValueKey('loading_${widget.serviceName}_$_uniqueId'),
decoration: BoxDecoration(
color: AppColors.surfaceColorAlt,
borderRadius: BorderRadius.circular(widget.size * 0.2),
border: Border.all(
color: AppColors.borderColor,
width: 0.5,
),
),
);
}
return Container(
key: ValueKey('loading_${widget.serviceName}_$_uniqueId'),
decoration: BoxDecoration(
@@ -633,20 +653,31 @@ class _WebsiteIconState extends State<WebsiteIcon>
width: widget.size,
height: widget.size,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.surfaceColorAlt,
child: Center(
child: SizedBox(
width: widget.size * 0.4,
height: widget.size * 0.4,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.primaryColor.withValues(alpha: 0.7)),
fadeInDuration: ReduceMotion.isEnabled(context)
? const Duration(milliseconds: 0)
: const Duration(milliseconds: 300),
fadeOutDuration: ReduceMotion.isEnabled(context)
? const Duration(milliseconds: 0)
: const Duration(milliseconds: 300),
placeholder: (context, url) {
if (ReduceMotion.isEnabled(context)) {
return Container(color: AppColors.surfaceColorAlt);
}
return Container(
color: AppColors.surfaceColorAlt,
child: Center(
child: SizedBox(
width: widget.size * 0.4,
height: widget.size * 0.4,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.primaryColor.withValues(alpha: 0.7)),
),
),
),
),
),
);
},
errorWidget: (context, url, error) => _buildFallbackIcon(),
),
);