鸿蒙原生系列之懒加载分组列表

懒加载分组列表

〇、前言

在使用 ArkTS 开发鸿蒙应用时,遇到需要使用列表布局的页面时,通常,为了节省性能,会使用 LazyForEach 去创建列表,从而让列表具备懒加载能力,特别是当列表项很多的时候。

然而,在使用C++开发鸿蒙应用时,并不存在这样直接而方便的API,需要开发者手动实现相关方法,那么,具体该如何实现呢?下面一一分析。

一、认识 ArkUI_NodeAdapter

在使用 C++ 代码实现具备懒加载 效果的列表时,离不开使用 ArkUI_NodeAdapter 对象,可以说,它就是在C++中用于顶替 ArkTS 的 LazyForEach 的。对于 ArkUI_NodeAdapter 的能力,就是用来定义组件适配器对象,为滚动类组件赋予懒加载能力

1、了解 ArkUI_NodeAdapter 相关 API

获取ArkUI_NodeAdapter实例化对象,需要通过专门的 API ------ OH_ArkUI_NodeAdapter_Create(),不仅是创建对象,在 ArkUI_NodeAdapter 对象的生命周期中,进行相关操作都需要使用专门的 API:

  • OH_ArkUI_NodeAdapter_Create():创建组件适配器对象
  • H_ArkUI_NodeAdapter_Dispose(ArkUI_NodeAdapterHandle handle):销毁组件适配器对象
  • OH_ArkUI_NodeAdapter_SetTotalNodeCount(ArkUI_NodeAdapterHandle handle, uint32_t size):设置Adapter中的元素总数,也可以说是是设置组件缓存栈的大小
  • OH_ArkUI_NodeAdapter_RegisterEventReceiver(
    ArkUI_NodeAdapterHandle handle, void* userData, void (receiver)(ArkUI_NodeAdapterEvent event)):注册Adapter相关回调事件。
  • OH_ArkUI_NodeAdapter_UnregisterEventReceiver(ArkUI_NodeAdapterHandle handle):注销Adapter相关回调事件
  • OH_ArkUI_NodeAdapter_ReloadAllItems(ArkUI_NodeAdapterHandle handle):通知Adapter进行全量元素变化。
  • OH_ArkUI_NodeAdapter_ReloadItem(
    ArkUI_NodeAdapterHandle handle, uint32_t startPosition, uint32_t itemCount):通知Adapter进行局部元素变化。
  • OH_ArkUI_NodeAdapter_RemoveItem(
    ArkUI_NodeAdapterHandle handle, uint32_t startPosition, uint32_t itemCount):通知Adapter进行局部元素删除。
  • OH_ArkUI_NodeAdapter_InsertItem(
    ArkUI_NodeAdapterHandle handle, uint32_t startPosition, uint32_t itemCount):通知Adapter进行局部元素插入。
  • OH_ArkUI_NodeAdapter_MoveItem(ArkUI_NodeAdapterHandle handle, uint32_t from, uint32_t to):通知Adapter进行局部元素移位。
  • OH_ArkUI_NodeAdapter_GetAllItems(ArkUI_NodeAdapterHandle handle, ArkUI_NodeHandle** items, uint32_t* size):获取存储在Adapter中的所有元素。接口调用会返回元素的数组对象指针,该指针指向的内存数据需要开发者手动释放。
  • OH_ArkUI_NodeAdapterEvent_GetUserData(ArkUI_NodeAdapterEvent* event):获取注册事件时传入的自定义数据。
  • OH_ArkUI_NodeAdapterEvent_GetType(ArkUI_NodeAdapterEvent* event):获取事件类型。
  • OH_ArkUI_NodeAdapterEvent_GetRemovedNode(ArkUI_NodeAdapterEvent* event):获取需要销毁的事件中待销毁的元素。
  • OH_ArkUI_NodeAdapterEvent_GetItemIndex(ArkUI_NodeAdapterEvent* event):获取适配器事件时需要操作的元素序号。
  • OH_ArkUI_NodeAdapterEvent_GetHostNode(ArkUI_NodeAdapterEvent* event):获取使用该适配器的滚动类容器节点。
  • OH_ArkUI_NodeAdapterEvent_SetItem(ArkUI_NodeAdapterEvent* event, ArkUI_NodeHandle node):设置需要新增到Adapter中的组件。
  • OH_ArkUI_NodeAdapterEvent_SetNodeId(ArkUI_NodeAdapterEvent* event, int32_t id):设置生成的组件标识。

