diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 3be91ba..a30acc7 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -37,6 +37,11 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State with WidgetsBindingObserver { + static const _androidSplashAsset = 'assets/icon/splash_logo_android12.png'; + static const _defaultSplashAsset = 'assets/icon/splash_logo.png'; + static const _androidSplashSize = 288.0; + static const _defaultSplashSize = 384.0; + bool _resumeRetryInFlight = false; @override @@ -100,15 +105,7 @@ class _HomePageState extends State with WidgetsBindingObserver { AppLocalizations l10n, ) { if (catalog.loading) { - return AnnotatedRegion( - value: SystemUiOverlayStyle.light, - child: const Scaffold( - backgroundColor: AppColors.splashInk, - body: Center( - child: CircularProgressIndicator(color: AppColors.blue), - ), - ), - ); + return catalog.settled ? _retryLoading() : _startupSplash(); } if (catalog.error != null) { return Scaffold( @@ -140,6 +137,36 @@ class _HomePageState extends State with WidgetsBindingObserver { onRefreshFeed: catalog.refresh, ); } + + Widget _startupSplash() { + final isAndroid = defaultTargetPlatform == TargetPlatform.android; + final size = isAndroid ? _androidSplashSize : _defaultSplashSize; + return AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: ColoredBox( + color: AppColors.splashInk, + child: Center( + child: Image.asset( + isAndroid ? _androidSplashAsset : _defaultSplashAsset, + key: const Key('startup-splash'), + width: size, + height: size, + fit: BoxFit.contain, + ), + ), + ), + ); + } + + Widget _retryLoading() { + return AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: const Scaffold( + backgroundColor: AppColors.splashInk, + body: Center(child: CircularProgressIndicator(color: AppColors.blue)), + ), + ); + } } /// 首页目录加载失败与重试入口。 diff --git a/pubspec.yaml b/pubspec.yaml index 902ace3..09d3088 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,6 +80,9 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + assets: + - assets/icon/splash_logo.png + - assets/icon/splash_logo_android12.png # To add assets to your application, add an assets section, like this: # assets: diff --git a/test/home/home_startup_splash_test.dart b/test/home/home_startup_splash_test.dart new file mode 100644 index 0000000..9e37466 --- /dev/null +++ b/test/home/home_startup_splash_test.dart @@ -0,0 +1,182 @@ +// Author: fruit.com.cn + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; + +import 'package:fruitshorts/app.dart'; +import 'package:fruitshorts/core/config/app_config.dart'; +import 'package:fruitshorts/core/data/models/drama.dart'; +import 'package:fruitshorts/core/data/models/home_feed.dart'; +import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; +import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; +import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; +import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; +import 'package:fruitshorts/core/data/services/catalog_controller.dart'; +import 'package:fruitshorts/core/theme/app_theme.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/home/home_view.dart'; + +import '../support/fake_catalog.dart'; +import '../support/fake_category.dart'; +import '../support/fake_haptics.dart'; +import '../support/fake_interaction_repositories.dart'; +import '../support/fake_notice_repository.dart'; +import '../support/widget_player.dart'; + +void main() { + setUp(() { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + AppConfig.clearRuntimeBaseUrl(); + AppConfig.ready = Future.value(); + }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + AppConfig.clearRuntimeBaseUrl(); + AppConfig.ready = Future.value(); + }); + + testWidgets('should_show_platform_startup_image_before_catalog_settles', ( + tester, + ) async { + final controller = CatalogController(_EmptyCatalogRepository()) + ..loading = true + ..settled = false; + addTearDown(controller.dispose); + + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + await _pumpHomePage(tester, controller); + + final image = tester.widget( + find.byKey(const Key('startup-splash')), + ); + final expectedAsset = platform == TargetPlatform.android + ? 'assets/icon/splash_logo_android12.png' + : 'assets/icon/splash_logo.png'; + final expectedSize = platform == TargetPlatform.android ? 288.0 : 384.0; + + expect( + image.image, + isA().having( + (asset) => asset.assetName, + 'assetName', + expectedAsset, + ), + ); + expect( + tester.getSize(find.byKey(const Key('startup-splash'))), + Size.square(expectedSize), + ); + expect(find.byType(CircularProgressIndicator), findsNothing); + } + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('should_show_spinner_when_retry_loads_after_catalog_settled', ( + tester, + ) async { + final controller = CatalogController(_EmptyCatalogRepository()) + ..loading = true + ..settled = true; + addTearDown(controller.dispose); + + await _pumpHomePage(tester, controller); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.byKey(const Key('startup-splash')), findsNothing); + }); + + testWidgets('should_enter_home_and_remove_startup_image_when_data_is_ready', ( + tester, + ) async { + AppConfig.ready = Future.value(); + final catalog = _PendingCatalogRepository(); + await tester.pumpWidget( + LumaDramaApp( + playerFactory: createWidgetPlayer, + catalogRepository: catalog, + categoryRepository: FakeCategoryRepository(catalog: catalog), + watchHistoryRepository: InMemoryWatchHistoryRepository(), + favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), + playbackRepository: FakePlaybackRepository(), + commentRepository: FakeCommentRepository(), + interactionRepository: FakeInteractionRepository(), + noticeRepository: FakeNoticeRepository(), + haptics: FakeHaptics(), + ), + ); + await tester.pump(); + + expect(find.byKey(const Key('startup-splash')), findsOneWidget); + expect(find.byType(CircularProgressIndicator), findsNothing); + + catalog.complete(_feed); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('startup-splash')), findsNothing); + expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + }); +} + +Future _pumpHomePage( + WidgetTester tester, + CatalogController controller, +) async { + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: controller, + child: MaterialApp( + theme: AppTheme.light(), + locale: const Locale('en'), + supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: const HomePage(), + ), + ), + ); + await tester.pump(); +} + +final _feed = HomeFeed( + sections: [ + FeedSection(code: 'hot', title: 'Hot', dramas: [demoDramas.first]), + ], +); + +class _EmptyCatalogRepository implements CatalogRepository { + @override + Future fetchHomeFeed() async => const HomeFeed(); + + @override + Future fetchDiscoverFeed() async => const HomeFeed(); + + @override + Future fetchDrama(String dramaId) async => _feed.dramas.first; +} + +class _PendingCatalogRepository implements CatalogRepository { + final Completer _home = Completer(); + + List get dramas => _feed.dramas; + + void complete(HomeFeed feed) { + if (!_home.isCompleted) _home.complete(feed); + } + + @override + Future fetchHomeFeed() => _home.future; + + @override + Future fetchDiscoverFeed() async => _feed; + + @override + Future fetchDrama(String dramaId) async => _feed.dramas.first; +}