Compare commits

...

5 Commits

Author SHA1 Message Date
Luke 35887faf50 introduce fluro native transition for android - more closely matches android animation 2018-04-05 16:55:19 -07:00
Luke 9dcec93b4a reformat code 2018-04-05 14:32:06 -07:00
Luke a48b722468 fix warnings 2018-04-05 14:09:47 -07:00
Luke 9f7f680d4e tidying 2018-04-05 14:07:43 -07:00
Luke ab8ef67243 wip - routable lifecycle 2018-03-28 12:52:50 -07:00
11 changed files with 238 additions and 77 deletions

View File

@ -5,11 +5,10 @@
* Copyright (c) 2018 Posse Productions LLC. All rights reserved. * Copyright (c) 2018 Posse Productions LLC. All rights reserved.
* See LICENSE for distribution and usage details. * See LICENSE for distribution and usage details.
*/ */
import '../../config/application.dart'; import 'package:router_example/config/application.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart'; import 'package:fluro/fluro.dart';
import '../../config/routes.dart'; import 'package:router_example/config/routes.dart';
import '../home/home_component.dart';
class AppComponent extends StatefulWidget { class AppComponent extends StatefulWidget {

View File

@ -1,3 +1,10 @@
/*
* fluro
* A Posse Production
* http://goposse.com
* Copyright (c) 2018 Posse Productions LLC. All rights reserved.
* See LICENSE for distribution and usage details.
*/
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DemoResultComponent extends StatefulWidget { class DemoResultComponent extends StatefulWidget {

View File

@ -5,7 +5,7 @@
* Copyright (c) 2018 Posse Productions LLC. All rights reserved. * Copyright (c) 2018 Posse Productions LLC. All rights reserved.
* See LICENSE for distribution and usage details. * See LICENSE for distribution and usage details.
*/ */
import '../../helpers/color_helpers.dart'; import 'package:router_example/helpers/color_helpers.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DemoSimpleComponent extends StatelessWidget { class DemoSimpleComponent extends StatelessWidget {

View File

@ -7,7 +7,7 @@
*/ */
import 'dart:async'; import 'dart:async';
import '../../config/application.dart'; import 'package:router_example/config/application.dart';
import 'package:fluro/fluro.dart'; import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -145,7 +145,7 @@ class HomeComponentState extends State<HomeComponent> {
String message = ""; String message = "";
String hexCode = "#FFFFFF"; String hexCode = "#FFFFFF";
String result; String result;
TransitionType transitionType = TransitionType.native; TransitionType transitionType = TransitionType.fluroNative;
if (key != "custom" && key != "function-call") { if (key != "custom" && key != "function-call") {
if (key == "native") { if (key == "native") {
hexCode = "#F76F00"; hexCode = "#F76F00";
@ -159,7 +159,7 @@ class HomeComponentState extends State<HomeComponent> {
message = "This screen should have appeared with a fade in transition"; message = "This screen should have appeared with a fade in transition";
transitionType = TransitionType.fadeIn; transitionType = TransitionType.fadeIn;
} else if (key == "pop-result") { } else if (key == "pop-result") {
transitionType = TransitionType.native; transitionType = TransitionType.fluroNative;
hexCode = "#7d41f4"; hexCode = "#7d41f4";
message = "When you close this screen you should see the current day of the week"; message = "When you close this screen you should see the current day of the week";
result = "Today is ${_daysOfWeek[new DateTime.now().weekday]}!"; result = "Today is ${_daysOfWeek[new DateTime.now().weekday]}!";

View File

@ -5,9 +5,9 @@
* Copyright (c) 2018 Posse Productions LLC. All rights reserved. * Copyright (c) 2018 Posse Productions LLC. All rights reserved.
* See LICENSE for distribution and usage details. * See LICENSE for distribution and usage details.
*/ */
import '../helpers/color_helpers.dart'; import 'package:router_example/helpers/color_helpers.dart';
import '../components/demo/demo_simple_component.dart'; import 'package:router_example/components/demo/demo_simple_component.dart';
import '../components/home/home_component.dart'; import 'package:router_example/components/home/home_component.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:fluro/fluro.dart'; import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -33,28 +33,30 @@ var demoFunctionHandler = new Handler(
String message = params["message"]?.first; String message = params["message"]?.first;
showDialog( showDialog(
context: context, context: context,
child: new AlertDialog( builder: (context) {
title: new Text( return new AlertDialog(
"Hey Hey!", title: new Text(
style: new TextStyle( "Hey Hey!",
color: const Color(0xFF00D6F7), style: new TextStyle(
fontFamily: "Lazer84", color: const Color(0xFF00D6F7),
fontSize: 22.0, fontFamily: "Lazer84",
), fontSize: 22.0,
),
content: new Text("$message"),
actions: <Widget>[
new Padding(
padding: new EdgeInsets.only(bottom: 8.0, right: 8.0),
child: new FlatButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: new Text("OK"),
), ),
), ),
], content: new Text("$message"),
), actions: <Widget>[
new Padding(
padding: new EdgeInsets.only(bottom: 8.0, right: 8.0),
child: new FlatButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: new Text("OK"),
),
),
],
);
},
); );
}); });
@ -70,5 +72,5 @@ var deepLinkHandler = new Handler(handlerFunc: (BuildContext context, Map<String
if (colorHex != null && colorHex.length > 0) { if (colorHex != null && colorHex.length > 0) {
color = new Color(ColorHelpers.fromHexString(colorHex)); color = new Color(ColorHelpers.fromHexString(colorHex));
} }
return new DemoSimpleComponent(message: "DEEEEEP LINK!!!", color: color, result: result); return new DemoSimpleComponent(message: message, color: color, result: result);
}); });

View File

@ -7,7 +7,7 @@
*/ */
import 'package:fluro/fluro.dart'; import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import './route_handlers.dart'; import 'package:router_example/config/route_handlers.dart';
class Routes { class Routes {

View File

@ -8,10 +8,12 @@
library fluro; library fluro;
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
part 'src/common.dart'; part 'src/common.dart';
part 'src/routable.dart';
part 'src/router.dart'; part 'src/router.dart';
part 'src/tree.dart'; part 'src/tree.dart';

View File

@ -21,10 +21,12 @@ class Handler {
} }
/// ///
typedef Route<T> RouteCreator<T>(RouteSettings route, Map<String, List<String>> parameters); typedef Route<T> RouteCreator<T>(
RouteSettings route, Map<String, List<String>> parameters);
/// ///
typedef Widget HandlerFunc(BuildContext context, Map<String, List<String>> parameters); typedef Widget HandlerFunc(
BuildContext context, Map<String, List<String>> parameters);
/// ///
class AppRoute { class AppRoute {
@ -41,12 +43,18 @@ enum RouteMatchType {
/// ///
class RouteMatch { class RouteMatch {
RouteMatch({ RouteMatch(
this.matchType = RouteMatchType.noMatch, {this.matchType = RouteMatchType.noMatch,
this.route, this.route,
this.errorMessage = "Unable to match route. Please check the logs." this.errorMessage = "Unable to match route. Please check the logs."});
});
final Route<dynamic> route; final Route<dynamic> route;
final RouteMatchType matchType; final RouteMatchType matchType;
final String errorMessage; final String errorMessage;
} }
TargetPlatform currentPlatform() {
if (Platform.isIOS) return TargetPlatform.iOS;
if (Platform.isAndroid) return TargetPlatform.android;
if (Platform.isFuchsia) return TargetPlatform.fuchsia;
throw Exception("Unsupported platform");
}

84
lib/src/routable.dart Normal file
View File

@ -0,0 +1,84 @@
/*
* fluro
* A Posse Production
* http://goposse.com
* Copyright (c) 2018 Posse Productions LLC. All rights reserved.
* See LICENSE for distribution and usage details.
*/
part of fluro;
abstract class Routable {
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {}
void didAppear(
bool wasPushed,
Route<dynamic> route,
Route<dynamic> previousRoute,
) {}
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {}
}
typedef String ScreenNameExtractor(RouteSettings settings);
String defaultNameExtractor(RouteSettings settings) => settings.name;
class RoutableObserver extends RouteObserver<PageRoute<dynamic>> {
final ScreenNameExtractor nameExtractor = defaultNameExtractor;
@override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
super.didPush(route, previousRoute);
if (route is PageRoute) {
final routeWidget = route.buildPage(
route.navigator.context, route.animation, route.secondaryAnimation);
if (routeWidget is Routable) {
Routable w = (routeWidget as Routable);
w.didPush(route, previousRoute);
w.didAppear(true, route, previousRoute);
}
}
}
@override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
super.didPop(route, previousRoute);
if (route is PageRoute) {
final leavingWidget = route.buildPage(
route.navigator.context, route.animation, route.secondaryAnimation);
if (leavingWidget is Routable) {
Routable w = (leavingWidget as Routable);
w.didPop(route, previousRoute);
}
}
if (previousRoute is PageRoute) {
final returningWidget = previousRoute.buildPage(
previousRoute.navigator.context,
previousRoute.animation,
previousRoute.secondaryAnimation);
if (returningWidget is Routable) {
Routable w = (returningWidget as Routable);
w.didAppear(false, route, previousRoute);
}
}
}
@override
void didReplace({Route newRoute, Route oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
if (oldRoute is PageRoute) {
final leavingWidget = oldRoute.buildPage(oldRoute.navigator.context,
oldRoute.animation, oldRoute.secondaryAnimation);
if (leavingWidget is Routable) {
Routable w = (leavingWidget as Routable);
w.didPop(oldRoute, newRoute);
}
}
if (newRoute is PageRoute) {
final returningWidget = newRoute.buildPage(newRoute.navigator.context,
newRoute.animation, newRoute.secondaryAnimation);
if (returningWidget is Routable) {
Routable w = (returningWidget as Routable);
w.didAppear(false, newRoute, oldRoute);
}
}
}
}

View File

@ -10,6 +10,7 @@ part of fluro;
enum TransitionType { enum TransitionType {
native, native,
nativeModal, nativeModal,
fluroNative,
inFromLeft, inFromLeft,
inFromRight, inFromRight,
inFromBottom, inFromBottom,
@ -18,7 +19,6 @@ enum TransitionType {
} }
class Router { class Router {
static final appRouter = new Router(); static final appRouter = new Router();
/// The tree structure that stores the defined routes /// The tree structure that stores the defined routes
@ -40,12 +40,15 @@ class Router {
} }
/// ///
Future navigateTo(BuildContext context, String path, {bool replace = false, TransitionType transition = TransitionType.native, Future navigateTo(BuildContext context, String path,
Duration transitionDuration = const Duration(milliseconds: 250), {bool replace = false,
RouteTransitionsBuilder transitionBuilder}) TransitionType transition = TransitionType.fluroNative,
{ Duration transitionDuration = const Duration(milliseconds: 250),
RouteMatch routeMatch = matchRoute(context, path, transitionType: transition, RouteTransitionsBuilder transitionBuilder}) {
transitionsBuilder: transitionBuilder, transitionDuration: transitionDuration); RouteMatch routeMatch = matchRoute(context, path,
transitionType: transition,
transitionsBuilder: transitionBuilder,
transitionDuration: transitionDuration);
Route<dynamic> route = routeMatch.route; Route<dynamic> route = routeMatch.route;
Completer completer = new Completer(); Completer completer = new Completer();
Future future = completer.future; Future future = completer.future;
@ -56,7 +59,9 @@ class Router {
route = _notFoundRoute(context, path); route = _notFoundRoute(context, path);
} }
if (route != null) { if (route != null) {
future = replace ? Navigator.pushReplacement(context, route) : Navigator.push(context, route); future = replace
? Navigator.pushReplacement(context, route)
: Navigator.push(context, route);
completer.complete(); completer.complete();
} else { } else {
String error = "No registered route was found to handle '$path'."; String error = "No registered route was found to handle '$path'.";
@ -68,21 +73,33 @@ class Router {
return future; return future;
} }
bool pop(BuildContext context) => Navigator.of(context).pop();
List<NavigatorObserver> get routerObservers {
return [
new RoutableObserver(),
];
}
/// ///
Route<Null> _notFoundRoute(BuildContext context, String path) { Route<Null> _notFoundRoute(BuildContext context, String path) {
RouteCreator<Null> creator = (RouteSettings routeSettings, Map<String, List<String>> parameters) { RouteCreator<Null> creator =
return new MaterialPageRoute<Null>(settings: routeSettings, builder: (BuildContext context) { (RouteSettings routeSettings, Map<String, List<String>> parameters) {
return notFoundHandler.handlerFunc(context, parameters); return new MaterialPageRoute<Null>(
}); settings: routeSettings,
builder: (BuildContext context) {
return notFoundHandler.handlerFunc(context, parameters);
});
}; };
return creator(new RouteSettings(name: path), null); return creator(new RouteSettings(name: path), null);
} }
/// ///
RouteMatch matchRoute(BuildContext buildContext, String path, {RouteSettings routeSettings, RouteMatch matchRoute(BuildContext buildContext, String path,
TransitionType transitionType, Duration transitionDuration = const Duration(milliseconds: 250), {RouteSettings routeSettings,
RouteTransitionsBuilder transitionsBuilder}) TransitionType transitionType,
{ Duration transitionDuration = const Duration(milliseconds: 250),
RouteTransitionsBuilder transitionsBuilder}) {
RouteSettings settingsToUse = routeSettings; RouteSettings settingsToUse = routeSettings;
if (routeSettings == null) { if (routeSettings == null) {
settingsToUse = new RouteSettings(name: path); settingsToUse = new RouteSettings(name: path);
@ -91,30 +108,47 @@ class Router {
AppRoute route = match?.route; AppRoute route = match?.route;
Handler handler = (route != null ? route.handler : notFoundHandler); Handler handler = (route != null ? route.handler : notFoundHandler);
if (route == null && notFoundHandler == null) { if (route == null && notFoundHandler == null) {
return new RouteMatch(matchType: RouteMatchType.noMatch, errorMessage: "No matching route was found"); return new RouteMatch(
matchType: RouteMatchType.noMatch,
errorMessage: "No matching route was found");
} }
Map<String, List<String>> parameters = match?.parameters ?? <String, List<String>>{}; Map<String, List<String>> parameters =
match?.parameters ?? <String, List<String>>{};
if (handler.type == HandlerType.function) { if (handler.type == HandlerType.function) {
handler.handlerFunc(buildContext, parameters); handler.handlerFunc(buildContext, parameters);
return new RouteMatch(matchType: RouteMatchType.nonVisual); return new RouteMatch(matchType: RouteMatchType.nonVisual);
} }
RouteCreator creator = (RouteSettings routeSettings, Map<String, List<String>> parameters) { final platform = currentPlatform();
bool isNativeTransition = (transitionType == TransitionType.native || transitionType == TransitionType.nativeModal); RouteCreator creator =
(RouteSettings routeSettings, Map<String, List<String>> parameters) {
// We use the standard material route for .native, .nativeModal and
// .fluroNative if you're on iOS
bool isNativeTransition = (transitionType == TransitionType.native ||
transitionType == TransitionType.nativeModal ||
(transitionType == TransitionType.fluroNative &&
platform != TargetPlatform.android));
if (isNativeTransition) { if (isNativeTransition) {
return new MaterialPageRoute<dynamic>(settings: routeSettings, fullscreenDialog: transitionType == TransitionType.nativeModal, return new MaterialPageRoute<dynamic>(
builder: (BuildContext context) { settings: routeSettings,
return handler.handlerFunc(context, parameters); fullscreenDialog: transitionType == TransitionType.nativeModal,
}); builder: (BuildContext context) =>
handler.handlerFunc(context, parameters));
} else { } else {
var routeTransitionsBuilder; var routeTransitionsBuilder;
if (transitionType == TransitionType.custom) { if (transitionType == TransitionType.custom) {
routeTransitionsBuilder = transitionsBuilder; routeTransitionsBuilder = transitionsBuilder;
} else { } else {
if (transitionType == TransitionType.fluroNative &&
platform == TargetPlatform.android) {
transitionDuration = new Duration(milliseconds: 150);
}
routeTransitionsBuilder = _standardTransitionsBuilder(transitionType); routeTransitionsBuilder = _standardTransitionsBuilder(transitionType);
} }
return new PageRouteBuilder<dynamic>(settings: routeSettings, return new PageRouteBuilder<dynamic>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { settings: routeSettings,
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return handler.handlerFunc(context, parameters); return handler.handlerFunc(context, parameters);
}, },
transitionDuration: transitionDuration, transitionDuration: transitionDuration,
@ -128,9 +162,29 @@ class Router {
); );
} }
RouteTransitionsBuilder _standardTransitionsBuilder(TransitionType transitionType) { RouteTransitionsBuilder _standardTransitionsBuilder(
return (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { TransitionType transitionType) {
if (transitionType == TransitionType.fadeIn) { return (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
if (transitionType == TransitionType.fluroNative) {
return new SlideTransition(
position: new Tween<Offset>(
begin: const Offset(0.0, 0.12),
end: const Offset(0.0, 0.0),
).animate(new CurvedAnimation(
parent: animation,
curve: new Interval(0.125, 0.950, curve: Curves.fastOutSlowIn),
reverseCurve: Curves.easeOut,
)),
child: new FadeTransition(
opacity: new Tween<double>(
begin: 0.0,
end: 1.0,
).animate(animation),
child: child,
),
);
} else if (transitionType == TransitionType.fadeIn) {
return new FadeTransition(opacity: animation, child: child); return new FadeTransition(opacity: animation, child: child);
} else { } else {
const Offset topLeft = const Offset(0.0, 0.0); const Offset topLeft = const Offset(0.0, 0.0);
@ -161,7 +215,8 @@ class Router {
/// if any defined handler is found. It can also be used with the [MaterialApp.onGenerateRoute] /// if any defined handler is found. It can also be used with the [MaterialApp.onGenerateRoute]
/// property as callback to create routes that can be used with the [Navigator] class. /// property as callback to create routes that can be used with the [Navigator] class.
Route<dynamic> generator(RouteSettings routeSettings) { Route<dynamic> generator(RouteSettings routeSettings) {
RouteMatch match = matchRoute(null, routeSettings.name, routeSettings: routeSettings); RouteMatch match =
matchRoute(null, routeSettings.name, routeSettings: routeSettings);
return match.route; return match.route;
} }

View File

@ -39,8 +39,7 @@ class RouteTreeNodeMatch {
class RouteTreeNode { class RouteTreeNode {
// constructors // constructors
RouteTreeNode(this.part, RouteTreeNode(this.part, this.type);
this.type);
// properties // properties
String part; String part;
@ -114,10 +113,12 @@ class RouteTree {
components = ["/"]; components = ["/"];
} }
Map<RouteTreeNode, RouteTreeNodeMatch> nodeMatches = <RouteTreeNode, RouteTreeNodeMatch>{}; Map<RouteTreeNode, RouteTreeNodeMatch> nodeMatches =
<RouteTreeNode, RouteTreeNodeMatch>{};
List<RouteTreeNode> nodesToCheck = _nodes; List<RouteTreeNode> nodesToCheck = _nodes;
for (String checkComponent in components) { for (String checkComponent in components) {
Map<RouteTreeNode, RouteTreeNodeMatch> currentMatches = <RouteTreeNode, RouteTreeNodeMatch>{}; Map<RouteTreeNode, RouteTreeNodeMatch> currentMatches =
<RouteTreeNode, RouteTreeNodeMatch>{};
List<RouteTreeNode> nextNodes = <RouteTreeNode>[]; List<RouteTreeNode> nextNodes = <RouteTreeNode>[];
for (RouteTreeNode node in nodesToCheck) { for (RouteTreeNode node in nodesToCheck) {
String pathPart = checkComponent; String pathPart = checkComponent;
@ -130,7 +131,8 @@ class RouteTree {
bool isMatch = (node.part == pathPart || node.isParameter()); bool isMatch = (node.part == pathPart || node.isParameter());
if (isMatch) { if (isMatch) {
RouteTreeNodeMatch parentMatch = nodeMatches[node.parent]; RouteTreeNodeMatch parentMatch = nodeMatches[node.parent];
RouteTreeNodeMatch match = new RouteTreeNodeMatch.fromMatch(parentMatch, node); RouteTreeNodeMatch match =
new RouteTreeNodeMatch.fromMatch(parentMatch, node);
if (node.isParameter()) { if (node.isParameter()) {
String paramKey = node.part.substring(1); String paramKey = node.part.substring(1);
match.parameters[paramKey] = [pathPart]; match.parameters[paramKey] = [pathPart];
@ -156,7 +158,9 @@ class RouteTree {
RouteTreeNodeMatch match = matches.first; RouteTreeNodeMatch match = matches.first;
RouteTreeNode nodeToUse = match.node; RouteTreeNode nodeToUse = match.node;
// print("using match: ${match}, ${nodeToUse?.part}, ${match?.parameters}"); // print("using match: ${match}, ${nodeToUse?.part}, ${match?.parameters}");
if (nodeToUse != null && nodeToUse.routes != null && nodeToUse.routes.length > 0) { if (nodeToUse != null &&
nodeToUse.routes != null &&
nodeToUse.routes.length > 0) {
List<AppRoute> routes = nodeToUse.routes; List<AppRoute> routes = nodeToUse.routes;
AppRouteMatch routeMatch = new AppRouteMatch(routes[0]); AppRouteMatch routeMatch = new AppRouteMatch(routes[0]);
routeMatch.parameters = match.parameters; routeMatch.parameters = match.parameters;