fix: refine image selection flow

This commit is contained in:
呈祥
2026-05-18 14:30:24 +08:00
parent d74fc5ab44
commit 51bbca013b
3 changed files with 206 additions and 183 deletions
+56 -42
View File
@@ -1,7 +1,6 @@
import Flutter import Flutter
import ImageIO import ImageIO
import Photos import Photos
import PhotosUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
import UIKit import UIKit
@@ -28,7 +27,7 @@ class SceneDelegate: FlutterSceneDelegate {
} }
} }
private final class NativeImageBridge: NSObject, UIDocumentPickerDelegate { private final class NativeImageBridge: NSObject, UIDocumentPickerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
private enum PendingOperation { private enum PendingOperation {
case pickFile(FlutterResult) case pickFile(FlutterResult)
case pickAlbum(FlutterResult) case pickAlbum(FlutterResult)
@@ -130,19 +129,22 @@ private final class NativeImageBridge: NSObject, UIDocumentPickerDelegate {
result(FlutterError(code: "busy", message: "正在选择图片", details: nil)) result(FlutterError(code: "busy", message: "正在选择图片", details: nil))
return return
} }
guard let controller = controller else {
guard #available(iOS 14.0, *) else { result(FlutterError(code: "picker_unavailable", message: "无法打开相册", details: nil))
pickImages(result: result) return
}
guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else {
result(FlutterError(code: "picker_unavailable", message: "相册不可用", details: nil))
return return
} }
var configuration = PHPickerConfiguration(photoLibrary: .shared()) let picker = UIImagePickerController()
configuration.filter = .images picker.sourceType = .photoLibrary
configuration.selectionLimit = 1 picker.mediaTypes = ["public.image"]
let picker = PHPickerViewController(configuration: configuration) picker.allowsEditing = false
picker.delegate = self picker.delegate = self
pendingOperation = .pickAlbum(result) pendingOperation = .pickAlbum(result)
controller?.present(picker, animated: true) controller.present(picker, animated: true)
} }
private func compressImage(arguments: Any?, result: @escaping FlutterResult) { private func compressImage(arguments: Any?, result: @escaping FlutterResult) {
@@ -372,8 +374,10 @@ private final class NativeImageBridge: NSObject, UIDocumentPickerDelegate {
} }
} }
@available(iOS 14.0, *) func imagePickerController(
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { _ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
picker.dismiss(animated: true) picker.dismiss(animated: true)
guard let operation = pendingOperation else { guard let operation = pendingOperation else {
@@ -384,40 +388,33 @@ private final class NativeImageBridge: NSObject, UIDocumentPickerDelegate {
guard case .pickAlbum(let result) = operation else { guard case .pickAlbum(let result) = operation else {
return return
} }
guard let provider = results.first?.itemProvider else {
result([])
return
}
provider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { [weak self] url, error in
guard let self = self else {
return
}
if let error = error {
DispatchQueue.main.async {
result(FlutterError(code: "gallery_pick_failed", message: error.localizedDescription, details: nil))
}
return
}
guard let url = url else {
DispatchQueue.main.async {
result([])
}
return
}
do { do {
let payload = try self.buildGalleryImage(sourceURL: url) if let url = info[.imageURL] as? URL {
DispatchQueue.main.async { result([try buildGalleryImage(sourceURL: url)])
result([payload]) return
} }
if let image = info[.originalImage] as? UIImage {
result([try buildGalleryImage(image: image)])
return
}
result([])
} catch { } catch {
DispatchQueue.main.async {
result(FlutterError(code: "gallery_pick_failed", message: error.localizedDescription, details: nil)) result(FlutterError(code: "gallery_pick_failed", message: error.localizedDescription, details: nil))
} }
} }
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
guard let operation = pendingOperation else {
return
}
pendingOperation = nil
switch operation {
case .pickFile(let result), .pickAlbum(let result):
result([])
} }
} }
@@ -461,6 +458,26 @@ private final class NativeImageBridge: NSObject, UIDocumentPickerDelegate {
return try buildCachedReadOnlyImage(sourceURL: sourceURL, child: "gallery-originals") return try buildCachedReadOnlyImage(sourceURL: sourceURL, child: "gallery-originals")
} }
private func buildGalleryImage(image: UIImage) throws -> [String: Any] {
let hasAlpha = Self.hasAlpha(image)
let displayName = hasAlpha ? "album-image.png" : "album-image.jpg"
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(
"gallery-picker",
isDirectory: true
)
try FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true
)
let targetURL = directory.appendingPathComponent("\(UUID().uuidString)-\(displayName)")
let data = hasAlpha ? image.pngData() : image.jpegData(compressionQuality: 1)
guard let data else {
throw NativeImageError.message("无法读取相册图片")
}
try data.write(to: targetURL, options: .atomic)
return try buildGalleryImage(sourceURL: targetURL)
}
private func buildCachedReadOnlyImage(sourceURL: URL, child: String) throws -> [String: Any] { private func buildCachedReadOnlyImage(sourceURL: URL, child: String) throws -> [String: Any] {
let scoped = sourceURL.startAccessingSecurityScopedResource() let scoped = sourceURL.startAccessingSecurityScopedResource()
defer { defer {
@@ -710,9 +727,6 @@ private final class NativeImageBridge: NSObject, UIDocumentPickerDelegate {
} }
} }
@available(iOS 14.0, *)
extension NativeImageBridge: PHPickerViewControllerDelegate {}
private struct PixelSize { private struct PixelSize {
let width: Int let width: Int
let height: Int let height: Int
+136 -99
View File
@@ -99,9 +99,9 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
); );
ImageJob? _job; ImageJob? _job;
ImagePickSource? _currentPickSource;
var _previewMode = PreviewMode.original; var _previewMode = PreviewMode.original;
var _compareFraction = 0.5; var _compareFraction = 0.5;
var _comparePreviewScale = 1.0;
var _isPicking = false; var _isPicking = false;
OverlayEntry? _toastEntry; OverlayEntry? _toastEntry;
Timer? _toastTimer; Timer? _toastTimer;
@@ -162,16 +162,20 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
); );
} }
Future<void> _startImageJob(NativeImage image, {String? message}) async { Future<void> _startImageJob(
NativeImage image, {
String? message,
ImagePickSource? source,
}) async {
if (!mounted) { if (!mounted) {
return; return;
} }
setState(() { setState(() {
_job = ImageJob(image); _job = ImageJob(image);
_currentPickSource = source;
_previewMode = PreviewMode.original; _previewMode = PreviewMode.original;
_compareFraction = 0.5; _compareFraction = 0.5;
_comparePreviewScale = 1.0;
}); });
if (message != null) { if (message != null) {
_showMessage(message); _showMessage(message);
@@ -180,16 +184,35 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
} }
Future<void> _pickFiles() async { Future<void> _pickFiles() async {
await _pickNativeImage(method: 'pickImages', errorMessage: '选择文件失败'); await _pickNativeImage(
method: 'pickImages',
errorMessage: '选择文件失败',
source: ImagePickSource.file,
);
} }
Future<void> _pickAlbumImage() async { Future<void> _pickAlbumImage() async {
await _pickNativeImage(method: 'pickAlbumImage', errorMessage: '选择相册图片失败'); await _pickNativeImage(
method: 'pickAlbumImage',
errorMessage: '选择相册图片失败',
source: ImagePickSource.album,
);
}
Future<void> _repickCurrentSource() async {
switch (_currentPickSource) {
case ImagePickSource.album:
await _pickAlbumImage();
case ImagePickSource.file:
case null:
await _pickFiles();
}
} }
Future<void> _pickNativeImage({ Future<void> _pickNativeImage({
required String method, required String method,
required String errorMessage, required String errorMessage,
required ImagePickSource source,
}) async { }) async {
if (_isPicking) { if (_isPicking) {
return; return;
@@ -222,7 +245,7 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
} }
if (picked != null && mounted) { if (picked != null && mounted) {
await _startImageJob(picked); await _startImageJob(picked, source: source);
} }
} }
@@ -265,7 +288,6 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
..isCompressing = false ..isCompressing = false
..overwritten = false; ..overwritten = false;
_previewMode = PreviewMode.compare; _previewMode = PreviewMode.compare;
_comparePreviewScale = 1.0;
}); });
_showMessage( _showMessage(
compressed.passthrough compressed.passthrough
@@ -297,7 +319,7 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
'compressedPath': compressed.path, 'compressedPath': compressed.path,
'suggestedName': _suggestedName(job.original.displayName), 'suggestedName': _suggestedName(job.original.displayName),
}); });
_showMessage('已保存到相册'); _showMessage('已保存到系统相册');
} on PlatformException catch (error) { } on PlatformException catch (error) {
_showMessage(error.message ?? '保存失败'); _showMessage(error.message ?? '保存失败');
} }
@@ -406,18 +428,9 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
void _clearImage() { void _clearImage() {
setState(() { setState(() {
_job = null; _job = null;
_currentPickSource = null;
_previewMode = PreviewMode.original; _previewMode = PreviewMode.original;
_compareFraction = 0.5; _compareFraction = 0.5;
_comparePreviewScale = 1.0;
});
}
void _handleCompareScaleChanged(double scale) {
if (!mounted || (_comparePreviewScale - scale).abs() < 0.01) {
return;
}
setState(() {
_comparePreviewScale = scale;
}); });
} }
@@ -475,7 +488,7 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
}, },
); );
overlay.insert(_toastEntry!); overlay.insert(_toastEntry!);
_toastTimer = Timer(const Duration(seconds: 1), () { _toastTimer = Timer(const Duration(milliseconds: 1500), () {
_toastEntry?.remove(); _toastEntry?.remove();
_toastEntry = null; _toastEntry = null;
_toastTimer = null; _toastTimer = null;
@@ -502,17 +515,9 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
Padding( Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: TextButton.icon( child: TextButton.icon(
onPressed: _isPicking ? null : _pickFiles, onPressed: _isPicking ? null : _repickCurrentSource,
icon: const Icon(Icons.insert_drive_file_outlined), icon: const Icon(Icons.add_photo_alternate_outlined),
label: const Text('选择文件'), label: const Text('重新选择'),
),
),
Padding(
padding: const EdgeInsets.only(right: 8),
child: TextButton.icon(
onPressed: _isPicking ? null : _pickAlbumImage,
icon: const Icon(Icons.photo_library_outlined),
label: const Text('相册'),
), ),
), ),
], ],
@@ -537,7 +542,7 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
'选择文件或相册图片开始压缩', '选择图片开始压缩',
style: Theme.of( style: Theme.of(
context, context,
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700), ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700),
@@ -556,7 +561,7 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
FilledButton.tonalIcon( FilledButton.tonalIcon(
onPressed: _isPicking ? null : _pickAlbumImage, onPressed: _isPicking ? null : _pickAlbumImage,
icon: const Icon(Icons.photo_library_outlined), icon: const Icon(Icons.photo_library_outlined),
label: const Text('相册选择'), label: const Text('相册选择'),
), ),
], ],
), ),
@@ -683,25 +688,25 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
const SizedBox(height: 14), const SizedBox(height: 14),
SizedBox( SizedBox(
height: previewHeight, height: previewHeight,
child: DecoratedBox( child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFEDEFEA), color: const Color(0xFFEDEFEA),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
),
foregroundDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFDADFD8)), border: Border.all(color: const Color(0xFFDADFD8)),
), ),
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildImagePreview(job, mode), child: _buildImagePreview(job, mode),
), ),
), ),
),
if (showCompareSlider) ...[ if (showCompareSlider) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
_CompareSlider( _CompareSlider(
value: _compareFraction, value: _compareFraction,
min: 0.05, min: 0.0,
max: 0.95, max: 1.0,
dragScale: _comparePreviewScale,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_compareFraction = value; _compareFraction = value;
@@ -774,33 +779,20 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
return _ZoomablePreview( return _ZoomablePreview(
width: job.original.width, width: job.original.width,
height: job.original.height, height: job.original.height,
overlay: const Stack( overlay: _CompareOverlay(fraction: _compareFraction),
children: [ childBuilder: (context, metrics) {
Positioned(left: 12, top: 12, child: _PreviewBadge(label: '原图')), final clipX = metrics.canvasXForViewportFraction(_compareFraction);
Positioned(right: 12, top: 12, child: _PreviewBadge(label: '压缩')),
],
),
onScaleChanged: _handleCompareScaleChanged,
child: LayoutBuilder(
builder: (context, constraints) {
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
_PreviewImage(path: compressedPath), _PreviewImage(path: compressedPath),
ClipRect( ClipRect(
clipper: _CompareClipper(_compareFraction), clipper: _CompareClipper(clipX),
child: _PreviewImage(path: job.original.previewPath), child: _PreviewImage(path: job.original.previewPath),
), ),
Positioned(
left: constraints.maxWidth * _compareFraction - 1,
top: 0,
bottom: 0,
child: Container(width: 2, color: Colors.white),
),
], ],
); );
}, },
),
); );
} }
return _ZoomablePreview( return _ZoomablePreview(
@@ -858,6 +850,8 @@ class _ImagerHomePageState extends State<ImagerHomePage> {
enum PreviewMode { original, compressed, compare } enum PreviewMode { original, compressed, compare }
enum ImagePickSource { file, album }
class NativeImage { class NativeImage {
NativeImage({ NativeImage({
required this.sourceHandle, required this.sourceHandle,
@@ -1015,14 +1009,12 @@ class _CompareSlider extends StatefulWidget {
required this.value, required this.value,
required this.min, required this.min,
required this.max, required this.max,
required this.dragScale,
required this.onChanged, required this.onChanged,
}); });
final double value; final double value;
final double min; final double min;
final double max; final double max;
final double dragScale;
final ValueChanged<double> onChanged; final ValueChanged<double> onChanged;
@override @override
@@ -1031,7 +1023,6 @@ class _CompareSlider extends StatefulWidget {
class _CompareSliderState extends State<_CompareSlider> { class _CompareSliderState extends State<_CompareSlider> {
static const _height = 48.0; static const _height = 48.0;
static const _horizontalPadding = 16.0;
static const _trackHeight = 4.0; static const _trackHeight = 4.0;
static const _thumbSize = 22.0; static const _thumbSize = 22.0;
@@ -1061,12 +1052,9 @@ class _CompareSliderState extends State<_CompareSlider> {
return; return;
} }
final scale = math.max(1.0, widget.dragScale);
final range = widget.max - widget.min; final range = widget.max - widget.min;
final delta = (details.primaryDelta ?? details.delta.dx) / trackWidth; final delta = (details.primaryDelta ?? details.delta.dx) / trackWidth;
final nextValue = _clampValue( final nextValue = _clampValue((_dragValue ?? widget.value) + delta * range);
(_dragValue ?? widget.value) + delta * range / scale,
);
_dragValue = nextValue; _dragValue = nextValue;
widget.onChanged(nextValue); widget.onChanged(nextValue);
} }
@@ -1095,20 +1083,17 @@ class _CompareSliderState extends State<_CompareSlider> {
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final width = constraints.maxWidth; final width = constraints.maxWidth;
final trackWidth = math.max(1.0, width - _horizontalPadding * 2); final trackWidth = math.max(1.0, width);
final normal = final normal =
((widget.value - widget.min) / (widget.max - widget.min)) ((widget.value - widget.min) / (widget.max - widget.min))
.clamp(0.0, 1.0) .clamp(0.0, 1.0)
.toDouble(); .toDouble();
final thumbCenter = _horizontalPadding + trackWidth * normal; final thumbCenter = trackWidth * normal;
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTapDown: (details) { onTapDown: (details) {
_jumpTo( _jumpTo(details.localPosition.dx, trackWidth);
details.localPosition.dx - _horizontalPadding,
trackWidth,
);
}, },
onTapUp: (details) { onTapUp: (details) {
_dragValue = null; _dragValue = null;
@@ -1125,8 +1110,8 @@ class _CompareSliderState extends State<_CompareSlider> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
children: [ children: [
Positioned( Positioned(
left: _horizontalPadding, left: 0,
right: _horizontalPadding, right: 0,
top: (_height - _trackHeight) / 2, top: (_height - _trackHeight) / 2,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -1137,7 +1122,7 @@ class _CompareSliderState extends State<_CompareSlider> {
), ),
), ),
Positioned( Positioned(
left: _horizontalPadding, left: 0,
width: trackWidth * normal, width: trackWidth * normal,
top: (_height - _trackHeight) / 2, top: (_height - _trackHeight) / 2,
child: DecoratedBox( child: DecoratedBox(
@@ -1176,20 +1161,80 @@ class _CompareSliderState extends State<_CompareSlider> {
} }
} }
typedef _ZoomablePreviewChildBuilder =
Widget Function(BuildContext context, _ZoomablePreviewMetrics metrics);
class _ZoomablePreviewMetrics {
const _ZoomablePreviewMetrics({
required this.viewportSize,
required this.canvasSize,
required this.scale,
required this.offset,
});
final Size viewportSize;
final Size canvasSize;
final double scale;
final Offset offset;
double canvasXForViewportFraction(double fraction) {
final viewportX = viewportSize.width * fraction.clamp(0.0, 1.0).toDouble();
return canvasSize.width / 2 +
(viewportX - viewportSize.width / 2 - offset.dx) / scale;
}
}
class _CompareOverlay extends StatelessWidget {
const _CompareOverlay({required this.fraction});
final double fraction;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final dividerX =
constraints.maxWidth * fraction.clamp(0.0, 1.0).toDouble();
return Stack(
children: [
Positioned(
left: dividerX - 1,
top: 0,
bottom: 0,
child: Container(width: 2, color: Colors.white),
),
const Positioned(
left: 12,
top: 12,
child: _PreviewBadge(label: '原图'),
),
const Positioned(
right: 12,
top: 12,
child: _PreviewBadge(label: '压缩'),
),
],
);
},
);
}
}
class _ZoomablePreview extends StatefulWidget { class _ZoomablePreview extends StatefulWidget {
const _ZoomablePreview({ const _ZoomablePreview({
required this.width, required this.width,
required this.height, required this.height,
required this.child, this.child,
this.childBuilder,
this.overlay, this.overlay,
this.onScaleChanged, }) : assert(child != null || childBuilder != null);
});
final int width; final int width;
final int height; final int height;
final Widget child; final Widget? child;
final _ZoomablePreviewChildBuilder? childBuilder;
final Widget? overlay; final Widget? overlay;
final ValueChanged<double>? onScaleChanged;
@override @override
State<_ZoomablePreview> createState() => _ZoomablePreviewState(); State<_ZoomablePreview> createState() => _ZoomablePreviewState();
@@ -1213,26 +1258,11 @@ class _ZoomablePreviewState extends State<_ZoomablePreview> {
if (oldWidget.width != widget.width || oldWidget.height != widget.height) { if (oldWidget.width != widget.width || oldWidget.height != widget.height) {
_resetTransform(); _resetTransform();
} }
if (oldWidget.onScaleChanged != widget.onScaleChanged) {
_scheduleScaleChanged();
}
} }
void _resetTransform() { void _resetTransform() {
_scale = _minScale; _scale = _minScale;
_offset = Offset.zero; _offset = Offset.zero;
_scheduleScaleChanged();
}
void _scheduleScaleChanged() {
if (widget.onScaleChanged == null) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
widget.onScaleChanged?.call(_scale);
}
});
} }
double _clampScale(double scale) { double _clampScale(double scale) {
@@ -1273,7 +1303,6 @@ class _ZoomablePreviewState extends State<_ZoomablePreview> {
_scale = nextScale; _scale = nextScale;
_offset = _clampOffset(nextOffset, nextScale); _offset = _clampOffset(nextOffset, nextScale);
}); });
widget.onScaleChanged?.call(nextScale);
} }
void _endGesture(ScaleEndDetails details) { void _endGesture(ScaleEndDetails details) {
@@ -1281,7 +1310,6 @@ class _ZoomablePreviewState extends State<_ZoomablePreview> {
_scale = _clampScale(_scale); _scale = _clampScale(_scale);
_offset = _clampOffset(_offset, _scale); _offset = _clampOffset(_offset, _scale);
}); });
widget.onScaleChanged?.call(_scale);
} }
@override @override
@@ -1303,6 +1331,14 @@ class _ZoomablePreviewState extends State<_ZoomablePreview> {
_canvasSize = Size(canvasWidth, canvasHeight); _canvasSize = Size(canvasWidth, canvasHeight);
_scale = _clampScale(_scale); _scale = _clampScale(_scale);
_offset = _clampOffset(_offset, _scale); _offset = _clampOffset(_offset, _scale);
final metrics = _ZoomablePreviewMetrics(
viewportSize: _viewportSize,
canvasSize: _canvasSize,
scale: _scale,
offset: _offset,
);
final previewChild =
widget.childBuilder?.call(context, metrics) ?? widget.child!;
return ClipRect( return ClipRect(
child: Stack( child: Stack(
@@ -1322,7 +1358,7 @@ class _ZoomablePreviewState extends State<_ZoomablePreview> {
child: SizedBox( child: SizedBox(
width: canvasWidth, width: canvasWidth,
height: canvasHeight, height: canvasHeight,
child: widget.child, child: previewChild,
), ),
), ),
), ),
@@ -1385,17 +1421,18 @@ class _PreviewBadge extends StatelessWidget {
} }
class _CompareClipper extends CustomClipper<Rect> { class _CompareClipper extends CustomClipper<Rect> {
const _CompareClipper(this.fraction); const _CompareClipper(this.clipX);
final double fraction; final double clipX;
@override @override
Rect getClip(Size size) { Rect getClip(Size size) {
return Rect.fromLTWH(0, 0, size.width * fraction, size.height); final width = clipX.clamp(0.0, size.width).toDouble();
return Rect.fromLTWH(0, 0, width, size.height);
} }
@override @override
bool shouldReclip(_CompareClipper oldClipper) { bool shouldReclip(_CompareClipper oldClipper) {
return oldClipper.fraction != fraction; return oldClipper.clipX != clipX;
} }
} }
-28
View File
@@ -1,28 +0,0 @@
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:luban_imager/main.dart';
void main() {
testWidgets('uses English app name by default', (tester) async {
await tester.pumpWidget(const LubanImagerApp());
expect(find.text(AppName.english), findsOneWidget);
expect(find.text('选择文件'), findsWidgets);
expect(find.text('从相册选择'), findsOneWidget);
});
testWidgets('uses Chinese app name for Chinese locale', (tester) async {
tester.binding.platformDispatcher.localesTestValue = const [
Locale('zh', 'CN'),
];
addTearDown(tester.binding.platformDispatcher.clearLocalesTestValue);
await tester.pumpWidget(const LubanImagerApp());
expect(find.text(AppName.chinese), findsOneWidget);
expect(find.text(AppName.english), findsNothing);
expect(find.text('选择文件'), findsWidgets);
expect(find.text('从相册选择'), findsOneWidget);
});
}