feat: improve UI of the initial loading screen (#309)

pull/316/head
Jorge Coca 3 years ago committed by GitHub
parent 5edfc2f17a
commit 58468bde2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
export 'cubit/assets_manager_cubit.dart';
export 'views/views.dart';

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template assets_loading_page}
/// Widget used to indicate the loading progress of the different assets used
/// in the game
/// {@endtemplate}
class AssetsLoadingPage extends StatelessWidget {
/// {@macro assets_loading_page}
const AssetsLoadingPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final headline1 = Theme.of(context).textTheme.headline1;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.ioPinball,
style: headline1!.copyWith(fontSize: 80),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
AnimatedEllipsisText(
l10n.loading,
style: headline1,
),
const SizedBox(height: 40),
FractionallySizedBox(
widthFactor: 0.8,
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
builder: (context, state) {
return PinballLoadingIndicator(value: state.progress);
},
),
),
],
),
);
}
}

@ -0,0 +1 @@
export 'assets_loading_page.dart';

@ -1,4 +1,3 @@
export 'assets_manager/cubit/assets_manager_cubit.dart';
export 'bloc/game_bloc.dart'; export 'bloc/game_bloc.dart';
export 'components/components.dart'; export 'components/components.dart';
export 'game_assets.dart'; export 'game_assets.dart';

@ -4,10 +4,12 @@ import 'package:flame/game.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_ui/pinball_ui.dart';
class PinballGamePage extends StatelessWidget { class PinballGamePage extends StatelessWidget {
const PinballGamePage({ const PinballGamePage({
@ -71,32 +73,13 @@ class PinballGameView extends StatelessWidget {
final isLoading = context.select( final isLoading = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress != 1, (AssetsManagerCubit bloc) => bloc.state.progress != 1,
); );
return Container(
return Scaffold( decoration: const CrtBackground(),
backgroundColor: Colors.blue, child: Scaffold(
backgroundColor: PinballColors.transparent,
body: isLoading body: isLoading
? const _PinballGameLoadingView() ? const AssetsLoadingPage()
: PinballGameLoadedView(game: game), : PinballGameLoadedView(game: game),
);
}
}
class _PinballGameLoadingView extends StatelessWidget {
const _PinballGameLoadingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final loadingProgress = context.select(
(AssetsManagerCubit bloc) => bloc.state.progress,
);
return Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: LinearProgressIndicator(
color: Colors.white,
value: loadingProgress,
),
), ),
); );
} }

@ -123,5 +123,13 @@
"footerGoogleIOText": "Google I/O", "footerGoogleIOText": "Google I/O",
"@footerGoogleIOText": { "@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O" "description": "Text shown on the footer which mentions Google I/O"
},
"loading": "Loading",
"@loading": {
"description": "Text shown to indicate loading times"
},
"ioPinball": "I/O Pinball",
"@ioPinball": {
"description": "I/O Pinball - Name of the game"
} }
} }

@ -8,4 +8,9 @@ abstract class PinballColors {
static const Color orange = Color(0xFFE5AB05); static const Color orange = Color(0xFFE5AB05);
static const Color blue = Color(0xFF4B94F6); static const Color blue = Color(0xFF4B94F6);
static const Color transparent = Color(0x00000000); static const Color transparent = Color(0x00000000);
static const Color loadingDarkRed = Color(0xFFE33B2D);
static const Color loadingLightRed = Color(0xFFEC5E2B);
static const Color loadingDarkBlue = Color(0xFF4087F8);
static const Color loadingLightBlue = Color(0xFF6CCAE4);
static const Color crtBackground = Color(0xFF274E54);
} }

@ -0,0 +1,61 @@
import 'dart:async';
import 'package:flutter/material.dart';
/// {@tempalte animated_ellipsis_text}
/// Every 500 milliseconds, it will add a new `.` at the end of the given
/// [text]. Once 3 `.` have been added (e.g. `Loading...`), it will reset to
/// zero ellipsis and start over again.
/// {@endtemplate}
class AnimatedEllipsisText extends StatefulWidget {
/// {@macro animated_ellipsis_text}
const AnimatedEllipsisText(
this.text, {
Key? key,
this.style,
}) : super(key: key);
/// The text that will be animated.
final String text;
/// Optional [TextStyle] of the given [text].
final TextStyle? style;
@override
State<StatefulWidget> createState() => _AnimatedEllipsisText();
}
class _AnimatedEllipsisText extends State<AnimatedEllipsisText>
with SingleTickerProviderStateMixin {
late final Timer timer;
var _numberOfEllipsis = 0;
@override
void initState() {
super.initState();
timer = Timer.periodic(const Duration(milliseconds: 500), (_) {
setState(() {
_numberOfEllipsis++;
_numberOfEllipsis = _numberOfEllipsis % 4;
});
});
}
@override
void dispose() {
if (timer.isActive) timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(
'${widget.text}${_numberOfEllipsis.toEllipsis()}',
style: widget.style,
);
}
}
extension on int {
String toEllipsis() => '.' * this;
}

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template crt_background}
/// [BoxDecoration] that provides a CRT-like background efffect.
/// {@endtemplate}
class CrtBackground extends BoxDecoration {
/// {@macro crt_background}
const CrtBackground()
: super(
gradient: const LinearGradient(
begin: Alignment(1, 0.015),
stops: [0.0, 0.5, 0.5, 1],
colors: [
PinballColors.darkBlue,
PinballColors.darkBlue,
PinballColors.crtBackground,
PinballColors.crtBackground,
],
tileMode: TileMode.repeated,
),
);
}

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template pinball_loading_indicator}
/// Pixel-art loading indicator
/// {@endtemplate}
class PinballLoadingIndicator extends StatelessWidget {
/// {@macro pinball_loading_indicator}
const PinballLoadingIndicator({
Key? key,
required this.value,
}) : assert(
value >= 0.0 && value <= 1.0,
'Progress must be between 0 and 1',
),
super(key: key);
/// Progress value
final double value;
@override
Widget build(BuildContext context) {
return Column(
children: [
_InnerIndicator(value: value, widthFactor: 0.95),
_InnerIndicator(value: value, widthFactor: 0.98),
_InnerIndicator(value: value),
_InnerIndicator(value: value),
_InnerIndicator(value: value, widthFactor: 0.98),
_InnerIndicator(value: value, widthFactor: 0.95)
],
);
}
}
class _InnerIndicator extends StatelessWidget {
const _InnerIndicator({
Key? key,
required this.value,
this.widthFactor = 1.0,
}) : super(key: key);
final double value;
final double widthFactor;
@override
Widget build(BuildContext context) {
return FractionallySizedBox(
widthFactor: widthFactor,
child: Column(
children: [
LinearProgressIndicator(
backgroundColor: PinballColors.loadingDarkBlue,
color: PinballColors.loadingDarkRed,
value: value,
),
LinearProgressIndicator(
backgroundColor: PinballColors.loadingLightBlue,
color: PinballColors.loadingLightRed,
value: value,
),
],
),
);
}
}

@ -1 +1,4 @@
export 'animated_ellipsis_text.dart';
export 'crt_background.dart';
export 'pinball_button.dart'; export 'pinball_button.dart';
export 'pinball_loading_indicator.dart';

@ -27,5 +27,25 @@ void main() {
test('transparent is 0x00000000', () { test('transparent is 0x00000000', () {
expect(PinballColors.transparent, const Color(0x00000000)); expect(PinballColors.transparent, const Color(0x00000000));
}); });
test('loadingDarkRed is 0xFFE33B2D', () {
expect(PinballColors.loadingDarkRed, const Color(0xFFE33B2D));
});
test('loadingLightRed is 0xFFEC5E2B', () {
expect(PinballColors.loadingLightRed, const Color(0xFFEC5E2B));
});
test('loadingDarkBlue is 0xFF4087F8', () {
expect(PinballColors.loadingDarkBlue, const Color(0xFF4087F8));
});
test('loadingLightBlue is 0xFF6CCAE4', () {
expect(PinballColors.loadingLightBlue, const Color(0xFF6CCAE4));
});
test('crtBackground is 0xFF274E54', () {
expect(PinballColors.crtBackground, const Color(0xFF274E54));
});
}); });
} }

