文件最后提交记录最后更新时间
9 个月前
9 个月前
9 个月前
9 个月前
9 个月前
9 个月前
9 个月前
9 个月前
9 个月前
9 个月前
README.md

电影选座示例

made by 林子淇

本项目是 HarmonyOS 官方 电影选座 ArkTS 示例 的仓颉语言重写版本。旨在演示如何使用仓颉鸿蒙构建一个功能完整的电影选座应用场景。

电影选座是便捷生活类应用中的典型交互场景。用户可以直观地看到影厅的座位布局,根据座位的不同类型(如黄金区、特惠区)进行选择。用户可以点击座位进行选择、再次点击取消选择,并在底部信息卡片中看到已选座位的列表,最后进行确认。

在仓颉鸿蒙开发方面,本项目旨在清晰地演示声明式UI的两个关键概念:

  1. 状态驱动UI更新:通过 @Observed@Publish 宏,实现数据状态与UI视图的自动同步。
  2. 组件间状态共享:利用 LocalStorage 机制,让分散在不同组件中的UI元素能够安全、高效地共享和响应同一份数据源。

在此基础上,您可以参考股市图表示例,它将声明式UI与命令式UI作对比,增强了对 Canvas 组件的命令式绘图控制和数据交互的演示,实现了更复杂的 Canvas 组件的使用场景。

效果预览

实现思路

项目整体遵循了经典的 MVVM (Model-View-ViewModel) 架构模式,确保了UI、状态和业务逻辑的清晰分离。

1. 状态管理与组件间共享

在声明式UI编程中,我们不直接操作UI组件,而是通过改变状态来驱动UI的自动更新,即 view = f(state),其中 f 由框架实现,并向开发者隐藏。

  • 使用 @Observed@Publish 声明状态

    UI的“长相”完全由数据状态决定。当数据变化时,UI会自动刷新以匹配新的数据。

    1. 状态定义: 在 SeatSelection.cj (ViewModel) 中,我们将整个类标记为 @Observed,使其成为一个可观察的数据源。其中,直接影响UI呈现的 seatsState(座位图状态)和 selectedSeats(已选座位列表)两个变量,我们使用 @Publish 宏进行标记。这意味着任何对这两个变量的修改都将被UI框架侦听。
    2. 状态消费: 在UI组件中,我们直接使用这些状态来构建界面。例如,SeatSelectionBar.cj 组件根据 viewModel.selectedSeats 数组的大小来决定显示“推荐座位”还是已选座位列表。同样,Card.cj 中的“确认”按钮也通过 .enabled(viewModel.selectedSeats.size > 0) 来动态控制其是否可点击。
    3. 自动更新: 当用户点击座位,ViewModel 更新了 selectedSeats 数组(比如,向其中添加一个座位)后,仓颉的UI框架会自动检测到变化,并重新渲染所有依赖 selectedSeats 的UI部分。开发者无需编写任何手动刷新UI的代码
    // entry/src/main/cangjie/view_model/SeatSelection.cj
    @Observed
    public class SeatSelection {
        // @Publish 标记此状态的变更需要通知UI
        @Publish public var seatsState: ObservedArrayList<ObservedArray<SEAT_TYPE>> = ...
        @Publish public var selectedSeats: ObservedArrayList<SeatInfo> = ObservedArrayList<SeatInfo>()
        // ...
    }
    
    // entry/src/main/cangjie/components/SeatSelectionBar.cj
    @Component
    public class SeatSelectionBar {
        @LocalStorageLink['viewModel'] // 链接到共享的状态
        var viewModel: SeatSelection = SeatSelection()
    
        func build() {
            // UI直接消费状态,根据数组大小决定显示内容
            if (viewModel.selectedSeats.size == 0) {
                Text("推荐座位")
                // ...
            } else {
                ForEach(viewModel.selectedSeats, { seat: SeatInfo, _: Int64 =>
                    SelectedSeat(seatInfo: seat)
                })
            }
        }
    }
    
  • 使用 LocalStorage 实现状态共享

    为了让 SeatCanvasSeatSelectionBarCard 等多个独立的组件能访问和修改同一个 SeatSelection 实例,我们使用了 LocalStorage

    1. 创建存储: 在应用的入口 index.cj 中,我们创建了一个 LocalStorage 实例,并将其与根组件 EntryView 关联。
    2. 链接状态: 在需要访问共享状态的子组件中,我们使用 @LocalStorageLink['viewModel'] 宏来获取 LocalStorage 中名为 'viewModel' 的数据对象。这确保了所有组件操作的都是同一个 ViewModel 实例,从而实现了状态的全局共享。
    // entry/src/main/cangjie/index.cj
    let storage = LocalStorage() // 1. 创建存储实例
    
    @Entry[storage] // 2. 与根组件关联
    @Component
    class EntryView {
        // ...
    }
    
    // entry/src/main/cangjie/components/Card.cj
    @Component
    public class Card {
        // 3. 在子组件中通过名称链接到该状态
        @LocalStorageLink['viewModel']
        var viewModel: SeatSelection = SeatSelection()
        // ...
    }
    

