65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Xamarin Forms 中共享图像资源的更好方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (18投票s)

2014年11月13日

CPOL

11分钟阅读

viewsIcon

88260

了解如何结合使用 T4 和自定义标记扩展在项目之间共享和使用图像文件。

本系列文章

Calcium 和示例应用程序的源代码

本文的源代码位于 Calcium 源代码存储库中,网址为 https://calcium.codeplex.com/SourceControl/latest

请参阅解决方案  \Trunk\Source\Calcium\Xamarin\Installation\CalciumTemplates.Xamarin.sln

引言

在构建 Xamarin 跨平台应用时,图像资源管理是一个挑战。每个平台都需要以不同的方式处理图像。Android 和 iOS 项目都使用自己的系统;将图像放置在不同的目录中。然而,Windows Phone 允许您将图像放置在几乎任何您喜欢的位置。如果您希望在项目之间使用相同的图像,那么您需要将它们链接到各种位置。在这方面,Windows Phone 是最大的输家,因为图像必须放置在项目的根目录中,这远非理想。另一种方法是将图像资源嵌入到程序集中,但这对于存在 CLR 的情况来说并不是个好主意,因为程序集大小的增加会增加程序集加载所需的时间。

Visual Studio 的 Android 和 iOS 项目要求所有图像都放置在单个目录中。我不喜欢必须将图像放置在单个目录中。我更喜欢按功能相关性进行分组。当大型 Web 项目中充斥着大量孤立的图像,而它们所属的页面早已被删除时,这种情况并不少见。

我着手发明一种在项目之间共享图像资源的更好方法;一种能够保留 Windows Phone 系统的灵活性,但又能与 iOS 和 Android 兼容的方法。我的方法使用 T4 模板,该模板将文件从您的共享项目中复制到各种 iOS 和 Android 资源目录中,将它们添加到您的 iOS 和 Android 项目中,并正确设置图像的“生成操作”。您稍后将在文章中看到自定义标记扩展如何转换图像资源 URL,以便图像能够正确解析,无论平台如何。这种方法为您提供了 Windows Phone 资源系统的所有灵活性,同时仍保留了与 iOS 和 Android 的兼容性。

在 Xamarin Forms 中,Image 元素通常用于在页面上显示 PNG 或 JPG 图像。请看以下示例:

<Image Source="CalciumLogo.png" />

为了使图像在每个平台上都能正确显示,它必须位于 iOS 项目的 Resources 目录、Android 项目的 Resources/Drawable 目录或 Windows Phone 的根目录中。

提示。在 Xamarin 网站上,有一个关于在 Xamarin Forms 中使用 Image 元素的详细介绍,网址为 http://developer.xamarin.com/guides/cross-platform/xamarin-forms/working-with/images/
虽然对 Xamarin Forms 中的图像资源有扎实的理解很有用,但在使用本文所述的方法时,大部分的麻烦都会消失。

在共享项目的 /Views/MainView/Images 目录中有一个名为 CalciumLogo.png 的 PNG 图像。与 Windows Phone 不同,在 Xamarin Forms 中,以下路径无法解析。

<Image Source="/Views/MainView/Images/CalciumLogo.png" />

让我们来解决这个问题。

Image 元素的 Source 属性是一个 ImageSource。类型转换器用于将 XAML 中提供的字符串值转换为实际的 ImageSource 实例。通过创建自定义标记扩展,我们可以接管根据指定 URL 创建 ImageSource 对象的责任。

Xamarin Forms 的一个了不起的特性是包含标记扩展。我在 Xamarin Forms 的第一个版本中看到它们时感到非常惊喜。能够创建自定义标记扩展为我们提供了扩展 XAML 功能的强大方法。我们不必等待“官方”支持所有功能;我们可以自己动手实现。

ImageUrlTransformer 类位于 Calcium 源代码存储库的 _Outcoder.Calcium.Android_ 项目中,它将字符串值转换为特定于平台的 URL。它有一个名为 TransformForCurrentPlatform 的方法。请参见列表 1。该方法接受一个 URL,如果应用程序正在 iOS 或 Android 上运行,它会用下划线替换路径分隔符斜杠。例如,如果我们向该方法传递 “/Views/MainView/Images/CalciumLogo.png”,则在 iOS 或 Android 上,该方法应返回 “Views_MainView_Images_CalciumLogo.png”

相反,如果应用程序正在 Windows Phone 上运行,则任务会变得更加简单;标记扩展会使用未更改的路径解析图像。

