Xamarin TV 脚本





5.00/5 (13投票s)
使用 Xamarin Forms 和 Android,以 C# 语言编写的娱乐应用程序,使用了 SQLite 本地数据库
引言
您是否非常喜欢看老电视剧,以至于想随身携带?而且,您是否愿意静静地阅读剧集,而不是听?
在这篇文章中,我将介绍 Xamarin TV Scripts,一款简单的 Xamarin Forms 娱乐应用程序。如果您感兴趣,请下载源代码并尽情观看!
背景
这是我一直想做的一款应用程序。不是从零开始,而是移植了我曾经在另一个平台(Windows Phone)上应用过的概念。
很久以前,在 2011 年,我在 Code Project 上发表了一篇名为 The Big Bang Transcripts Viewer 的文章,在其中我探索了我新获得的 Windows Phone 7 设备的神奇之处。
我曾认为 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。
解决方案
该解决方案基于 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 视图 | ActivityIndicator 、BoxView 、Button 、DatePicker 、Editor 、Entry 、Image 、Label 、ListView 、OpenGLView 、Picker 、ProgressBar 、SearchBar 、Slider 、Stepper 、Switch 、TableView 、TimePicker 、WebView |
Xamarin Forms 页面 | ContentPage、TabbedPage、MasterDetailPage、NavigationPage、Carousel、TemplatedPage |
Xamarin Forms 布局 | AbsoluteLayout 、ContentPresenter 、ContentView 、Frame 、Grid 、RelativeLayout 、ScrollViewer 、StackLayout 、TemplatedLayout |
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());
}
}
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());
}
}
NavigationPage
是一个 Xamarin 组件,它将处理由我们自定义页面组成的堆栈的导航和用户体验。此“堆栈”保存导航历史记录,就像在 Web 浏览器中一样,允许我们在视图之间来回导航。
用户界面
我们的用户界面由 3 个视图组成:HomePage
、SeasonPage
和 EpisodePage
。
HomePage
仅显示电视剧的封面图片,以及一个包含该剧集的 ListView
。ListView
是 Xamarin 组件,用作显示数据列表的视图,特别是需要滚动的长列表。
ListView
可以用作简单的未格式化文本列表,或者像本例一样,您可以自定义它来显示一组样式化的项目。通过为 ListView.ItemTemplate
属性提供所需的布局(由各种 Xamarin 组件的排列表示)来替换默认的纯文本显示。
由于季度的图片是方形的,我们可以通过使用 **图像蒙版** 来将其变成圆形图标,以此应用一个小技巧。
同样,SeasonPage
显示一个简单的剧集列表,并没有太多样式。每个剧集名称由包含圆角 Frame
组件的 TextBlock
定义。
另一方面,EpisodePage
的设计更为精心。它是迄今为止最重要的视图,用户将在应用程序中花费大部分时间。该视图与其他视图有相似之处,因为它使用了样式的 ListView
来显示数据集合(电视剧的台词),但它的实现方式非常定制化。每个台词都可以显示为以下三种样式之一。
- 评论(无角色)
- 左侧说话的角色
- 右侧说话的角色
当在关联的 Quote
实例中找不到角色名称时,会显示剧集评论。否则,角色图片将显示在一侧,对话气泡显示在另一侧。角色的照片将显示在左侧或右侧,具体取决于引语编号是偶数还是奇数。这样我们可以轻松地在这两种模式之间切换。
角色的图片是通过将角色名称绑定到 Droid 项目的“drawable”文件夹中相应的图像来检索的。
对话气泡由两部分组成:一个包含 TextBlock
的圆角 Frame
,以及一个紧邻它的三角形图像。这给我们留下了“**对话气泡**”的印象。
在下图中,我们可以识别出 3 种不同类型的“卡片”。第一个称为**旁白卡**,通常代表场景描述,不是由角色说的。另外两个称为**奇数卡**和**偶数卡**,是角色说的脚本,并以交替的方式显示。
每个**奇数/偶数卡**的布局由一个 2 列 Grid
定义,用于将图像与对话气泡分开。由于图像列仅占用 80 像素,对话气泡占用 Grid 中其余的空间。即使设备屏幕旋转到横向模式,它看起来也很棒。
对话气泡很棘手。起初,我尝试实现某种矢量绘图,但 Xamarin 不支持。然后,我最终组合了一个圆形框架和一个包含三角形的图像,效果很好。
绑定
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"/>
如前所述,来自 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";
}
另一方面,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>
这是因为我们将此绑定显式定义为 DynamicResource
(TextColor="{DynamicResource ListFontColor}").
。
文件
应用程序数据由 3 种类型的文件组成:剧集文件、剧集文件和台词文件(每个剧集都有自己的单独台词文件)。
1 Season 1
2 Season 2
3 Season 3
4 Season 4
. .
. .
. .
. . .
. . .
. . .
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
. . .
. . .
. . .
seasons.txt 和 episodes.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.
另一方面,台词文件包含 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
类实现了 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(*创建*、*读取*、*更新*、*删除*)方法。这些类对应于我们模型的模型类(Season
、Episode
和 Quote
),并且它们继承自一个名为 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>
是一个泛型类,它对 T
类型施加了约束。这提高了代码的可重用性,因为我们可以将许多方法放在基类中,否则这些方法将在派生类中成为不同的方法,只是在操作的模型实体方面有所不同。
现在,让我们看看 GetConnection()
方法。
protected SQLite.SQLiteConnection GetConnection()
{
return DependencyService.Get<ISQLite>().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);
}
}
}
}
与 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);
});
}
这展示了如何利用(并扩展)您应用程序的功能,以帮助有阅读障碍的人(包括盲人和阅读障碍者)。而且,您无需在应用程序中开发复杂且次要的功能即可获得这些结果。现在您可以节省时间来开发应用程序的关键代码。
结论
如果您读到了这里,非常感谢您的关注和耐心。我确实认为 Xamarin Forms 为 Visual Studio/C# 开发者带来了许多 Android 开发的乐趣,不仅可以用于严肃的应用,还可以将像我这样的疯狂想法变为现实。我希望其中一些能激发您尝试新事物。
那么,您喜欢这篇文章和代码吗?我期待看到您的想法。请在下面的评论部分给我反馈!
历史
- 2018-03-31:初版