You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET MAUI:iOS16与Android33直接读取相册图片至CollectionView方案

.NET MAUI:iOS 16/Android 33 无手动选择读取相册图片+监听更新方案

针对需求中直接读取系统相册并监听更新的要求,由于Android 30+的Scoped Storage限制和iOS的沙盒机制,无法直接通过文件路径访问资源,以下是分平台的具体实现:


Android 33+ 实现

Android 30+引入Scoped Storage后,GetExternalStoragePublicDirectory这类直接访问公共目录的API已被废弃,必须通过MediaStore查询图片资源。

1. 权限配置

在AndroidManifest.xml中添加必要权限:

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- 兼容Android 12及以下版本 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

2. 查询相册图片

通过ContentResolver查询MediaStore获取所有图片:

using Android.Content;
using Android.Provider;
using Android.Database;
using Android.Net;

public async Task<List<MediaAsset>> FetchAndroidPhotos()
{
    var photoList = new List<MediaAsset>();
    var context = Android.App.Application.Context;

    // 指定要查询的字段
    string[] projection = {
        MediaStore.Images.Media.InterfaceConsts.Id,
        MediaStore.Images.Media.InterfaceConsts.Data,
        MediaStore.Images.Media.InterfaceConsts.DisplayName
    };

    // 查询外部存储的图片,按添加时间倒序排列
    using (ICursor cursor = context.ContentResolver.Query(
        MediaStore.Images.Media.ExternalContentUri,
        projection,
        null,
        null,
        $"{MediaStore.Images.Media.InterfaceConsts.DateAdded} DESC"))
    {
        if (cursor?.MoveToFirst() == true)
        {
            do
            {
                try
                {
                    string assetId = cursor.GetString(cursor.GetColumnIndexOrThrow(MediaStore.Images.Media.InterfaceConsts.Id));
                    string filePath = cursor.GetString(cursor.GetColumnIndexOrThrow(MediaStore.Images.Media.InterfaceConsts.Data));
                    string fileName = cursor.GetString(cursor.GetColumnIndexOrThrow(MediaStore.Images.Media.InterfaceConsts.DisplayName));

                    // 转换为MAUI可识别的Uri
                    var contentUri = ContentUris.WithAppendedId(MediaStore.Images.Media.ExternalContentUri, long.Parse(assetId));
                    var imageSource = ImageSource.FromUri(contentUri);

                    photoList.Add(new MediaAsset
                    {
                        Path = filePath,
                        Name = fileName,
                        ImageSource = imageSource
                    });
                }
                catch { continue; }
            } while (cursor.MoveToNext());
        }
    }
    return photoList;
}

3. 监听相册更新

通过ContentObserver监听MediaStore的内容变化:

public class PhotoUpdateObserver : ContentObserver
{
    private readonly Action _onUpdate;

    public PhotoUpdateObserver(Action onUpdate) : base(Android.OS.Handler.MainLooper)
    {
        _onUpdate = onUpdate;
    }

    public override void OnChange(bool selfChange)
    {
        base.OnChange(selfChange);
        _onUpdate?.Invoke();
    }
}

// 注册监听(放在页面初始化或ViewModel构造中)
var observer = new PhotoUpdateObserver(() => 
{
    MainThread.BeginInvokeOnMainThread(async () => 
    {
        MediaAssets.Clear();
        var freshPhotos = await FetchAndroidPhotos();
        MediaAssets.AddRange(freshPhotos);
    });
});

Android.App.Application.Context.ContentResolver.RegisterContentObserver(
    MediaStore.Images.Media.ExternalContentUri,
    true,
    observer);

iOS 16+ 实现

iOS沙盒机制禁止直接访问相册路径,必须使用Photos框架获取图片资源。

1. 权限配置

在Info.plist中添加权限说明:

<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以展示图片</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要访问相册以监听图片更新</string>

2. 查询相册图片

通过PHAsset和PHImageManager获取图片资源:

using Photos;
using CoreGraphics;

public async Task<List<MediaAsset>> FetchiOSPhotos()
{
    var photoList = new List<MediaAsset>();

    // 请求相册权限
    var authStatus = await PHPhotoLibrary.RequestAuthorizationAsync();
    if (authStatus != PHAuthorizationStatus.Authorized)
    {
        return photoList;
    }

    // 查询所有图片,按创建时间倒序
    var fetchOpts = new PHFetchOptions
    {
        SortDescriptors = new[] { new NSSortDescriptor("creationDate", true) }
    };
    var assets = PHAsset.FetchAssets(PHAssetMediaType.Image, fetchOpts);

    foreach (PHAsset asset in assets)
    {
        try
        {
            var tcs = new TaskCompletionSource<ImageSource>();
            
            // 请求缩略图(尺寸可按需调整)
            PHImageManager.DefaultManager.RequestImageForAsset(
                asset,
                new CGSize(300, 300),
                PHImageContentMode.AspectFill,
                null,
                (image, info) =>
                {
                    if (image != null)
                    {
                        tcs.SetResult(ImageSource.FromStream(() => 
                        {
                            using var pngStream = image.AsPNG().AsStream();
                            var memoryStream = new MemoryStream();
                            pngStream.CopyTo(memoryStream);
                            memoryStream.Position = 0;
                            return memoryStream;
                        }));
                    }
                    else
                    {
                        tcs.SetResult(null);
                    }
                });

            var source = await tcs.Task;
            if (source != null)
            {
                photoList.Add(new MediaAsset
                {
                    LocalIdentifier = asset.LocalIdentifier,
                    ImageSource = source
                });
            }
        }
        catch { continue; }
    }
    return photoList;
}

3. 监听相册更新

通过PHPhotoLibraryChangeObserver监听相册变化:

public class iOSPhotoObserver : PHPhotoLibraryChangeObserver
{
    private readonly Action _onUpdate;

    public iOSPhotoObserver(Action onUpdate)
    {
        _onUpdate = onUpdate;
    }

    public override void PhotoLibraryDidChange(PHChange changeInstance)
    {
        // 必须回到主线程更新UI
        MainThread.BeginInvokeOnMainThread(() => 
        {
            _onUpdate?.Invoke();
        });
    }
}

// 注册监听
var observer = new iOSPhotoObserver(() => 
{
    MainThread.BeginInvokeOnMainThread(async () => 
    {
        MediaAssets.Clear();
        var freshPhotos = await FetchiOSPhotos();
        MediaAssets.AddRange(freshPhotos);
    });
});

PHPhotoLibrary.SharedPhotoLibrary.RegisterChangeObserver(observer);

通用绑定与模型

XAML中的CollectionView

<CollectionView ItemsSource="{Binding MediaAssets}">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Image Source="{Binding ImageSource}" 
                   WidthRequest="100" 
                   HeightRequest="100" 
                   Aspect="AspectFill"
                   Margin="4"/>
        </DataTemplate>
    </CollectionView.ItemTemplate>
    <CollectionView.ItemsLayout>
        <GridItemsLayout Span="3" Orientation="Vertical"/>
    </CollectionView.ItemsLayout>
</CollectionView>

MediaAsset模型类

public class MediaAsset
{
    public string Path { get; set; }
    public string Name { get; set; }
    public string LocalIdentifier { get; set; }
    public ImageSource ImageSource { get; set; }
}

内容的提问来源于stack exchange,提问作者Alexander Petrov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 16:50:43