Skip to content

Commit e864172

Browse files
authored
[image_picker] Multiple image support (flutter#3783)
1 parent 67885c5 commit e864172

File tree

13 files changed

+854
-371
lines changed

13 files changed

+854
-371
lines changed

packages/image_picker/image_picker/CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.8.1
2+
3+
* Add a new method `getMultiImage` to allow picking multiple images on iOS 14 or higher
4+
and Android 4.3 or higher. Returns only 1 image for lower versions of iOS and Android.
5+
* Known issue: On Android, `getLostData` will only get the last picked image when picking multiple images,
6+
see: [#84634](https://github.com/flutter/flutter/issues/84634).
7+
18
## 0.8.0+4
29

310
* Cleaned up the README example
@@ -49,15 +56,15 @@ is not included selected photos and image is scaled.
4956

5057
## 0.7.3
5158

52-
* Endorse image_picker_for_web
59+
* Endorse image_picker_for_web.
5360

5461
## 0.7.2+1
5562

5663
* Android: fixes an issue where videos could be wrongly picked with `.jpg` extension.
5764

5865
## 0.7.2
5966

60-
* Run CocoaPods iOS tests in RunnerUITests target
67+
* Run CocoaPods iOS tests in RunnerUITests target.
6168

6269
## 0.7.1
6370

packages/image_picker/image_picker/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ First, add `image_picker` as a [dependency in your pubspec.yaml file](https://fl
1111

1212
### iOS
1313

14+
Starting with version **0.8.1** the iOS implementation uses PHPicker to pick (multiple) images on iOS 14 or higher.
15+
As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue.[63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5)
16+
1417
Add the following keys to your _Info.plist_ file, located in `<project root>/ios/Runner/Info.plist`:
1518

1619
* `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor.
@@ -19,6 +22,8 @@ Add the following keys to your _Info.plist_ file, located in `<project root>/ios
1922

2023
### Android
2124

25+
Starting with version **0.8.1** the Android implementation support to pick (multiple) images on Android 4.3 or higher.
26+
2227
No configuration required - the plugin should work out of the box.
2328

2429
It is no longer required to add `android:requestLegacyExternalStorage="true"` as an attribute to the `<application>` tag in AndroidManifest.xml, as `image_picker` has been updated to make use of scoped storage.
@@ -63,6 +68,8 @@ Future<void> retrieveLostData() async {
6368

6469
There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it.
6570

71+
On Android, `getLostData` will only get the last picked image when picking multiple images, see: [#84634](https://github.com/flutter/flutter/issues/84634).
72+
6673
## Deprecation warnings in `pickImage`, `pickVideo` and `LostDataResponse`
6774

6875
Starting with version **0.6.7** of the image_picker plugin, the API of the plugin changed slightly to allow for web implementations to exist.

packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.flutter.plugin.common.PluginRegistry;
2323
import java.io.File;
2424
import java.io.IOException;
25+
import java.util.ArrayList;
2526
import java.util.List;
2627
import java.util.Map;
2728
import java.util.UUID;
@@ -75,6 +76,7 @@ public class ImagePickerDelegate
7576
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY = 2342;
7677
@VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343;
7778
@VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345;
79+
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346;
7880
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352;
7981
@VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
8082
@VisibleForTesting static final int REQUEST_CAMERA_VIDEO_PERMISSION = 2355;
@@ -315,13 +317,32 @@ public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result r
315317
launchPickImageFromGalleryIntent();
316318
}
317319

320+
public void chooseMultiImageFromGallery(MethodCall methodCall, MethodChannel.Result result) {
321+
if (!setPendingMethodCallAndResult(methodCall, result)) {
322+
finishWithAlreadyActiveError(result);
323+
return;
324+
}
325+
326+
launchMultiPickImageFromGalleryIntent();
327+
}
328+
318329
private void launchPickImageFromGalleryIntent() {
319330
Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
320331
pickImageIntent.setType("image/*");
321332

322333
activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
323334
}
324335

336+
private void launchMultiPickImageFromGalleryIntent() {
337+
Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
338+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
339+
pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
340+
}
341+
pickImageIntent.setType("image/*");
342+
343+
activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY);
344+
}
345+
325346
public void takeImageWithCamera(MethodCall methodCall, MethodChannel.Result result) {
326347
if (!setPendingMethodCallAndResult(methodCall, result)) {
327348
finishWithAlreadyActiveError(result);
@@ -440,6 +461,9 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
440461
case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY:
441462
handleChooseImageResult(resultCode, data);
442463
break;
464+
case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY:
465+
handleChooseMultiImageResult(resultCode, data);
466+
break;
443467
case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
444468
handleCaptureImageResult(resultCode);
445469
break;
@@ -467,6 +491,24 @@ private void handleChooseImageResult(int resultCode, Intent data) {
467491
finishWithSuccess(null);
468492
}
469493

494+
private void handleChooseMultiImageResult(int resultCode, Intent intent) {
495+
if (resultCode == Activity.RESULT_OK && intent != null) {
496+
ArrayList<String> paths = new ArrayList<>();
497+
if (intent.getClipData() != null) {
498+
for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
499+
paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()));
500+
}
501+
} else {
502+
paths.add(fileUtils.getPathFromUri(activity, intent.getData()));
503+
}
504+
handleMultiImageResult(paths, false);
505+
return;
506+
}
507+
508+
// User cancelled choosing a picture.
509+
finishWithSuccess(null);
510+
}
511+
470512
private void handleChooseVideoResult(int resultCode, Intent data) {
471513
if (resultCode == Activity.RESULT_OK && data != null) {
472514
String path = fileUtils.getPathFromUri(activity, data.getData());
@@ -516,26 +558,45 @@ public void onPathReady(String path) {
516558
finishWithSuccess(null);
517559
}
518560

519-
private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
561+
private void handleMultiImageResult(
562+
ArrayList<String> paths, boolean shouldDeleteOriginalIfScaled) {
520563
if (methodCall != null) {
521-
Double maxWidth = methodCall.argument("maxWidth");
522-
Double maxHeight = methodCall.argument("maxHeight");
523-
Integer imageQuality = methodCall.argument("imageQuality");
524-
525-
String finalImagePath =
526-
imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality);
527-
528-
finishWithSuccess(finalImagePath);
564+
for (int i = 0; i < paths.size(); i++) {
565+
String finalImagePath = getResizedImagePath(paths.get(i));
566+
567+
//delete original file if scaled
568+
if (finalImagePath != null
569+
&& !finalImagePath.equals(paths.get(i))
570+
&& shouldDeleteOriginalIfScaled) {
571+
new File(paths.get(i)).delete();
572+
}
573+
paths.set(i, finalImagePath);
574+
}
575+
finishWithListSuccess(paths);
576+
}
577+
}
529578

579+
private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
580+
if (methodCall != null) {
581+
String finalImagePath = getResizedImagePath(path);
530582
//delete original file if scaled
531583
if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) {
532584
new File(path).delete();
533585
}
586+
finishWithSuccess(finalImagePath);
534587
} else {
535588
finishWithSuccess(path);
536589
}
537590
}
538591

