feat: херо анимейшон фром реанимейшон
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:komet/frontend/widgets/attachment/photo_hero.dart';
|
||||
|
||||
const Rect _origin = Rect.fromLTWH(50, 500, 100, 100);
|
||||
|
||||
class _TestImageProvider extends ImageProvider<_TestImageProvider> {
|
||||
_TestImageProvider(this.image);
|
||||
|
||||
final ui.Image image;
|
||||
|
||||
@override
|
||||
Future<_TestImageProvider> obtainKey(ImageConfiguration configuration) =>
|
||||
SynchronousFuture<_TestImageProvider>(this);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
_TestImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) => OneFrameImageStreamCompleter(
|
||||
SynchronousFuture<ImageInfo>(ImageInfo(image: image.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _page() => Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PhotoHeroTarget(
|
||||
child: Center(
|
||||
child: Container(key: const ValueKey('target'), color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Finder get _flying => find.byType(Image);
|
||||
|
||||
double _flyingWidth(WidgetTester tester) => tester.getSize(_flying).width;
|
||||
|
||||
bool _targetHidden(WidgetTester tester) => tester.any(
|
||||
find.ancestor(
|
||||
of: find.byKey(const ValueKey('target')),
|
||||
matching: find.byType(Opacity),
|
||||
),
|
||||
);
|
||||
|
||||
double _pageOpacity(WidgetTester tester) => tester
|
||||
.widget<FadeTransition>(
|
||||
find
|
||||
.ancestor(
|
||||
of: find.byKey(const ValueKey('target')),
|
||||
matching: find.byType(FadeTransition),
|
||||
)
|
||||
.first,
|
||||
)
|
||||
.opacity
|
||||
.value;
|
||||
|
||||
void main() {
|
||||
late ui.Image image;
|
||||
|
||||
setUpAll(() async {
|
||||
image = await createTestImage(width: 4, height: 3);
|
||||
});
|
||||
|
||||
testWidgets('photo flies from the origin rect to the contained target', (
|
||||
tester,
|
||||
) async {
|
||||
final hero = PhotoHeroController(
|
||||
origin: () => _origin,
|
||||
image: _TestImageProvider(image),
|
||||
);
|
||||
late BuildContext context;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (ctx) {
|
||||
context = ctx;
|
||||
return const Scaffold();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(PhotoHeroRoute<void>(hero: hero, builder: (_) => _page()));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
|
||||
expect(_flying, findsOneWidget);
|
||||
expect(_targetHidden(tester), isTrue);
|
||||
final early = _flyingWidth(tester);
|
||||
expect(early, greaterThanOrEqualTo(133));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
final late_ = _flyingWidth(tester);
|
||||
expect(late_, greaterThan(early));
|
||||
expect(late_, lessThan(667));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(_flying, findsNothing);
|
||||
expect(_targetHidden(tester), isFalse);
|
||||
expect(tester.getSize(find.byKey(const ValueKey('target'))).height, 500);
|
||||
});
|
||||
|
||||
testWidgets('photo flies back to the origin rect on pop', (tester) async {
|
||||
final hero = PhotoHeroController(
|
||||
origin: () => _origin,
|
||||
image: _TestImageProvider(image),
|
||||
);
|
||||
late BuildContext context;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (ctx) {
|
||||
context = ctx;
|
||||
return const Scaffold();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final navigator = Navigator.of(context);
|
||||
navigator.push(PhotoHeroRoute<void>(hero: hero, builder: (_) => _page()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
navigator.pop();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(_flying, findsOneWidget);
|
||||
expect(_targetHidden(tester), isTrue);
|
||||
expect(_flyingWidth(tester), lessThan(667));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(_flying, findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('disabled hero fades the page instead of flying', (tester) async {
|
||||
final hero = PhotoHeroController(
|
||||
origin: () => _origin,
|
||||
image: _TestImageProvider(image),
|
||||
)..enabled = false;
|
||||
late BuildContext context;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (ctx) {
|
||||
context = ctx;
|
||||
return const Scaffold();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(PhotoHeroRoute<void>(hero: hero, builder: (_) => _page()));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(_flying, findsNothing);
|
||||
expect(_targetHidden(tester), isFalse);
|
||||
expect(_pageOpacity(tester), lessThan(1));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(_pageOpacity(tester), 1);
|
||||
});
|
||||
|
||||
testWidgets('target renders untouched without a hero scope', (tester) async {
|
||||
await tester.pumpWidget(MaterialApp(home: _page()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(Opacity), findsNothing);
|
||||
expect(tester.getSize(find.byKey(const ValueKey('target'))).height, 500);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:komet/frontend/widgets/profile_hero.dart';
|
||||
|
||||
const _headerStyle = TextStyle(fontSize: 17, fontWeight: FontWeight.w600);
|
||||
const _profileStyle = TextStyle(fontSize: 22, fontWeight: FontWeight.w700);
|
||||
|
||||
final _tag = UniqueKey();
|
||||
|
||||
Widget _header({Object? tag}) => Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
ProfileHeroAvatar(
|
||||
tag: tag,
|
||||
size: 44,
|
||||
child: Container(width: 44, height: 44, color: Colors.red),
|
||||
),
|
||||
ProfileHeroName(
|
||||
tag: tag,
|
||||
text: 'Ann',
|
||||
style: _headerStyle,
|
||||
child: const Text('Ann', style: _headerStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _profile({Object? tag, bool loaded = false}) => Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
ProfileHeroAvatar(
|
||||
tag: tag,
|
||||
size: 96,
|
||||
child: Container(width: 96, height: 96, color: Colors.red),
|
||||
),
|
||||
ProfileHeroName(
|
||||
tag: tag,
|
||||
text: 'Ann',
|
||||
style: _profileStyle,
|
||||
child: const Text('Ann', style: _profileStyle),
|
||||
),
|
||||
if (loaded) const Text('details'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Iterable<double> _fontSizes(WidgetTester tester) => tester
|
||||
.widgetList<Text>(find.byType(Text))
|
||||
.map(
|
||||
(t) =>
|
||||
t.style?.fontSize ??
|
||||
DefaultTextStyle.of(tester.element(find.byWidget(t))).style.fontSize,
|
||||
)
|
||||
.whereType<double>();
|
||||
|
||||
double _flyingAvatarWidth(WidgetTester tester) =>
|
||||
tester.getSize(find.byType(FittedBox).first).width;
|
||||
|
||||
TextStyle _flyingNameStyle(WidgetTester tester) {
|
||||
final transition = find.byType(DefaultTextStyleTransition);
|
||||
expect(transition, findsOneWidget);
|
||||
return DefaultTextStyle.of(
|
||||
tester.element(
|
||||
find.descendant(of: transition, matching: find.byType(Text)),
|
||||
),
|
||||
).style;
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('avatar and name fly between header and profile', (tester) async {
|
||||
final navigator = GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: navigator,
|
||||
home: _header(tag: _tag),
|
||||
),
|
||||
);
|
||||
|
||||
navigator.currentState!.push(
|
||||
MaterialPageRoute(builder: (_) => _profile(tag: _tag)),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
expect(_fontSizes(tester).any((s) => s > 17 && s < 22), isTrue);
|
||||
final pushWidth = _flyingAvatarWidth(tester);
|
||||
expect(pushWidth, greaterThan(44));
|
||||
expect(pushWidth, lessThan(96));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FittedBox), findsNothing);
|
||||
|
||||
navigator.currentState!.pop();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
expect(_fontSizes(tester).any((s) => s > 17 && s < 22), isTrue);
|
||||
final popWidth = _flyingAvatarWidth(tester);
|
||||
expect(popWidth, greaterThan(44));
|
||||
expect(popWidth, lessThan(96));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FittedBox), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'flying name inherits no decoration from the app fallback style',
|
||||
(tester) async {
|
||||
final navigator = GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: navigator,
|
||||
home: _header(tag: _tag),
|
||||
),
|
||||
);
|
||||
|
||||
navigator.currentState!.push(
|
||||
MaterialPageRoute(builder: (_) => _profile(tag: _tag)),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
final style = _flyingNameStyle(tester);
|
||||
expect(style.decoration, isNull);
|
||||
expect(style.fontFamily, isNull);
|
||||
expect(style.fontSize, greaterThan(17));
|
||||
expect(style.fontSize, lessThan(22));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('flight stays opaque when the destination finishes loading', (
|
||||
tester,
|
||||
) async {
|
||||
final navigator = GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: navigator,
|
||||
home: _header(tag: _tag),
|
||||
),
|
||||
);
|
||||
|
||||
var loaded = false;
|
||||
late StateSetter setProfileState;
|
||||
navigator.currentState!.push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StatefulBuilder(
|
||||
builder: (_, setState) {
|
||||
setProfileState = setState;
|
||||
return _profile(tag: _tag, loaded: loaded);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
setProfileState(() => loaded = true);
|
||||
|
||||
var minOpacity = 1.0;
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
final flying = find.byType(FittedBox);
|
||||
if (flying.evaluate().isEmpty) break;
|
||||
final fade = tester.widget<FadeTransition>(
|
||||
find.ancestor(of: flying, matching: find.byType(FadeTransition)),
|
||||
);
|
||||
minOpacity = fade.opacity.value < minOpacity
|
||||
? fade.opacity.value
|
||||
: minOpacity;
|
||||
}
|
||||
expect(minOpacity, 1.0);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FittedBox), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a null tag opts out of the flight entirely', (tester) async {
|
||||
final navigator = GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
navigatorKey: navigator,
|
||||
home: _header(tag: _tag),
|
||||
),
|
||||
);
|
||||
|
||||
navigator.currentState!.push(
|
||||
MaterialPageRoute(builder: (_) => _profile(tag: null)),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
expect(find.byType(FittedBox), findsNothing);
|
||||
expect(find.byType(DefaultTextStyleTransition), findsNothing);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user