当然,为了让这在 iOS 和 Android 上正常工作,相应的平台资源目录中必须有一个名为 Views_MainView_Images_CalciumLogo.png 的图像。我们将在本节后面更详细地研究这一点。

请注意,如果平台是 iOS 或 Android,并且指定的 URL 以 file:/// 开头,则表示使用 Uri 对象而不是字符串来指定图像位置。这没关系,但为了正确解析图像,必须从 URL 中删除它。

列表 1。ImageUrlTransformer.TransformForCurrentPlatform 方法

public string TransformForCurrentPlatform(string url)
{
    string result = ArgumentValidator.AssertNotNull(url, "url");

    if (Device.OS == TargetPlatform.Android || Device.OS == TargetPlatform.iOS)
    {
        const string filePrefix = "file:///";

        if (url.StartsWith(filePrefix))
        {
            result = url.Substring(filePrefix.Length);
        }

        result = result.Replace("/", "_").Replace("\\", "_");

        if (result.StartsWith("_") && result.Length > 1)
        {
            result = result.Substring(1);
        }
    }
    else if (Device.OS == TargetPlatform.WinPhone)
    {
        if (url.StartsWith("/") && url.Length > 1)
        {
            result = result.Substring(1);
        }
    }

    return result;
}

自定义标记扩展为您提供了一个扩展点,您可以在其中使用自定义逻辑来在运行时解析值。Xamarin.Forms.Xaml.IMarkupExtension 接口包含一个名为 ProvideValue 的方法,该方法旨在处理一个值并返回一个对象给 XAML 元素中设置的属性。

ImageResourceExtension 类,位于 Calcium 源代码存储库中,是一个自定义标记扩展,它利用 ImageUrlTransformer 类。请参见列表 2。

列表 2。ImageResourceExtension 类 (摘录)

[ContentProperty("Source")]
public class ImageResourceExtension : IMarkupExtension
{
    public string Source { get; set; }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        …
    }
}

ImageResourceExtensionProvideValue 方法返回一个 ImageSource 对象。请参见列表 3。该方法使用 IImageUrlTransformer 的实现来将 Source 属性的值转换为特定于平台的 URL。

注意。如果您想更改 URL 的处理方式并修改 ImageUrlTransformer 类的行为,可以通过注册您自己的 IImageUrlTransformer 实现来实现。

该类使用 Xamarin Forms 的 Device.OS 属性来确定应用程序正在运行的平台。如果应用程序恰好在 iOS 或 Android 上运行,则 URL 会被展平,并且图像预计会驻留在资源目录中。静态 ImageSource.FromFile 方法用于创建 ImageSource 对象。

如果应用程序正在 Windows Phone 上运行,则会创建一个 Stream 来读取图像文件,该文件在 lambda 表达式中传递给静态 ImageSource.FromStream 方法。

列表 3。ImageResourceExtension.ProvideValue 方法

public object ProvideValue(IServiceProvider serviceProvider)
{
    if (Source == null)
    {
        return null;
    }

    ImageSource imageSource = null;

    var transformer = Dependency.Resolve<IImageUrlTransformer, ImageUrlTransformer>(true);
    string url = transformer.TransformForCurrentPlatform(Source);

    if (Device.OS == TargetPlatform.Android)
    {
        imageSource = ImageSource.FromFile(url);
    }
    else if (Device.OS == TargetPlatform.iOS)
    {
        imageSource = ImageSource.FromFile(url);
    }
    else if (Device.OS == TargetPlatform.WinPhone)
    {
#if WINDOWS_PHONE
        if (url.StartsWith("/") && url.Length > 1)
        {
            url = url.Substring(1);
        }

        var stream = System.Windows.Application.GetResourceStream(new Uri(url, UriKind.Relative));

        if (stream != null)
        {
            imageSource = ImageSource.FromStream(() => stream.Stream);
        }
        else
        {
            ILog log;
            if (Dependency.TryResolve<ILog>(out log))
            {
               log.Debug("Unable to located create ImageSource using URL: " + url);
            }
        }
#endif
    }

    if (imageSource == null)
    {
        imageSource = ImageSource.FromFile(url);
    }

    return imageSource;
}

使用 ImageResourceExtension 类后,您现在可以像这样指定图像文件的路径:

<Image Source="{calcium:ImageResource /Views/MainView/Images/CalciumLogo.png}" />

我们可以到此为止,但这将迫使我们为每个平台复制、重命名和移动图像。我们甚至可以尝试链接和重命名文件。但我们很快就会遇到与我们开始解决的问题相同的可维护性问题。让我们做得更好。

