feat: add sliding animation on tab change
This commit is contained in:
+97
-8
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'features/connection/startup_screen.dart';
|
||||
import 'features/connection/server_discovery_screen.dart';
|
||||
import 'features/connection/login_screen.dart';
|
||||
import 'features/onboarding/onboarding_screen.dart';
|
||||
@@ -13,20 +15,21 @@ import 'features/tags/tag_detail_screen.dart';
|
||||
import 'features/settings/settings_screen.dart';
|
||||
import 'providers.dart';
|
||||
|
||||
const _unauthenticated = {'/connect', '/login', '/onboarding'};
|
||||
// Routes that manage their own auth state — no redirect applied.
|
||||
const _unauthenticated = {'/', '/connect', '/login', '/onboarding'};
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
return GoRouter(
|
||||
initialLocation: '/connect',
|
||||
initialLocation: '/',
|
||||
redirect: (context, state) {
|
||||
final loc = state.matchedLocation;
|
||||
if (_unauthenticated.contains(loc)) return null;
|
||||
|
||||
final hasConfig = ref.read(serverConfigProvider) != null;
|
||||
final hasToken = ref.read(authTokenProvider) != null;
|
||||
final loc = state.matchedLocation;
|
||||
|
||||
if (!hasConfig && loc != '/connect') return '/connect';
|
||||
if (hasConfig && !hasToken && !_unauthenticated.contains(loc)) {
|
||||
return '/login';
|
||||
}
|
||||
if (!hasConfig) return '/connect';
|
||||
if (!hasToken) return '/login';
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
@@ -46,8 +49,10 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
path: '/onboarding',
|
||||
builder: (context, state) => const OnboardingScreen(),
|
||||
),
|
||||
StatefulShellRoute.indexedStack(
|
||||
StatefulShellRoute(
|
||||
builder: (context, state, shell) => MainShell(shell: shell),
|
||||
navigatorContainerBuilder: (context, shell, children) =>
|
||||
_SlidingTabSwitcher(index: shell.currentIndex, children: children),
|
||||
branches: [
|
||||
StatefulShellBranch(routes: [
|
||||
GoRoute(
|
||||
@@ -94,3 +99,87 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
class _SlidingTabSwitcher extends StatefulWidget {
|
||||
const _SlidingTabSwitcher({required this.index, required this.children});
|
||||
|
||||
final int index;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
State<_SlidingTabSwitcher> createState() => _SlidingTabSwitcherState();
|
||||
}
|
||||
|
||||
class _SlidingTabSwitcherState extends State<_SlidingTabSwitcher>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
late int _current;
|
||||
int _previous = -1;
|
||||
bool _forward = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_current = widget.index;
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 220),
|
||||
value: 1.0,
|
||||
);
|
||||
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_SlidingTabSwitcher old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (widget.index != _current) {
|
||||
_previous = _current;
|
||||
_forward = widget.index > _current;
|
||||
setState(() => _current = widget.index);
|
||||
_controller.forward(from: 0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, _) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: List.generate(widget.children.length, (i) {
|
||||
if (i == _current) {
|
||||
final begin = Offset(_forward ? 1.0 : -1.0, 0);
|
||||
final offset = Offset.lerp(begin, Offset.zero, _animation.value)!;
|
||||
return FractionalTranslation(
|
||||
translation: offset,
|
||||
child: TickerMode(enabled: true, child: widget.children[i]),
|
||||
);
|
||||
}
|
||||
if (i == _previous) {
|
||||
final end = Offset(_forward ? -1.0 : 1.0, 0);
|
||||
final offset = Offset.lerp(Offset.zero, end, _animation.value)!;
|
||||
return FractionalTranslation(
|
||||
translation: offset,
|
||||
child: IgnorePointer(
|
||||
child: TickerMode(enabled: false, child: widget.children[i]),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Offstage(
|
||||
offstage: true,
|
||||
child: TickerMode(enabled: false, child: widget.children[i]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user