2. 核心交互的数据流

理解用户点击一个座位后发生的事情,是理解本项目数据流的关键。这个过程巧妙地结合了命令式UI(手动绘制Canvas)和声明式UI(自动更新组件)的优点。

  1. 用户操作: 用户在 SeatCanvas 组件上点击。
  2. 事件处理: CanvasonClick 事件被触发,调用 viewModel.handleCanvasClick(evt)
  3. ViewModel 状态更新:
    • handleCanvasClick 方法计算出用户点击的是哪个座位(行和列)。
    • 它接着调用 handleSeatClick(seatInfo),这是核心的业务逻辑。
    • handleSeatClick 方法会修改两个核心状态:
      1. 更新 seatsState 数组,将被点击座位的状态改为 SELECTED 或改回原始状态。
      2. 更新 selectedSeats 列表,添加或移除对应的 SeatInfo
  4. UI 响应:
    • 命令式更新 (Canvas): ViewModel 在更新完状态后,会调用一个预先注册的回调函数 onStateChangeCallbackSeatCanvas 组件在初始化时将自己的 drawSeat 方法注册为这个回调。因此,ViewModel 的调用会让 SeatCanvas 对指定的座位进行重绘。
    • 声明式更新 (其他组件): 与此同时,由于 @Publish 标记的 selectedSeats 数组发生了变化,UI框架会自动地重新渲染所有依赖它的组件。这包括:
      • SeatSelectionBar 会从“推荐座位”视图切换到已选座位列表。
      • Card 组件中的“确认”按钮会根据 selectedSeats 是否为空,自动变为可用或禁用状态。

通过这种方式,应用实现了高效和易于维护的完美结合:对于复杂的、需要精细控制的 Canvas 采用手动重绘,而对于常规的UI组件则充分享受声明式编程带来的便利。

工程目录

├── entry/src/main/cangjie
│   ├── MainAbility.cj
│   ├── AbilityStage.cj
│   ├── index.cj
│   ├── constants                   // 常量
│   │   ├── AreaInfo.cj
│   │   ├── DataConstants.cj
│   │   ├── SeatInfo.cj
│   │   ├── SeatType.cj
│   │   └── StyleConstants.cj
│   ├── view_model                  // 视图模型
│   │   └── SeatSelection.cj
│   └── components                  // UI组件
│       ├── Card.cj
│       ├── MovieInfo.cj
│       ├── MyDialog.cj
│       ├── SeatCanvas.cj           // 核心的座位图Canvas组件
│       ├── SeatMap.cj
│       ├── SeatSelectionBar.cj     // 底部已选座位/推荐座位滚动条
│       ├── SelectedSeat.cj
│       ├── tag.cj
│       └── title.cj
└── entry/src/main/resources        // 应用资源目录