[image_picker] getMedia platform changes (#4174)

Adds `getMedia` and `getMultipleMedia` methods to  image_picker_platform_interface.

precursor to https://github.com/flutter/packages/pull/3892

part of https://github.com/flutter/flutter/issues/89159
This commit is contained in:
Tarrin Neal
2023-06-09 16:15:51 -07:00
committed by GitHub
parent ecf2b68093
commit 6565f17bcd
12 changed files with 316 additions and 60 deletions

View File

@ -1,3 +1,7 @@
## 2.8.0
* Adds `getMedia` method.
## 2.7.0
* Adds `CameraDelegatingImagePickerPlatform` as a base class for platform

View File

@ -252,6 +252,30 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
return paths.map((dynamic path) => XFile(path as String)).toList();
}
@override
Future<List<XFile>> getMedia({
required MediaOptions options,
}) async {
final ImageOptions imageOptions = options.imageOptions;
final Map<String, dynamic> args = <String, dynamic>{
'maxImageWidth': imageOptions.maxWidth,
'maxImageHeight': imageOptions.maxHeight,
'imageQuality': imageOptions.imageQuality,
'allowMultiple': options.allowMultiple,
};
final List<XFile>? paths = await _channel
.invokeMethod<List<dynamic>?>(
'pickMedia',
args,
)
.then((List<dynamic>? paths) =>
paths?.map((dynamic path) => XFile(path as String)).toList());
return paths ?? <XFile>[];
}
@override
Future<XFile?> getVideo({
required ImageSource source,
@ -280,13 +304,21 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
assert(result.containsKey('path') != result.containsKey('errorCode'));
final String? type = result['type'] as String?;
assert(type == kTypeImage || type == kTypeVideo);
assert(
type == kTypeImage || type == kTypeVideo || type == kTypeMedia,
);
RetrieveType? retrieveType;
if (type == kTypeImage) {
retrieveType = RetrieveType.image;
} else if (type == kTypeVideo) {
retrieveType = RetrieveType.video;
switch (type) {
case kTypeImage:
retrieveType = RetrieveType.image;
break;
case kTypeVideo:
retrieveType = RetrieveType.video;
break;
case kTypeMedia:
retrieveType = RetrieveType.media;
break;
}
PlatformException? exception;

View File

@ -213,6 +213,24 @@ abstract class ImagePickerPlatform extends PlatformInterface {
throw UnimplementedError('getMultiImage() has not been implemented.');
}
/// Returns a [List<XFile>] with the images and/or videos that were picked.
/// The images and videos come from the gallery.
///
/// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and
/// above only support HEIC images if used in addition to a size modification,
/// of which the usage is explained below.
///
/// In Android, the MainActivity can be destroyed for various reasons.
/// If that happens, the result will be lost in this call. You can then
/// call [getLostData] when your app relaunches to retrieve the lost data.
///
/// If no images or videos were picked, the return value is an empty list.
Future<List<XFile>> getMedia({
required MediaOptions options,
}) {
throw UnimplementedError('getMedia() has not been implemented.');
}
/// Returns a [XFile] containing the video that was picked.
///
/// The [source] argument controls where the video comes from. This can

View File

@ -2,6 +2,40 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'types.dart';
/// Specifies options for picking a single image from the device's camera or gallery.
///
/// This class inheritance is a byproduct of the api changing over time.
/// It exists solely to avoid breaking changes.
class ImagePickerOptions extends ImageOptions {
/// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality],
/// [referredCameraDevice] and [requestFullMetadata].
const ImagePickerOptions({
super.maxHeight,
super.maxWidth,
super.imageQuality,
super.requestFullMetadata,
this.preferredCameraDevice = CameraDevice.rear,
}) : super();
/// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality],
/// [referredCameraDevice] and [requestFullMetadata].
ImagePickerOptions.createAndValidate({
super.maxHeight,
super.maxWidth,
super.imageQuality,
super.requestFullMetadata,
this.preferredCameraDevice = CameraDevice.rear,
}) : super.createAndValidate();
/// Used to specify the camera to use when the `source` is [ImageSource.camera].
///
/// Ignored if the source is not [ImageSource.camera], or the chosen camera is not
/// supported on the device. Defaults to [CameraDevice.rear].
final CameraDevice preferredCameraDevice;
}
/// Specifies image-specific options for picking.
class ImageOptions {
/// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality]
@ -13,6 +47,18 @@ class ImageOptions {
this.requestFullMetadata = true,
});
/// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality]
/// and [requestFullMetadata]. Throws if options are not valid.
ImageOptions.createAndValidate({
this.maxHeight,
this.maxWidth,
this.imageQuality,
this.requestFullMetadata = true,
}) {
_validateOptions(
maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality);
}
/// The maximum width of the image, in pixels.
///
/// If null, the image will only be resized if [maxHeight] is specified.
@ -38,4 +84,19 @@ class ImageOptions {
//
// Defaults to true.
final bool requestFullMetadata;
/// Validates that all values are within required ranges. Throws if not.
static void _validateOptions(
{double? maxWidth, final double? maxHeight, int? imageQuality}) {
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
throw ArgumentError.value(
imageQuality, 'imageQuality', 'must be between 0 and 100');
}
if (maxWidth != null && maxWidth < 0) {
throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
}
if (maxHeight != null && maxHeight < 0) {
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
}
}
}

View File

@ -1,50 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'types.dart';
/// Specifies options for picking a single image from the device's camera or gallery.
class ImagePickerOptions {
/// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality],
/// [referredCameraDevice] and [requestFullMetadata].
const ImagePickerOptions({
this.maxHeight,
this.maxWidth,
this.imageQuality,
this.preferredCameraDevice = CameraDevice.rear,
this.requestFullMetadata = true,
});
/// The maximum width of the image, in pixels.
///
/// If null, the image will only be resized if [maxHeight] is specified.
final double? maxWidth;
/// The maximum height of the image, in pixels.
///
/// If null, the image will only be resized if [maxWidth] is specified.
final double? maxHeight;
/// Modifies the quality of the image, ranging from 0-100 where 100 is the
/// original/max quality.
///
/// Compression is only supported for certain image types such as JPEG. If
/// compression is not supported for the image that is picked, a warning
/// message will be logged.
///
/// If null, the image will be returned with the original quality.
final int? imageQuality;
/// Used to specify the camera to use when the `source` is [ImageSource.camera].
///
/// Ignored if the source is not [ImageSource.camera], or the chosen camera is not
/// supported on the device. Defaults to [CameraDevice.rear].
final CameraDevice preferredCameraDevice;
/// If true, requests full image metadata, which may require extra permissions
/// on some platforms, (e.g., NSPhotoLibraryUsageDescription on iOS).
//
// Defaults to true.
final bool requestFullMetadata;
}

View File

@ -36,7 +36,8 @@ class LostDataResponse {
/// An empty response should have [file], [exception] and [type] to be null.
bool get isEmpty => _empty;
/// The file that was lost in a previous [getImage], [getMultiImage] or [getVideo] call due to MainActivity being destroyed.
/// The file that was lost in a previous [getImage], [getMultiImage],
/// [getVideo] or [getMedia] call due to MainActivity being destroyed.
///
/// Can be null if [exception] exists.
final XFile? file;
@ -51,7 +52,7 @@ class LostDataResponse {
/// Note that it is not the exception that caused the destruction of the MainActivity.
final PlatformException? exception;
/// Can either be [RetrieveType.image] or [RetrieveType.video];
/// Can either be [RetrieveType.image], [RetrieveType.video], or [RetrieveType.media].
///
/// If the lost data is empty, this will be null.
final RetrieveType? type;

View File

@ -0,0 +1,23 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import '../../image_picker_platform_interface.dart';
/// Specifies options for selecting items when using [ImagePickerPlatform.getMedia].
@immutable
class MediaOptions {
/// Construct a new MediaOptions instance.
const MediaOptions({
this.imageOptions = const ImageOptions(),
required this.allowMultiple,
});
/// Options that will apply to images upon selection.
final ImageOptions imageOptions;
/// Whether to allow for selecting multiple media.
final bool allowMultiple;
}

View File

@ -0,0 +1,14 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../image_picker_platform_interface.dart';
/// The type of media to allow the user to select with [ImagePickerPlatform.getMedia].
enum MediaSelectionType {
/// Static pictures.
image,
/// Videos.
video,
}

View File

@ -8,5 +8,8 @@ enum RetrieveType {
image,
/// A video. See [ImagePicker.pickVideo].
video
video,
/// Either a video or a static picture. See [ImagePicker.pickMedia].
media,
}

View File

@ -5,9 +5,10 @@
export 'camera_delegate.dart';
export 'camera_device.dart';
export 'image_options.dart';
export 'image_picker_options.dart';
export 'image_source.dart';
export 'lost_data_response.dart';
export 'media_options.dart';
export 'media_selection_type.dart';
export 'multi_image_picker_options.dart';
export 'picked_file/picked_file.dart';
export 'retrieve_type.dart';
@ -17,3 +18,6 @@ const String kTypeImage = 'image';
/// Denotes that a video is being picked.
const String kTypeVideo = 'video';
/// Denotes that either a video or image is being picked.
const String kTypeMedia = 'media';

View File

@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/image_picker/
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
version: 2.7.0
version: 2.8.0
environment:
sdk: ">=2.18.0 <4.0.0"

View File

@ -872,6 +872,152 @@ void main() {
});
});
group('#getMedia', () {
test('calls the method correctly', () async {
returnValue = <String>['0'];
await picker.getMedia(options: const MediaOptions(allowMultiple: true));
expect(
log,
<Matcher>[
isMethodCall('pickMedia', arguments: <String, dynamic>{
'maxImageWidth': null,
'maxImageHeight': null,
'imageQuality': null,
'allowMultiple': true,
}),
],
);
});
test('passes the selection options correctly', () async {
// Default options
returnValue = <String>['0'];
await picker.getMedia(options: const MediaOptions(allowMultiple: true));
// Various image options
returnValue = <String>['0'];
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxWidth: 10.0,
),
),
);
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxHeight: 10.0,
),
),
);
await picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
imageQuality: 70,
),
),
);
expect(
log,
<Matcher>[
isMethodCall('pickMedia', arguments: <String, dynamic>{
'maxImageWidth': null,
'maxImageHeight': null,
'imageQuality': null,
'allowMultiple': true,
}),
isMethodCall('pickMedia', arguments: <String, dynamic>{
'maxImageWidth': 10.0,
'maxImageHeight': null,
'imageQuality': null,
'allowMultiple': true,
}),
isMethodCall('pickMedia', arguments: <String, dynamic>{
'maxImageWidth': null,
'maxImageHeight': 10.0,
'imageQuality': null,
'allowMultiple': true,
}),
isMethodCall('pickMedia', arguments: <String, dynamic>{
'maxImageWidth': null,
'maxImageHeight': null,
'imageQuality': 70,
'allowMultiple': true,
}),
],
);
});
test('does not accept a negative width or height argument', () {
returnValue = <String>['0', '1'];
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxWidth: -1.0,
),
),
),
throwsArgumentError,
);
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxHeight: -1.0,
),
),
),
throwsArgumentError,
);
});
test('does not accept a invalid imageQuality argument', () {
returnValue = <String>['0', '1'];
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
imageQuality: -1,
),
),
),
throwsArgumentError,
);
expect(
() => picker.getMedia(
options: MediaOptions(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
imageQuality: 101,
),
),
),
throwsArgumentError,
);
});
test('handles a null path response gracefully', () async {
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(
picker.channel, (MethodCall methodCall) => null);
expect(
await picker.getMedia(
options: const MediaOptions(allowMultiple: true)),
<XFile>[]);
});
});
group('#getVideo', () {
test('passes the image source argument correctly', () async {
await picker.getVideo(source: ImageSource.camera);