Fix duplicate launch screen and native splash exit flashing
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
package com.tpschedule.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var calendar: CalendarBridge? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Once Flutter has drawn, remove the system preview immediately.
|
||||
// Its default blue exit fade can flash over an already ready app.
|
||||
splashScreen.setOnExitAnimationListener { view -> view.remove() }
|
||||
}
|
||||
}
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
calendar = CalendarBridge(this, flutterEngine.dartExecutor.binaryMessenger)
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">@color/tpschedule_blue</item>
|
||||
<item name="android:statusBarColor">@color/tpschedule_blue</item>
|
||||
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:windowBackground">@color/tpschedule_surface</item>
|
||||
<item name="android:statusBarColor">@color/tpschedule_surface</item>
|
||||
<item name="android:navigationBarColor">@color/tpschedule_surface</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">@color/tpschedule_blue</item>
|
||||
<item name="android:statusBarColor">@color/tpschedule_blue</item>
|
||||
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:windowBackground">@color/tpschedule_surface</item>
|
||||
<item name="android:statusBarColor">@color/tpschedule_surface</item>
|
||||
<item name="android:navigationBarColor">@color/tpschedule_surface</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="tpschedule_surface">#F3F7FF</color>
|
||||
<color name="tpschedule_blue">#38B3FF</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,48 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'tp_launch_page.dart';
|
||||
|
||||
/// Displays the brand during real initialization, without a minimum wait.
|
||||
class TpStartup extends StatelessWidget {
|
||||
const TpStartup({super.key, required this.ready, required this.child});
|
||||
final Future<void> ready;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FutureBuilder<void>(
|
||||
future: ready,
|
||||
builder: (context, snapshot) {
|
||||
final complete = snapshot.connectionState == ConnectionState.done;
|
||||
final reduceMotion =
|
||||
MediaQuery.maybeOf(context)?.disableAnimations ??
|
||||
WidgetsBinding
|
||||
.instance
|
||||
.platformDispatcher
|
||||
.accessibilityFeatures
|
||||
.disableAnimations;
|
||||
return AnimatedSwitcher(
|
||||
duration: reduceMotion
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 220),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: complete
|
||||
? AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
key: const ValueKey('tp-ready'),
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: const Color(0xFFF3F7FF),
|
||||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
: const MaterialApp(
|
||||
key: ValueKey('tp-launch'),
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: TpLaunchPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
/// The native launch screen remains until runApp submits the initialized app.
|
||||
/// Use normal bootstrap instead of a second splash or deferred warm-up frames.
|
||||
Future<void> launchApplication({
|
||||
required Future<void> ready,
|
||||
required Widget child,
|
||||
}) async {
|
||||
try {
|
||||
await ready;
|
||||
} catch (_) {
|
||||
// AppController exposes initialization failures in its recovery UI.
|
||||
// Preserve access to that UI instead of leaving the native launch visible.
|
||||
}
|
||||
runApp(
|
||||
AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: const Color(0xFFF3F7FF),
|
||||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,14 +34,12 @@ Future<void> main() async {
|
||||
AndroidCalendarService(MethodCalendarGateway()),
|
||||
DateTime.now,
|
||||
);
|
||||
runApp(
|
||||
TpStartup(
|
||||
ready: controller.initialize(),
|
||||
child: buildApplication(
|
||||
controller,
|
||||
notificationSelection: selection,
|
||||
school: SchoolSession(),
|
||||
),
|
||||
await launchApplication(
|
||||
ready: controller.initialize(),
|
||||
child: buildApplication(
|
||||
controller,
|
||||
notificationSelection: selection,
|
||||
school: SchoolSession(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: hepan_everyday
|
||||
description: TPSchedule 学生课表
|
||||
publish_to: none
|
||||
version: 0.5.0+8
|
||||
version: 0.5.1+9
|
||||
environment:
|
||||
sdk: ^3.13.5
|
||||
dependencies:
|
||||
|
||||
@@ -5,25 +5,62 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/app/tp_startup.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('waiting initialization does not replace the native launch', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
final ready = Completer<void>();
|
||||
var startupFinished = false;
|
||||
final startup = launchApplication(
|
||||
ready: ready.future,
|
||||
child: const MaterialApp(home: Text('已准备好课表')),
|
||||
).then((_) => startupFinished = true);
|
||||
await tester.pump();
|
||||
expect(startupFinished, isFalse);
|
||||
expect(tester.binding.sendFramesToEngine, isTrue);
|
||||
expect(find.text('TPSchedule'), findsNothing);
|
||||
expect(find.text('已准备好课表'), findsNothing);
|
||||
ready.complete();
|
||||
await startup;
|
||||
await tester.pump();
|
||||
expect(find.text('已准备好课表'), findsOneWidget);
|
||||
expect(find.text('TPSchedule'), findsNothing);
|
||||
expect(tester.binding.sendFramesToEngine, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('cached startup and resume never insert an extra launch frame', (
|
||||
tester,
|
||||
) async {
|
||||
final ready = Future<void>.value();
|
||||
await ready;
|
||||
await launchApplication(
|
||||
ready: ready,
|
||||
child: const MaterialApp(home: Text('保存的课表')),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('保存的课表'), findsOneWidget);
|
||||
expect(find.text('TPSchedule'), findsNothing);
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused);
|
||||
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
||||
await tester.pump();
|
||||
expect(find.text('保存的课表'), findsOneWidget);
|
||||
expect(find.text('TPSchedule'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'startup shows brand while initializing and immediately proceeds when ready',
|
||||
'initialization errors still show the recovery UI on first frame',
|
||||
(tester) async {
|
||||
final ready = Completer<void>();
|
||||
await tester.pumpWidget(
|
||||
TpStartup(
|
||||
ready: ready.future,
|
||||
child: const MaterialApp(home: Scaffold(body: Text('已准备好课表'))),
|
||||
),
|
||||
final startup = launchApplication(
|
||||
ready: ready.future,
|
||||
child: const MaterialApp(home: Text('恢复保存文件')),
|
||||
);
|
||||
ready.completeError(StateError('storage failed'));
|
||||
await startup;
|
||||
await tester.pump();
|
||||
expect(find.text('TPSchedule'), findsOneWidget);
|
||||
expect(find.text('已准备好课表'), findsNothing);
|
||||
ready.complete();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('已准备好课表'), findsOneWidget);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('TPSchedule'), findsNothing);
|
||||
expect(find.text('恢复保存文件'), findsOneWidget);
|
||||
expect(tester.binding.sendFramesToEngine, isTrue);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user