perf(ui): enable KeepAlive on subscription list, tune prefetch, and reduce list/gesture animations
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import '../utils/reduce_motion.dart';
|
||||||
|
|
||||||
/// 스태거 애니메이션이 적용된 리스트 위젯
|
/// 스태거 애니메이션이 적용된 리스트 위젯
|
||||||
class StaggeredListAnimation extends StatefulWidget {
|
class StaggeredListAnimation extends StatefulWidget {
|
||||||
@@ -95,13 +96,14 @@ class _StaggeredListAnimationState extends State<StaggeredListAnimation>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (ReduceMotion.platform()) {
|
||||||
return widget.direction == Axis.vertical
|
return widget.direction == Axis.vertical
|
||||||
? Column(
|
? Column(children: widget.children)
|
||||||
children: _buildAnimatedChildren(),
|
: Row(children: widget.children);
|
||||||
)
|
}
|
||||||
: Row(
|
return widget.direction == Axis.vertical
|
||||||
children: _buildAnimatedChildren(),
|
? Column(children: _buildAnimatedChildren())
|
||||||
);
|
: Row(children: _buildAnimatedChildren());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildAnimatedChildren() {
|
List<Widget> _buildAnimatedChildren() {
|
||||||
@@ -156,8 +158,9 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
final reduced = ReduceMotion.platform();
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
duration: widget.duration,
|
duration: reduced ? Duration.zero : widget.duration,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -170,7 +173,9 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
|
|||||||
));
|
));
|
||||||
|
|
||||||
_slideAnimation = Tween<Offset>(
|
_slideAnimation = Tween<Offset>(
|
||||||
begin: const Offset(0, 0.3),
|
begin: ReduceMotion.platform()
|
||||||
|
? const Offset(0, 0.05)
|
||||||
|
: const Offset(0, 0.3),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).animate(CurvedAnimation(
|
).animate(CurvedAnimation(
|
||||||
parent: _controller,
|
parent: _controller,
|
||||||
@@ -185,11 +190,11 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
|
|||||||
curve: widget.curve,
|
curve: widget.curve,
|
||||||
));
|
));
|
||||||
|
|
||||||
// 지연 후 애니메이션 시작
|
// 지연 후 애니메이션 시작 (모션 축소 시 지연 없음)
|
||||||
Future.delayed(widget.delay * widget.index, () {
|
final startDelay =
|
||||||
if (mounted) {
|
ReduceMotion.platform() ? Duration.zero : widget.delay * widget.index;
|
||||||
_controller.forward();
|
Future.delayed(startDelay, () {
|
||||||
}
|
if (mounted) _controller.forward();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +206,7 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (ReduceMotion.platform()) return widget.child;
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class SubscriptionListWidget extends StatelessWidget {
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
cacheExtent: 500,
|
||||||
prototypeItem: const SizedBox(height: 156),
|
prototypeItem: const SizedBox(height: 156),
|
||||||
itemCount: subscriptions.length,
|
itemCount: subscriptions.length,
|
||||||
itemBuilder: (context, subIndex) {
|
itemBuilder: (context, subIndex) {
|
||||||
@@ -102,6 +103,7 @@ class SubscriptionListWidget extends StatelessWidget {
|
|||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: SwipeableSubscriptionCard(
|
child: SwipeableSubscriptionCard(
|
||||||
subscription: subscriptions[subIndex],
|
subscription: subscriptions[subIndex],
|
||||||
|
keepAlive: true,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Log.d(
|
Log.d(
|
||||||
'[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨');
|
'[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨');
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import 'package:flutter/material.dart';
|
|||||||
import '../models/subscription_model.dart';
|
import '../models/subscription_model.dart';
|
||||||
import '../utils/haptic_feedback_helper.dart';
|
import '../utils/haptic_feedback_helper.dart';
|
||||||
import 'subscription_card.dart';
|
import 'subscription_card.dart';
|
||||||
|
import '../utils/reduce_motion.dart';
|
||||||
|
|
||||||
class SwipeableSubscriptionCard extends StatefulWidget {
|
class SwipeableSubscriptionCard extends StatefulWidget {
|
||||||
final SubscriptionModel subscription;
|
final SubscriptionModel subscription;
|
||||||
final VoidCallback? onEdit;
|
final VoidCallback? onEdit;
|
||||||
final Future<void> Function()? onDelete;
|
final Future<void> Function()? onDelete;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final bool keepAlive;
|
||||||
|
|
||||||
const SwipeableSubscriptionCard({
|
const SwipeableSubscriptionCard({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -15,6 +17,7 @@ class SwipeableSubscriptionCard extends StatefulWidget {
|
|||||||
this.onEdit,
|
this.onEdit,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.keepAlive = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -23,7 +26,7 @@ class SwipeableSubscriptionCard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||||
// 상수 정의
|
// 상수 정의
|
||||||
static const double _tapTolerance = 20.0; // 탭 허용 범위
|
static const double _tapTolerance = 20.0; // 탭 허용 범위
|
||||||
static const double _actionThresholdPercent = 0.15;
|
static const double _actionThresholdPercent = 0.15;
|
||||||
@@ -49,7 +52,9 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: ReduceMotion.platform()
|
||||||
|
? const Duration(milliseconds: 0)
|
||||||
|
: const Duration(milliseconds: 300),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
_animation = Tween<double>(
|
_animation = Tween<double>(
|
||||||
@@ -215,10 +220,14 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
right: isLeft ? 0 : 24,
|
right: isLeft ? 0 : 24,
|
||||||
),
|
),
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: ReduceMotion.platform()
|
||||||
|
? const Duration(milliseconds: 0)
|
||||||
|
: const Duration(milliseconds: 200),
|
||||||
opacity: showIcon ? 1.0 : 0.0,
|
opacity: showIcon ? 1.0 : 0.0,
|
||||||
child: AnimatedScale(
|
child: AnimatedScale(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: ReduceMotion.platform()
|
||||||
|
? const Duration(milliseconds: 0)
|
||||||
|
: const Duration(milliseconds: 200),
|
||||||
scale: showIcon ? 1.0 : 0.5,
|
scale: showIcon ? 1.0 : 0.5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isDeleteThreshold
|
isDeleteThreshold
|
||||||
@@ -236,9 +245,10 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
return Transform.translate(
|
return Transform.translate(
|
||||||
offset: Offset(_currentOffset, 0),
|
offset: Offset(_currentOffset, 0),
|
||||||
child: Transform.scale(
|
child: Transform.scale(
|
||||||
scale: 1.0 - (_currentOffset.abs() / 2000),
|
scale:
|
||||||
|
ReduceMotion.platform() ? 1.0 : 1.0 - (_currentOffset.abs() / 2000),
|
||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle: _currentOffset / 2000,
|
angle: ReduceMotion.platform() ? 0.0 : _currentOffset / 2000,
|
||||||
child: SubscriptionCard(
|
child: SubscriptionCard(
|
||||||
subscription: widget.subscription,
|
subscription: widget.subscription,
|
||||||
onTap: widget
|
onTap: widget
|
||||||
@@ -251,6 +261,7 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
// 웹과 모바일 모두 동일한 스와이프 기능 제공
|
// 웹과 모바일 모두 동일한 스와이프 기능 제공
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -266,4 +277,7 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => widget.keepAlive;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user