2、认识 ArkUI_NodeAdapterEvent

ArkUI_NodeAdapterEvent,也就是适配器事件。适配器事件种类,由ArkUI_NodeAdapterEventType,目前,该枚举类共有如下成员:

而节点适配器中具体发生了哪种事件,则可以用上文提到的OH_ArkUI_NodeAdapterEvent_GetType去获取,从而为自定义处理适配器事件提供入手点。

3、如何使用 ArkUI_NodeAdapter

对于 ArkUI_NodeAdapter,通过上面的相关API介绍一节,已经可以明白它并不适合直接拿来用。

实际上,使用 ArkUI_NodeAdapter 都是通过自定义适配器类去进行的,在自定义适配器类中,通常会有如下操作:

1)在类构造函数中,通过 OH_ArkUI_NodeAdapter_Create() 创建ArkUI_NodeAdapterHandle并持有,同时,利用OH_ArkUI_NodeAdapter_SetTotalNodeCount 和 OH_ArkUI_NodeAdapter_RegisterEventReceiver 分别设置好懒加载数据和懒加载事件处理器

2)在私有成员中,应当定义好一个充当管理回收复用组件池的栈对象

3)必须的适配器事件处理函数

4)析构函数中,要做好资源释放,比如注销事件监听器、销毁ArkUI_NodeAdapterHandle

借助自定义的适配器类,便可以像设置宽高一样,给滚动类容器组件设置节点适配器。

二、滚动类容器组件

在当前,鸿蒙SDK中,属于滚动类容器 的组件,有List/ListItemGroup、Grid、WaterFlow、Swiper

1、适配器属性

每个滚动类容器组件,都有一个适配器属性,用于支持传入自定义的节点适配器,具体有如下。

  • NODE_LIST_NODE_ADAPTER:list 组件的适配器属性
  • NODE_LIST_ITEM_GROUP_NODE_ADAPTER:listItemGroup 组件的适配器属性
  • NODE_SWIPER_NODE_ADAPTER:swiper 组件的适配器属性
  • NODE_WATER_FLOW_NODE_ADAPTER:WaterFlow 组件的适配器属性
  • NODE_GRID_NODE_ADAPTER:grid 组件的适配器属性。

三、代码实践

接下来,以分布列表即 ListItemGroup 为载体,演示具有懒加载能力的容器组件的实现。

1、定义 ArkUIListItemGroupNode

由于此前的案例中,尚未涉及分组列表,所以,此类型的节点定义类,尚未存在于 NativeModule 中,需要使用如下代码进行定义:

cpp 复制代码
namespace NativeModule {
class ArkUIListItemGroupNode : public ArkUINode {
public:
    ArkUIListItemGroupNode()
        : ArkUINode((NativeModuleInstance::GetInstance()->GetNativeNodeAPI())->createNode(ARKUI_NODE_LIST_ITEM_GROUP)) {
    }
    void SetHeader(std::shared_ptr<ArkUINode> node) {
        if (node) {
            ArkUI_AttributeItem Item = {.object = node->GetHandle()};
            nativeModule_->setAttribute(handle_, NODE_LIST_ITEM_GROUP_SET_HEADER, &Item);
        } else {
            nativeModule_->resetAttribute(handle_, NODE_LIST_ITEM_GROUP_SET_HEADER);
        }
    }
    void SetFooter(std::shared_ptr<ArkUINode> node) {
        if (node) {
            ArkUI_AttributeItem Item = {.object = node->GetHandle()};
            nativeModule_->setAttribute(handle_, NODE_LIST_ITEM_GROUP_SET_FOOTER, &Item);
        } else {
            nativeModule_->resetAttribute(handle_, NODE_LIST_ITEM_GROUP_SET_FOOTER);
        }
    }
    std::shared_ptr<ArkUINode> GetHeader() const { return header_; }
    std::shared_ptr<ArkUINode> GetFooter() const { return footer_; }
    // 引入懒加载模块。
    void SetLazyAdapter(const std::shared_ptr<ArkUIListItemAdapter> &adapter) {
        ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetHandle()};
        nativeModule_->setAttribute(handle_, NODE_LIST_ITEM_GROUP_NODE_ADAPTER, &item);
        adapter_ = adapter;
    }

private:
    std::shared_ptr<ArkUINode> header_;
    std::shared_ptr<ArkUINode> footer_;
    std::shared_ptr<ArkUIListItemAdapter> adapter_;
};
}

这里,最为关键的属性设置方法,就是 SetLazyAdapter方法。

为了让列表组的标题具有吸顶灯效果,在ArkUIListNode类中补充一个 SetSticky 方法:

cpp 复制代码
void SetSticky(ArkUI_StickyStyle style) {
    ArkUI_NumberValue value[] = {{.i32 = style}};
    ArkUI_AttributeItem item = {value, 1};
    nativeModule_->setAttribute(handle_, NODE_LIST_STICKY, &item);
}

2、定义 ArkUIListItemAdapter

接下来,是整个案例中最关键的代码实现:自定义的列表适配器

cpp 复制代码
namespace NativeModule {
const int32_t NUMBER_1000 = 1000;
const int32_t NUMBER_100 = 100;
const int32_t NUMBER_16 = 16;
class ArkUIListItemAdapter {
public:
    ArkUIListItemAdapter()
        : module_(NativeModuleInstance::GetInstance()->GetNativeNodeAPI()),
          handle_(OH_ArkUI_NodeAdapter_Create()) { // 使用NodeAdapter创建函数。
        // 初始化懒加载数据。
        for (int32_t i = 0; i < 3; i++) {
            data_.emplace_back(std::to_string(i));
        }
        // 设置懒加载数据。
        OH_ArkUI_NodeAdapter_SetTotalNodeCount(handle_, data_.size());
        // 设置懒加载回调事件。
        OH_ArkUI_NodeAdapter_RegisterEventReceiver(handle_, this, OnStaticAdapterEvent);
    }

    ~ArkUIListItemAdapter() {
        // 释放创建的组件。
        while (!cachedItems_.empty()) {
            cachedItems_.pop();
        }
        items_.clear();
        // 释放Adapter相关资源。
        OH_ArkUI_NodeAdapter_UnregisterEventReceiver(handle_);
        OH_ArkUI_NodeAdapter_Dispose(handle_);
    }

    ArkUI_NodeAdapterHandle GetHandle() const { return handle_; }
    
    void RemoveItem(size_t index) {
        // 删除第index个数据。
        data_.erase(data_.begin() + index);
        // 如果index会导致可视区域元素发生可见性变化,则会回调NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER事件删除元素,
        // 根据是否有新增元素回调NODE_ADAPTER_EVENT_ON_GET_NODE_ID和NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER事件。
        OH_ArkUI_NodeAdapter_RemoveItem(handle_, index, 1);
        // 更新新的数量。
        OH_ArkUI_NodeAdapter_SetTotalNodeCount(handle_, data_.size());
    }
    
    void InsertItem(int32_t index, const std::string &value) {
        data_.insert(data_.begin() + index, value);
        // 如果index会导致可视区域元素发生可见性变化,则会回调NODE_ADAPTER_EVENT_ON_GET_NODE_ID和NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER事件,
        // 根据是否有删除元素回调NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER事件。
        OH_ArkUI_NodeAdapter_InsertItem(handle_, index, 1);
        // 更新新的数量。
        OH_ArkUI_NodeAdapter_SetTotalNodeCount(handle_, data_.size());
    }

    void MoveItem(int32_t oldIndex, int32_t newIndex) {
        auto temp = data_[oldIndex];
        data_.insert(data_.begin() + newIndex, temp);
        data_.erase(data_.begin() + oldIndex);
        // 移到位置如果未发生可视区域内元素的可见性变化,则不回调事件,反之根据新增和删除场景回调对应的事件。
        OH_ArkUI_NodeAdapter_MoveItem(handle_, oldIndex, newIndex);
    }

    void ReloadItem(int32_t index, const std::string &value) {
        data_[index] = value;
        // 如果index位于可视区域内,先回调NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER删除老元素,
        // 再回调NODE_ADAPTER_EVENT_ON_GET_NODE_ID和NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER事件。
        OH_ArkUI_NodeAdapter_ReloadItem(handle_, index, 1);
    }

    void ReloadAllItem() {
        std::reverse(data_.begin(), data_.end());
        // 全部重新加载场景下,会回调NODE_ADAPTER_EVENT_ON_GET_NODE_ID接口获取新的组件ID,
        // 根据新的组件ID进行对比,ID不发生变化的进行复用,
        // 针对新增ID的元素,调用NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER事件创建新的组件,
        // 然后判断老数据中遗留的未使用ID,调用NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER删除老元素。
        OH_ArkUI_NodeAdapter_ReloadAllItems(handle_);
    }

private:
    static void OnStaticAdapterEvent(ArkUI_NodeAdapterEvent *event) {
        // 获取实例对象,回调实例事件。
        auto itemAdapter = reinterpret_cast<ArkUIListItemAdapter *>(OH_ArkUI_NodeAdapterEvent_GetUserData(event));
        itemAdapter->OnAdapterEvent(event);
    }

    void OnAdapterEvent(ArkUI_NodeAdapterEvent *event) {
        auto type = OH_ArkUI_NodeAdapterEvent_GetType(event);
        switch (type) {
        case NODE_ADAPTER_EVENT_ON_GET_NODE_ID:
            OnNewItemIdCreated(event);
            break;
        case NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER:
            OnNewItemAttached(event);
            break;
        case NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER:
            OnItemDetached(event);
            break;
        default:
            break;
        }
    }

    // 分配ID给需要显示的Item,用于ReloadAllItems场景的元素diff。
    void OnNewItemIdCreated(ArkUI_NodeAdapterEvent *event) {
        auto index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event);
        static std::hash<std::string> hashId = std::hash<std::string>();
        auto id = hashId(data_[index]);
        OH_ArkUI_NodeAdapterEvent_SetNodeId(event, id);
        showUITextCallback("懒加载事件", "创建新元素ID:" + std::to_string(id));
    }
    
    void OnNewItemAttached(ArkUI_NodeAdapterEvent *event) {
        auto index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event);
        ArkUI_NodeHandle handle = nullptr;
        if (!cachedItems_.empty()) {
            // 使用并更新回收复用的缓存。
            auto recycledItem = cachedItems_.top();
            auto textItem = std::dynamic_pointer_cast<ArkUITextNode>(recycledItem->GetChildren().back());
            textItem->SetTextContent("item"+std::to_string(index+1)+"-"+data_[index]);
            handle = recycledItem->GetHandle();
            auto swipeContent = recycledItem->GetSwipeContent();
            swipeContent->RegisterOnClick([this, data = data_[index]](ArkUI_NodeEvent *event) {
                auto it = std::find(data_.begin(), data_.end(), data);
                if (it != data_.end()) {
                    auto index = std::distance(data_.begin(), it);
                    RemoveItem(index);
                }
            });
            // 释放缓存池的引用。
            cachedItems_.pop();
        } else {
            // 创建新的元素。
            auto listItem = std::make_shared<ArkUIListItemNode>();
                auto textNode = std::make_shared<ArkUITextNode>();
                textNode->SetTextContent("item"+std::to_string(index+1)+"-"+data_[index]);
                textNode->SetFontSize(NUMBER_16);
                textNode->SetPercentWidth(1);
                textNode->SetHeight(NUMBER_100);
                textNode->SetBackgroundColor(0xFFfffacd);
                textNode->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
                listItem->AddChild(textNode);
                // 创建ListItem划出菜单。
                auto swipeNode = std::make_shared<ArkUITextNode>();
                swipeNode->SetTextContent("del");
                swipeNode->SetFontSize(NUMBER_16);
                swipeNode->SetFontColor(0xFFFFFFFF);
                swipeNode->SetWidth(NUMBER_100);
                swipeNode->SetHeight(NUMBER_100);
                swipeNode->SetBackgroundColor(0xFFFF0000);
                swipeNode->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
                swipeNode->RegisterOnClick([this, data = data_[index]](ArkUI_NodeEvent *event) {
                    auto it = std::find(data_.begin(), data_.end(), data);
                    if (it != data_.end()) {
                        auto index = std::distance(data_.begin(), it);
                        RemoveItem(index);
                    }
                });
                listItem->SetSwiperAction(swipeNode);
                handle = listItem->GetHandle();
                // 保持文本列表项的引用。
                items_.emplace(handle, listItem);
            
        }
        // 设置需要展示的元素。
        OH_ArkUI_NodeAdapterEvent_SetItem(event, handle);
        showUITextCallback("懒加载事件", "附加元素:" + std::to_string(index));
    }
    // Item从可见区域移除。
    void OnItemDetached(ArkUI_NodeAdapterEvent *event) {
        auto item = OH_ArkUI_NodeAdapterEvent_GetRemovedNode(event);
        // 放置到缓存池中进行回收复用。
        cachedItems_.emplace(items_[item]);
        showUITextCallback("懒加载事件", "移除元素");
    }

    std::vector<std::string> data_;
    ArkUI_NativeNodeAPI_1 *module_ = nullptr;
    ArkUI_NodeAdapterHandle handle_ = nullptr;

    // 管理NodeAdapter生成的元素。
    std::unordered_map<ArkUI_NodeHandle, std::shared_ptr<ArkUIListItemNode>> items_;

    // 管理回收复用组件池。
    std::stack<std::shared_ptr<ArkUIListItemNode>> cachedItems_;
};
}

在这个适配器类中,主要处理 NODE_ADAPTER_EVENT_ON_GET_NODE_IDNODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTERNODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER 这三种事件。

三种事件中,最关键的就是 NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER 事件,而其事件处理函数,负责元素的新建与复用;创建新元素的时候,通过 swiper 组件为每个列表项赋予左划菜单;复用组件时,则直接将 cachedItems_ 的栈顶元素取出来。

3、CreateLazyTextListExample

下面,将自定义的 ArkUIListItemGroupNode 和 ArkUIListItemAdapter 用起来:

cpp 复制代码
namespace NativeModule {
const int32_t NUMBER_3 = 3;
const int32_t NUMBER_50 = 50;
const int32_t NUMBER_10 = 10;

std::shared_ptr<ArkUIBaseNode> CreateLazyTextListExample()
{
    // 创建组件并挂载。
    // 1: 创建List组件。
    auto list = std::make_shared<ArkUIListNode>();
    list->SetPercentWidth(1);
    list->SetPercentHeight(1);
    // 设置吸顶。
    list->SetSticky(ARKUI_STICKY_STYLE_BOTH);
    // 2: 创建ListItemGroup并挂载到List上。
    for(int32_t i = 0; i < NUMBER_10; i++){
        auto header = std::make_shared<ArkUITextNode>();
        header->SetTextContent("header"+std::to_string(i+1));
        header->SetFontSize(NUMBER_16);
        header->SetPercentWidth(1);
        header->SetHeight(NUMBER_50);
        header->SetBackgroundColor(0xFFDCDCDC);
        header->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
        auto listItemGroup = std::make_shared<ArkUIListItemGroupNode>();
        listItemGroup->SetHeader(header);
        auto adapter = std::make_shared<ArkUIListItemAdapter>();
        listItemGroup->SetLazyAdapter(adapter);
        for(int32_t j = 0; j < NUMBER_3; j++){
            auto listItem = std::make_shared<ArkUIListItemNode>();
            auto text = std::make_shared<ArkUITextNode>();
            text->SetTextContent("item"+std::to_string(i+1)+"-"+std::to_string(j+1));
            text->SetFontSize(NUMBER_16);
            text->SetPercentWidth(1);
            text->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
            listItem->AddChild(text);
            listItemGroup->AddChild(listItem);
        }
        list->AddChild(listItemGroup);
    }
    return list;
}
}

4、真机运行

在 NativeEntry.cpp 的 CreateNativeRoot 函数中修改代码,将CreateLazyTextListExample 作为演示方法:

cpp 复制代码
napi_value CreateNativeRoot(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {nullptr};

    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 获取 NodeContent
    ArkUI_NodeContentHandle contentHandle;
    OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &contentHandle);
    NativeEntry::GetInstance()->SetContentHandle(contentHandle);

    // 创建文本列表
//    auto list = CreateTextListExample();
//    NativeEntry::GetInstance()->SetRootNode(list);
    // 创建 Column
//    auto column = testGestureExample();
//    NativeEntry::GetInstance()->SetRootNode(column);
    // 创建 image
//        auto image = CreateDragImageExample();
//        NativeEntry::GetInstance()->SetRootNode(image);
    // 演示动画
//        auto column = testFrameAnimate();
//        NativeEntry::GetInstance()->SetRootNode(column);
//        auto column = testPropertiesAnimate();
//        NativeEntry::GetInstance()->SetRootNode(column);
//        auto column = testTransitionAnimate();
//        NativeEntry::GetInstance()->SetRootNode(column);
//        auto column = testKeyFrameAnimate();
//        NativeEntry::GetInstance()->SetRootNode(column);
    // 创建懒加载分组列表
    auto list_group = CreateLazyTextListExample();
    NativeEntry::GetInstance()->SetRootNode(list_group);
    
    return nullptr;
}

最终运行效果如下:

相关推荐
哈哈你是真的厉害2 小时前
React Native 鸿蒙跨平台开发:FlatList 基础列表代码指南
react native·react.js·harmonyos
南村群童欺我老无力.2 小时前
Flutter 框架跨平台鸿蒙开发 - 阅读进度追踪应用开发指南
flutter·华为·harmonyos
世人万千丶3 小时前
鸿蒙跨端框架 Flutter 学习 Day 4:程序生存法则——异常捕获与异步错误处理的熔断艺术
学习·flutter·华为·harmonyos·鸿蒙
小白阿龙3 小时前
鸿蒙+Flutter 跨平台开发——简易猜数字竞猜游戏实现
flutter·游戏·harmonyos
时光慢煮4 小时前
基于 Flutter × OpenHarmony 图书馆管理系统之构建书籍列表
flutter·华为·开源·openharmony
不爱吃糖的程序媛5 小时前
在 腾讯Kuikly 跨平台框架中实现设备信息检测(支持鸿蒙)
华为·harmonyos
IT陈图图6 小时前
跨端智慧图书馆:Flutter × OpenHarmony 下的读者管理模块构建实践
flutter·华为·鸿蒙·openharmony
IT陈图图6 小时前
优雅管理,智慧阅读:基于 Flutter × OpenHarmony 构建图书馆读者列表模块
flutter·华为·鸿蒙·openharmony
小白阿龙7 小时前
鸿蒙+flutter 跨平台开发——简易井字棋小游戏实现
flutter·华为·harmonyos·鸿蒙