使用 T4 共享图像资源

在本节中,您将了解如何创建一个可重用的 T4 模板,该模板将所有图像从共享项目复制到 iOS 或 Android 项目的资源目录。然后,它会自动将文件导入到您的项目中,并根据平台为文件设置正确的“生成操作”。

在看过本系列第 3 部分中的本地化解决方案后,我想出这个方法并不费力:使用 Xamarin Forms 和 Calcium 构建本地化跨平台应用。T4 在处理此类任务时非常有用。您可能可以使用自定义 MS Build 任务实现相同的结果。我选择 T4 方法是因为我认为它在配置和使其工作方面更省事。代码易于修改,并且不埋藏在外部程序集中。不过,我猜测,从长远来看,Xamarin 可能会选择 MS Build。但目前,T4 能够完成这项工作。

使用 T4 将图像导入 Android 项目

让我们从研究如何将 T4 模板集成到 Android 项目中开始。下载样本中的 _ProjectTemplate.Xamarin.CSharp.Android_ 项目包含两个与本示例相关的目录:一个 _/Resources/Drawable_ 目录,这是默认分辨率图像的必需位置;以及 _/ResourcesModel/T4Templates_ 目录,其中包含一些必需的 T4 include 文件。

以下步骤概述了设置 T4 图像共享模板的过程

  1. 在 Android 项目中创建一个名为 ResourcesModel 的目录,并在其中创建一个 T4Templates 目录。
  2. 将下载样本中位于 /ResourcesModel/T4Templates 目录下的 Images.ttinclude 文件和 MultiFileOutput.ttinclude 文件添加到该目录。
  3. 将 Images.ttinclude 文件和 MultiFileOutput.ttinclude 文件的“生成操作”设置为 None。
  4. 使用 Visual Studio 的“添加新项”对话框向 Android 项目的 /Resources/Drawable 目录添加一个文本文件。在单击“确定”之前,将文件重命名为 Images.tt。
  5. 将以下文本粘贴到 Images.tt 文件中
    <#@ include file="..\..\ResourcesModel\T4Templates\Images.ttinclude" #>
    <#@ output extension=".txt" #>
    <#@ template language="C#" hostSpecific="true" #><#

    Process("CalciumSampleApp", "AndroidResource");

    #>
  6. 将文本“CalciumSampleApp”更改为您共享项目的名称。

请注意,Process 方法的第二个参数定义了图像的“生成操作”。对于 Android,必须将其指定为 AndroidResource

如果路径已正确指定,当您保存 Images.tt 文件时,图像应显示为 Images.tt 文件的子项。请参见图 1。位于共享项目中的图像已成功复制、重命名并包含到 Android 项目中。

图 1。图像已导入到资源目录。

使用 T4 将图像导入 iOS 项目

设置 iOS 项目以导入共享图像的过程与 Android 项目几乎相同。但是,有两个不同之处:iOS 的图像目录是 /Resources,并且“生成操作”参数必须指定为 BundleResource。Images.tt 的内容应如下所示:

<#@ include file="..\ResourcesModel\T4Templates\Images.ttinclude" #>
<#@ output extension=".txt" #>
<#@ template language="C#" hostSpecific="true" #><#

Process("CalciumSampleApp", "BundleResource");

#>

幕后:使用 T4 导入图像

生成图像的逻辑包含在 Images.ttinclude 文件的 Process 方法中。该方法使用 Visual Studio DTE 对象遍历解决方案中的项目和文件。

在 Process 方法中,DTE 的检索方式如下:

IServiceProvider hostServiceProvider = (IServiceProvider)Host;
EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));

该方法使用一个名为 GetProjects 的方法检索解决方案的所有项目。项目列表的检索是一个递归过程,因为项目可能位于解决方案文件夹内。

在获得项目列表后,Process 方法通过迭代解决方案中的项目来尝试定位图像所在的项目。

var solution = dte.Solution;
var items = GetProjects();

foreach (EnvDTE.Project item in items)
{     
    if (item.Name.EndsWith(projectName))
    {
        project = item;
        break;
    }
}

GetFiles 函数使用尾部递归来检索具有指定文件扩展名的所有文件。请参见列表 4。

列表 4。Images.ttinclude GetFiles 函数

void GetFiles(ProjectItem projectItem, string fileExtension, IList<string> fileList)
{
    string fullPath = projectItem.Properties.Item("FullPath").Value.ToString();

    if (fullPath.ToLower().EndsWith(fileExtension))
    {
        fileList.Add(fullPath);
    }

    var childItems = projectItem.ProjectItems;

    if (childItems != null)
    {
        foreach (ProjectItem childItem in childItems)
        {
            GetFiles(childItem, fileExtension, fileList);  
        }
    }
}

