我的第一个练手项目,便签本的基本增删改查功能终于基本完成了。在这段时间里,虽然踩了不少坑,但最后应用终于按照预期运行起来的时候,真的有种柳暗花明的感觉。在进行下一阶段的开发之前,我打算先小结一下这段时间里踩的一些坑以及解决方式。
一、RelativeLayout
1. 浮动按钮(Floating Action Button)
Xamarin.Forms 目前没有自带 FAB 控件,因此参考 Stackoverflow 上的回答后,我选择了使用 RelativeLayout + ImageButton 自行实现。
<RelativeLayout> <!-- Your other codes --> <ImageButton x:Name="AddNoteButton" Clicked="AddNoteButton_Clicked" Source="addFAB.png" BackgroundColor="Transparent" WidthRequest="60" HeightRequest="60" RelativeLayout.XConstraint= "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=1, Constant=-80}" RelativeLayout.YConstraint= "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=1, Constant=-80}" /> </RelativeLayout>
浮动按钮需要放置在 AbsoluteLayout 或 RelativeLayout 中来实现固定在右下角的效果,我选择了 RelativeLayout,因为看起来它能让控件更方便地以屏幕右侧和底部为基准进行绝对值定位。
RelativeLayout 的子元素使用 XConstraint 和 YConstraint 设置坐标,在本例中,ImageButton 的 X 坐标被设置为父元素宽度减去80,Y 坐标设置为父元素高度减去80,即固定在屏幕右下角。
按钮图标使用了在线生成器,这个网站可以在线生成圆形的 Material design 风格图标。将下载下来的 png 图片放入 Android 项目下的 drawable 文件夹内,设置好宽高并将背景颜色设置为透明,就完成了。
不过这里还有一个问题,ImageButton 背景色设置为透明将无法使用 “Android 上的 ImageButton 投影”,要使用此自带投影效果,必须为 ImageButton 设置背景色。
2. RelativeLayout 中子视图的宽高设置
将 ListView 的上层换为 RelativeLayout 后,ListView 的尺寸失去了控制,长度超出屏幕,横屏后宽度也不会自动填满。检查文档后才发现 RelativeLayout 的子元素不支持使用 HorizonOptions 和 VerticalOptions 设置,而是使用 WidthConstraint 和 HeightConstraint。修改后一切回归正常。
<ListView x:Name="NotesListView" RelativeLayout.WidthConstraint= "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=1, Constant=0}" RelativeLayout.HeightConstraint= "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=1, Constant=0}"> </ListView>
二、列表视图自动更新
Xamarin.Forms 提供了数据绑定功能。数据绑定将两个对象的属性链接起来,如此,对某一属性的更改将自动反映在另一个属性中。利用数据绑定,可以实现新增、删除或修改便签项后,便签列表自动更新。
我原本的做法,是列表视图显示时读取 SQLite 数据库,将读取到的便签列表存入 List<Note> 类型的列表 notesList 中,并将这个 notesList 设置为 ListView 的数据源。对便签项进行修改操作时,直接对数据库操作。但是这样操作数据库过后,便签列表视图是不会自动更新的,因为 SQLite 不会主动通知数据库中的数据变动。我尝试过每次加载视图时读取一次数据库到 notesList 中,也试过每次修改便签同时修改 notesList 中的对应元素,都没有成功。
这时,我找到了 ObservableCollection,它能够提供我所需的动态更新 UI 能力。
表示一个动态数据集合,在添加项、移除项或刷新整个列表时,此集合将提供通知。
若要设置动态绑定, 以便集合中的插入或删除操作自动更新 UI, 该集合必须实现 INotifyCollectionChanged 接口。 此接口公开 CollectionChanged 事件,即每当基础集合发生更改时应引发的事件。
我计划用 ObservableCollection<Note> 替换原本的 List<Note>,不过从数据库读取出来的是 List<T> 类型,无法隐式自动转换为 ObservableCollection<T> 类型,因此需要先取数据到 List<T> 类型的临时列表中,再手工遍历到 ObservableCollection<T> 类型的集合里。因为 SQLite 不会通知数据更改,所以每次修改便签都要同时对数据库和集合进行操作。
至此,便签列表视图 UI 已经能够随着便签的新增和删除自动更新了。但是直接修改便签内容还不会触发更新。我们需要在描述便签的 Note 类中实现 INotifyPropertyChanged 接口,它会向客户端发出某一属性值已更改的通知。这样就完成了。
三、ListView 滑动后刷新数据产生空白 Cell 问题
这似乎是一个 bug,目前将 ListView 的缓存策略设置为 RecycleElement 即可解决。
<ListView x:Name="NotesListView" Refreshing="NotesListView_Refreshing" IsPullToRefreshEnabled="True" CachingStrategy="RecycleElement"> </LIstView>
四、检测并删除空白项
当一条便签内容为空时,应该删除这条便签而不是保留一条空白便签(部分原因是我目前没有做撤销功能的计划,因此留着空便签并没有意义)。因此我添加了两条检测逻辑:若是修改已有便签,在点击保存按钮时,如果输入文本框 Editor 的内容 Editor.Text 为空字符串,则转为删除动作;若是在新建便签页,则仅当 Editor.Text 不为空字符串时保存此便签。前一种情境正常运行了,后一种却出现了奇怪的问题。
通过新建按钮进入新建便签页面时,如果不进行任何输入就按下保存的话,就会绕过检测逻辑保存一条空内容的便签。再次进入这条便签,输入任意内容然后清空的话,点击保存按钮会正常执行删除;但如果依然不进行任何输入的话,点击保存按钮也依然不会删除这条便签。
经过排查,最后推测是新建便签没有输入过任何文本时,Editor.Text 没有被赋予初始值,或是初始值不为 “” 或 null。提前给 Editor.Text 赋一个初始值就正常了。
protected override async void OnAppearing() { base.OnAppearing(); if (noteID != 0) { BindingContext = await AppShell.Database.GetNoteAsync(noteID); } else { BindingContext = new Note(); Editor.Text = ""; } } async void SaveNoteButton_Clicked(object sender, EventArgs e) { var note = (Note)BindingContext; note.Date = DateTime.Now; if (noteID != 0) { var noteToUpdate = AppShell.NoteCollection .Where(x => x.ID == note.ID) .FirstOrDefault(); int noteToUpdateIndex = AppShell.NoteCollection.IndexOf(noteToUpdate); if (Editor.Text != "" ) { await AppShell.Database.SaveNoteAsync(note); AppShell.NoteCollection[noteToUpdateIndex] = note; } else { //如果文本为空,则删除此项 await AppShell.Database.DeleteNoteAsync(note); AppShell.NoteCollection.Remove(noteToUpdate); } } else { if (Editor.Text != "") { await AppShell.Database.SaveNoteAsync(note); AppShell.NoteCollection.Add(note); } } await Shell.Current.Navigation.PopAsync(); }
追记:后来才想起来 C# 有个 String.IsNullOrEmpty 可以用……这个项目已经被我删系统时手快一起删掉了,暂时也懒得再开,不清楚这里是否也可以这样判断。