從圖片庫挑選相片
本文會逐步說明如何建立可讓使用者從手機圖片媒體櫃挑選相片的應用程式。 因為 Xamarin.Forms 不包含這項功能,因此必須使用 DependencyService
來存取每個平臺上的原生 API。
建立介面
首先,在共用程式碼中建立介面以表示所需的功能。 如果是相片挑選應用程式,則只有一個方法是必要的。 這是定義於 IPhotoPickerService
範例程式代碼之 .NET Standard 連結庫中的 介面:
namespace DependencyServiceDemos
{
public interface IPhotoPickerService
{
Task<Stream> GetImageStreamAsync();
}
}
GetImageStreamAsync
方法會定義為非同步,因為此方法雖然必須快速傳回,但要等到使用者瀏覽圖片媒體櫃並選取其中一張相片時才能傳回所選相片的 Stream
物件。
這個介面會在所有平台上使用平台特定程式碼來實作。
iOS 實作
IPhotoPickerService
介面的 iOS 實作會使用 UIImagePickerController
,如 Choose a Photo from the Gallery (從資源庫選擇相片) 配方和範例程式碼中所述。
範例程式碼 iOS 專案中的 PhotoPickerService
類別包含 iOS 實作。 若要讓 DependencyService
管理員看得見此類別,該類別必須使用 Dependency
類型的 [assembly
] 屬性來識別,且該類別必須為公用且明確地實作 IPhotoPickerService
介面:
[assembly: Dependency (typeof (PhotoPickerService))]
namespace DependencyServiceDemos.iOS
{
public class PhotoPickerService : IPhotoPickerService
{
TaskCompletionSource<Stream> taskCompletionSource;
UIImagePickerController imagePicker;
public Task<Stream> GetImageStreamAsync()
{
// Create and define UIImagePickerController
imagePicker = new UIImagePickerController
{
SourceType = UIImagePickerControllerSourceType.PhotoLibrary,
MediaTypes = UIImagePickerController.AvailableMediaTypes(UIImagePickerControllerSourceType.PhotoLibrary)
};
// Set event handlers
imagePicker.FinishedPickingMedia += OnImagePickerFinishedPickingMedia;
imagePicker.Canceled += OnImagePickerCancelled;
// Present UIImagePickerController;
UIWindow window = UIApplication.SharedApplication.KeyWindow;
var viewController = window.RootViewController;
viewController.PresentViewController(imagePicker, true, null);
// Return Task object
taskCompletionSource = new TaskCompletionSource<Stream>();
return taskCompletionSource.Task;
}
...
}
}
GetImageStreamAsync
方法會建立 UIImagePickerController
並將其初始化,以從相片媒體櫃選取影像。 需要兩個事件處理常式:一個在使用者選取相片時使用;另一個在使用者取消顯示相片媒體櫃時使用。 方法 PresentViewController
接著會向用戶顯示相片庫。
此時,GetImageStreamAsync
方法必須將 Task<Stream>
物件傳回給呼叫它的程式碼。 這項工作只有在使用者完成與相片媒體櫃的互動,並呼叫其中一個事件處理常式時才會完成。 針對這種情況,TaskCompletionSource
類別是不可或缺的。 這個類別會提供適當泛型型別的 Task
物件以從 GetImageStreamAsync
方法傳回,該類別即可稍後於工作完成時收到通知。
當使用者選取圖片時,即會呼叫 FinishedPickingMedia
事件處理常式。 不過,處理常式會提供 UIImage
物件,且 Task
必須傳回 .NET Stream
物件。 這是在兩個步驟中完成:物件 UIImage
會先轉換成儲存在 物件中的 NSData
記憶體 PNG 或 JPEG 檔案,然後 NSData
對象會轉換成 .NET Stream
物件。 呼叫 TaskCompletionSource
物件的 SetResult
方法時,其可提供 Stream
物件以完成工作:
namespace DependencyServiceDemos.iOS
{
public class PhotoPickerService : IPhotoPickerService
{
TaskCompletionSource<Stream> taskCompletionSource;
UIImagePickerController imagePicker;
...
void OnImagePickerFinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs args)
{
UIImage image = args.EditedImage ?? args.OriginalImage;
if (image != null)
{
// Convert UIImage to .NET Stream object
NSData data;
if (args.ReferenceUrl.PathExtension.Equals("PNG") || args.ReferenceUrl.PathExtension.Equals("png"))
{
data = image.AsPNG();
}
else
{
data = image.AsJPEG(1);
}
Stream stream = data.AsStream();
UnregisterEventHandlers();
// Set the Stream as the completion of the Task
taskCompletionSource.SetResult(stream);
}
else
{
UnregisterEventHandlers();
taskCompletionSource.SetResult(null);
}
imagePicker.DismissModalViewController(true);
}
void OnImagePickerCancelled(object sender, EventArgs args)
{
UnregisterEventHandlers();
taskCompletionSource.SetResult(null);
imagePicker.DismissModalViewController(true);
}
void UnregisterEventHandlers()
{
imagePicker.FinishedPickingMedia -= OnImagePickerFinishedPickingMedia;
imagePicker.Canceled -= OnImagePickerCancelled;
}
}
}
iOS 應用程式需要使用者的權限,以存取手機上的相片媒體櫃。 將下列內容新增至 Info.plist 檔案的 dict
區段:
<key>NSPhotoLibraryUsageDescription</key>
<string>Picture Picker uses photo library</string>
Android 實作
Android 實作會使用 Select an Image (選取影像) 配方和程式碼範例中所述的技巧。 不過,當使用者從圖片媒體櫃選取影像時,所呼叫的方法是衍生自 Activity
之類別中的 OnActivityResult
覆寫。 基於這個理由,Android 專案中的一般 MainActivity
類別已補充一個欄位、屬性,以及 OnActivityResult
方法的覆寫:
public class MainActivity : FormsAppCompatActivity
{
internal static MainActivity Instance { get; private set; }
protected override void OnCreate(Bundle savedInstanceState)
{
// ...
Instance = this;
}
// ...
// Field, property, and method for Picture Picker
public static readonly int PickImageId = 1000;
public TaskCompletionSource<Stream> PickImageTaskCompletionSource { set; get; }
protected override void OnActivityResult(int requestCode, Result resultCode, Intent intent)
{
base.OnActivityResult(requestCode, resultCode, intent);
if (requestCode == PickImageId)
{
if ((resultCode == Result.Ok) && (intent != null))
{
Android.Net.Uri uri = intent.Data;
Stream stream = ContentResolver.OpenInputStream(uri);
// Set the Stream as the completion of the Task
PickImageTaskCompletionSource.SetResult(stream);
}
else
{
PickImageTaskCompletionSource.SetResult(null);
}
}
}
}
OnActivityResult
覆寫表示選取的圖片檔案具有 Android Uri
物件,但其可藉由呼叫 ContentResolver
物件 (擷取自活動的 ContentResolver
屬性) 的 OpenInputStream
方法來轉換成 .NET Stream
物件。
如同 iOS 實作,Android 實作會使用 TaskCompletionSource
以在工作完成時發出通知。 這個 TaskCompletionSource
物件會定義為 MainActivity
類別中的公用屬性。 這可讓該屬性在 Android 專案的 PhotoPickerService
類別中受到參考。 這是搭配 GetImageStreamAsync
方法的類別:
[assembly: Dependency(typeof(PhotoPickerService))]
namespace DependencyServiceDemos.Droid
{
public class PhotoPickerService : IPhotoPickerService
{
public Task<Stream> GetImageStreamAsync()
{
// Define the Intent for getting images
Intent intent = new Intent();
intent.SetType("image/*");
intent.SetAction(Intent.ActionGetContent);
// Start the picture-picker activity (resumes in MainActivity.cs)
MainActivity.Instance.StartActivityForResult(
Intent.CreateChooser(intent, "Select Picture"),
MainActivity.PickImageId);
// Save the TaskCompletionSource object as a MainActivity property
MainActivity.Instance.PickImageTaskCompletionSource = new TaskCompletionSource<Stream>();
// Return Task object
return MainActivity.Instance.PickImageTaskCompletionSource.Task;
}
}
}
這個方法可基於多種用途存取 MainActivity
類別:可用於 Instance
屬性、用於 PickImageId
欄位、用於 TaskCompletionSource
屬性,以及呼叫 StartActivityForResult
。 這個方法是由 FormsAppCompatActivity
類別所定義,該類別為 MainActivity
的基底類別。
UWP 實作
不同於 iOS 和 Android 實作,通用 Windows 平台的相片選擇器實作不需要 TaskCompletionSource
類別。 PhotoPickerService
類別會使用 FileOpenPicker
類別,以取得相片媒體櫃的存取權。 因為 FileOpenPicker
的 PickSingleFileAsync
方法本身為非同步,所以 GetImageStreamAsync
方法可以直接使用 await
搭配該方法 (和其他非同步方法),並傳回 Stream
物件:
[assembly: Dependency(typeof(PhotoPickerService))]
namespace DependencyServiceDemos.UWP
{
public class PhotoPickerService : IPhotoPickerService
{
public async Task<Stream> GetImageStreamAsync()
{
// Create and initialize the FileOpenPicker
FileOpenPicker openPicker = new FileOpenPicker
{
ViewMode = PickerViewMode.Thumbnail,
SuggestedStartLocation = PickerLocationId.PicturesLibrary,
};
openPicker.FileTypeFilter.Add(".jpg");
openPicker.FileTypeFilter.Add(".jpeg");
openPicker.FileTypeFilter.Add(".png");
// Get a file and return a Stream
StorageFile storageFile = await openPicker.PickSingleFileAsync();
if (storageFile == null)
{
return null;
}
IRandomAccessStreamWithContentType raStream = await storageFile.OpenReadAsync();
return raStream.AsStreamForRead();
}
}
}
在共用程式碼中實作
現在,每個平台均已實作介面,.NET Standard 程式庫中的共用程式碼即可加以利用。
UI 包括 Button
,只要按一下它就可以選擇相片:
<Button Text="Pick Photo"
Clicked="OnPickPhotoButtonClicked" />
Clicked
事件處理常式使用 DependencyService
類別來呼叫 GetImageStreamAsync
。 這會造成呼叫平台專案。 若該方法傳回 Stream
物件,則處理常式會將 image
物件的 Source
屬性設定為 Stream
資料:
async void OnPickPhotoButtonClicked(object sender, EventArgs e)
{
(sender as Button).IsEnabled = false;
Stream stream = await DependencyService.Get<IPhotoPickerService>().GetImageStreamAsync();
if (stream != null)
{
image.Source = ImageSource.FromStream(() => stream);
}
(sender as Button).IsEnabled = true;
}