mirror of
https://github.com/flutter/holobooth.git
synced 2025-08-06 14:50:05 +08:00
feat: use composited image in share modal (#566)
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -9,7 +9,7 @@
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["-d", "chrome"]
|
||||
"args": ["-d", "chrome", "--web-renderer", "html"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@ -1,68 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:io_photobooth/photobooth/photobooth.dart';
|
||||
|
||||
const _mobileMargin = EdgeInsets.all(15);
|
||||
const _mobilePadding = EdgeInsets.only(
|
||||
bottom: 30,
|
||||
left: 19,
|
||||
right: 10,
|
||||
top: 10,
|
||||
);
|
||||
|
||||
const _desktopMargin = EdgeInsets.all(20);
|
||||
const _desktopPadding = EdgeInsets.only(
|
||||
bottom: 30,
|
||||
left: 39,
|
||||
right: 19,
|
||||
top: 5,
|
||||
);
|
||||
|
||||
/// A widget that displays [CharactersLayer] and [StickersLayer] on top of
|
||||
/// the raw [image] took from the camera.
|
||||
///
|
||||
/// The [FramedPhotoboothPhoto] widget is styled to mimic a framed card photo.
|
||||
class FramedPhotoboothPhoto extends StatelessWidget {
|
||||
const FramedPhotoboothPhoto({
|
||||
Key? key,
|
||||
required this.image,
|
||||
required this.aspectRatio,
|
||||
this.isTilted = true,
|
||||
}) : super(key: key);
|
||||
|
||||
final double aspectRatio;
|
||||
final String image;
|
||||
final bool isTilted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = aspectRatio < 1;
|
||||
var photo = Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: Container(
|
||||
margin: isMobile ? _mobileMargin : _desktopMargin,
|
||||
padding: isMobile ? _mobilePadding : _desktopPadding,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: AssetImage(
|
||||
isMobile
|
||||
? 'assets/images/photo_frame_mobile.png'
|
||||
: 'assets/images/photo_frame.png',
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PhotoboothPhoto(image: image),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!isTilted) return photo;
|
||||
return Transform(
|
||||
alignment: const Alignment(0, -3 / 4),
|
||||
transform: Matrix4.identity()..rotateZ(-5 * (math.pi / 180)),
|
||||
child: photo,
|
||||
);
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ export 'animated_characters/animated_characters.dart';
|
||||
export 'character_icon_button.dart';
|
||||
export 'characters_caption.dart';
|
||||
export 'characters_layer.dart';
|
||||
export 'framed_photobooth_photo.dart';
|
||||
export 'photobooth_background.dart';
|
||||
export 'photobooth_error.dart';
|
||||
export 'photobooth_photo.dart';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:io_photobooth/l10n/l10n.dart';
|
||||
import 'package:io_photobooth/photobooth/photobooth.dart';
|
||||
import 'package:io_photobooth/share/share.dart';
|
||||
import 'package:photobooth_ui/photobooth_ui.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ class ShareBottomSheet extends StatelessWidget {
|
||||
required this.image,
|
||||
}) : super(key: key);
|
||||
|
||||
final CameraImage image;
|
||||
final Uint8List image;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -36,14 +36,8 @@ class ShareBottomSheet extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25.0),
|
||||
child: FramedPhotoboothPhoto(
|
||||
aspectRatio: PhotoboothAspectRatio.portrait,
|
||||
image: image.data,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
SharePreviewPhoto(image: image),
|
||||
const SizedBox(height: 60),
|
||||
SelectableText(
|
||||
l10n.shareDialogHeading,
|
||||
|
@ -1,19 +1,14 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:io_photobooth/l10n/l10n.dart';
|
||||
import 'package:io_photobooth/photobooth/photobooth.dart';
|
||||
import 'package:io_photobooth/share/share.dart';
|
||||
import 'package:photobooth_ui/photobooth_ui.dart';
|
||||
|
||||
class ShareDialog extends StatelessWidget {
|
||||
const ShareDialog({
|
||||
Key? key,
|
||||
required this.aspectRatio,
|
||||
required this.image,
|
||||
}) : super(key: key);
|
||||
const ShareDialog({Key? key, required this.image}) : super(key: key);
|
||||
|
||||
final double aspectRatio;
|
||||
final CameraImage image;
|
||||
final Uint8List image;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -44,14 +39,7 @@ class ShareDialog extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 430,
|
||||
width: 600,
|
||||
child: FramedPhotoboothPhoto(
|
||||
aspectRatio: aspectRatio,
|
||||
image: image.data,
|
||||
),
|
||||
),
|
||||
SharePreviewPhoto(image: image),
|
||||
const SizedBox(height: 60),
|
||||
SelectableText(
|
||||
l10n.shareDialogHeading,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -18,6 +19,9 @@ class ShareBody extends StatelessWidget {
|
||||
final compositeStatus = context.select(
|
||||
(ShareBloc bloc) => bloc.state.compositeStatus,
|
||||
);
|
||||
final compositedImage = context.select(
|
||||
(ShareBloc bloc) => bloc.state.bytes,
|
||||
);
|
||||
final isUploadSuccess = context.select(
|
||||
(ShareBloc bloc) => bloc.state.uploadStatus.isSuccess,
|
||||
);
|
||||
@ -55,12 +59,14 @@ class ShareBody extends StatelessWidget {
|
||||
),
|
||||
child: ShareCopyableLink(link: shareUrl),
|
||||
),
|
||||
if (image != null && file != null)
|
||||
if (compositedImage != null && file != null)
|
||||
ResponsiveLayoutBuilder(
|
||||
small: (_, __) =>
|
||||
MobileButtonsLayout(image: image, file: file),
|
||||
small: (_, __) => MobileButtonsLayout(
|
||||
image: compositedImage,
|
||||
file: file,
|
||||
),
|
||||
large: (_, __) => DesktopButtonsLayout(
|
||||
image: image,
|
||||
image: compositedImage,
|
||||
file: file,
|
||||
),
|
||||
),
|
||||
@ -87,7 +93,7 @@ class DesktopButtonsLayout extends StatelessWidget {
|
||||
required this.file,
|
||||
}) : super(key: key);
|
||||
|
||||
final CameraImage image;
|
||||
final Uint8List image;
|
||||
final XFile file;
|
||||
|
||||
@override
|
||||
@ -113,7 +119,7 @@ class MobileButtonsLayout extends StatelessWidget {
|
||||
required this.file,
|
||||
}) : super(key: key);
|
||||
|
||||
final CameraImage image;
|
||||
final Uint8List image;
|
||||
final XFile file;
|
||||
|
||||
@override
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:io_photobooth/l10n/l10n.dart';
|
||||
@ -16,8 +17,8 @@ class ShareButton extends StatelessWidget {
|
||||
}) : platformHelper = platformHelper ?? PlatformHelper(),
|
||||
super(key: key);
|
||||
|
||||
/// Raw image from camera
|
||||
final CameraImage image;
|
||||
/// Composited image
|
||||
final Uint8List image;
|
||||
|
||||
/// Optional [PlatformHelper] instance.
|
||||
final PlatformHelper platformHelper;
|
||||
@ -36,10 +37,7 @@ class ShareButton extends StatelessWidget {
|
||||
BlocProvider.value(value: context.read<PhotoboothBloc>()),
|
||||
BlocProvider.value(value: context.read<ShareBloc>()),
|
||||
],
|
||||
child: ShareDialog(
|
||||
aspectRatio: PhotoboothAspectRatio.landscape,
|
||||
image: image,
|
||||
),
|
||||
child: ShareDialog(image: image),
|
||||
),
|
||||
portraitChild: MultiBlocProvider(
|
||||
providers: [
|
||||
|
36
lib/share/widgets/share_preview_photo.dart
Normal file
36
lib/share/widgets/share_preview_photo.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photobooth_ui/photobooth_ui.dart';
|
||||
|
||||
class SharePreviewPhoto extends StatelessWidget {
|
||||
const SharePreviewPhoto({Key? key, required this.image}) : super(key: key);
|
||||
|
||||
final Uint8List image;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.rotate(
|
||||
angle: -5 * (math.pi / 180),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 400),
|
||||
decoration: const BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: PhotoboothColors.black54,
|
||||
blurRadius: 7,
|
||||
offset: Offset(-3, 9),
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Image.memory(
|
||||
image,
|
||||
isAntiAlias: true,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ export 'share_button.dart';
|
||||
export 'share_caption.dart';
|
||||
export 'share_copyable_link.dart';
|
||||
export 'share_heading.dart';
|
||||
export 'share_preview_photo.dart';
|
||||
export 'share_progress_overlay.dart';
|
||||
export 'share_social_media_clarification.dart';
|
||||
export 'share_state_listener.dart';
|
||||
|
@ -1,66 +0,0 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:io_photobooth/photobooth/photobooth.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:photobooth_ui/photobooth_ui.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
class FakePhotoboothEvent extends Fake implements PhotoboothEvent {}
|
||||
|
||||
class FakePhotoboothState extends Fake implements PhotoboothState {}
|
||||
|
||||
void main() {
|
||||
const width = 1;
|
||||
const height = 1;
|
||||
const data = '';
|
||||
const image = CameraImage(width: width, height: height, data: data);
|
||||
const aspectRatio = PhotoboothAspectRatio.landscape;
|
||||
|
||||
late PhotoboothBloc photoboothBloc;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue<PhotoboothEvent>(FakePhotoboothEvent());
|
||||
registerFallbackValue<PhotoboothState>(FakePhotoboothState());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
photoboothBloc = MockPhotoboothBloc();
|
||||
when(() => photoboothBloc.state).thenReturn(PhotoboothState(image: image));
|
||||
});
|
||||
|
||||
group('FramedPhotoboothPhoto', () {
|
||||
testWidgets('displays PhotoboothPhoto', (tester) async {
|
||||
await tester.pumpApp(
|
||||
FramedPhotoboothPhoto(aspectRatio: aspectRatio, image: data),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byType(PhotoboothPhoto), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('transform image by default', (tester) async {
|
||||
await tester.pumpApp(
|
||||
FramedPhotoboothPhoto(aspectRatio: aspectRatio, image: data),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byType(Transform), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'does not transform image '
|
||||
'when isTilted is false', (tester) async {
|
||||
await tester.pumpApp(
|
||||
FramedPhotoboothPhoto(
|
||||
aspectRatio: aspectRatio,
|
||||
image: data,
|
||||
isTilted: false,
|
||||
),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byType(Transform), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -17,6 +19,7 @@ void main() {
|
||||
const height = 1;
|
||||
const data = '';
|
||||
const image = CameraImage(width: width, height: height, data: data);
|
||||
final bytes = Uint8List.fromList(transparentImage);
|
||||
|
||||
late PhotoboothBloc photoboothBloc;
|
||||
|
||||
@ -33,7 +36,7 @@ void main() {
|
||||
group('ShareBottomSheet', () {
|
||||
testWidgets('displays heading', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Scaffold(body: ShareBottomSheet(image: image)),
|
||||
Scaffold(body: ShareBottomSheet(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byKey(Key('shareBottomSheet_heading')), findsOneWidget);
|
||||
@ -41,7 +44,7 @@ void main() {
|
||||
|
||||
testWidgets('displays subheading', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Scaffold(body: ShareBottomSheet(image: image)),
|
||||
Scaffold(body: ShareBottomSheet(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byKey(Key('shareBottomSheet_subheading')), findsOneWidget);
|
||||
@ -49,7 +52,7 @@ void main() {
|
||||
|
||||
testWidgets('displays a TwitterButton', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Scaffold(body: ShareBottomSheet(image: image)),
|
||||
Scaffold(body: ShareBottomSheet(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byType(TwitterButton), findsOneWidget);
|
||||
@ -57,7 +60,7 @@ void main() {
|
||||
|
||||
testWidgets('displays a FacebookButton', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Scaffold(body: ShareBottomSheet(image: image)),
|
||||
Scaffold(body: ShareBottomSheet(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byType(FacebookButton), findsOneWidget);
|
||||
@ -65,7 +68,7 @@ void main() {
|
||||
|
||||
testWidgets('taps on close will dismiss the popup', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Scaffold(body: ShareBottomSheet(image: image)),
|
||||
Scaffold(body: ShareBottomSheet(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
await tester.tap(find.byIcon(Icons.clear));
|
||||
|
@ -1,11 +1,12 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:io_photobooth/photobooth/photobooth.dart';
|
||||
import 'package:io_photobooth/share/share.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:photobooth_ui/photobooth_ui.dart';
|
||||
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
@ -18,7 +19,7 @@ void main() {
|
||||
const height = 1;
|
||||
const data = '';
|
||||
const image = CameraImage(width: width, height: height, data: data);
|
||||
const aspectRatio = PhotoboothAspectRatio.landscape;
|
||||
final bytes = Uint8List.fromList(transparentImage);
|
||||
|
||||
late PhotoboothBloc photoboothBloc;
|
||||
|
||||
@ -35,7 +36,7 @@ void main() {
|
||||
group('ShareDialog', () {
|
||||
testWidgets('displays heading', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
|
||||
Material(child: ShareDialog(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byKey(Key('shareDialog_heading')), findsOneWidget);
|
||||
@ -43,7 +44,7 @@ void main() {
|
||||
|
||||
testWidgets('displays subheading', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
|
||||
Material(child: ShareDialog(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byKey(Key('shareDialog_subheading')), findsOneWidget);
|
||||
@ -51,7 +52,7 @@ void main() {
|
||||
|
||||
testWidgets('displays a TwitterButton', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
|
||||
Material(child: ShareDialog(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byType(TwitterButton), findsOneWidget);
|
||||
@ -59,7 +60,7 @@ void main() {
|
||||
|
||||
testWidgets('displays a FacebookButton', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
|
||||
Material(child: ShareDialog(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
expect(find.byType(FacebookButton), findsOneWidget);
|
||||
@ -67,7 +68,7 @@ void main() {
|
||||
|
||||
testWidgets('taps on close will dismiss the popup', (tester) async {
|
||||
await tester.pumpApp(
|
||||
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
|
||||
Material(child: ShareDialog(image: bytes)),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
await tester.tap(find.byIcon(Icons.clear));
|
||||
|
@ -146,6 +146,7 @@ void main() {
|
||||
setUp(() {
|
||||
when(() => shareBloc.state).thenReturn(ShareState(
|
||||
compositeStatus: ShareStatus.success,
|
||||
bytes: Uint8List(0),
|
||||
file: file,
|
||||
));
|
||||
});
|
||||
@ -328,10 +329,6 @@ void main() {
|
||||
headers: const {},
|
||||
),
|
||||
).thenAnswer((_) async => true);
|
||||
when(() => shareBloc.state).thenReturn(ShareState(
|
||||
compositeStatus: ShareStatus.success,
|
||||
file: file,
|
||||
));
|
||||
tester.setDisplaySize(Size(2500, 2500));
|
||||
await tester.pumpApp(
|
||||
ShareView(),
|
||||
|
@ -1,4 +1,6 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -23,6 +25,7 @@ void main() {
|
||||
const height = 1;
|
||||
const data = '';
|
||||
const image = CameraImage(width: width, height: height, data: data);
|
||||
final bytes = Uint8List.fromList(transparentImage);
|
||||
|
||||
late PhotoboothBloc photoboothBloc;
|
||||
late PlatformHelper platformHelper;
|
||||
@ -45,10 +48,7 @@ void main() {
|
||||
when(() => platformHelper.isMobile).thenReturn(true);
|
||||
|
||||
await tester.pumpApp(
|
||||
ShareButton(
|
||||
image: image,
|
||||
platformHelper: platformHelper,
|
||||
),
|
||||
ShareButton(image: bytes),
|
||||
photoboothBloc: photoboothBloc,
|
||||
);
|
||||
|
||||
@ -66,7 +66,7 @@ void main() {
|
||||
|
||||
await tester.pumpApp(
|
||||
ShareButton(
|
||||
image: image,
|
||||
image: bytes,
|
||||
platformHelper: platformHelper,
|
||||
),
|
||||
photoboothBloc: photoboothBloc,
|
||||
@ -85,7 +85,7 @@ void main() {
|
||||
tester.setLandscapeDisplaySize();
|
||||
await tester.pumpApp(
|
||||
ShareButton(
|
||||
image: image,
|
||||
image: bytes,
|
||||
platformHelper: platformHelper,
|
||||
),
|
||||
photoboothBloc: photoboothBloc,
|
||||
|
Reference in New Issue
Block a user