需求
网上买菜的微信小程序,其典型界面是左侧一个列表显示商品分类,右侧一个列表,显示商品明细。左侧列表要显示当前选中的是哪条分类记录(高亮这条记录)。右侧列表滑到底部后,往上再滑一次,自动切换到下一个分类,此时左侧列表的选中画面也同时要更新(高亮下一条记录)。
使用 Delphi 的实现
使用 Delphi 做手机 APP,不管是安卓还是 iOS,都是同一套代码,也就是采用 Delphi 的 FireMonkey 框架。
在 FireMonkey 框架下,要显示列表,比较常见的是 TListBox 控件,和 TListView 控件。
这里,采用 TListView 控件,比较简单。
实现方法
首先搞定数据。不管数据从哪里来,比如现在流行的是服务器有 REST 接口,那么,Delphi 程序可以调用 REST 接口获得 JSON 数据。具体做法这里不讨论。
获得数据以后,把数据放进 TFdMemTable 里面,用这个内存表来维护数据。
这里因为有一个分类数据和一个具体商品数据,那么,数据结构大概是:
分类数据:
分类编号,类名称
商品数据:
分类编号,商品编号,商品名称
内存表
那么,拖两个 TFdMemTable 到界面上(当然,应该放到一个 DataModule 里面去,这里仅仅是描述界面实现方法,所以就直接放界面上了),一个 FdMemTable1 用来存放分类数据,一个 FdMemTable2 用来存放商品名称数据。为它创建好字段。然后,鼠标右键点击一个 FdMemTable,在下拉菜单里面选择 Edit DataSet 就会看到一个表格,在这个表格里面填入一些数据。两个 FdMemTable 都填入数据,在设计期,你就已经有数据了。程序运行后,也能看到数据。这样不需要等写完从服务器拉数据的代码,就能看到界面效果。
界面
拖两个 ListView 到界面上,第一个 ListView1 的 Align 设置为 Left,让它占满屏幕左侧;第二个 ListView2 的 Align 设置为 Client,让它占满屏幕剩下部分。要想界面美观,设置一下这两个 ListView 的 Margin 的 Left 和 Right 的数字,从默认的 0 改为 2.
鼠标右键点击随便哪个 ListView ,在弹出来的下拉菜单,选择:Bind Visually,Delphi 的 IDE 会出现 Live Bindings Designer 窗口。在这个窗口里面,对 ListView 和 FdMemTable 拉线,实现在设计期的数据绑定。 这时候,已经可以在设计期的界面上看到 ListView 显示之前在 FdMemTable 里面输入的数据了。运行程序,确实能看到界面数据。
调试
在 Delphi 里面开发手机 APP,不必每次都把程序运行到手机上去调试看运行效果。可以选择编译目标是 WIN32,点 Delphi 界面上的绿色三角按钮(播放按钮,也就是运行程序的按钮),直接就在 Windows 里面看到这个程序编译为 Windows 程序运行起来了。同样也能看到 ListView 的效果。这样做比编译发布运行到手机上,时间短很多。
界面效果
1. 左侧分类的 ListView1 上面,用户选择(触摸,或者鼠标点击)一条记录,右侧 ListView2 上面要显示对应的分类的商品,而不是显示全部商品;同时左侧要高亮选中的分类条目;
2. 用户在右侧往上划动,右侧商品列表网上滚动,当滚动到最底一条时,用户再次往上划,左侧分类自动走到下一条分类,右侧的 ListView 显示对应的新的分类的商品列表。
界面效果实现
1. 把两个内存表做成主从表。这样,当主表的当前游标指向某条记录,从表跟随主表,自动被过滤,只能看到从表对应的主表的记录;
2. ListView2 滑到底部后,如何自动让分类表也就是主表走到下一条?
一. 主从表的实现
Delphi 里面,两个或者多个 DataSet 可以实现主次关系。实现起来非常简单,网上很多文章有讲到。这里只简单说一下:
1. 拖一个 DataSource1 到界面上,设置它的 DataSet 属性为 FdMemTable1(也就是商品分类表,主表);
2. 选择 FdMemTable2 商品列表,在属性窗口里面找到三个属性:
2.1. MasterSource,设置为 DataSource1;
2.2. MasterField,设置为【商品分类编号】字段的名字;
2.3. IndexFieldNames,设置为【商品分类编号】字段的名字。
到此搞定这两个表的主从表关系。
二. 显示商品列表的从表的 ListView2 滑到底
ListView2 滑到底,就执行 FdMemTable1.Next,则 ListView1 上显示的高亮记录自动走到下一条。如果分类走到底,就让它自动回到第一条:
with FdMemTable1 do
begin
Next;
if Eof then First;
end;
但是,我怎么知道这个 ListView2 已经滑到底了?
FireMonkey 的 TListView 有个下拉刷新的功能。但这个是下拉,而不是上划。
如何判断 ListView 的条目往上滚动,已经滚动到最后一条?代码如下:
MyBottom := Self.GetBottomValueByItemCount(FdMemTable2.RecordCount);
if (ListView2.ScrollViewPos = MyBottom) or (MyBottom <= 0) then
begin
//说明滚动到底了
end;
function TForm1.GetBottomValueByItemCount(const ACount: Integer): Integer;
begin
//当前 ListView2.ScrollViewPos 的最大值,滚动到底部的位置
//滚动到最底部的位置就是滚动到最底部时的 ListView2.ScrollViewPos 这个值;
//最底部时,ListView2.ScrollViewPos 刚好等于【条数 X 条高 - ListView 自己的高度】
//如果条数不够,没装满,则这里计算出来的值就是个负数值。刚好装满,则计算出来的值应该是 0 ;
Result := Trunc(Listview2.ItemAppearance.ItemHeight * ACount - ListView2.Height);
end;
这个已经滑到底的代码,在哪里执行?
把它放到上划手势的事件里面。
手势操作
1. 拖一个 GestureManager1 到界面上,选择 ListView2 的属性面板里面的:Touch -- Gesture Manager 属性,下拉选择为这个 GestureManager1。
2. ListView2 属性面板里面的 Touch -- Gestures -- Standard -- 勾选 Up,这样当你在 APP 里面上划,就能触发 ListView2 的 OnGesture 事件。
3. 在 ListView2 的 OnGesture 事件里面写代码,把上述判断是否到底的代码填进去。
到这里,需求里面描述的效果,已经做出来了。如果是在 Windows 底下测试,你用鼠标拉右侧 ListView2 的滚动条,拉到最底(这里是往下拉),然后,鼠标到 ListView2 里面,按住左键往上移动鼠标,同样会触发手势的 Up 操作,就能看到测试结果。
这时候,可以把安卓手机插到电脑上,然后在 Delphi 里面直接选择编译目标为这个安卓手机,点击绿色三角形运行箭头,等 Delphi 执行完编译和下载安装 APP,就能看到手机上这个 APP 跑起来了。能够看到两个 ListView 也能看到里面的数据。滑动,确实有效果。
优化
这里还有两个问题,让用户体验没那么好。
1. 手指往上划,ListView2 滚动到底部,还没来得及看清楚最后一条,它已经切换分类,显示另外一个分类的商品了;
2. 切换太快,没留意到它已经切换了。
解决办法
一. 不要滑到底就切换
而是滑到底,能够停下来让用户看清楚商品列表。当用户再次往上划,才切换。
实现方法:增加一个布尔标志变量,第一次滑到底时它是 False,而 False 时,它不执行切换分类的代码;
二. 切换放慢
模仿那些网页版 APP 刷新数据要从远程服务器重新获取数据,有点慢,转个圈,也就是转圈提示用户,商品分类在切换中,商品列表正在更新。
实现方法:拖一个转圈的控件(AniIndicator1)到界面上,设置它的 Visible 为 False,不要显示。然后在切换商品分类的时候,显示这个转圈,并且暂停1秒到2秒,然后再切换。暂停程序不能让界面冻结,所以暂停2秒要放到一个线程里面去执行。这里我使用 TTask 来执行。
切换到完整代码如下:
function TForm1.GetBottomValueByItemCount(const ACount: Integer): Integer;
begin
//当前 ListView2.ScrollViewPos 的最大值,滚动到底部的位置
//滚动到最底部的位置就是滚动到最底部时的 ListView2.ScrollViewPos 这个值;
//最底部时,ListView2.ScrollViewPos 刚好等于【条数 X 条高 - ListView 自己的高度】
//如果条数不够,没装满,则这里计算出来的值就是个负数值。刚好装满,则计算出来的值应该是 0 ;
Result := Trunc(Listview2.ItemAppearance.ItemHeight * ACount - ListView2.Height);
end;
procedure TForm1.ListView2Gesture(Sender: TObject; const EventInfo:
TGestureEventInfo; var Handled: Boolean);
var
MyBottom: Integer;
begin
//('手势');
MyBottom := Self.GetBottomValueByItemCount(FdMemTable2.RecordCount);
if (ListView2.ScrollViewPos = MyBottom) or (MyBottom <= 0) then
begin
if not FGoBottom then
begin
FGoBottom := not FGoBottom;
Exit; //第一次拉到底,不要走。第二次拉到底,才算用户要刷新。
end;
//这样直接切换,太突然,视觉上感觉不好。可以考虑切换代码放到一个动画结束后。
TTask.Run(
procedure
begin
TThread.Synchronize(nil,
procedure
begin
AniIndicator1.Visible := True;
AniIndicator1.Enabled := AniIndicator1.Visible; //显示转圈圈
end
);
Sleep(1500);
TThread.Synchronize(nil,
procedure
begin
FdMemTable1.Next;
if FdMemTable1.Eof then FdMemTable1.First;
FGoBottom := not FGoBottom;
AniIndicator1.Enabled := not AniIndicator1.Enabled;
AniIndicator1.Visible := AniIndicator1.Enabled; //转圈隐藏不显示
end
)
end
);
end;
end;
说了那么多,代码就这几行。这就是用 Delphi 做界面比较厉害的地方。大部分的代码不需要写,仅仅在设计期鼠标点几下做点设置就搞定。这就是现在流行的所谓【Low code】
进阶
TListView 的条目的内容,本身可以做得很复杂,比如包含商品名称,商品价格,商品图片,等等。这个也可以在设计期搞定而无需写代码。具体如何搞,请翻一翻本博客前面的文章。