@ -0,0 +1,30 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('AnimatedEllipsisText', () {
testWidgets(
'adds a new `.` every 500ms and '
'resets back to zero after adding 3', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AnimatedEllipsisText('test'),
),
),
);
expect(find.text('test'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test.'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test..'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test...'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 600));
expect(find.text('test'), findsOneWidget);
});
});
}

@ -0,0 +1,25 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('CrtBackground', () {
test('is a BoxDecoration with a LinearGradient', () {
// ignore: prefer_const_constructors
final crtBg = CrtBackground();
const expectedGradient = LinearGradient(
begin: Alignment(1, 0.015),
stops: [0.0, 0.5, 0.5, 1],
colors: [
PinballColors.darkBlue,
PinballColors.darkBlue,
PinballColors.crtBackground,
PinballColors.crtBackground,
],
tileMode: TileMode.repeated,
);
expect(crtBg, isA<BoxDecoration>());
expect(crtBg.gradient, expectedGradient);
});
});
}

@ -0,0 +1,45 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_ui/pinball_ui.dart';
void main() {
group('PinballLoadingIndicator', () {
group('assert value', () {
test('throws error if value <= 0.0', () {
expect(
() => PinballLoadingIndicator(value: -0.5),
throwsA(isA<AssertionError>()),
);
});
test('throws error if value >= 1.0', () {
expect(
() => PinballLoadingIndicator(value: 1.5),
throwsA(isA<AssertionError>()),
);
});
});
testWidgets(
'renders 12 LinearProgressIndicators and '
'6 FractionallySizedBox to indicate progress', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: PinballLoadingIndicator(value: 0.75),
),
),
);
expect(find.byType(FractionallySizedBox), findsNWidgets(6));
expect(find.byType(LinearProgressIndicator), findsNWidgets(12));
final progressIndicators = tester.widgetList<LinearProgressIndicator>(
find.byType(LinearProgressIndicator),
);
for (final i in progressIndicators) {
expect(i.value, 0.75);
}
});
});
}

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
void main() { void main() {
group('AssetsManagerCubit', () { group('AssetsManagerCubit', () {

@ -1,7 +1,7 @@
// ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/assets_manager/assets_manager.dart';
void main() { void main() {
group('AssetsManagerState', () { group('AssetsManagerState', () {

@ -0,0 +1,38 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball_ui/pinball_ui.dart';
import '../../helpers/helpers.dart';
class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}
void main() {
late AssetsManagerCubit assetsManagerCubit;
setUp(() {
final initialAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: const [],
);
assetsManagerCubit = _MockAssetsManagerCubit();
whenListen(
assetsManagerCubit,
Stream.value(initialAssetsState),
initialState: initialAssetsState,
);
});
group('AssetsLoadingPage', () {
testWidgets('renders an animated text and a pinball loading indicator',
(tester) async {
await tester.pumpApp(
const AssetsLoadingPage(),
assetsManagerCubit: assetsManagerCubit,
);
expect(find.byType(AnimatedEllipsisText), findsOneWidget);
expect(find.byType(PinballLoadingIndicator), findsOneWidget);
});
});
}

@ -5,6 +5,7 @@ import 'package:flame/game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart'; import 'package:pinball/start_game/start_game.dart';
@ -66,7 +67,6 @@ void main() {
Stream.value(initialAssetsState), Stream.value(initialAssetsState),
initialState: initialAssetsState, initialState: initialAssetsState,
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView( PinballGameView(
game: game, game: game,
@ -74,14 +74,7 @@ void main() {
assetsManagerCubit: assetsManagerCubit, assetsManagerCubit: assetsManagerCubit,
characterThemeCubit: characterThemeCubit, characterThemeCubit: characterThemeCubit,
); );
expect(find.byType(AssetsLoadingPage), findsOneWidget);
expect(
find.byWidgetPredicate(
(widget) =>
widget is LinearProgressIndicator && widget.value == 0.0,
),
findsOneWidget,
);
}, },
); );

@ -12,6 +12,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/select_character/select_character.dart';

Loading…
Cancel
Save