Process 函数使用 GetFiles 函数填充一个包含 PNG 和 JPG 文件路径的列表,如下所示:

var fileExtensions = new string[] { ".png", ".jpg" };

var fileList = new List<string>();
var projectItems = project.ProjectItems;

if (projectItems != null)
{
    foreach (ProjectItem projectItem in project.ProjectItems)
    {
        foreach (string extension in fileExtensions)
        {
            GetFiles(projectItem, extension, fileList);
        }
    }
}

然后,Process 函数通过用下划线替换路径分隔符字符来为每个图像路径创建一个扁平化的名称。请参见列表 5。如果图像已作为资源存在,则会比较两个文件的 LastWriteTime 值。如果原始图像的 LastWriteTime 值较旧,则无需执行任何操作。此步骤可避免不必要地从源代码管理中检出文件。

列表 5。Images.ttinclude 在 Process 函数中复制文件

string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);           

foreach (string item in fileList)
{
    if (!item.StartsWith(projectDirectory))
    {
        continue;
    }

    string fileNameInProject = Path.GetFileName(item);
    string fileDirectory = Path.GetDirectoryName(item);
    string pathSubstring = fileDirectory.Substring(projectDirectoryLength);

    if (pathSubstring.StartsWith("\\"))
    {
        pathSubstring = pathSubstring.Substring(1);
    }

    string flattenedPathSubstring = pathSubstring.Replace("\\","_");

    if (flattenedPathSubstring.Length > 0)
    {
        flattenedPathSubstring = flattenedPathSubstring + "_";
    }

    string flattenedFileName = flattenedPathSubstring + fileNameInProject;
    string newOutputPath = Path.Combine(templateDirectory, flattenedFileName);

    if (File.Exists(newOutputPath))
    {
        var projectFileInfo = new System.IO.FileInfo(item);
        var newFileInfo = new System.IO.FileInfo(newOutputPath);

        if (projectFileInfo.LastWriteTimeUtc < newFileInfo.LastWriteTimeUtc)
        {
            continue;
        }
    }

    File.Copy(item, newOutputPath, true);
    ...
}

最后,使用 _MultiFileOutput.ttinclude_ 文件中的 AddProjectItem 函数将项目添加到项目中,如下面的摘录所示:

       AddProjectItem(flattenedFileName, buildAction);

感谢 Oleg Sych 的有益文章 关于从单个 T4 模板生成多个输出。 

如果您阅读了上一篇文章 第 4 部分:使用 Xamarin Forms 和 Calcium 创建跨平台应用程序栏,您可能还记得我提到图标图像是使用 IImageUrlTransformer 解析的。在列表 6 中,您再次看到 TransformForCurrentPlatform 如何返回一个与 Images.tt 模板的扁平化输出兼容的图像 URL。

列表 6。使用 IImageUrlTransformer 解析图标图像。

var uri = appBarItem.IconUri;

if (uri != null)
{
    string url = uri.ToString();
    string transformedUrl = imageUrlTransformer.TransformForCurrentPlatform(url);
    item.Icon = transformedUrl;
}

注意。有时,在构建项目时,Visual Studio 可能会抱怨文件 x 已定义。请确保 T4 (.tt) 模板的“生成操作”设置为 none。Visual Studio 有时会更改这些文件的“生成操作”。

结论

图像在各种平台上的解析方式存在差异,这给 Xamarin Forms 中图像的放置位置带来了一些限制。通过使用本文提供的技术,您可以绕过这些限制;为您提供 Windows Phone 资源系统的灵活性,同时仍保留与 iOS 和 Android 的兼容性。

在本文中,您了解了如何在项目之间以统一的方式共享和使用图像文件。您了解了如何使用 T4 模板将图像文件从共享项目复制到 iOS 和 Android 资源目录;将它们添加到 iOS 和 Android 项目;并设置图像的“生成操作”。最后,您了解了如何使用自定义标记扩展来转换图像资源 URL,以便图像可以在 XAML 中引用并正确解析,无论平台如何。

我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。

在下一篇文章中,您将探索 Calcium 的用户选项系统,该系统允许您通过一行代码定义一个选项。然后,该选项将自动显示在应用程序的选项视图中,当用户修改该选项时,选项系统会自动保留更改。

历史

2014 年 11 月

  • 首次发布。

 

© . All rights reserved.