This commit is contained in:
toly
2023-11-04 08:48:16 +08:00
parent 8ef81ddb33
commit 88cd6fb3b4
130 changed files with 2563 additions and 5327 deletions

View File

@@ -1,15 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'iroute.dart';
import 'route_history.dart';
const List<String> kDestinationsPaths = [
'/color',
'/counter',
'/user',
'/settings',
];
import 'iroute_config.dart';
import 'route_history_manager.dart';
import 'routes.dart';
import 'views/not_find_view.dart';
AppRouterDelegate router = AppRouterDelegate();
@@ -18,120 +13,82 @@ class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
String get path => _path;
AppRouterDelegate() {
// keepAlivePath.add('/color');
_histories.add(RouteHistory(path));
final IRoutePageBuilder? notFindPageBuilder;
AppRouterDelegate({this.notFindPageBuilder}) {
_historyManager.recode(IRouteConfig(uri: Uri.parse(path)));
}
final List<RouteHistory> _histories = [];
final List<RouteHistory> _backHistories = [];
Page _defaultNotFindPageBuilder(_, __) => const MaterialPage(
child: Material(child: NotFindPage()),
);
List<RouteHistory> get histories => _histories.reversed.toList();
final RouteHistoryManager _historyManager = RouteHistoryManager();
bool get hasHistory => _histories.length > 1;
bool get hasBackHistory => _backHistories.isNotEmpty;
RouteHistoryManager get historyManager => _historyManager;
/// 历史回退操作
/// 将当前顶层移除,并加入 _backHistories 撤销列表
/// 并转到前一路径
void back() {
if (!hasHistory) return;
RouteHistory top = _histories.removeLast();
_backHistories.add(top);
if (_histories.isNotEmpty) {
_path = _histories.last.path;
if (_histories.last.extra != null) {
_pathExtraMap[_path] = _histories.last.extra;
}
notifyListeners();
}
}
/// 详见: [RouteHistoryManager.back]
void back() => _historyManager.back(changeRoute);
void toHistory(RouteHistory history) {
_path = history.path;
if (history.extra != null) {
_pathExtraMap[_path] = history.extra;
}
notifyListeners();
}
/// 撤销回退操作
/// 详见: [RouteHistoryManager.revocation]
void revocation() => _historyManager.revocation(changeRoute);
void closeHistory(int index) {
_histories.removeAt(index);
_historyManager.close(index);
notifyListeners();
}
void clearHistory() {
_histories.clear();
_historyManager.clear();
notifyListeners();
}
/// 撤销回退操作
/// 取出回退列表的最后元素,跳转到该路径
void revocation() {
RouteHistory target = _backHistories.removeLast();
_path = target.path;
if (target.extra != null) {
_pathExtraMap[_path] = target.extra;
}
_histories.add(target);
notifyListeners();
}
int? get activeIndex {
if (path.startsWith('/color')) return 0;
if (path.startsWith('/counter')) return 1;
if (path.startsWith('/user')) return 2;
if (path.startsWith('/settings')) return 3;
return null;
}
final Map<String, Completer<dynamic>> _completerMap = {};
Completer<dynamic>? completer;
final Map<String, dynamic> _pathExtraMap = {};
final List<String> keepAlivePath = [];
FutureOr<dynamic> changePath(
String value, {
bool forResult = false,
Object? extra,
bool keepAlive = false,
bool recordHistory = true,
}) {
FutureOr<dynamic> changeRoute(IRouteConfig config) {
String value = config.uri.path;
if (_path == value) null;
if (forResult) {
if (config.forResult) {
_completerMap[value] = Completer();
}
if (keepAlive) {
if (config.keepAlive) {
if (keepAlivePath.contains(value)) {
keepAlivePath.remove(value);
}
keepAlivePath.add(value);
}
if (extra != null) {
_pathExtraMap[value] = extra;
if (config.extra != null) {
_pathExtraMap[value] = config.extra;
}
if (recordHistory) {
_addPathToHistory(value,extra);
if (config.recordHistory) {
_historyManager.recode(config);
}
_path = value;
notifyListeners();
if (forResult) {
if (config.forResult) {
return _completerMap[value]!.future;
}
}
void _addPathToHistory(String value, Object? extra) {
if (_histories.isNotEmpty && value == _histories.last.path) return;
_histories.add(RouteHistory(
value,
extra: _pathExtraMap[path],
FutureOr<dynamic> changePath(
String value, {
bool forResult = false,
Object? extra,
bool keepAlive = false,
bool recordHistory = true,
}) {
return changeRoute(IRouteConfig(
uri: Uri.parse(value),
forResult: forResult,
extra: extra,
keepAlive: keepAlive,
recordHistory: recordHistory,
));
}
@@ -156,7 +113,7 @@ class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
/// 去除和 topPages 中重复的界面
pages.removeWhere(
(element) => topPages.map((e) => e.key).contains(element.key));
(element) => topPages.map((e) => e.key).contains(element.key));
}
pages.addAll(topPages);
@@ -165,23 +122,26 @@ class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
List<Page> _buildPageByPathFromTree(BuildContext context, String path) {
List<Page> result = [];
List<IRoute> iRoutes = root.find(path);
List<IRouteNode> iRoutes = rootRoute.find(path);
if (iRoutes.isNotEmpty) {
for (int i = 0; i < iRoutes.length; i++) {
IRoute iroute = iRoutes[i];
IRouteNode iroute = iRoutes[i];
String path = iroute.path;
Object? extra = _pathExtraMap[path];
bool keepAlive = keepAlivePath.contains(path);
bool forResult = _completerMap.containsKey(path);
Page? page = iroute.builder?.call(
context,
IRouteData(
uri: Uri.parse(path),
extra: extra,
keepAlive: keepAlive,
forResult: forResult,
),
IRouteConfig config = IRouteConfig(
uri: Uri.parse(path),
extra: extra,
keepAlive: keepAlive,
forResult: forResult,
);
Page? page;
if (iroute is NotFindNode) {
page = (notFindPageBuilder ?? _defaultNotFindPageBuilder)(context, config);
} else {
page = iroute.createPage(context, config);
}
if (page != null) {
result.add(page);
}
@@ -201,8 +161,7 @@ class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
_completerMap[path]?.complete(result);
_completerMap.remove(path);
}
changePath(backPath(path),recordHistory: false);
changePath(backPath(path), recordHistory: false);
return route.didPop(result);
}

View File

@@ -1,185 +1,105 @@
import 'package:flutter/material.dart';
import '../../../pages/color/color_add_page.dart';
import '../../../pages/color/color_detail_page.dart';
import '../../../pages/color/color_page.dart';
import '../../../pages/counter/counter_page.dart';
import '../../../pages/user/user_page.dart';
import '../../../pages/settings/settings_page.dart';
import '../transition/fade_transition_page.dart';
import 'iroute_config.dart';
class IRoute {
typedef IRoutePageBuilder = Page? Function(
BuildContext context,
IRouteConfig data,
);
typedef IRouteWidgetBuilder = Widget? Function(
BuildContext context,
IRouteConfig data,
);
abstract class IRouteNode {
final String path;
final IRoutePageBuilder? builder;
final List<IRoute> children;
final List<IRouteNode> children;
const IRoute({
const IRouteNode({
required this.path,
this.children = const [],
this.builder,
required this.children,
});
@override
String toString() {
return 'IRoute{path: $path, children: $children}';
Page? createPage(BuildContext context, IRouteConfig config);
List<IRouteNode> find(String input,) {
return findNodes(this, Uri.parse(input), 0, '/', []);
}
IRoute? match(String path) {
return matchRoute(this, path);
}
List<IRoute> find(String input){
String fixInput = input.substring(1);
List<IRoute> nodes = findNodes(this,fixInput,0,'/',[]);
if(nodes.isNotEmpty&&nodes.last.path!=input){
return [];
}
return nodes;
}
List<IRoute> findNodes(IRoute node,String input,int deep,String prefix,List<IRoute> result){
String separator = '/';
List<String> parts = input.split(separator);
if(deep>parts.length-1){
List<IRouteNode> findNodes(
IRouteNode node,
Uri uri,
int deep,
String prefix,
List<IRouteNode> result,
) {
List<String> parts = uri.pathSegments;
if (deep > parts.length - 1) {
return result;
}
String target = parts[deep];
if(node.children.isNotEmpty){
List<IRoute> nodes = node.children.where((e) => e.path==prefix+target).toList();
if (node.children.isNotEmpty) {
target = prefix + target;
List<IRouteNode> nodes = node.children.where((e) => e.path == target).toList();
bool match = nodes.isNotEmpty;
if(match){
IRoute matched = nodes.first;
if (match) {
IRouteNode matched = nodes.first;
result.add(matched);
String nextPrefix = '${matched.path}$separator';
findNodes(matched, input, ++deep,nextPrefix,result);
String nextPrefix = '${matched.path}/';
findNodes(matched, uri, ++deep, nextPrefix, result);
}else{
result.add(NotFindNode(path: target));
return result;
}
}else{
return result;
}
return result;
}
}
// List<IRoute> findNodes(IRoute node,String input,int deep,String prefix,List<IRoute> result){
// String separator = '/';
// List<String> parts = input.split(separator);
// if(deep>parts.length-1){
// return result;
// }
// String target = parts[deep];
// if(node.children.isNotEmpty){
// List<IRoute> nodes = node.children.where((e) => e.path==prefix+target).toList();
// bool match = nodes.isNotEmpty;
// if(match){
// IRoute matched = nodes.first;
// result.add(matched);
// String nextPrefix = '${matched.path}$separator';
// findNodes(matched, input, ++deep,nextPrefix,result);
// }
// }else{
// return result;
// }
// return result;
// }
/// 优先调用 [pageBuilder] 构建 Page
/// 没有 [pageBuilder] 时, 使用 [widgetBuilder] 构建组件
/// 没有 [pageBuilder] 和 [widgetBuilder] 时, 使用 [widget] 构建组件
class IRoute extends IRouteNode {
final IRoutePageBuilder? pageBuilder;
final IRouteWidgetBuilder? widgetBuilder;
final Widget? widget;
final Map<String,dynamic>? mata;
IRoute? matchRoute(IRoute route, String path) {
if (route.path == path) {
return route;
} else {
if (route.children.isNotEmpty) {
for (int i = 0; i < route.children.length; i++) {
IRoute current = route.children[i];
IRoute? target = matchRoute(current, path);
if (target != null) {
return target;
}
}
} else {
return null;
}
const IRoute({
required super.path,
super.children = const [],
this.mata,
this.widget,
this.pageBuilder,
this.widgetBuilder,
});
@override
Page? createPage(BuildContext context, IRouteConfig config) {
if (pageBuilder != null) {
return pageBuilder!(context, config);
}
Widget? child;
if (widgetBuilder != null) {
child = widgetBuilder!(context, config);
}
child ??= widget;
if (child != null) {
return MaterialPage(child: child, key: config.pageKey);
}
return null;
}
}
typedef IRoutePageBuilder = Page? Function(
BuildContext context, IRouteData data);
class IRouteData {
final Object? extra;
final bool forResult;
final Uri uri;
final bool keepAlive;
IRouteData({
this.extra,
required this.uri,
this.forResult = false,
this.keepAlive = false,
});
class NotFindNode extends IRouteNode{
NotFindNode({required super.path, super.children= const[]});
@override
Page? createPage(BuildContext context, IRouteConfig config) {
return null;
}
}
IRoute root = IRoute(path: 'root', children: kDestinationsIRoutes);
List<IRoute> kDestinationsIRoutes = [
IRoute(
path: '/color',
builder: (ctx, data) {
return const FadeTransitionPage(
key: ValueKey('/color'),
child: ColorPage(),
);
},
children: [
IRoute(
path: '/color/detail',
builder: (ctx, data) {
final Map<String, String> queryParams = data.uri.queryParameters;
String? selectedColor = queryParams['color'];
Color color = Colors.black;
if (selectedColor != null) {
color = Color(int.parse(selectedColor, radix: 16));
} else if (data.extra is Color) {
color = data.extra as Color;
}
return FadeTransitionPage(
key: const ValueKey('/color/detail'),
child: ColorDetailPage(color: color),
);
},
),
IRoute(
path: '/color/add',
builder: (ctx, data) {
return const FadeTransitionPage(
key: ValueKey('/color/add'),
child: ColorAddPage(),
);
}),
],
),
IRoute(
path: '/counter',
builder: (ctx, data) {
return const FadeTransitionPage(
key: ValueKey('/counter'),
child: CounterPage(),
);
}),
IRoute(
path: '/user',
builder: (ctx, data) {
return const FadeTransitionPage(
key: ValueKey('/user'),
child: UserPage(),
);
}),
IRoute(
path: '/settings',
builder: (ctx, data) {
return const FadeTransitionPage(
key: ValueKey('/settings'),
child: SettingPage(),
);
}),
];

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class IRouteConfig {
final Object? extra;
final bool forResult;
final Uri uri;
final bool keepAlive;
final bool recordHistory;
const IRouteConfig({
this.extra,
required this.uri,
this.forResult = false,
this.keepAlive = false,
this.recordHistory = false,
});
String get path => uri.path;
IRouteConfig copyWith({
Object? extra,
bool? forResult,
bool? keepAlive,
bool? recordHistory,
}) =>
IRouteConfig(
extra: extra ?? this.extra,
forResult: forResult ?? this.forResult,
keepAlive: keepAlive ?? this.keepAlive,
recordHistory: recordHistory ?? this.recordHistory,
uri: uri,
);
ValueKey get pageKey => ValueKey(path);
}

View File

@@ -1,6 +0,0 @@
class RouteHistory{
final String path;
final Object? extra;
RouteHistory(this.path, { this.extra});
}

View File

@@ -1,12 +1,47 @@
// import 'route_history.dart';
//
// class RouteHistoryManager{
// final List<RouteHistory> _histories = [];
// final List<RouteHistory> _backHistories = [];
//
// List<RouteHistory> get histories => _histories.reversed.toList();
//
// bool get hasHistory => _histories.length > 1;
//
// bool get hasBackHistory => _backHistories.isNotEmpty;
// }
import 'iroute_config.dart';
typedef OnRouteChange = void Function(IRouteConfig config);
class RouteHistoryManager{
final List<IRouteConfig> _histories = [];
final List<IRouteConfig> _backHistories = [];
List<IRouteConfig> get histories => _histories.reversed.toList();
bool get hasHistory => _histories.length > 1;
bool get hasBackHistory => _backHistories.isNotEmpty;
/// 将 [config] 加入历史记录
void recode(IRouteConfig config){
if (_histories.isNotEmpty && config.path == _histories.last.path) return;
_histories.add(config);
}
/// 历史回退操作
/// 将当前顶层移除,并加入 [_backHistories] 撤销列表
/// 并转到前一路径 [_histories.last]
void back(OnRouteChange callback) {
if (!hasHistory) return;
IRouteConfig top = _histories.removeLast();
_backHistories.add(top);
if (_histories.isNotEmpty) {
callback(_histories.last);
}
}
/// 撤销回退操作
/// 取出回退列表的最后元素,跳转到该路径
void revocation(OnRouteChange callback) {
IRouteConfig target = _backHistories.removeLast();
callback(target);
}
void close(int index) {
_histories.removeAt(index);
}
void clear() {
_histories.clear();
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'iroute_config.dart';
import 'iroute.dart';
import '../../../pages/color/color_add_page.dart';
import '../../../pages/color/color_detail_page.dart';
import '../../../pages/color/color_page.dart';
import '../../../pages/counter/counter_page.dart';
import '../../../pages/user/user_page.dart';
import '../../../pages/settings/settings_page.dart';
import '../../../pages/sort/views/sort_page.dart';
IRoute rootRoute = const IRoute(
path: 'root',
children: [
IRoute(
path: '/color',
widget: ColorPage(),
children: [
IRoute(path: '/color/detail', widgetBuilder: _buildColorDetail),
IRoute(path: '/color/add', widget: ColorAddPage()),
],
),
IRoute(path: '/counter', widget: CounterPage()),
IRoute(path: '/sort', widget: SortPage()),
IRoute(path: '/user', widget: UserPage()),
IRoute(path: '/settings', widget: SettingPage()),
],
);
Widget? _buildColorDetail(BuildContext context, IRouteConfig data) {
final Map<String, String> queryParams = data.uri.queryParameters;
String? selectedColor = queryParams['color'];
Color color = Colors.black;
if (selectedColor != null) {
color = Color(int.parse(selectedColor, radix: 16));
} else if (data.extra is Color) {
color = data.extra as Color;
}
return ColorDetailPage(color: color);
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class NotFindPage extends StatelessWidget {
const NotFindPage({super.key});
@override
Widget build(BuildContext context) {
return const Material(
child: Center(
child: Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
direction: Axis.vertical,
children: [
Icon(Icons.nearby_error,size: 64, color: Colors.redAccent),
Text(
'404 Page Not Find',
style: TextStyle(fontSize: 24, color: Colors.grey),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
class FadePageTransitionsBuilder extends PageTransitionsBuilder {
const FadePageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T>? route,
BuildContext? context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _FadePagePageTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
}
}
class _FadePagePageTransition extends StatelessWidget {
const _FadePagePageTransition({
required this.animation,
required this.secondaryAnimation,
required this.child,
});
final Animation<double> animation;
final Animation<double> secondaryAnimation;
final Widget child;
@override
Widget build(BuildContext context) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: child,
);
}
}

View File

@@ -15,8 +15,7 @@ class FadeTransitionPage<T> extends Page<T> {
});
@override
Route<T> createRoute(BuildContext context) =>
PageBasedFadeTransitionRoute<T>(this);
Route<T> createRoute(BuildContext context) => PageBasedFadeTransitionRoute<T>(this);
}
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {

View File

@@ -12,10 +12,11 @@ class AppNavigationRail extends StatefulWidget {
class _AppNavigationRailState extends State<AppNavigationRail> {
final List<MenuMeta> deskNavBarMenus = const [
MenuMeta(label: '颜色板', icon: Icons.color_lens_outlined),
MenuMeta(label: '计数器', icon: Icons.add_chart),
MenuMeta(label: '我的', icon: Icons.person),
MenuMeta(label: '设置', icon: Icons.settings),
MenuMeta(label: '颜色板', icon: Icons.color_lens_outlined,path: '/color'),
MenuMeta(label: '计数器', icon: Icons.add_chart,path: '/counter'),
MenuMeta(label: '排序', icon: Icons.sort,path: '/sort'),
MenuMeta(label: '我的', icon: Icons.person,path: '/user'),
MenuMeta(label: '设置', icon: Icons.settings,path: '/settings'),
];
@override
@@ -41,21 +42,33 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
),
tail: Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Text('V0.0.6',style: TextStyle(color: Colors.white,fontSize: 12),),
child: Text('V0.0.7',style: TextStyle(color: Colors.white,fontSize: 12),),
),
backgroundColor: const Color(0xff3975c6),
onDestinationSelected: _onDestinationSelected,
selectedIndex: router.activeIndex,
selectedIndex: activeIndex,
),
);
}
RegExp _segReg = RegExp(r'/\w+');
int? get activeIndex{
String path = router.path;
RegExpMatch? match = _segReg.firstMatch(path);
if(match==null) return null;
String? target = match.group(0);
int index = deskNavBarMenus.indexWhere((menu) => menu.path==target);
if(index==-1) return null;
return index;
}
void _onDestinationSelected(int index) {
String path = deskNavBarMenus[index].path!;
if(index==1){
router.changePath(kDestinationsPaths[index],keepAlive: true);
router.changePath(path,keepAlive: true);
}else{
router.changePath(kDestinationsPaths[index]);
router.changePath(path);
}
}

View File

@@ -57,6 +57,7 @@ Map<String, String> kRouteLabelMap = {
'/color/add': '添加颜色',
'/color/detail': '颜色详情',
'/counter': '计数器',
'/sort': '可视化排序算法',
'/user': '我的',
'/settings': '系统设置',
};

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:iroute/components/components.dart';
import '../../router/app_router_delegate.dart';
import '../../router/route_history.dart';
import '../../router/iroute_config.dart';
import 'app_top_bar.dart';
class HistoryViewIcon extends StatelessWidget{
@@ -55,7 +55,7 @@ class HistoryViewIcon extends StatelessWidget{
}
class HistoryItem extends StatefulWidget {
final RouteHistory history;
final IRouteConfig history;
final VoidCallback onPressed;
final VoidCallback onDelete;
@@ -83,7 +83,7 @@ class _HistoryItemState extends State<HistoryItem> {
const SizedBox(
height: 2,
),
Text(kRouteLabelMap[widget.history.path]!),
Text(kRouteLabelMap[widget.history.path]??'未知路由'),
],
)),
GestureDetector(
@@ -124,7 +124,8 @@ class _HistoryPanelState extends State<HistoryPanel> {
@override
Widget build(BuildContext context) {
if(router.histories.isEmpty){
List<IRouteConfig> histories = router.historyManager.histories;
if(histories.isEmpty){
return const Center(
child: Text(
'暂无浏览历史记录',
@@ -135,18 +136,18 @@ class _HistoryPanelState extends State<HistoryPanel> {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
itemExtent: 46,
itemCount: router.histories.length,
itemCount: histories.length,
itemBuilder: (_, index) =>
HistoryItem(
onDelete: (){
int fixIndex = router.histories.length - 1 - index;
int fixIndex = histories.length - 1 - index;
router.closeHistory(fixIndex);
},
onPressed: (){
router.toHistory(router.histories[index]);
router.changeRoute(histories[index].copyWith(recordHistory: false));
Navigator.of(context).pop();
},
history: router.histories[index]),
history: histories[index]),
);
}

View File

@@ -25,8 +25,8 @@ class _RouteHistoryButtonState extends State<RouteHistoryButton> {
@override
Widget build(BuildContext context) {
bool hasHistory = router.hasHistory;
bool hasBackHistory = router.hasBackHistory;
bool hasHistory = router.historyManager.hasHistory;
bool hasBackHistory = router.historyManager.hasBackHistory;
Color activeColor = const Color(0xff9195AC);
Color inActiveColor = const Color(0xffC7CAD5);
Color historyColor = hasHistory?activeColor:inActiveColor;