mirror of https://github.com/flutter/pinball.git
feat: improve UI of the initial loading screen (#309)
parent
5edfc2f17a
commit
58468bde2f
@ -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';
|
@ -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';
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue