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_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
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/assets_manager/assets_manager.dart';
|
||||
|
||||
void main() {
|
||||
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