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

Xamarin TV 脚本

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2018 年 3 月 31 日

CPOL

13分钟阅读

viewsIcon

21785

downloadIcon

866

使用 Xamarin Forms 和 Android,以 C# 语言编写的娱乐应用程序,使用了 SQLite 本地数据库

引言

您是否非常喜欢看老电视剧,以至于想随身携带?而且,您是否愿意静静地阅读剧集,而不是听?

在这篇文章中,我将介绍 Xamarin TV Scripts,一款简单的 Xamarin Forms 娱乐应用程序。如果您感兴趣,请下载源代码并尽情观看!

背景

这是我一直想做的一款应用程序。不是从零开始,而是移植了我曾经在另一个平台(Windows Phone)上应用过的概念。

很久以前,在 2011 年,我在 Code Project 上发表了一篇名为 The Big Bang Transcripts Viewer 的文章,在其中我探索了我新获得的 Windows Phone 7 设备的神奇之处。

图:The Big Bang Transcript Viewer - Windows Phone 的美好时光

我曾认为 Windows Phone 有巨大的增长潜力,并且在一段时间内确实如此。对于用户来说,界面快速、时尚且独特。尽管在功能方面总是落后于 Android 和 iOS,但其体验仍然不错。对于开发者来说,它提供了 Visual Studio IDE、.NET Framework 和出色的模拟器的便利性。但不幸的是,许多制造商仍然不愿意在其“旗舰”设备型号上采用该平台,这些型号留给了 Android 平台。此外,Windows Phone 缺乏一些最关键的应用程序和游戏,即使在发布多年后,WinPhone 在美国移动市场也从未获得多少吸引力。

进入 Xamarin Forms

对于从 Windows Phone 开发迁移到跨平台开发的开发者来说,Xamarin(尤其是 Xamarin Forms)是一个自然而然的下一步。

Xamarin Forms 的开发与 Windows Forms 有一些相似之处,并且我能从中受益;两者都可以使用 Visual Studio 和 C# 语言进行开发,并且用户界面可以使用 XAML 文件设计。这两个平台都是以 MVVM 模式为中心设计的,这意味着它们都共享视图模型和绑定(binding)的概念。此外,首选的事件通知技术基于发布/订阅消息传递。

当然,Windows Phone 和 Xamarin Forms 开发之间的差异也必须相应地解决。由于 Xamarin Forms 是一个跨平台框架,您不能简单地访问设备的​​文件系统来读取或写入文件,或将数据保存/检索到本地数据库。相反,每个平台(Android、iOS 等)都有自己的文件系统 API、数据库 API 等。这就是为什么我需要找出如何从 Xamarin Forms(.NET Standard)项目中调用 Xamarin Android 的设备级别 API。

解决方案

图:.NET Standard 和 Xamarin Android 项目

该解决方案基于 Visual Studio 2017 构建,像大多数 Xamarin 解决方案一样,它包含 2 个项目:一个用于目标设备的平台(Android),另一个是 .NET Standard 2.0 项目。

创建新的 Xamarin 解决方案时,会显示一组选项:平台UI 技术代码共享策略

由于为 iOS 开发需要 Mac OS 机器(我没有)而 Windows(通用 Windows 平台)仅适用于 Windows Phone,所以我只想创建一个 Android 应用,因此我取消了 iOS 和 Windows 复选框。

至于 UI 技术,Xamarin Forms 提供了丰富的组件集,我认为如果尝试使用 Android 原生 UI 技术 Android 来重新创建它们,我会遇到麻烦。

组件类型 Components
Xamarin Forms 视图 ActivityIndicatorBoxViewButtonDatePickerEditorEntryImageLabelListViewOpenGLViewPickerProgressBarSearchBarSliderStepperSwitchTableViewTimePickerWebView

Xamarin Forms 页面

