mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Fix ghosting in native screen transitions
Routed screens are transparent fragments rendered into the shell's single Scaffold. A screen pushed on top (e.g. device detail) was therefore see-through, leaving the screen beneath visible as it slid in, and the fade page ignored secondaryAnimation so the covered screen sat frozen instead of parallaxing out. - Paint each routed page opaque (theme surface) so pushes cover cleanly. - Drill into detail screens with a CupertinoPageTransition slide; the covered page parallaxes out via the same route animation, so both move in lockstep on the native iOS curve. Tabs keep their crossfade. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
80f0f54785
commit
dedfe1c838
@@ -1,7 +1,6 @@
|
|||||||
# OOTT ToDo list
|
# OOTT ToDo list
|
||||||
|
|
||||||
- [ ] Add support for push notifications to the app (iOS and Android)
|
- [ ] Add support for push notifications to the app (iOS and Android)
|
||||||
- [x] Modify the release script to check that gh is logged in and that frontend tests are run before releasing
|
|
||||||
|
|
||||||
## Release plan for 0.2.0
|
## Release plan for 0.2.0
|
||||||
- [x] Test Android UI on emulator
|
- [x] Test Android UI on emulator
|
||||||
@@ -20,19 +19,10 @@
|
|||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
|
|
||||||
- [x] In notifications, when its a new device(s) found notification, remove the status block (new devices are never registered)
|
|
||||||
- [x] In notifications, when the vendor is empty put (unknown)
|
|
||||||
- [ ] Implement the pushover API call directly to support HTML content and review notification text to use it
|
- [ ] Implement the pushover API call directly to support HTML content and review notification text to use it
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
- [x] The app has the default Flutter icon in iOS, use OOTT's icon
|
|
||||||
- [x] Ask for network access when the app launches, now it asks when you try to test the connection for the first time and it always fails once
|
|
||||||
- [x] On iOS when the user navigates between screens the previous screen remains visible on the backgroun while the other one animates in, it looks bad
|
|
||||||
- [x] In narrow devices, the "Notifications" title in the home screen is to close to the filter combobox and does not scroll with the page (it should)
|
|
||||||
- [x] When the user goes to the / URI in a production server redirect him to /web
|
|
||||||
- [x] Add a link to the API docs (/api/docs) in the navigation (new window - only visible in wide)
|
|
||||||
|
|
||||||
## Improve engine
|
## Improve engine
|
||||||
|
|
||||||
Passive (low noise, no probing):
|
Passive (low noise, no probing):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/cupertino.dart' show CupertinoPageTransition;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:frontend/about/about.dart';
|
import 'package:frontend/about/about.dart';
|
||||||
import 'package:frontend/settings/settings.dart';
|
import 'package:frontend/settings/settings.dart';
|
||||||
@@ -112,11 +113,11 @@ final GoRouter router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.deviceDetailSegment,
|
path: Routes.deviceDetailSegment,
|
||||||
name: 'deviceDetail',
|
name: 'deviceDetail',
|
||||||
// A genuine forward drill-in: keep the default platform slide,
|
// A genuine forward drill-in: slide the detail screen in over the
|
||||||
// which is the correct affordance for pushing a detail screen.
|
// list (see [_drillInPage]).
|
||||||
builder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final mac = state.pathParameters['macAddress']!;
|
final mac = state.pathParameters['macAddress']!;
|
||||||
return DeviceDetail(macAddress: mac);
|
return _drillInPage(state, DeviceDetail(macAddress: mac));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -146,20 +147,68 @@ final GoRouter router = GoRouter(
|
|||||||
// Duration of the crossfade between top-level destinations.
|
// Duration of the crossfade between top-level destinations.
|
||||||
const Duration _kTabFadeDuration = Duration(milliseconds: 200);
|
const Duration _kTabFadeDuration = Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
// Duration of the drill-in slide between a list and a detail screen. Governs the
|
||||||
|
// whole movement: a pushed route's animation also drives the underlying route's
|
||||||
|
// secondary animation, so the incoming and outgoing screens stay in lockstep.
|
||||||
|
const Duration _kDrillInDuration = Duration(milliseconds: 350);
|
||||||
|
|
||||||
|
// Routed screens are transparent fragments painted into the shell's single
|
||||||
|
// [Scaffold]. That is fine for a crossfade, but a screen *pushed on top* (e.g.
|
||||||
|
// the device detail) must be opaque, otherwise the screen beneath shows straight
|
||||||
|
// through it as it slides in ("ghosting"). Painting the theme surface behind
|
||||||
|
// every routed page makes pushes cover cleanly.
|
||||||
|
Widget _opaque(BuildContext context, Widget child) =>
|
||||||
|
ColoredBox(color: Theme.of(context).colorScheme.surface, child: child);
|
||||||
|
|
||||||
// Page used when switching between top-level destinations (the tabs). Switching
|
// Page used when switching between top-level destinations (the tabs). Switching
|
||||||
// peers via [context.go] is a replace, not a forward push, so the default iOS
|
// peers via [context.go] is a replace, not a forward push, so the default iOS
|
||||||
// slide is wrong here: it slides the incoming screen in over the outgoing one,
|
// slide is wrong here: it slides the incoming screen in over the outgoing one,
|
||||||
// leaving the old screen visible underneath. A crossfade animates both screens
|
// leaving the old screen visible underneath. A crossfade animates both screens
|
||||||
// together (each driven by its own primary animation), so nothing is left
|
// together (each driven by its own primary animation), so nothing is left
|
||||||
// stranded in the background. Detail screens reached via [context.push] keep
|
// stranded in the background.
|
||||||
// the default platform slide.
|
//
|
||||||
|
// When a detail screen is pushed on top of one of these pages (see
|
||||||
|
// [_drillInPage]) this page's *secondary* animation runs, so it also
|
||||||
|
// parallax-slides out to the leading edge with the native iOS curve, moving in
|
||||||
|
// lockstep with the incoming screen instead of sitting stranded behind it. A
|
||||||
|
// completed primary animation keeps the page in place during this; the slide is
|
||||||
|
// contributed entirely by the secondary animation.
|
||||||
CustomTransitionPage<void> _fadePage(GoRouterState state, Widget child) {
|
CustomTransitionPage<void> _fadePage(GoRouterState state, Widget child) {
|
||||||
return CustomTransitionPage<void>(
|
return CustomTransitionPage<void>(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
transitionDuration: _kTabFadeDuration,
|
transitionDuration: _kTabFadeDuration,
|
||||||
reverseTransitionDuration: _kTabFadeDuration,
|
reverseTransitionDuration: _kTabFadeDuration,
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
FadeTransition(opacity: animation, child: child),
|
FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: CupertinoPageTransition(
|
||||||
|
primaryRouteAnimation: kAlwaysCompleteAnimation,
|
||||||
|
secondaryRouteAnimation: secondaryAnimation,
|
||||||
|
linearTransition: false,
|
||||||
|
child: _opaque(context, child),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page used to drill into a detail screen. Slides the incoming screen in from
|
||||||
|
// the trailing edge with the native iOS curve; the screen left behind is
|
||||||
|
// parallaxed out by the same route animation via its own secondary transition
|
||||||
|
// (see [_fadePage]), so both move together. The child is painted opaque so it
|
||||||
|
// fully covers the screen beneath while sliding.
|
||||||
|
CustomTransitionPage<void> _drillInPage(GoRouterState state, Widget child) {
|
||||||
|
return CustomTransitionPage<void>(
|
||||||
|
key: state.pageKey,
|
||||||
|
transitionDuration: _kDrillInDuration,
|
||||||
|
reverseTransitionDuration: _kDrillInDuration,
|
||||||
|
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||||
|
CupertinoPageTransition(
|
||||||
|
primaryRouteAnimation: animation,
|
||||||
|
secondaryRouteAnimation: secondaryAnimation,
|
||||||
|
linearTransition: false,
|
||||||
|
child: _opaque(context, child),
|
||||||
|
),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ void main() {
|
|||||||
// The app's real [router] is configured so that switching between top-level
|
// The app's real [router] is configured so that switching between top-level
|
||||||
// destinations crossfades (a [CustomTransitionPage]) instead of using the
|
// destinations crossfades (a [CustomTransitionPage]) instead of using the
|
||||||
// default platform slide, which would otherwise leave the outgoing screen
|
// default platform slide, which would otherwise leave the outgoing screen
|
||||||
// visible behind the incoming one. Drilling into a detail screen keeps the
|
// visible behind the incoming one. Drilling into a detail screen uses its own
|
||||||
// default slide (a plain [GoRoute.builder]).
|
// custom page builder (an opaque iOS-style slide), not the default builder.
|
||||||
List<GoRoute> topLevelRoutes() {
|
List<GoRoute> topLevelRoutes() {
|
||||||
final shell = router.configuration.routes.single as ShellRoute;
|
final shell = router.configuration.routes.single as ShellRoute;
|
||||||
return shell.routes.cast<GoRoute>();
|
return shell.routes.cast<GoRoute>();
|
||||||
@@ -29,17 +29,17 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the device detail drill-in keeps the default slide builder', () {
|
test('the device detail drill-in uses a custom page builder (slide)', () {
|
||||||
final devices = routeByName('devices');
|
final devices = routeByName('devices');
|
||||||
final detail = devices.routes.single as GoRoute;
|
final detail = devices.routes.single as GoRoute;
|
||||||
|
|
||||||
expect(detail.name, 'deviceDetail');
|
expect(detail.name, 'deviceDetail');
|
||||||
expect(
|
expect(
|
||||||
detail.builder,
|
detail.pageBuilder,
|
||||||
isNotNull,
|
isNotNull,
|
||||||
reason: 'the detail push should keep the default platform slide',
|
reason: 'the detail push should use the opaque drill-in slide page',
|
||||||
);
|
);
|
||||||
expect(detail.pageBuilder, isNull);
|
expect(detail.builder, isNull, reason: 'the detail should not also build');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user