592+
private String getResizedImagePath(String path) {
593+
Double maxWidth = methodCall.argument("maxWidth");
594+
Double maxHeight = methodCall.argument("maxHeight");
595+
Integer imageQuality = methodCall.argument("imageQuality");
596+
597+
return imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality);
598+
}
599+
539600
private void handleVideoResult(String path) {
540601
finishWithSuccess(path);
541602
}
@@ -564,6 +625,17 @@ private void finishWithSuccess(String imagePath) {
564625
clearMethodCallAndResult();
565626
}
566627

628+
private void finishWithListSuccess(ArrayList<String> imagePaths) {
629+
if (pendingResult == null) {
630+
for (String imagePath : imagePaths) {
631+
cache.saveResult(imagePath, null, null);
632+
}
633+
return;
634+
}
635+
pendingResult.success(imagePaths);
636+
clearMethodCallAndResult();
637+
}
638+
567639
private void finishWithAlreadyActiveError(MethodChannel.Result result) {
568640
result.error("already_active", "Image picker is already active", null);
569641
}

packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public void onActivityStopped(Activity activity) {
9191
}
9292

9393
static final String METHOD_CALL_IMAGE = "pickImage";
94+
static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage";
9495
static final String METHOD_CALL_VIDEO = "pickVideo";
9596
private static final String METHOD_CALL_RETRIEVE = "retrieve";
9697
private static final int CAMERA_DEVICE_FRONT = 1;
@@ -302,6 +303,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) {
302303
throw new IllegalArgumentException("Invalid image source: " + imageSource);
303304
}
304305
break;
306+
case METHOD_CALL_MULTI_IMAGE:
307+
delegate.chooseMultiImageFromGallery(call, result);
308+
break;
305309
case METHOD_CALL_VIDEO:
306310
imageSource = call.argument("source");
307311
switch (imageSource) {

packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ public void chooseImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyAc
104104
}
105105

106106
@Test
107+
public void chooseMultiImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyActiveError() {
108+
ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
109+
110+
delegate.chooseMultiImageFromGallery(mockMethodCall, mockResult);
111+
112+
verifyFinishedWithAlreadyActiveError();
113+
verifyNoMoreInteractions(mockResult);
114+
}
115+
107116
public void
108117
chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
109118
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))

packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class ImagePickerPluginTest {
3232
private static final int SOURCE_CAMERA = 0;
3333
private static final int SOURCE_GALLERY = 1;
3434
private static final String PICK_IMAGE = "pickImage";
35+
private static final String PICK_MULTI_IMAGE = "pickMultiImage";
3536
private static final String PICK_VIDEO = "pickVideo";
3637

3738
@Rule public ExpectedException exception = ExpectedException.none();
@@ -92,6 +93,14 @@ public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() {
9293
verifyZeroInteractions(mockResult);
9394
}
9495

96+
@Test
97+
public void onMethodCall_InvokesChooseMultiImageFromGallery() {
98+
MethodCall call = buildMethodCall(PICK_MULTI_IMAGE);
99+
plugin.onMethodCall(call, mockResult);
100+
verify(mockImagePickerDelegate).chooseMultiImageFromGallery(eq(call), any());
101+
verifyZeroInteractions(mockResult);
102+
}
103+
95104
@Test
96105
public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() {
97106
MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA);
@@ -173,4 +182,8 @@ private MethodCall buildMethodCall(String method, final int source) {
173182

174183
return new MethodCall(method, arguments);
175184
}
185+
186+
private MethodCall buildMethodCall(String method) {
187+
return new MethodCall(method, null);
188+
}
176189
}

packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ - (UIViewController *)presentedViewController {
2222

2323
@interface FLTImagePickerPlugin (Test)
2424
@property(copy, nonatomic) FlutterResult result;
25-
- (void)handleSavedPath:(NSString *)path;
25+
- (void)handleSavedPathList:(NSMutableArray *)pathList;
2626
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
2727
@end
2828

@@ -122,21 +122,6 @@ - (void)testPickingVideoWithDuration {
122122
XCTAssertEqual([plugin getImagePickerController].videoMaximumDuration, 95);
123123
}
124124

125-
- (void)testPluginPickImageSelectMultipleTimes {
126-
FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
127-
FlutterMethodCall *call =
128-
[FlutterMethodCall methodCallWithMethodName:@"pickImage"
129-
arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}];
130-
[plugin handleMethodCall:call
131-
result:^(id _Nullable r){
132-
}];
133-
plugin.result = ^(id result) {
134-
135-
};
136-
[plugin handleSavedPath:@"test"];
137-
[plugin handleSavedPath:@"test"];
138-
}
139-
140125
- (void)testViewController {
141126
UIWindow *window = [UIWindow new];
142127
MockViewController *vc1 = [MockViewController new];
@@ -149,4 +134,62 @@ - (void)testViewController {
149134
XCTAssertEqual([plugin viewControllerWithWindow:window], vc2);
150135
}
151136

137+
- (void)testPluginMultiImagePathIsNil {
138+
FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
139+
140+
dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
141+
__block FlutterError *pickImageResult = nil;
142+
143+
plugin.result = ^(id _Nullable r) {
144+
pickImageResult = r;
145+
dispatch_semaphore_signal(resultSemaphore);
146+
};
147+
[plugin handleSavedPathList:nil];
148+
149+
dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
150+
151+
XCTAssertEqualObjects(pickImageResult.code, @"create_error");
152+
}
153+
154+
- (void)testPluginMultiImagePathHasNullItem {
155+
FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
156+
NSMutableArray *pathList = [NSMutableArray new];
157+
158+
[pathList addObject:[NSNull null]];
159+
160+
dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
161+
__block FlutterError *pickImageResult = nil;
162+
163+
plugin.result = ^(id _Nullable r) {
164+
pickImageResult = r;
165+
dispatch_semaphore_signal(resultSemaphore);
166+
};
167+
[plugin handleSavedPathList:pathList];
168+
169+
dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
170+
171+
XCTAssertEqualObjects(pickImageResult.code, @"create_error");
172+
}
173+
174+
- (void)testPluginMultiImagePathHasItem {
175+
FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new];
176+
NSString *savedPath = @"test";
177+
NSMutableArray *pathList = [NSMutableArray new];
178+
179+
[pathList addObject:savedPath];
180+
181+
dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0);
182+
__block id pickImageResult = nil;
183+
184+
plugin.result = ^(id _Nullable r) {
185+
pickImageResult = r;
186+
dispatch_semaphore_signal(resultSemaphore);
187+
};
188+
[plugin handleSavedPathList:pathList];
189+
190+
dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER);
191+
192+
XCTAssertEqual(pickImageResult, pathList);
193+
}
194+
152195
@end

0 commit comments

Comments
 (0)