ContentPage、TabbedPage、MasterDetailPage、NavigationPage、Carousel、TemplatedPage
Xamarin Forms 布局 AbsoluteLayoutContentPresenterContentViewFrameGridRelativeLayoutScrollViewerStackLayoutTemplatedLayout
Xamarin Forms 单元格 EditorCell、EntryCell、ImageCell、SwitchCell

.NET Standard 项目类型取代了旧的 PCL(可移植类库),成为 .NET 项目的事实上的标准跨平台项目类型。也就是说,您可以与 .NET Framework、.NET Core 和 Xamarin 应用共享 .NET Standard 程序集。“Xamarin,TVScripts.csproj” .NET Standard 项目不仅包含 Xamarin Forms 依赖项,还包含添加 Xamarin Forms 项目新组件和资源的模板。

在上图中,我们可以看到 Xamarin.Forms.Xaml.dll 程序集,它附带 Xamarin.Forms 程序集。该程序集使我们能够使用 XAML(eXtensible Markup Language)设计用户界面,XAML 是 Microsoft 为用户界面和其他应用程序开发的声明式 XML 文档语言。

但我们的 Xamarin Forms 应用程序何时开始呢?

由于 Xamarin.TVScripts.Android 必须是解决方案的启动项目,因此它最终会将应用程序的控制权传递给 Xamarin.TVScripts .NET Standard 代码。它通过 MainActivity 类来实现这一点,该类实际上是应用程序的入口点,更具体地说是在 OnCreate 方法中。

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    ...
    protected override void OnCreate(Bundle bundle)
    {
        TabLayoutResource = Resource.Layout.Tabbar;
        ToolbarResource = Resource.Layout.Toolbar;
        UserDialogs.Init(this);
        base.OnCreate(bundle);

        global::Xamarin.Forms.Forms.Init(this, bundle);
        LoadApplication(new App());
    }
}
列表:MainActivity 类

LoadApplication(new App()); 代码将加载并运行应用程序,该应用程序驻留在另一个(.NET Standard)应用程序中。

现在我们可以谈谈 App 类了。

App 类位于 App.Xaml.cs 文件,这是 App.Xaml 的代码隐藏文件。尽管扩展名为 xaml,但此文件不代表视图,而是 XAML 语言中的 Application 类。

回到 App.Xaml.cs 文件:它实现了 Xamarin.Forms.pplication 超类。应用程序启动时,它必须定义 App 类的 MainPage 属性。然后 Xamarin Forms 将通过我们的自定义 XAML 页面启动导航流程。下面的代码实例化一个新的 NavigationPage,从 HomePage 开始。

public partial class App : Application
{
    public App ()
    {
        InitializeComponent();

        MainPage = new NavigationPage(new HomePage());
    }
}
列表:App 类

NavigationPage 是一个 Xamarin 组件,它将处理由我们自定义页面组成的堆栈的导航和用户体验。此“堆栈”保存导航历史记录,就像在 Web 浏览器中一样,允许我们在视图之间来回导航。

用户界面

我们的用户界面由 3 个视图组成:HomePageSeasonPageEpisodePage

HomePage 仅显示电视剧的封面图片,以及一个包含该剧集的 ListViewListView 是 Xamarin 组件,用作显示数据列表的视图,特别是需要滚动的长列表。

图:HomePage.xaml 中的封面图片和 ListView

ListView 可以用作简单的未格式化文本列表,或者像本例一样,您可以自定义它来显示一组样式化的项目。通过为 ListView.ItemTemplate 属性提供所需的布局(由各种 Xamarin 组件的排列表示)来替换默认的纯文本显示。

图:构建 ListView 的 ItemTemplate

由于季度的图片是方形的,我们可以通过使用 **图像蒙版** 来将其变成圆形图标,以此应用一个小技巧。

图:在图像顶部应用圆形蒙版

同样,SeasonPage 显示一个简单的剧集列表,并没有太多样式。每个剧集名称由包含圆角 Frame 组件的 TextBlock 定义。

图:SeasonPage.xaml 视图,显示第 2 季的剧集列表

另一方面,EpisodePage 的设计更为精心。它是迄今为止最重要的视图,用户将在应用程序中花费大部分时间。该视图与其他视图有相似之处,因为它使用了样式的 ListView 来显示数据集合(电视剧的台词),但它的实现方式非常定制化。每个台词都可以显示为以下三种样式之一。

  • 评论(无角色)
  • 左侧说话的角色
  • 右侧说话的角色

当在关联的 Quote 实例中找不到角色名称时,会显示剧集评论。否则,角色图片将显示在一侧,对话气泡显示在另一侧。角色的照片将显示在左侧或右侧,具体取决于引语编号是偶数还是奇数。这样我们可以轻松地在这两种模式之间切换。

角色的图片是通过将角色名称绑定到 Droid 项目的“drawable”文件夹中相应的图像来检索的。

对话气泡由两部分组成:一个包含 TextBlock 的圆角 Frame,以及一个紧邻它的三角形图像。这给我们留下了“**对话气泡**”的印象。

图:EpisodePage.xaml 视图,显示剧集的对话

在下图中,我们可以识别出 3 种不同类型的“卡片”。第一个称为**旁白卡**,通常代表场景描述,不是由角色说的。另外两个称为**奇数卡**和**偶数卡**,是角色说的脚本,并以交替的方式显示。

图:EpisodePage.xaml 视图中的三种语音类型

每个**奇数/偶数卡**的布局由一个 2 列 Grid 定义,用于将图像与对话气泡分开。由于图像列仅占用 80 像素,对话气泡占用 Grid 中其余的空间。即使设备屏幕旋转到横向模式,它看起来也很棒。

图:语音卡片的结构

对话气泡很棘手。起初,我尝试实现某种矢量绘图,但 Xamarin 不支持。然后,我最终组合了一个圆形框架和一个包含三角形的图像,效果很好。

图:语音气泡中的 XAML 代码

绑定

Xamarin Forms 完全基于 MVVM(*模型-视图-视图模型*)模式,因此您应该期望广泛使用**绑定**。由于 MVVM 将**视图**层与其显示**数据**解耦,**绑定**充当将视图及其数据动态绑定在一起的**胶水**,因此数据的任何更改都应反映在视图端。

在下面的片段中,我们可以注意到 3 个绑定。

  • 标签的 Text 属性绑定到 Season 对象的 Name 属性。
  • 标签的 Style 属性绑定到 ListItemTextStyle 值。
  • 标签的 TextColor 属性绑定到 ListFontColor 值。
<Label Text="{Binding Name}" 
   LineBreakMode="NoWrap" 
   TextColor="{DynamicResource ListFontColor}"
   Style="{DynamicResource ListItemTextStyle}" 
   FontSize="16"
   VerticalOptions="Center"
   VerticalTextAlignment="Center"
   HorizontalOptions="StartAndExpand"/>
列表:HomePage.xaml 文件中 ListView 内的 Label 组件

如前所述,来自 Name 属性的数据来自 Season 类。

[Table("Seasons")]
public class Season : BaseModel
{
    public Season() { }

    public Season(int seasonNumber, string name)
    {
        SeasonNumber = seasonNumber;
        Name = name;
    }

    public int SeasonNumber { get; set; }
    public string Name { get; set; }

    [Ignore]
    public string ImageSource => $@"_season{SeasonNumber}.jpg";
}
列表:模型中的 Season 类

另一方面,TextColor 不是来自 Season 类,而是由 App.xaml 文件中的 ResourceDictionary 包含的一个值提供。

<?xml version="1.0" encoding="utf-8" ?>
<Application ...>
    <Application.Resources>
        <ResourceDictionary>
            ...
            <Color x:Key="ListBackGroundColor">#FFFFFF</Color>
            ...
        </ResourceDictionary>
    </Application.Resources>
</Application>
列表:App.xaml 文件

这是因为我们将此绑定显式定义为 DynamicResourceTextColor="{DynamicResource ListFontColor}").

文件

应用程序数据由 3 种类型的文件组成:剧集文件、剧集文件和台词文件(每个剧集都有自己的单独台词文件)。

1    Season 1
2    Season 2
3    Season 3
4    Season 4
.    .
.    .
.    .
列表:seasons.txt 文件
.    .     .
.    .     .
.    .     .
2    23    The One With The Chicken Pox
2    24    The One With Barry and Mindy's Wedding
3    1     The One With The Princess Leia Fantasy
3    2     The One Where No One's Ready
3    3     The One With All The Jam
.    .     .
.    .     .
.    .     .
列表:episodes.txt 文件

seasons.txtepisodes.txt 是纯文本文件,仅显示剧集和剧集的名称。

2    [Scene     Chandler's Office, Chandler is on a coffee break. Shelley enters.) Shelley
3    Chandler     Dehydrated Japanese noodles under fluorescent lights... does it get better than this?
4    Shelley     Question. You're not dating anybody, are you, 
                 because I met somebody who would be perfect for you.
5    Chandler     Ah, y'see, perfect might be a problem. Had you said 'co-dependent', 
                  or 'self-destructive'...
6    Shelley     Do you want a date Saturday?
7    Chandler     Yes please.
列表:0108.txt 文件(第 1 季,第 8 集)

另一方面,台词文件包含 3 列:台词序号、说话的角色和台词(引语)本身。

由于纯文本文件可以包含大量非结构化数据,因此查询它们通常很困难。如果我们能将它们的数据传输到设备上的本地数据库,那么查询和获取我们想要的数据就会更容易。

本地 SQLite 数据库

在本地关系数据库中,**SQLite** 是一个稳定可靠的选择。我们可以通过代码优先的方法生成本地 SQLite 数据库,即首先将实体(剧集、剧集、台词)实现为 C# 类,然后使用它们作为 SQLite 数据库架构的源。

纯文本文件在 Xamarin Droid 项目中访问,因为实现依赖于平台的​​文件系统 API。所以我们将文件访问方法直接放在 MainActivity 类中,该类包含可以从中访问文件的 assets 对象。就像文件访问一样,数据库访问也在 Droid 项目中实现。

遵循 Xamarin 文档指南,在 Xamarin.TVScripts.Android 项目中实现一个类来检索 SQLite 连接非常容易。

[assembly: Xamarin.Forms.Dependency(typeof(SQLite_android))]
namespace Xamarin.TVScripts.Droid
{
    class SQLite_android : ISQLite
    {
        private const string dbFileName = "TVScripts.db3";

        public SQLiteConnection GetConnection()
        {
            string dbPath = Path.Combine(
                System.Environment
                    .GetFolderPath(System.Environment.SpecialFolder.Personal),
                        dbFileName);

            return new SQLite.SQLiteConnection(dbPath);
        }
    }
}
列表:SQLite_android 类

请注意,SQLite_android 类实现了 ISQLite 接口(*class SQLite_android : ISQLite*),并且我们在命名空间上方使用了注解 *[assembly: Xamarin.Forms.Dependency(typeof(SQLite_android))]*。这是什么意思?具体类名和接口都用于 Xamarin Forms 内置的**依赖注入服务**,当从另一个项目调用数据库功能时。这特别有用,因为 Xamarin.TVScripts 项目不引用 Xamarin.TVScripts.Android 项目(SQLite_android 类驻留在其中,并且永远不会引用,因为它会导致交叉引用错误)。

那么,SQLite_android.GetConnection() 究竟是从哪里调用的?

Xamarin.TVScripts 项目包含一组**数据访问对象**类,它们实现了 SQLite 数据库的基本 CRUD(*创建*、*读取*、*更新*、*删除*)方法。这些类对应于我们模型的模型类(SeasonEpisodeQuote),并且它们继承自一个名为 BaseDAO<T> 的公共超类。每当数据访问类需要访问数据库时,它们首先通过基类中的 GetConnection 方法获取连接。

public class BaseDAO<T> where T : BaseModel, new()
{
    public BaseDAO()
    {
        using (SQLiteConnection connection = GetConnection())
        {
            connection.CreateTable<T>();
        }
    }

    public IList<T> GetList()
    {
        using (SQLiteConnection connection = GetConnection())
        {
            return new List<T>(connection.Table<T>());
        }
    }

    .
    .
    .
    protected SQLite.SQLiteConnection GetConnection()
    {
        return DependencyService.Get<ISQLite>().GetConnection();
    }
}
列表:BaseDAO<T> 类

请注意 BaseDAO<T> 是一个泛型类,它对 T 类型施加了约束。这提高了代码的可重用性,因为我们可以将许多方法放在基类中,否则这些方法将在派生类中成为不同的方法,只是在操作的模型实体方面有所不同。

现在,让我们看看 GetConnection() 方法。

protected SQLite.SQLiteConnection GetConnection()
{ 
   return DependencyService.Get<ISQLite>().GetConnection();
}
列表:BaseDAO<T>.GetConnection 方法

在这里,我们可以看到我们如何使用 DependencyService 类获取 ISQLite 实现。Get<T>() 方法将使 Xamarin 框架查找实现该接口的类并从中返回一个新实例。

文本到语音

该应用程序还提供了一项功能,可以使用 Android Playstore 提供的文本到语音应用程序的功能来朗读对话气泡中的文本。

[assembly: Xamarin.Forms.Dependency(typeof(Xamarin.TVScripts.Droid.TextToSpeech))]
namespace Xamarin.TVScripts.Droid
{
    public class TextToSpeech : Java.Lang.Object, ITextToSpeech, 
                                global::Android.Speech.Tts.TextToSpeech.IOnInitListener
    {
        global::Android.Speech.Tts.TextToSpeech speaker;
        string toSpeak;

        public void Speak(string text)
        {
            toSpeak = text;
            if (speaker == null)
            {
                speaker = new global::Android.Speech.Tts.TextToSpeech(Application.Context, this);
            }
            else
            {
                speaker.Speak(toSpeak, QueueMode.Add, null, null);
            }
        }

        public void OnInit(OperationResult status)
        {
            if (status.Equals(OperationResult.Success))
            {
                speaker.Speak(toSpeak, QueueMode.Add, null, null);
            }
        }
    }
}
列表:TextSpeech 类

SQLite_android 类一样,TextToSpeech 通过使用 Xamarin.Forms.DependencyAttribute 注解在 Xamarin 依赖注入容器中注册。

当对话气泡被点击时,ListView 将引发 OnItemSelected 事件,该事件反过来会调用文本到语音功能来朗读角色名称及其语音。

async void OnItemSelected(object sender, SelectedItemChangedEventArgs args)
{
    var quote = args.SelectedItem as Quote;
    if (quote == null)
        return;

    await Speak(quote.Character);
    await Speak(quote.Speech);
}
        
private async Task Speak(string text)
{
    await Task.Run(() =>
    {
        DependencyService.Get<ITextToSpeech>().Speak(text);
    });
}
列表:EpisodePage.xaml.cs 文件中用于文本到语音功能的客户端代码

这展示了如何利用(并扩展)您应用程序的功能,以帮助有阅读障碍的人(包括盲人和阅读障碍者)。而且,您无需在应用程序中开发复杂且次要的功能即可获得这些结果。现在您可以节省时间来开发应用程序的关键代码。

结论

如果您读到了这里,非常感谢您的关注和耐心。我确实认为 Xamarin Forms 为 Visual Studio/C# 开发者带来了许多 Android 开发的乐趣,不仅可以用于严肃的应用,还可以将像我这样的疯狂想法变为现实。我希望其中一些能激发您尝试新事物。

那么,您喜欢这篇文章和代码吗?我期待看到您的想法。请在下面的评论部分给我反馈!

历史

  • 2018-03-31:初版
© . All rights reserved.