From 5a749d05fecf60c99fa4897f46cfcf48b312a307 Mon Sep 17 00:00:00 2001 From: fuhanfeng Date: Fri, 26 Sep 2025 11:37:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=BB=9A=E5=8A=A8=E7=B1=BB?= =?UTF-8?q?=E7=BB=84=E4=BB=B6CAPI=20=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fuhanfeng Signed-off-by: fuhanfeng --- .../entry/src/main/cpp/ArkUINodeAdapter.h | 243 +++++++ .../entry/src/main/cpp/CMakeLists.txt | 40 +- .../entry/src/main/cpp/GridMaker.cpp | 159 +++++ .../entry/src/main/cpp/GridMaker.h | 134 ++++ .../entry/src/main/cpp/ListItemGroup.h | 113 ++++ .../entry/src/main/cpp/ListItemSwipe.h | 345 ++++++++++ .../entry/src/main/cpp/ListMaker.cpp | 618 ++++++++++++++++++ .../entry/src/main/cpp/ListMaker.h | 542 +++++++++++++++ .../entry/src/main/cpp/RefreshMaker.cpp | 617 +++++++++++++++++ .../entry/src/main/cpp/RefreshMaker.h | 163 +++++ .../entry/src/main/cpp/ScrollMaker.cpp | 388 +++++++++++ .../entry/src/main/cpp/ScrollMaker.h | 342 ++++++++++ .../entry/src/main/cpp/ScrollableEvent.h | 130 ++++ .../entry/src/main/cpp/ScrollableNode.h | 174 +++++ .../entry/src/main/cpp/ScrollableUtils.cpp | 429 ++++++++++++ .../entry/src/main/cpp/ScrollableUtils.h | 209 ++++++ .../entry/src/main/cpp/WaterFlowMaker.cpp | 323 +++++++++ .../entry/src/main/cpp/WaterFlowMaker.h | 287 ++++++++ .../entry/src/main/cpp/WaterFlowSection.h | 84 +++ .../entry/src/main/cpp/manager.cpp | 165 +++-- .../entry/src/main/cpp/manager.h | 5 + .../entry/src/main/cpp/napi_init.cpp | 15 +- .../src/main/cpp/types/libentry/Index.d.ts | 7 +- .../entry/src/main/ets/pages/Index.ets | 6 +- .../entry/src/main/ets/pages/page_grid.ets | 34 + .../entry/src/main/ets/pages/page_list.ets | 34 + .../entry/src/main/ets/pages/page_refresh.ets | 34 + .../entry/src/main/ets/pages/page_scroll.ets | 34 + .../src/main/ets/pages/page_waterflow.ets | 34 + .../resources/base/profile/main_pages.json | 7 +- .../entry/src/main/cpp/ArkUIBaseNode.h | 1 - .../entry/src/main/cpp/ArkUINodeAdapter.h | 243 +++++++ .../entry/src/main/cpp/CMakeLists.txt | 40 +- .../entry/src/main/cpp/GridMaker.cpp | 159 +++++ .../entry/src/main/cpp/GridMaker.h | 134 ++++ .../entry/src/main/cpp/ListItemGroup.h | 113 ++++ .../entry/src/main/cpp/ListItemSwipe.h | 345 ++++++++++ .../entry/src/main/cpp/ListMaker.cpp | 618 ++++++++++++++++++ .../entry/src/main/cpp/ListMaker.h | 542 +++++++++++++++ .../entry/src/main/cpp/RefreshMaker.cpp | 617 +++++++++++++++++ .../entry/src/main/cpp/RefreshMaker.h | 163 +++++ .../entry/src/main/cpp/ScrollMaker.cpp | 388 +++++++++++ .../entry/src/main/cpp/ScrollMaker.h | 342 ++++++++++ .../entry/src/main/cpp/ScrollableEvent.h | 130 ++++ .../entry/src/main/cpp/ScrollableNode.h | 174 +++++ .../entry/src/main/cpp/ScrollableUtils.cpp | 429 ++++++++++++ .../entry/src/main/cpp/ScrollableUtils.h | 209 ++++++ .../entry/src/main/cpp/WaterFlowMaker.cpp | 323 +++++++++ .../entry/src/main/cpp/WaterFlowMaker.h | 287 ++++++++ .../entry/src/main/cpp/WaterFlowSection.h | 84 +++ .../entry/src/main/cpp/manager.cpp | 140 ++++ .../entry/src/main/cpp/manager.h | 5 + .../entry/src/main/cpp/napi_init.cpp | 20 +- .../src/main/cpp/types/libentry/Index.d.ts | 7 +- .../entry/src/main/ets/pages/Index.ets | 7 +- .../entry/src/main/ets/pages/page_grid.ets | 34 + .../entry/src/main/ets/pages/page_list.ets | 34 + .../entry/src/main/ets/pages/page_refresh.ets | 34 + .../entry/src/main/ets/pages/page_scroll.ets | 34 + .../src/main/ets/pages/page_waterflow.ets | 34 + .../resources/base/profile/main_pages.json | 9 +- 61 files changed, 11295 insertions(+), 119 deletions(-) create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ArkUINodeAdapter.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.cpp create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemGroup.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemSwipe.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.cpp create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.cpp create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.cpp create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableEvent.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableNode.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.cpp create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.cpp create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowSection.h create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_grid.ets create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_list.ets create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_refresh.ets create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_scroll.ets create mode 100644 ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_waterflow.ets create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUINodeAdapter.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.cpp create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemGroup.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemSwipe.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.cpp create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.cpp create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.cpp create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableEvent.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableNode.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.cpp create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.cpp create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowSection.h create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_grid.ets create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_list.ets create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_refresh.ets create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_scroll.ets create mode 100644 ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_waterflow.ets diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ArkUINodeAdapter.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ArkUINodeAdapter.h new file mode 100644 index 000000000..560cae8e3 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ArkUINodeAdapter.h @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ARKUINODEADAPTER_H +#define ARKUINODEADAPTER_H + +#include +#include +#include + +#include +#include +#include + +#include "ScrollableUtils.h" + +/** + * 通用 NodeAdapter 封装 + */ +struct NodeAdapterCallbacks { + std::function getTotalCount; + std::function getStableId; + std::function onCreate; + std::function onBind; + std::function onRecycle; +}; + +class ArkUINodeAdapter { +public: + using Callbacks = NodeAdapterCallbacks; + + ArkUINodeAdapter() : placeholderNodeType_(kInvalidNodeType) { InitializeApiAndAdapter(); } + + explicit ArkUINodeAdapter(int32_t placeholderNodeType) : placeholderNodeType_(placeholderNodeType) + { + InitializeApiAndAdapter(); + } + + ~ArkUINodeAdapter() + { + ClearNodeCache(); + OH_ArkUI_NodeAdapter_UnregisterEventReceiver(adapterHandle_); + OH_ArkUI_NodeAdapter_Dispose(adapterHandle_); + adapterHandle_ = nullptr; + } + + ArkUI_NodeAdapterHandle GetAdapter() const { return adapterHandle_; } + + void SetPlaceholderType(int32_t placeholderNodeType) { placeholderNodeType_ = placeholderNodeType; } + + void EnsurePlaceholderTypeOr(int32_t fallbackNodeType) + { + if (placeholderNodeType_ < 0) { + placeholderNodeType_ = fallbackNodeType; + } + } + + void SetCallbacks(const NodeAdapterCallbacks &callbacks) + { + callbacks_ = callbacks; + SynchronizeItemCount(GetTotalItemCount()); + } + + // ======================================== + // 数据变动通知接口 + // ======================================== + void ReloadAllItems() { OH_ArkUI_NodeAdapter_ReloadAllItems(adapterHandle_); } + + void InsertRange(int32_t index, int32_t count) + { + if (count <= 0) { + return; + } + int32_t validIndex = ClampIndexToRange(index, GetTotalItemCount()); + OH_ArkUI_NodeAdapter_InsertItem(adapterHandle_, validIndex, count); + SynchronizeItemCount(GetTotalItemCount()); + } + + void RemoveRange(int32_t index, int32_t count) + { + if (count <= 0) { + return; + } + if (!Utils::IsValidIndex(index, GetTotalItemCount())) { + return; + } + OH_ArkUI_NodeAdapter_RemoveItem(adapterHandle_, index, count); + SynchronizeItemCount(GetTotalItemCount()); + } + +protected: + // ======================================== + // 事件分发处理 + // ======================================== + static void OnStaticEvent(ArkUI_NodeAdapterEvent *event) + { + auto *self = reinterpret_cast(OH_ArkUI_NodeAdapterEvent_GetUserData(event)); + if (Utils::IsNotNull(self)) { + self->OnEvent(event); + } + } + + void OnEvent(ArkUI_NodeAdapterEvent *event) + { + const int32_t eventType = OH_ArkUI_NodeAdapterEvent_GetType(event); + switch (eventType) { + case NODE_ADAPTER_EVENT_ON_GET_NODE_ID: { + HandleGetNodeId(event); + break; + } + case NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER: { + HandleAddNodeToAdapter(event); + break; + } + case NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER: { + HandleRemoveNodeFromAdapter(event); + break; + } + default: { + break; + } + } + } + +private: + // ======================================== + // 私有常量和工具方法 + // ======================================== + static constexpr int32_t kInvalidNodeType = -1; + + void InitializeApiAndAdapter() + { + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, nodeApi_); + adapterHandle_ = OH_ArkUI_NodeAdapter_Create(); + OH_ArkUI_NodeAdapter_RegisterEventReceiver(adapterHandle_, this, &ArkUINodeAdapter::OnStaticEvent); + SynchronizeItemCount(GetTotalItemCount()); + } + + void ClearNodeCache() + { + while (!nodeCache_.empty()) { + nodeCache_.pop(); + } + } + + int32_t GetTotalItemCount() const + { + if (callbacks_.getTotalCount) { + return callbacks_.getTotalCount(); + } + return 0; + } + + int32_t ClampIndexToRange(int32_t index, int32_t maxCount) const + { + if (index < 0) { + return 0; + } + if (index > maxCount) { + return maxCount; + } + return index; + } + + void SynchronizeItemCount(int32_t count) + { + OH_ArkUI_NodeAdapter_SetTotalNodeCount(adapterHandle_, static_cast(count)); + } + + ArkUI_NodeHandle PopFromCacheOrCreate(int32_t index) + { + if (!nodeCache_.empty()) { + ArkUI_NodeHandle handle = nodeCache_.top(); + nodeCache_.pop(); + return handle; + } + if (callbacks_.onCreate) { + return callbacks_.onCreate(nodeApi_, index); + } + const ArkUI_NodeType nodeType = (placeholderNodeType_ >= 0) ? static_cast(placeholderNodeType_) + : ARKUI_NODE_LIST_ITEM; + return nodeApi_->createNode(nodeType); + } + + // ======================================== + // 事件处理实现 + // ======================================== + void HandleGetNodeId(ArkUI_NodeAdapterEvent *event) + { + const int32_t index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event); + uint64_t nodeId = static_cast(index); + + if (Utils::IsValidIndex(index, GetTotalItemCount()) && callbacks_.getStableId) { + nodeId = callbacks_.getStableId(index); + } + OH_ArkUI_NodeAdapterEvent_SetNodeId(event, nodeId); + } + + void HandleAddNodeToAdapter(ArkUI_NodeAdapterEvent *event) + { + const int32_t index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event); + ArkUI_NodeHandle item = PopFromCacheOrCreate(index); + + if (callbacks_.onBind && Utils::IsValidIndex(index, GetTotalItemCount())) { + callbacks_.onBind(nodeApi_, item, index); + } + OH_ArkUI_NodeAdapterEvent_SetItem(event, item); + } + + void HandleRemoveNodeFromAdapter(ArkUI_NodeAdapterEvent *event) + { + ArkUI_NodeHandle node = OH_ArkUI_NodeAdapterEvent_GetRemovedNode(event); + if (!Utils::IsNotNull(node)) { + return; + } + + if (callbacks_.onRecycle) { + callbacks_.onRecycle(nodeApi_, node); + } + nodeCache_.push(node); + } + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + ArkUI_NodeAdapterHandle adapterHandle_ = nullptr; + NodeAdapterCallbacks callbacks_; + std::stack nodeCache_; + int32_t placeholderNodeType_ = kInvalidNodeType; +}; + +#endif // ARKUINODEADAPTER_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/CMakeLists.txt b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/CMakeLists.txt index 9719cae2d..0743ed0d0 100644 --- a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/CMakeLists.txt +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/CMakeLists.txt @@ -1,20 +1,36 @@ # the minimum version of CMake. -cmake_minimum_required(VERSION 3.5.0) +cmake_minimum_required(VERSION 3.28.0) project(native_node_sample) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +if(DEFINED PACKAGE_FIND_FILE) + include(${PACKAGE_FIND_FILE}) +endif() + include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/include) -add_library(entry SHARED napi_init.cpp manager.cpp baseUtils.cpp SwiperMaker.cpp TextMaker.cpp AccessibilityMaker.cpp EmbeddedComponentMaker.cpp) - -find_library(hilog-lib hilog_ndk.z) - -find_library(libace-lib ace_ndk.z) - -find_library(libnapi-lib ace_napi.z) - - -target_link_libraries(entry PUBLIC ${hilog-lib} ${libace-lib} ${libnapi-lib}) -target_link_libraries(entry PUBLIC ${libace-lib} libace_napi.z.so libnative_drawing.so libhilog_ndk.z.so libability_base_want.so) \ No newline at end of file +file(GLOB_RECURSE ENTRY_SOURCES CONFIGURE_DEPENDS + "${NATIVERENDER_ROOT_PATH}/napi_init.cpp" + "${NATIVERENDER_ROOT_PATH}/*.cpp" +) + +add_library(entry SHARED ${ENTRY_SOURCES}) + +find_library( + # Sets the name of the path variable. + hilog-lib + libace-lib + libnapi-lib + ace_napi.z +) +target_link_libraries(entry PUBLIC + libace_ndk.z.so + libace_napi.z.so + libhilog_ndk.z.so + libnative_drawing.so + libability_base_want.so +) \ No newline at end of file diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.cpp new file mode 100644 index 000000000..c2b310efc --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +#include +#include +#include +#include // snprintf +#include +#include + +#include +#include + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" +#include "GridMaker.h" + +namespace { +// ===== 布局与样式常量 ===== +constexpr char K_ROWS_TEMPLATE[] = "auto"; +constexpr char K_COLUMNS_TEMPLATE[] = "1fr 1fr"; +constexpr float K_COLUMNS_GAP = 10.0f; +constexpr float K_ROWS_GAP = 15.0f; +constexpr uint32_t K_ITEM_BG_COLOR = 0xFFF1F3F5U; + +constexpr uint32_t K_GRID_CACHED_COUNT = 32; +constexpr bool K_GRID_SYNC_LOAD = true; +constexpr ArkUI_FocusWrapMode K_FOCUS_WRAP_MODE = ARKUI_FOCUS_WRAP_MODE_DEFAULT; + +constexpr int K_ITEM_COUNT = 60; +constexpr float K_ITEM_HEIGHT = 72.0f; +constexpr int K_GRID_INDEX_WIDTH = 2; +} // namespace + +// ---------- 生成数据:Grid01 ~ Grid60 ---------- +static std::vector MakeServicesData(size_t count = K_ITEM_COUNT) +{ + std::vector out; + out.reserve(count); + for (size_t i = 1; i <= count; ++i) { + std::ostringstream oss; + oss << "Grid" << std::setw(K_GRID_INDEX_WIDTH) << std::setfill('0') << i; + out.emplace_back(oss.str()); + } + return out; +} + +// ---------- 配置 Grid 外观/交互 ---------- +static void ConfigureGrid(const std::shared_ptr &grid) +{ + grid->SetWidthPercent(1.0f); + grid->SetDefaultScrollStyle(); + grid->SetColumnsTemplate(K_COLUMNS_TEMPLATE); + grid->SetCachedCount(K_GRID_CACHED_COUNT); + grid->SetFocusWrapMode(K_FOCUS_WRAP_MODE); + grid->SetSyncLoad(K_GRID_SYNC_LOAD); + grid->SetColumnsGap(K_COLUMNS_GAP); + grid->SetRowsGap(K_ROWS_GAP); +} + +// ---------- 适配器回调(创建/绑定) ---------- +static ArkUI_NodeHandle GridCreateItem(ArkUI_NativeNodeAPI_1 *api) +{ + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_GRID_ITEM); + api->addChild(item, text); + return item; +} + +static void GridBindItem(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index, + const std::shared_ptr> &data) +{ + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + Utils::SetAttributeFloat32(api, item, NODE_HEIGHT, K_ITEM_HEIGHT); + + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!text) { + return; + } + + const int32_t n = static_cast(data->size()); + const char *s = (index >= 0 && index < n) ? (*data)[static_cast(index)].c_str() : ""; + Utils::SetTextContent(api, text, s); +} + +// ---------- 构建 Adapter ---------- +static std::shared_ptr MakeGridAdapter(const std::shared_ptr> &data) +{ + auto adapter = std::make_shared(); + adapter->EnsurePlaceholderTypeOr(static_cast(ARKUI_NODE_GRID_ITEM)); + + ArkUINodeAdapter::Callbacks cb{}; + cb.getTotalCount = [data]() -> int32_t { return static_cast(data->size()); }; + cb.getStableId = [data](int32_t i) -> uint64_t { + const int32_t n = static_cast(data->size()); + if (i >= 0 && i < n) { + return static_cast(std::hash{}((*data)[static_cast(i)])); + } + return static_cast(i); + }; + cb.onCreate = [](ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) -> ArkUI_NodeHandle { return GridCreateItem(api); }; + cb.onBind = [data](ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index) { + GridBindItem(api, item, index, data); + }; + + adapter->SetCallbacks(cb); + return adapter; +} + +// ---------- 整体构建 Grid ---------- +static std::shared_ptr BuildGrid() +{ + auto grid = std::make_shared(); + ConfigureGrid(grid); + + auto data = std::make_shared>(MakeServicesData(K_ITEM_COUNT)); + auto adapter = MakeGridAdapter(data); + grid->SetLazyAdapter(adapter); + adapter->ReloadAllItems(); + GetKeepAliveContainer().emplace_back(grid); + return grid; +} + +ArkUI_NodeHandle GridMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + // 根容器全屏 + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + // 构建 Grid + std::shared_ptr grid = BuildGrid(); + if (grid && grid->GetHandle() != nullptr) { + Utils::SetAttributeFloat32(api, grid->GetHandle(), NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, grid->GetHandle()); + } + + return page; +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.h new file mode 100644 index 000000000..dd437a3dd --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/GridMaker.h @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GRID_NODE_H +#define GRID_NODE_H + +#include + +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" +#include "ArkUINodeAdapter.h" + +class GridMaker : public BaseNode { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + GridMaker() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_GRID)), + nodeApi_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()) + { + if (!Utils::IsNotNull(nodeApi_) || !Utils::IsNotNull(GetHandle())) { + return; + } + + nodeApi_->addNodeEventReceiver(GetHandle(), StaticEventReceiver); + scrollEventGuard_.Bind(nodeApi_, GetHandle(), this, SCROLL_EVT_ALL); + } + + ~GridMaker() override + { + scrollEventGuard_.Release(); + nodeAdapter_.reset(); + } + + // ======================================== + // 尺寸设置接口 + // ======================================== + void SetGridSize(float width, float height) + { + SetSize(width, height); + } + + void SetGridSizePercent(float widthPercent, float heightPercent) + { + SetSizePercent(widthPercent, heightPercent); + } + + // ======================================== + // 模板和间距设置 + // ======================================== + void SetRowsTemplate(const char *rowsTemplate) + { + Utils::SetAttributeString(nodeApi_, GetHandle(), NODE_GRID_ROW_TEMPLATE, rowsTemplate); + } + + void SetColumnsTemplate(const char *columnsTemplate) + { + Utils::SetAttributeString(nodeApi_, GetHandle(), NODE_GRID_COLUMN_TEMPLATE, columnsTemplate); + } + + void SetColumnsGap(float gap) + { + Utils::SetAttributeFloat32(nodeApi_, GetHandle(), NODE_GRID_COLUMN_GAP, gap); + } + + void SetRowsGap(float gap) + { + Utils::SetAttributeFloat32(nodeApi_, GetHandle(), NODE_GRID_ROW_GAP, gap); + } + + // ======================================== + // 行为和性能设置 + // ======================================== + void SetCachedCount(uint32_t count) + { + Utils::SetAttributeUInt32(nodeApi_, GetHandle(), NODE_GRID_CACHED_COUNT, count); + } + + void SetFocusWrapMode(ArkUI_FocusWrapMode mode) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_GRID_FOCUS_WRAP_MODE, static_cast(mode)); + } + + void SetSyncLoad(bool enabled) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_GRID_SYNC_LOAD, enabled ? 1 : 0); + } + + void SetDefaultScrollStyle() + { + Utils::SetDefaultScrollStyle(nodeApi_, GetHandle()); + } + + // ======================================== + // 适配器设置 + // ======================================== + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (!Utils::IsNotNull(adapter)) { + return; + } + ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetAdapter()}; + nodeApi_->setAttribute(GetHandle(), NODE_GRID_NODE_ADAPTER, &item); + nodeAdapter_ = adapter; + } + +protected: + void OnNodeEvent(ArkUI_NodeEvent *event) override + { + BaseNode::OnNodeEvent(event); + } + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + std::shared_ptr nodeAdapter_; + ScrollEventGuard scrollEventGuard_; +}; + +#endif // GRID_NODE_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemGroup.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemGroup.h new file mode 100644 index 000000000..014ebd987 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemGroup.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIST_ITEM_GROUP_H +#define LIST_ITEM_GROUP_H + +#include +#include +#include + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" + +/** 轻量封装:分组节点,仅提供示例所需 API */ +class ListItemGroupNode : public BaseNode { +public: + ListItemGroupNode() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_LIST_ITEM_GROUP)), + api_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()) + { + } + + ~ListItemGroupNode() override + { + if (!api_) { + return; + } + + // 清空 adapter + if (adapter_) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_NODE_ADAPTER, &it); + adapter_.reset(); + } + + // 清空 header / footer —— 传 nullptr 给同一属性即可 + if (header_) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_HEADER, &it); + header_.reset(); + } + if (footer_) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_FOOTER, &it); + footer_.reset(); + } + } + + // 设置/清空 Header:传 nullptr 即清空 + void SetHeader(const std::shared_ptr &header) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, header ? header->GetHandle() : nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_HEADER, &it); + header_ = header; + } + + // 设置/清空 Footer:传 nullptr 即清空 + void SetFooter(const std::shared_ptr &footer) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, footer ? footer->GetHandle() : nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_FOOTER, &it); + footer_ = footer; + } + + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (!adapter) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_NODE_ADAPTER, &it); + adapter_.reset(); + return; + } + adapter->EnsurePlaceholderTypeOr(static_cast(ARKUI_NODE_LIST_ITEM)); + ArkUI_AttributeItem it{nullptr, 0, nullptr, adapter->GetAdapter()}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_NODE_ADAPTER, &it); + adapter_ = adapter; + } + + // 可选:分组 divider + void SetDivider(float widthPx) + { + ArkUI_NumberValue v{.f32 = widthPx}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_DIVIDER, &it); + } + + // 可选:分组 children main size + void SetChildrenMainSizeOption(ArkUI_ListChildrenMainSize *opt) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, opt}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_CHILDREN_MAIN_SIZE, &it); + } + +private: + ArkUI_NativeNodeAPI_1 *api_ = nullptr; + std::shared_ptr adapter_; + std::shared_ptr header_; + std::shared_ptr footer_; +}; + +#endif // LIST_ITEM_GROUP_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemSwipe.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemSwipe.h new file mode 100644 index 000000000..33e8f06d0 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListItemSwipe.h @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIST_ITEM_SWIPE_H +#define LIST_ITEM_SWIPE_H + +#include + +#include +#include +#include + +#ifndef LOG_TAG +#define LOG_TAG "ListItemSwipe" +#endif + +/** + * 轻量封装:为 ARKUI_NODE_LIST_ITEM 配置 Swipe Action(左右动作区、阈值、回调等) + */ +class ListItemSwipe { +public: + explicit ListItemSwipe(ArkUI_NativeNodeAPI_1 *api) : api_(api) {} + ~ListItemSwipe() + { + if (option_) { + OH_ArkUI_ListItemSwipeActionOption_Dispose(option_); + option_ = nullptr; + } + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_Dispose(startItem_); + startItem_ = nullptr; + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_Dispose(endItem_); + endItem_ = nullptr; + } + } + + // ====== 构建左右动作区 ====== + ListItemSwipe &BuildStartArea(const std::function &builder) + { + EnsureOption(); + if (!startItem_) { + startItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + ArkUI_NodeHandle node = builder ? builder(api_) : nullptr; + OH_ArkUI_ListItemSwipeActionItem_SetContent(startItem_, node); + OH_ArkUI_ListItemSwipeActionOption_SetStart(option_, startItem_); + return *this; + } + + ListItemSwipe &BuildEndArea(const std::function &builder) + { + EnsureOption(); + if (!endItem_) { + endItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + ArkUI_NodeHandle node = builder ? builder(api_) : nullptr; + OH_ArkUI_ListItemSwipeActionItem_SetContent(endItem_, node); + OH_ArkUI_ListItemSwipeActionOption_SetEnd(option_, endItem_); + return *this; + } + + // ====== 阈值(长距离删除阈值) ====== + ListItemSwipe &SetActionAreaDistance(float distance) + { + EnsureStartEnd(); + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetActionAreaDistance(startItem_, distance); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetActionAreaDistance(endItem_, distance); + } + return *this; + } + + float GetActionAreaDistanceStart() const + { + if (startItem_) { + return OH_ArkUI_ListItemSwipeActionItem_GetActionAreaDistance(startItem_); + } + return -1.0f; + } + + float GetActionAreaDistanceEnd() const + { + if (endItem_) { + return OH_ArkUI_ListItemSwipeActionItem_GetActionAreaDistance(endItem_); + } + return -1.0f; + } + + // ====== 进入/退出/触发/状态变化 ====== + ListItemSwipe &OnEnter(const std::function &cb) + { + EnsureStartEnd(); + enter_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionArea(startItem_, &ThunkEnter); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionArea(endItem_, &ThunkEnter); + } + return *this; + } + + ListItemSwipe &OnExit(const std::function &cb) + { + EnsureStartEnd(); + exit_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionArea(startItem_, &ThunkExit); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionArea(endItem_, &ThunkExit); + } + return *this; + } + + ListItemSwipe &OnAction(const std::function &cb) + { + EnsureStartEnd(); + action_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnAction(startItem_, &ThunkAction); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnAction(endItem_, &ThunkAction); + } + return *this; + } + + ListItemSwipe &OnStateChange(const std::function &cb) + { + EnsureStartEnd(); + state_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChange(startItem_, &ThunkState); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChange(endItem_, &ThunkState); + } + return *this; + } + + ListItemSwipe &OnEnterWithUserData(const std::function &cb) + { + EnsureStartEnd(); + enterUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionAreaWithUserData(startItem_, this, &ThunkEnterUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionAreaWithUserData(endItem_, this, &ThunkEnterUD); + } + return *this; + } + + ListItemSwipe &OnExitWithUserData(const std::function &cb) + { + EnsureStartEnd(); + exitUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionAreaWithUserData(startItem_, this, &ThunkExitUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionAreaWithUserData(endItem_, this, &ThunkExitUD); + } + return *this; + } + + ListItemSwipe &OnActionWithUserData(const std::function &cb) + { + EnsureStartEnd(); + actionUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnActionWithUserData(startItem_, this, &ThunkActionUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnActionWithUserData(endItem_, this, &ThunkActionUD); + } + return *this; + } + + ListItemSwipe &OnStateChangeWithUserData(const std::function &cb) + { + EnsureStartEnd(); + stateUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChangeWithUserData(startItem_, this, &ThunkStateUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChangeWithUserData(endItem_, this, &ThunkStateUD); + } + return *this; + } + + // ====== Edge Effect / Offset 回调 ====== + ListItemSwipe &SetEdgeEffect(int edgeEffect /*ArkUI_ListItemSwipeEdgeEffect*/) + { + EnsureOption(); + OH_ArkUI_ListItemSwipeActionOption_SetEdgeEffect( + option_, static_cast(edgeEffect)); + return *this; + } + + int GetEdgeEffect() const + { + if (option_) { + return OH_ArkUI_ListItemSwipeActionOption_GetEdgeEffect(option_); + } + return -1; + } + + ListItemSwipe &OnOffsetChange(const std::function &cb) + { + EnsureOption(); + offset_ = cb; + OH_ArkUI_ListItemSwipeActionOption_SetOnOffsetChange(option_, &ThunkOffset); + return *this; + } + + ListItemSwipe &OnOffsetChangeWithUserData(const std::function &cb) + { + EnsureOption(); + offsetUD_ = cb; + OH_ArkUI_ListItemSwipeActionOption_SetOnOffsetChangeWithUserData(option_, this, &ThunkOffsetUD); + return *this; + } + + // ====== 挂载到指定 LIST_ITEM 节点 ====== + void AttachToListItem(ArkUI_NodeHandle listItem) + { + if (!api_ || !listItem) { + return; + } + EnsureOption(); + ArkUI_AttributeItem it{nullptr, 0, nullptr, option_}; + api_->setAttribute(listItem, NODE_LIST_ITEM_SWIPE_ACTION, &it); + + (void)GetActionAreaDistanceStart(); + (void)GetActionAreaDistanceEnd(); + (void)GetEdgeEffect(); + } + +private: + void EnsureOption() + { + if (!option_) { + option_ = OH_ArkUI_ListItemSwipeActionOption_Create(); + } + } + + void EnsureStartEnd() + { + EnsureOption(); + if (!startItem_) { + startItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + if (!endItem_) { + endItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + OH_ArkUI_ListItemSwipeActionOption_SetStart(option_, startItem_); + OH_ArkUI_ListItemSwipeActionOption_SetEnd(option_, endItem_); + } + + static void ThunkEnter() {} + static void ThunkExit() {} + static void ThunkAction() {} + static void ThunkState(ArkUI_ListItemSwipeActionState state) { (void)state; } + static void ThunkOffset(float offset) { (void)offset; } + + static void ThunkEnterUD(void *ud) + { + if (auto *self = static_cast(ud); self && self->enterUD_) { + self->enterUD_(ud); + } + } + + static void ThunkExitUD(void *ud) + { + if (auto *self = static_cast(ud); self && self->exitUD_) { + self->exitUD_(ud); + } + } + + static void ThunkActionUD(void *ud) + { + if (auto *self = static_cast(ud); self && self->actionUD_) { + self->actionUD_(ud); + } + } + + static void ThunkStateUD(ArkUI_ListItemSwipeActionState state, void *ud) + { + if (auto *self = static_cast(ud); self && self->stateUD_) { + self->stateUD_(static_cast(state), ud); + } + if (auto *self2 = static_cast(ud); self2 && self2->state_) { + self2->state_(static_cast(state)); + } + } + + static void ThunkOffsetUD(float offset, void *ud) + { + if (auto *self = static_cast(ud); self && self->offsetUD_) { + self->offsetUD_(offset, ud); + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_{nullptr}; + + ArkUI_ListItemSwipeActionItem *startItem_{nullptr}; + ArkUI_ListItemSwipeActionItem *endItem_{nullptr}; + ArkUI_ListItemSwipeActionOption *option_{nullptr}; + + // 无 userData 回调 + std::function enter_; + std::function exit_; + std::function action_; + std::function state_; + std::function offset_; + + // 带 userData 回调 + std::function enterUD_; + std::function exitUD_; + std::function actionUD_; + std::function stateUD_; + std::function offsetUD_; +}; + +#endif // LIST_ITEM_SWIPE_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.cpp new file mode 100644 index 000000000..6f219fdfb --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.cpp @@ -0,0 +1,618 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "ScrollableUtils.h" + +#ifndef LOG_TAG +#define LOG_TAG "ListMaker" +#endif + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" +#include "ListMaker.h" +#include "ListItemGroup.h" +#include "ListItemSwipe.h" + +// ===== 常量定义 ===== +namespace { +constexpr int K_ALPHABET_COUNT = 26; +constexpr int K_INDEX_ITEM_HEIGHT = 22; +constexpr int32_t K_GROUP_FIRST_ITEM_INDEX = 0; +constexpr int32_t K_FIRST_GROUP_INDEX = 0; + +constexpr float K_LIST_WIDTH_PERCENT = 0.82f; +constexpr float K_FULL_PERCENT = 1.0f; +constexpr float K_LIST_SPACE = 8.0f; + +constexpr float K_ITEM_FONT_SIZE = 16.0f; +constexpr float K_INDEX_FONT_SIZE = 14.0f; +constexpr float K_ROW_HEIGHT = 80.0f; + +constexpr float K_DELETE_HEIGHT = 64.0f; +constexpr float K_DELETE_WIDTH = 88.0f; +constexpr float K_SWIPE_ACTION_DISTANCE = 96.0f; + +constexpr float K_INDEX_BAR_WIDTH = 56.0f; +constexpr float K_INDEX_BAR_PAD_TOP = 0.0f; +constexpr float K_INDEX_BAR_PAD_RIGHT = 0.0f; +constexpr float K_INDEX_BAR_PAD_BOTTOM = 0.0f; +constexpr float K_INDEX_BAR_PAD_LEFT = 8.0f; + +constexpr float K_HEADER_HEIGHT = 40.0f; +constexpr float K_FOOTER_HEIGHT = 28.0f; + +constexpr uint32_t K_COLOR_WHITE = 0xFFFFFFFFU; +constexpr uint32_t K_COLOR_BLACK = 0xFF000000U; +constexpr uint32_t K_COLOR_DELETE_BG = 0xFFE53935U; +constexpr uint32_t K_COLOR_INDEX_ACTIVE = 0xFF003366U; +constexpr uint32_t K_COLOR_INDEX_INACTIVE = 0xFF222222U; +constexpr uint32_t K_COLOR_INDEX_ACTIVE_BG = 0xFFE0F0FFU; +constexpr uint32_t K_COLOR_INDEX_INACTIVE_BG = 0x00000000U; + +constexpr uint32_t K_COLOR_HEADER_BG = 0xFFEFEFEFU; +constexpr uint32_t K_COLOR_FOOTER_BG = 0xFFF7F7F7U; + +constexpr int K_EDGE_EFFECT_NONE = 0; + +constexpr const char *K_DELETE_TEXT = "Delete"; +constexpr const char *K_FOOTER_TEXT = "—— 已到底 ——"; +constexpr const char *K_INVALID_TEXT = ""; + +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; +} // namespace + +// ===== 数据源 ===== +static const char *const NAMES_A[] = {"Alice", "Andrew", "Amy", "Aaron", "安娜", "安琪", "爱华", "阿明"}; +static const char *const NAMES_B[] = {"Ben", "Bella", "Brian", "Brandon", "博文", "斌", "白雪", "彬彬"}; +static const char *const NAMES_C[] = {"Chris", "Charlotte", "Cindy", "Caleb", "晨曦", "承泽", "楚怡", "春燕"}; +static const char *const NAMES_D[] = {"Daniel", "David", "Diana", "Dylan", "大伟", "东旭", "德华", "丹妮"}; +static const char *const NAMES_E[] = {"Emma", "Ethan", "Emily", "Eric", "恩泽", "恩雅", "尔雅", "恩宁"}; +static const char *const NAMES_F[] = {"Frank", "Fiona", "Felix", "Fred", "方圆", "芙蓉", "芳怡", "飞扬"}; +static const char *const NAMES_G[] = {"Grace", "George", "Gavin", "Gloria", "国强", "国华", "光耀", "桂英"}; +static const char *const NAMES_H[] = {"Henry", "Hannah", "Helen", "Harry", "海峰", "红梅", "宏伟", "浩然"}; +static const char *const NAMES_I[] = {"Isaac", "Ice", "Ian", "Isabella", "一涵", "一诺", "怡君", "依琳"}; +static const char *const NAMES_J[] = {"Jack", "James", "Jason", "Julia", "佳怡", "建国", "靖雯", "俊杰"}; +static const char *const NAMES_K[] = {"Kevin", "Kate", "Kelly", "Kyle", "可欣", "可可", "昆明", "康宁"}; +static const char *const NAMES_L[] = {"Lucas", "Leo", "Lily", "Lauren", "丽丽", "丽华", "立国", "林涛"}; +static const char *const NAMES_M[] = {"Michael", "Mary", "Mark", "Molly", "美玲", "明慧", "明杰", "梦瑶"}; +static const char *const NAMES_N[] = {"Nancy", "Nathan", "Nick", "Nora", "楠楠", "宁静", "娜娜", "乃文"}; +static const char *const NAMES_O[] = {"Oliver", "Olivia", "Owen", "Oscar", "欧阳娜", "欧莉", "欧阳晨", "欧文"}; +static const char *const NAMES_P[] = {"Peter", "Paul", "Philip", "Penny", "佩琪", "佩华", "平安", "鹏飞"}; +static const char *const NAMES_Q[] = {"Quentin", "Queenie", "Quinn", "Quincy", "琪琳", "倩倩", "清华", "强辉"}; +static const char *const NAMES_R[] = {"Robert", "Rachel", "Ryan", "Ruby", "荣辉", "若曦", "瑞雪", "日新"}; +static const char *const NAMES_S[] = {"Steven", "Susan", "Sarah", "Simon", "思远", "素芳", "诗涵", "少华"}; +static const char *const NAMES_T[] = {"Thomas", "Tony", "Tina", "Taylor", "天宇", "婷怡", "涛涛", "同辉"}; +static const char *const NAMES_U[] = {"Ulysses", "Uma", "Ulrich", "Ursula", "宇航", "宇轩", "宇宁", "宇泽"}; +static const char *const NAMES_V[] = {"Victor", "Victoria", "Vincent", "Vivian", "薇薇", "维娜", "维德", "维琪"}; +static const char *const NAMES_W[] = {"William", "Wendy", "Walter", "Willow", "伟强", "文静", "文博", "卫东"}; +static const char *const NAMES_X[] = {"Xavier", "Xander", "Xenia", "Xiomara", "晓明", "欣怡", "旭东", "霞"}; +static const char *const NAMES_Y[] = {"Yvonne", "Yolanda", "Yara", "Yvette", "怡然", "颖颖", "逸飞", "毅然"}; +static const char *const NAMES_Z[] = {"Zoe", "Zachary", "Zane", "Zara", "紫琪", "志强", "梓涵", "泽宇"}; + +struct GroupNames { + char letter; + const char *const *arr; + int size; +}; + +static const GroupNames GROUPS[] = {{'A', NAMES_A, Utils::ArrSize(NAMES_A)}, {'B', NAMES_B, Utils::ArrSize(NAMES_B)}, + {'C', NAMES_C, Utils::ArrSize(NAMES_C)}, {'D', NAMES_D, Utils::ArrSize(NAMES_D)}, + {'E', NAMES_E, Utils::ArrSize(NAMES_E)}, {'F', NAMES_F, Utils::ArrSize(NAMES_F)}, + {'G', NAMES_G, Utils::ArrSize(NAMES_G)}, {'H', NAMES_H, Utils::ArrSize(NAMES_H)}, + {'I', NAMES_I, Utils::ArrSize(NAMES_I)}, {'J', NAMES_J, Utils::ArrSize(NAMES_J)}, + {'K', NAMES_K, Utils::ArrSize(NAMES_K)}, {'L', NAMES_L, Utils::ArrSize(NAMES_L)}, + {'M', NAMES_M, Utils::ArrSize(NAMES_M)}, {'N', NAMES_N, Utils::ArrSize(NAMES_N)}, + {'O', NAMES_O, Utils::ArrSize(NAMES_O)}, {'P', NAMES_P, Utils::ArrSize(NAMES_P)}, + {'Q', NAMES_Q, Utils::ArrSize(NAMES_Q)}, {'R', NAMES_R, Utils::ArrSize(NAMES_R)}, + {'S', NAMES_S, Utils::ArrSize(NAMES_S)}, {'T', NAMES_T, Utils::ArrSize(NAMES_T)}, + {'U', NAMES_U, Utils::ArrSize(NAMES_U)}, {'V', NAMES_V, Utils::ArrSize(NAMES_V)}, + {'W', NAMES_W, Utils::ArrSize(NAMES_W)}, {'X', NAMES_X, Utils::ArrSize(NAMES_X)}, + {'Y', NAMES_Y, Utils::ArrSize(NAMES_Y)}, {'Z', NAMES_Z, Utils::ArrSize(NAMES_Z)}}; + +// ===== 交互上下文 ===== +struct ClickCtx { + ArkUI_NativeNodeAPI_1 *api{nullptr}; + std::shared_ptr> items; + std::weak_ptr adapter; + int index{-1}; + uint64_t stableId{0}; + ArkUI_NodeHandle itemHandle{nullptr}; +}; + +static std::unordered_map> s_btnCtx; +static std::unordered_map s_itemToDeleteBtn; + +// ===== 工具函数 ===== +static int FindIndexByStableId(const std::vector &items, uint64_t sid) +{ + const int n = static_cast(items.size()); + for (int i = 0; i < n; ++i) { + uint64_t v = static_cast(std::hash{}(items[static_cast(i)])); + if (v == sid) { + return i; + } + } + return -1; +} + +static int ClampFallbackIndex(int fallback, int n) +{ + if (n <= 0) { + return -1; + } + int idx = fallback; + if (idx < 0) { + idx = 0; + } + if (idx >= n) { + idx = n - 1; + } + return idx; +} + +// ===== 静态事件处理 ===== +static void StaticDeleteBtnEvent(ArkUI_NodeEvent *ev) +{ + if (ev == nullptr) { + return; + } + if (OH_ArkUI_NodeEvent_GetEventType(ev) != NODE_ON_CLICK) { + return; + } + + ClickCtx *ctx = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(ev)); + if (ctx == nullptr || ctx->api == nullptr) { + return; + } + std::shared_ptr> items = ctx->items; + if (!items) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete skip: items is null"); + return; + } + + int idx = FindIndexByStableId(*items, ctx->stableId); + if (idx < 0) { + idx = ClampFallbackIndex(ctx->index, static_cast(items->size())); + if (idx < 0) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete skip: no valid index"); + return; + } + } + + const int n = static_cast(items->size()); + if (idx < 0 || idx >= n) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete skip: invalid idx=%d, size=%d", idx, n); + return; + } + + std::string deletedItem = (*items)[static_cast(idx)]; + items->erase(items->begin() + idx); + + if (auto ad = ctx->adapter.lock()) { + ad->RemoveRange(idx, 1); + OH_LOG_Print(LOG_APP, LOG_INFO, K_LOG_DOMAIN, LOG_TAG, "Deleted item [%s] at index=%d, remaining=%d", + deletedItem.c_str(), idx, static_cast(items->size())); + } else { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete success but adapter is null"); + } +} + +// ===== 视图构造工具 ===== +static std::shared_ptr MakeText(const char *s, float h, uint32_t bg) +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + std::shared_ptr node = std::make_shared(text); + + Utils::SetTextContent(api, text, s); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_ITEM_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, h); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, h); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeUInt32(api, text, NODE_BACKGROUND_COLOR, bg); + return node; +} + +static void SetIndexTextStyle(ArkUI_NodeHandle text, int h, bool active) +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_INDEX_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, static_cast(h)); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, static_cast(h)); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeUInt32(api, text, NODE_FONT_COLOR, active ? K_COLOR_INDEX_ACTIVE : K_COLOR_INDEX_INACTIVE); + Utils::SetAttributeUInt32(api, text, NODE_BACKGROUND_COLOR, + active ? K_COLOR_INDEX_ACTIVE_BG : K_COLOR_INDEX_INACTIVE_BG); +} + +struct IndexState { + std::vector> letters; + int selected; + std::vector groupVisible; + IndexState() : selected(-1) {} +}; + +static void UpdateIndexHighlight(const std::shared_ptr &st, int idx) +{ + if (!st) { + return; + } + if (st->selected == idx) { + return; + } + + int prev = st->selected; + if (prev >= 0 && prev < static_cast(st->letters.size())) { + ArkUI_NodeHandle prevHandle = st->letters[static_cast(prev)]->GetHandle(); + SetIndexTextStyle(prevHandle, K_INDEX_ITEM_HEIGHT, false); + } + if (idx >= 0 && idx < static_cast(st->letters.size())) { + ArkUI_NodeHandle nowHandle = st->letters[static_cast(idx)]->GetHandle(); + SetIndexTextStyle(nowHandle, K_INDEX_ITEM_HEIGHT, true); + st->selected = idx; + } +} + +static std::shared_ptr BuildRightIndexColumn(const std::shared_ptr &list, + const std::shared_ptr &st) +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle colHandle = api->createNode(ARKUI_NODE_COLUMN); + std::shared_ptr col = std::make_shared(colHandle); + + col->SetWidth(K_INDEX_BAR_WIDTH); + col->SetHeightPercent(K_FULL_PERCENT); + + Utils::SetPadding( + api, col->GetHandle(), + Utils::Padding::Only(K_INDEX_BAR_PAD_TOP, K_INDEX_BAR_PAD_RIGHT, K_INDEX_BAR_PAD_BOTTOM, K_INDEX_BAR_PAD_LEFT)); + + st->letters.reserve(K_ALPHABET_COUNT); + for (int i = 0; i < K_ALPHABET_COUNT; ++i) { + char label[2]; + label[0] = static_cast('A' + i); + label[1] = '\0'; + + ArkUI_NodeHandle textHandle = api->createNode(ARKUI_NODE_TEXT); + std::shared_ptr t = std::make_shared(textHandle); + + Utils::SetTextContent(api, textHandle, label); + SetIndexTextStyle(textHandle, K_INDEX_ITEM_HEIGHT, false); + + int group = i; + t->RegisterOnClick([list, st, i, group](ArkUI_NodeEvent *) { + UpdateIndexHighlight(st, i); + list->ScrollToIndexInGroup(group, 0); + }); + + col->AddChild(t); + st->letters.emplace_back(t); + } + return col; +} + +static std::shared_ptr ApplyListSafeProps() +{ + std::shared_ptr list = std::make_shared(); + list->SetWidthPercent(K_LIST_WIDTH_PERCENT); + list->SetHeightPercent(K_FULL_PERCENT); + list->SetScrollBarState(true); + list->SetClipContent(true); + list->SetSpace(K_LIST_SPACE); + list->SetNestedScrollMode(ListMaker::kNestedScrollParentFirst); + return list; +} + +static ArkUI_NodeHandle CreateDeleteButton(ArkUI_NativeNodeAPI_1 *api) +{ + ArkUI_NodeHandle t = api->createNode(ARKUI_NODE_TEXT); + Utils::SetTextContent(api, t, K_DELETE_TEXT); + Utils::SetAttributeFloat32(api, t, NODE_FONT_SIZE, K_INDEX_FONT_SIZE); + Utils::SetAttributeFloat32(api, t, NODE_HEIGHT, K_DELETE_HEIGHT); + Utils::SetAttributeFloat32(api, t, NODE_TEXT_LINE_HEIGHT, K_DELETE_HEIGHT); + Utils::SetAttributeInt32(api, t, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeUInt32(api, t, NODE_BACKGROUND_COLOR, K_COLOR_DELETE_BG); + Utils::SetAttributeUInt32(api, t, NODE_FONT_COLOR, K_COLOR_WHITE); + Utils::SetAttributeFloat32(api, t, NODE_WIDTH, K_DELETE_WIDTH); + return t; +} + +static void SetupSwipeForListItem(ArkUI_NodeHandle item, ArkUI_NativeNodeAPI_1 *api, + const std::shared_ptr> &items, + const std::weak_ptr &adapterWeak) +{ + static std::vector> s_swipeKeep; + + std::unique_ptr swipe = std::make_unique(api); + + auto makeDeleteBtn = [api, items, adapterWeak, item](ArkUI_NativeNodeAPI_1 *apiInner) -> ArkUI_NodeHandle { + ArkUI_NodeHandle btn = CreateDeleteButton(apiInner); + if (!btn) { + return nullptr; + } + + // 1) 创建时立即建立上下文 + std::shared_ptr ctx = std::make_shared(); + ctx->api = api; + ctx->items = items; + ctx->adapter = adapterWeak; + ctx->itemHandle = item; + + s_itemToDeleteBtn[item] = btn; + s_btnCtx[btn] = ctx; + + // 2) 双路径注册(两个 API 都试一次,避免实例不一致) + apiInner->addNodeEventReceiver(btn, &StaticDeleteBtnEvent); + apiInner->registerNodeEvent(btn, NODE_ON_CLICK, 0, ctx.get()); + + // 保险:用外层 api 再注册一层(有的环境两个指针其实等价,但这样更稳) + api->addNodeEventReceiver(btn, &StaticDeleteBtnEvent); + api->registerNodeEvent(btn, NODE_ON_CLICK, 0, ctx.get()); + + // 3) 父项兜底注册(在 item 上也注册 CLICK) + // 点击冒泡到 item 时,我们在 StaticDeleteBtnEvent 里根据目标句柄再判断 + api->addNodeEventReceiver(item, &StaticDeleteBtnEvent); + api->registerNodeEvent(item, NODE_ON_CLICK, 0, ctx.get()); + + return btn; + }; + + swipe->BuildEndArea(makeDeleteBtn) + .SetActionAreaDistance(K_SWIPE_ACTION_DISTANCE) + .SetEdgeEffect(K_EDGE_EFFECT_NONE) + .OnEnter([] {}) + .OnExit([] {}) + .OnAction([] {}) + .OnStateChange([](int /*st*/) {}) + .OnOffsetChange([](float /*off*/) {}) + .OnEnterWithUserData([](void *) {}) + .OnExitWithUserData([](void *) {}) + .OnActionWithUserData([](void *) {}) + .OnStateChangeWithUserData([](int /*st*/, void *) {}) + .OnOffsetChangeWithUserData([](float /*off*/, void *) {}); + + swipe->AttachToListItem(item); + s_swipeKeep.emplace_back(std::move(swipe)); +} + +static ArkUI_NodeHandle CreateListItemWithSwipe(ArkUI_NativeNodeAPI_1 *api, + const std::shared_ptr> &items, + const std::weak_ptr &adapterWeak) +{ + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_LIST_ITEM); + api->addChild(item, text); + + Utils::SetAttributeFloat32(api, item, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + + SetupSwipeForListItem(item, api, items, adapterWeak); + return item; +} + +static void BindListItemContent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index, + const std::shared_ptr> &items, + const std::weak_ptr &adapterWeak) +{ + if (!Utils::ValidateApiAndNode(api, item, "BindListItemContent")) { + return; + } + if (!Utils::IsNotNull(items)) { + return; + } + + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!Utils::IsNotNull(text)) { + return; + } + + const int32_t n = static_cast(items->size()); + const bool valid = Utils::IsValidIndex(index, n); + const char *content = valid ? (*items)[static_cast(index)].c_str() : K_INVALID_TEXT; + + Utils::SetTextContent(api, text, content); + Utils::SetBackgroundColor(api, item, K_COLOR_WHITE); + Utils::SetTextStyle(api, text, K_ITEM_FONT_SIZE, K_COLOR_BLACK, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, K_ROW_HEIGHT); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, K_ROW_HEIGHT); + + // 更新删除按钮上下文 + auto itBtn = s_itemToDeleteBtn.find(item); + if (itBtn == s_itemToDeleteBtn.end()) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, + "BindListItemContent: item not found in s_itemToDeleteBtn"); + return; + } + + auto itCtx = s_btnCtx.find(itBtn->second); + if (itCtx == s_btnCtx.end() || !itCtx->second) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "BindListItemContent: context not found for button"); + return; + } + + ClickCtx &ctx = *itCtx->second; + ctx.index = index; + ctx.items = items; // 更新到当前 Group 的 items + ctx.adapter = adapterWeak; // 更新到当前 Group 的 adapter + + if (valid) { + ctx.stableId = static_cast(std::hash{}((*items)[static_cast(index)])); + } + + OH_LOG_Print(LOG_APP, LOG_DEBUG, K_LOG_DOMAIN, LOG_TAG, + "BindListItemContent: updated context for index=%d, content=%s", index, content); +} + +static NodeAdapterCallbacks CreateGroupCallbacks(const std::shared_ptr> &items) +{ + NodeAdapterCallbacks cb{}; + cb.getTotalCount = [items]() -> int32_t { return static_cast(items->size()); }; + cb.getStableId = [items](int32_t i) -> uint64_t { + const int32_t n = static_cast(items->size()); + if (i >= 0 && i < n) { + return static_cast(std::hash{}((*items)[static_cast(i)])); + } + return static_cast(i); + }; + return cb; +} + +static std::shared_ptr> CreateGroupItemsData(const GroupNames &gn) +{ + std::shared_ptr> items = std::make_shared>(); + items->reserve(static_cast(gn.size)); + for (int i = 0; i < gn.size; ++i) { + items->emplace_back(gn.arr[i]); + } + return items; +} + +static void SetupGroupHeaderAndFooter(std::shared_ptr &group, const GroupNames &gn, bool isLast) +{ + char title[16]{}; + title[0] = gn.letter; + title[1] = '\0'; + + std::shared_ptr header = MakeText(title, K_HEADER_HEIGHT, K_COLOR_HEADER_BG); + group->SetHeader(header); + + if (isLast) { + std::shared_ptr footer = MakeText(K_FOOTER_TEXT, K_FOOTER_HEIGHT, K_COLOR_FOOTER_BG); + group->SetFooter(footer); + } +} + +// 修改后的 CreateGroupAdapterCallbacks 函数 +static NodeAdapterCallbacks CreateGroupAdapterCallbacks(const std::shared_ptr> &items, + std::shared_ptr adapter) +{ + NodeAdapterCallbacks cb = CreateGroupCallbacks(items); + + cb.onCreate = [items, adapterWeak = std::weak_ptr(adapter)]( + ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) -> ArkUI_NodeHandle { + return CreateListItemWithSwipe(api, items, adapterWeak); + }; + + cb.onBind = [items, adapterWeak = std::weak_ptr(adapter)](ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle item, int32_t index) { + BindListItemContent(api, item, index, items, adapterWeak); + }; + + return cb; +} + +static std::shared_ptr BuildGroup(int gIndex, bool isLast, + const std::shared_ptr & /*st*/) +{ + const GroupNames &gn = GROUPS[static_cast(gIndex)]; + std::shared_ptr group = std::make_shared(); + + SetupGroupHeaderAndFooter(group, gn, isLast); + + std::shared_ptr> items = CreateGroupItemsData(gn); + std::shared_ptr adapter = + std::make_shared(static_cast(ARKUI_NODE_LIST_ITEM)); + NodeAdapterCallbacks cb = CreateGroupAdapterCallbacks(items, adapter); + + adapter->SetCallbacks(cb); + group->SetLazyAdapter(adapter); + return group; +} + +/** + * 创建按字母索引的懒加载列表 + */ +std::shared_ptr CreateLazyTextListExample() +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle rootHandle = api->createNode(ARKUI_NODE_ROW); + std::shared_ptr root = std::make_shared(rootHandle); + + root->SetWidthPercent(K_FULL_PERCENT); + root->SetHeightPercent(K_FULL_PERCENT); + + std::shared_ptr list = ApplyListSafeProps(); + std::shared_ptr st = std::make_shared(); + st->groupVisible.assign(K_ALPHABET_COUNT, false); + + for (int g = 0; g < K_ALPHABET_COUNT; ++g) { + bool isLast = (g == K_ALPHABET_COUNT - 1); + std::shared_ptr grp = BuildGroup(g, isLast, st); + list->AddChild(std::static_pointer_cast(grp)); + } + + std::shared_ptr right = BuildRightIndexColumn(list, st); + list->RegisterOnScrollIndex([st](int32_t first, int32_t last) { + int idx = first; + if (last == K_ALPHABET_COUNT - 1) { + idx = last; + } + if (idx < 0) + return; + if (idx >= K_ALPHABET_COUNT) + idx = K_ALPHABET_COUNT - 1; + UpdateIndexHighlight(st, idx); + }); + + list->RegisterOnReachEnd([st]() { UpdateIndexHighlight(st, K_ALPHABET_COUNT - 1); }); + + UpdateIndexHighlight(st, K_FIRST_GROUP_INDEX); + + root->AddChild(list); + root->AddChild(right); + + static std::vector> g_keepAlive; + g_keepAlive.emplace_back(root); + return root; +} + +ArkUI_NodeHandle ListMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + std::shared_ptr root = CreateLazyTextListExample(); + if (root && root->GetHandle() != nullptr) { + Utils::SetAttributeFloat32(api, root->GetHandle(), NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, root->GetHandle()); + } + + return page; +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.h new file mode 100644 index 000000000..9442f57cb --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ListMaker.h @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIST_MAKER_H +#define LIST_MAKER_H + +#include +#include + +#include +#include +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" +#include "ArkUINodeAdapter.h" + +#ifndef LOG_TAG +#define LOG_TAG "ListMaker" +#endif + +class ListMaker : public BaseNode { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + // 嵌套滚动模式常量 + static constexpr int32_t kNestedScrollParentFirst = 0; + static constexpr int32_t kNestedScrollChildFirst = 1; + static constexpr int32_t kNestedScrollSelfFirst = 2; + + // 组件事件数据数组索引常量 + static constexpr int32_t kScrollIndexFirstDataIndex = 0; + static constexpr int32_t kScrollIndexLastDataIndex = 3; + + static constexpr int32_t kVisibleChangeFirstChildDataIndex = 0; + static constexpr int32_t kVisibleChangeStartAreaDataIndex = 1; + static constexpr int32_t kVisibleChangeStartIndexDataIndex = 2; + static constexpr int32_t kVisibleChangeLastChildDataIndex = 3; + static constexpr int32_t kVisibleChangeEndAreaDataIndex = 4; + static constexpr int32_t kVisibleChangeEndIndexDataIndex = 5; + + static constexpr int32_t kScrollFrameBeginDataIndex = 0; + + static constexpr uint32_t kColorTransparent = 0x00000000U; + + // —— 可视内容变化事件数据 —— // + struct VisibleContentChange { + int32_t firstChildIndex = -1; // 可视区域首个“子组件”(item/header/footer)索引 + ArkUI_ListItemGroupArea startArea = ARKUI_LIST_ITEM_GROUP_AREA_OUTSIDE; // 起点区 + int32_t startItemIndex = -1; // 若起点不是 item,则为 -1 + + int32_t lastChildIndex = -1; // 可视区域最后一个“子组件”索引 + ArkUI_ListItemGroupArea endArea = ARKUI_LIST_ITEM_GROUP_AREA_OUTSIDE; // 终点区 + int32_t endItemIndex = -1; // 若终点不是 item,则为 -1 + + bool StartOnItem() const { return startArea == ARKUI_LIST_ITEM_SWIPE_AREA_ITEM && startItemIndex >= 0; } + bool EndOnItem() const { return endArea == ARKUI_LIST_ITEM_SWIPE_AREA_ITEM && endItemIndex >= 0; } + }; + +public: + ListMaker() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_LIST)), + nodeApi_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()) + { + if (!Utils::IsNotNull(nodeApi_) || !Utils::IsNotNull(GetHandle())) { + return; + } + + nodeApi_->addNodeEventReceiver(GetHandle(), &ListMaker::StaticEventReceiver); + const uint32_t scrollEventMask = SCROLL_EVT_FRAME_BEGIN | SCROLL_EVT_REACH_END; + scrollEventGuard_.Bind(nodeApi_, GetHandle(), this, scrollEventMask); + } + + ~ListMaker() override + { + scrollEventGuard_.Release(); + + if (!Utils::IsNotNull(nodeApi_)) { + return; + } + UnregisterSpecificEvents(); + ResetListAdapter(); + CleanupChildrenMainSizeOption(); + } + + // ======================================== + // 通用属性设置接口 + // ======================================== + void SetClipContent(bool clipEnabled) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_CLIP_CONTENT, clipEnabled ? 1 : 0); + } + + void SetEdgeEffectSpring(bool springEnabled) + { + int32_t effectValue = springEnabled ? ARKUI_EDGE_EFFECT_SPRING : ARKUI_EDGE_EFFECT_NONE; + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_EDGE_EFFECT, effectValue); + } + + void SetScrollBarVisible(bool visible) + { + int32_t displayMode = visible ? ARKUI_SCROLL_BAR_DISPLAY_MODE_ON : ARKUI_SCROLL_BAR_DISPLAY_MODE_OFF; + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_BAR_DISPLAY_MODE, displayMode); + } + + void SetItemSpacing(float spacing) { Utils::SetAttributeFloat32(nodeApi_, GetHandle(), NODE_LIST_SPACE, spacing); } + + void SetScrollBarState(bool visible) { SetScrollBarVisible(visible); } + void SetSpace(float spacing) { SetItemSpacing(spacing); } + + void SetNestedScrollMode(int32_t mode) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_NESTED_SCROLL, mode); + } + + // ======================================== + // 滚动控制接口 + // ======================================== + void ScrollToIndex(int32_t index) { ScrollToIndex(index, false, ARKUI_SCROLL_ALIGNMENT_START); } + + void ScrollToIndex(int32_t index, bool smooth, ArkUI_ScrollAlignment align) + { + ArkUI_NumberValue v[3]; + v[0].i32 = index; // value[0] + v[1].i32 = smooth ? 1 : 0; // value[1] (optional) + v[2].i32 = static_cast(align); // value[2] (optional) + + ArkUI_AttributeItem it{v, 3}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SCROLL_TO_INDEX, &it); + } + + void ScrollToIndexInGroup(int32_t groupIndex, int32_t itemIndex) + { + ArkUI_NumberValue values[] = {{.i32 = groupIndex}, {.i32 = itemIndex}}; + ArkUI_AttributeItem item{values, 2}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SCROLL_TO_INDEX_IN_GROUP, &item); + } + + // ======================================== + // 适配器设置接口 + // ======================================== + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (!Utils::IsNotNull(adapter)) { + nodeApi_->resetAttribute(GetHandle(), NODE_LIST_NODE_ADAPTER); + listAdapter_.reset(); + return; + } + adapter->EnsurePlaceholderTypeOr(static_cast(ARKUI_NODE_LIST_ITEM)); + ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetAdapter()}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_NODE_ADAPTER, &item); + listAdapter_ = adapter; + } + + // —— 扩展属性 —— // + void SetDirection(ArkUI_Axis axis) + { + ArkUI_NumberValue v0{}; + v0.i32 = static_cast(axis); + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_DIRECTION, &it); + } + + void SetSticky(ArkUI_StickyStyle sticky) + { + ArkUI_NumberValue v0{}; + v0.i32 = static_cast(sticky); + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_STICKY, &it); + } + + void SetCachedCount(int32_t count) + { + ArkUI_NumberValue v0{}; + v0.i32 = count; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_CACHED_COUNT, &it); + } + + void SetInitialIndex(int32_t index) + { + ArkUI_NumberValue v0{}; + v0.i32 = index; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_INITIAL_INDEX, &it); + } + + void SetDivider(float widthPx) + { + ArkUI_NumberValue v0{}; + v0.f32 = widthPx; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_DIVIDER, &it); + } + + void SetAlignListItem(ArkUI_ListItemAlignment align) + { + ArkUI_NumberValue v0{}; + v0.i32 = static_cast(align); + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_ALIGN_LIST_ITEM, &it); + } + + void SetChildrenMainSizeOption(ArkUI_ListChildrenMainSize *opt) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, opt}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_CHILDREN_MAIN_SIZE, &it); + childrenMainSizeOption_ = opt; + } + + void SetFocusWrapMode(int mode) + { + ArkUI_NumberValue v0{}; + v0.i32 = mode; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_FOCUS_WRAP_MODE, &it); + } + + void SetMaintainVisibleContentPosition(bool on) + { + ArkUI_NumberValue v0{}; + v0.i32 = on ? 1 : 0; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_MAINTAIN_VISIBLE_CONTENT_POSITION, &it); + } + + void SetStackFromEnd(bool on) + { + ArkUI_NumberValue v0{}; + v0.i32 = on ? 1 : 0; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_STACK_FROM_END, &it); + } + + void SetSyncLoad(bool on) + { + ArkUI_NumberValue v0{}; + v0.i32 = on ? 1 : 0; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SYNC_LOAD, &it); + } + + void SetScrollSnapAlign(int align /*ARKUI_SCROLL_SNAP_ALIGN_**/) + { + ArkUI_NumberValue v0{}; + v0.i32 = align; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SCROLL_SNAP_ALIGN, &it); + } + + void SetLanes(int lanes) + { + ArkUI_NumberValue v0{}; + v0.i32 = lanes; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_LANES, &it); + } + + void SetContentOffsets(float startPx, float endPx) + { + ArkUI_NumberValue v0{}; + v0.f32 = startPx; + ArkUI_AttributeItem it0{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_SCROLL_CONTENT_START_OFFSET, &it0); + + ArkUI_NumberValue v1{}; + v1.f32 = endPx; + ArkUI_AttributeItem it1{&v1, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_SCROLL_CONTENT_END_OFFSET, &it1); + } + + // ======================================== + // 事件注册接口 + // ======================================== + void RegisterOnScrollIndex(const std::function &callback) + { + onScrollIndexCallback_ = callback; + if (!isScrollIndexEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_INDEX, 0, this); + isScrollIndexEventRegistered_ = true; + } + } + + // 可视区域变化 + void RegisterOnVisibleContentChange(const std::function &callback) + { + onVisibleChangeCallback_ = callback; + if (!isVisibleChangeEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_VISIBLE_CONTENT_CHANGE, 0, this); + isVisibleChangeEventRegistered_ = true; + } + } + + void RegisterOnWillScroll(const std::function &callback) + { + onWillScrollCallback_ = callback; + if (!isWillScrollEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_WILL_SCROLL, 0, this); + isWillScrollEventRegistered_ = true; + } + } + + void RegisterOnDidScroll(const std::function &callback) + { + onDidScrollCallback_ = callback; + if (!isDidScrollEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_DID_SCROLL, 0, this); + isDidScrollEventRegistered_ = true; + } + } + + void RegisterOnReachEnd(const std::function &callback) { onReachEndCallback_ = callback; } + + void RegisterOnScrollFrameBegin(const std::function &callback) + { + onScrollFrameBeginCallback_ = callback; + } + +protected: + void OnNodeEvent(ArkUI_NodeEvent *event) override + { + BaseNode::OnNodeEvent(event); + + ArkUI_NodeComponentEvent *componentEvent = OH_ArkUI_NodeEvent_GetNodeComponentEvent(event); + if (!Utils::IsNotNull(componentEvent)) { + return; + } + + int32_t eventType = OH_ArkUI_NodeEvent_GetEventType(event); + HandleSpecificListEvent(eventType, componentEvent); + } + +private: + void UnregisterSpecificEvents() + { + if (isScrollIndexEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_INDEX); + } + if (isVisibleChangeEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_VISIBLE_CONTENT_CHANGE); + } + if (isWillScrollEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_WILL_SCROLL); + } + if (isDidScrollEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_DID_SCROLL); + } + } + + void ResetListAdapter() + { + if (Utils::IsNotNull(listAdapter_)) { + nodeApi_->resetAttribute(GetHandle(), NODE_LIST_NODE_ADAPTER); + listAdapter_.reset(); + } + } + + void CleanupChildrenMainSizeOption() + { + if (Utils::IsNotNull(childrenMainSizeOption_)) { + nodeApi_->resetAttribute(GetHandle(), NODE_LIST_CHILDREN_MAIN_SIZE); + OH_ArkUI_ListChildrenMainSizeOption_Dispose(childrenMainSizeOption_); + childrenMainSizeOption_ = nullptr; + } + } + + void OnScrollIndexEvt(const ArkUI_NodeComponentEvent *ev) + { + if (!onScrollIndexCallback_) { + return; + } + const int32_t firstIndex = ev->data[kScrollIndexFirstDataIndex].i32; + const int32_t lastIndex = ev->data[kScrollIndexLastDataIndex].i32; + onScrollIndexCallback_(firstIndex, lastIndex); + } + + void OnVisibleChangeEvt(const ArkUI_NodeComponentEvent *ev) + { + if (!onVisibleChangeCallback_) { + return; + } + VisibleContentChange v{}; + v.firstChildIndex = ev->data[kVisibleChangeFirstChildDataIndex].i32; + v.startArea = static_cast(ev->data[kVisibleChangeStartAreaDataIndex].i32); + v.startItemIndex = ev->data[kVisibleChangeStartIndexDataIndex].i32; + v.lastChildIndex = ev->data[kVisibleChangeLastChildDataIndex].i32; + v.endArea = static_cast(ev->data[kVisibleChangeEndAreaDataIndex].i32); + v.endItemIndex = ev->data[kVisibleChangeEndIndexDataIndex].i32; + onVisibleChangeCallback_(v); + } + + void OnReachEndEvt() + { + if (onReachEndCallback_) { + onReachEndCallback_(); + } + } + + void OnScrollFrameBeginEvt(const ArkUI_NodeComponentEvent *ev) + { + if (onScrollFrameBeginCallback_) { + onScrollFrameBeginCallback_(ev->data[kScrollFrameBeginDataIndex].f32); + } + } + + void OnWillScrollEvt() + { + if (onWillScrollCallback_) { + onWillScrollCallback_(); + } + } + + void OnDidScrollEvt() + { + if (onDidScrollCallback_) { + onDidScrollCallback_(); + } + } + + void HandleSpecificListEvent(int32_t eventType, ArkUI_NodeComponentEvent *ev) + { + switch (eventType) { + case NODE_LIST_ON_SCROLL_INDEX: + OnScrollIndexEvt(ev); + break; + case NODE_LIST_ON_SCROLL_VISIBLE_CONTENT_CHANGE: + OnVisibleChangeEvt(ev); + break; + case NODE_SCROLL_EVENT_ON_REACH_END: + OnReachEndEvt(); + break; + case NODE_SCROLL_EVENT_ON_SCROLL_FRAME_BEGIN: + OnScrollFrameBeginEvt(ev); + break; + case NODE_LIST_ON_WILL_SCROLL: + OnWillScrollEvt(); + break; + case NODE_LIST_ON_DID_SCROLL: + OnDidScrollEvt(); + break; + default: + break; + } + } + + ArkUI_ListChildrenMainSize *EnsureChildrenMainSizeOption() + { + if (!childrenMainSizeOption_) { + auto *opt = OH_ArkUI_ListChildrenMainSizeOption_Create(); + SetChildrenMainSizeOption(opt); + } + return childrenMainSizeOption_; + } + + void ChildrenMainSizeSetDefault(float mainSize) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_SetDefaultMainSize(opt, mainSize); + } + + float ChildrenMainSizeGetDefault() const + { + if (!childrenMainSizeOption_) { + return 0.0f; + } + return OH_ArkUI_ListChildrenMainSizeOption_GetDefaultMainSize(childrenMainSizeOption_); + } + + void ChildrenMainSizeResize(int32_t size) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_Resize(opt, size); + } + + void ChildrenMainSizeSplice(int32_t index, int32_t deleteCount, int32_t addCount) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_Splice(opt, index, deleteCount, addCount); + } + + void ChildrenMainSizeUpdate(int32_t index, float mainSize) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_UpdateSize(opt, index, mainSize); + } + + float ChildrenMainSizeGet(int32_t index) const + { + if (!childrenMainSizeOption_) { + return 0.0f; + } + return OH_ArkUI_ListChildrenMainSizeOption_GetMainSize(childrenMainSizeOption_, index); + } + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + std::shared_ptr listAdapter_; + ArkUI_ListChildrenMainSize *childrenMainSizeOption_ = nullptr; + + // 事件回调函数 + std::function onScrollIndexCallback_; + std::function onVisibleChangeCallback_; + std::function onReachEndCallback_; + std::function onScrollFrameBeginCallback_; + std::function onWillScrollCallback_; + std::function onDidScrollCallback_; + + // 事件注册状态 + bool isScrollIndexEventRegistered_ = false; + bool isVisibleChangeEventRegistered_ = false; + bool isWillScrollEventRegistered_ = false; + bool isDidScrollEventRegistered_ = false; + + ScrollEventGuard scrollEventGuard_; +}; + +#endif // LIST_MAKER_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.cpp new file mode 100644 index 000000000..be12a4216 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.cpp @@ -0,0 +1,617 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ListMaker.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#ifndef LOG_TAG +#define LOG_TAG "RefreshMaker" +#endif + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" +#include "ListItemGroup.h" +#include "ListItemSwipe.h" +#include "RefreshMaker.h" + +// ================= 业务常量 ================= +namespace { + +constexpr uint32_t K_COLOR_PAGE_BG = 0xFFF1F3F5U; +constexpr uint32_t K_ITEM_BG_COLOR = 0xFFFFFFFFU; +constexpr const char* K_LOADING_TEXT = "加载中…"; +constexpr float K_ITEM_FONT_SIZE = 16.0f; + +constexpr int K_INIT_COUNT = 10; +constexpr int K_LOAD_BATCH = 10; +constexpr int K_MAX_ITEMS = 100; +constexpr int K_REFRESH_PREPEND_COUNT = 5; +constexpr int K_MIN_REFRESH_MS = 350; + +constexpr float K_WIDTH_PERCENT_FULL = 1.0f; +constexpr float K_HEIGHT_PERCENT_FULL = 1.0f; +constexpr float K_LIST_SPACE = 12.0f; + +constexpr float K_REFRESH_OFFSET_VP = 64.0f; +constexpr float K_MAX_PULL_VP = 128.0f; +constexpr float K_PULL_DOWN_RATIO = 0.6f; + +constexpr bool K_LIST_CLIP_CONTENT = true; +constexpr bool K_LIST_EDGE_SPRING = true; +constexpr bool K_LIST_SCROLLBAR_VISIBLE = false; + +constexpr int K_MIN_INDEX = 0; +constexpr int K_LAST_INDEX_OFFSET = 1; + +// 样式 +constexpr float K_ROW_HEIGHT = 80.0f; +constexpr float K_FOOTER_HEIGHT = 64.0f; + +constexpr int64_t K_FOOTER_STABLE_ID = -16; + +} // namespace + +namespace { +constexpr int RET_OK = 0; + +inline bool Ok(int rc, const char* what) +{ + if (rc != RET_OK) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, + "%{public}s failed rc=%{public}d", what, rc); + return false; + } + return true; +} +} // namespace + +// ================= 状态 ================= +namespace { + +struct RefreshListState { + std::shared_ptr adapter; + std::vector data; + bool showLoadingFooter = false; + + int total = K_INIT_COUNT; // 逻辑总数(与 data.size() 同步增长) + int lastTriggeredTot = -1; + bool loading = false; + + bool NoMore() const { return total >= K_MAX_ITEMS; } + + int ItemsCount() const { return static_cast(data.size()); } + int RenderCount() const { return ItemsCount() + (showLoadingFooter ? 1 : 0); } +}; + +} // namespace + +// ================= 业务工具 ================= +namespace { + +/** + * 添加加载状态尾部 + * @param st 刷新列表状态 + */ +inline void AddLoadingFooter(const std::shared_ptr &st) +{ + const int footerIndex = st->ItemsCount(); + st->adapter->InsertRange(footerIndex, 1); + st->showLoadingFooter = true; +} + +/** + * 移除加载状态尾部 + * @param st 刷新列表状态 + */ +inline void RemoveLoadingFooter(const std::shared_ptr &st) +{ + const int footerIndex = st->ItemsCount(); + st->adapter->RemoveRange(footerIndex, 1); + st->showLoadingFooter = false; +} + +/** + * 切换加载状态尾部显示 + * @param st 刷新列表状态 + * @param on 是否显示 + */ +static void ToggleLoadingFooter(const std::shared_ptr &st, bool on) +{ + if (!st || !st->adapter) { + return; + } + + if (on && !st->showLoadingFooter) { + AddLoadingFooter(st); + } else if (!on && st->showLoadingFooter) { + RemoveLoadingFooter(st); + } +} + +/** + * 末尾追加批量数据 + * @param st 刷新列表状态 + * @param add 添加数量 + */ +static void AppendTailBatch(const std::shared_ptr &st, int add) +{ + if (!st || !st->adapter) { + return; + } + + const int base = st->total; + const int addClamped = std::min(add, K_MAX_ITEMS - base); + if (addClamped <= 0) { + return; + } + + // 先去掉 footer,再插入数据,再恢复 footer(避免 index 混淆) + const bool hadFooter = st->showLoadingFooter; + ToggleLoadingFooter(st, false); + + st->data.reserve(st->data.size() + static_cast(addClamped)); + for (int i = 0; i < addClamped; ++i) { + st->data.emplace_back(std::to_string(base + i)); + } + st->adapter->InsertRange(base, addClamped); + + st->total += addClamped; + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, + "AppendTailBatch add=%{public}d total=%{public}d", addClamped, st->total); + + if (hadFooter) { + ToggleLoadingFooter(st, true); + } +} + +} // namespace + +// ================= 适配器回调(通用 ArkUINodeAdapter) ================= +namespace { + +/** + * 创建列表项 + * @param api 原生节点API接口 + * @return 列表项节点句柄 + */ +static ArkUI_NodeHandle CreateListItem(ArkUI_NativeNodeAPI_1 *api) +{ + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + if (text == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "create TEXT node null"); + return nullptr; + } + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_LIST_ITEM); + if (item == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "create LIST_ITEM node null"); + return nullptr; + } + + if (int rc = api->addChild(item, text); !Ok(rc, "addChild(item,text)")) { + return nullptr; + } + + Utils::SetAttributeFloat32(api, item, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + + // 让文本也占满宽度 + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, 1.0f); + return item; +} + +/** + * 绑定为普通列表项 + * @param api 原生节点API接口 + * @param item 列表项节点句柄 + * @param txt 文本内容 + */ +static void BindAsNormal(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, const char *txt) +{ + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!text) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "BindAsNormal getFirstChild null"); + return; + } + + // 背景放在 item 上 + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + + // 文本占满宽度 + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, 1.0f); + + Utils::SetTextContent(api, text, txt); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_ITEM_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, K_ROW_HEIGHT); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, K_ROW_HEIGHT); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); +} + +/** + * 绑定为加载尾部项 + * @param api 原生节点API接口 + * @param item 列表项节点句柄 + */ +static void BindAsFooter(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item) +{ + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!text) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "BindAsFooter getFirstChild null"); + return; + } + + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, 1.0f); + + Utils::SetTextContent(api, text, K_LOADING_TEXT); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_ITEM_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, K_FOOTER_HEIGHT); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, K_FOOTER_HEIGHT); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); +} + +/** + * 创建节点适配器回调 + * @param st 刷新列表状态 + * @return 节点适配器回调结构 + */ +static NodeAdapterCallbacks MakeCallbacks(const std::shared_ptr &st) +{ + NodeAdapterCallbacks cb{}; + + cb.getTotalCount = [st]() -> int32_t { + return st ? st->RenderCount() : 0; + }; + + cb.getStableId = [st](int32_t i) -> uint64_t { + if (!st) { + return static_cast(i); + } + const int items = st->ItemsCount(); + if (st->showLoadingFooter && i == items) { + return static_cast(K_FOOTER_STABLE_ID); + } + if (i >= 0 && i < items) { + return static_cast(std::hash{}(st->data[static_cast(i)])); + } + return static_cast(i); + }; + + cb.onCreate = [](ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) -> ArkUI_NodeHandle { + return CreateListItem(api); + }; + + cb.onBind = [st](ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index) { + if (!st) { + return; + } + + const int items = st->ItemsCount(); + if (st->showLoadingFooter && index == items) { + BindAsFooter(api, item); + return; + } + if (index >= 0 && index < items) { + BindAsNormal(api, item, st->data[static_cast(index)].c_str()); + return; + } + BindAsNormal(api, item, ""); + }; + + return cb; +} + +/** + * 创建 ArkUINodeAdapter + */ +static std::shared_ptr MakeListAdapter(const std::shared_ptr &st) +{ + std::shared_ptr adapter = + std::make_shared(static_cast(ARKUI_NODE_LIST_ITEM)); + adapter->SetCallbacks(MakeCallbacks(st)); + return adapter; +} + +} // namespace + +// ================= 视图拼装 ================= +namespace { + +/** + * 创建根节点 + * @return 根节点智能指针 + */ +static std::shared_ptr MakeRoot() +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle h = api->createNode(ARKUI_NODE_COLUMN); + + std::shared_ptr root = std::make_shared(h); + root->SetWidthPercent(K_WIDTH_PERCENT_FULL); + root->SetHeightPercent(K_HEIGHT_PERCENT_FULL); + Utils::SetAttributeUInt32(api, root->GetHandle(), NODE_BACKGROUND_COLOR, K_COLOR_PAGE_BG); + return root; +} + +static void ConfigureList(const std::shared_ptr &list) +{ + list->SetWidthPercent(K_WIDTH_PERCENT_FULL); + list->SetHeightPercent(K_HEIGHT_PERCENT_FULL); + list->SetTransparentBackground(); + list->SetClipContent(K_LIST_CLIP_CONTENT); + list->SetEdgeEffectSpring(K_LIST_EDGE_SPRING); + list->SetScrollBarState(K_LIST_SCROLLBAR_VISIBLE); + list->SetSpace(K_LIST_SPACE); + list->SetNestedScrollMode(ListMaker::kNestedScrollParentFirst); +} + +/** + * 创建列表节点 + * @param group 列表项组节点 + * @return 列表节点智能指针 + */ +inline std::shared_ptr MakeList(const std::shared_ptr &group) +{ + std::shared_ptr list = std::make_shared(); + ConfigureList(list); + list->AddChild(std::static_pointer_cast(group)); + return list; +} + +/** + * 创建刷新节点 + * @param list 列表节点 + * @return 刷新节点智能指针 + */ +static std::shared_ptr MakeRefresh(const std::shared_ptr &list) +{ + std::shared_ptr refresh = std::make_shared(); + refresh->AttachChild(list); + refresh->SetTransparentBackground(); + refresh->SetPullToRefresh(true); + refresh->SetRefreshOffset(K_REFRESH_OFFSET_VP); + refresh->SetPullDownRatio(K_PULL_DOWN_RATIO); + refresh->SetMaxPullDownDistance(K_MAX_PULL_VP); + return refresh; +} + +// 触底加载 +static void WireReachEnd(const std::shared_ptr &st, const std::shared_ptr &list) +{ + list->RegisterOnReachEnd([st, list]() { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnReachEnd triggered"); + + if (!st || st->loading || st->lastTriggeredTot == st->total) { + return; + } + st->lastTriggeredTot = st->total; + + if (st->NoMore()) { + const int count = st->RenderCount(); + const int idx = std::max(K_MIN_INDEX, count - K_LAST_INDEX_OFFSET); + list->ScrollToIndex(idx); + return; + } + + st->loading = true; + ToggleLoadingFooter(st, true); + AppendTailBatch(st, K_LOAD_BATCH); + ToggleLoadingFooter(st, false); + st->loading = false; + }); +} + +static void OnVisibleChangeCore(const std::shared_ptr &st, + const std::shared_ptr &list, + int32_t endIdxInGroup) +{ + if (!st) { + return; + } + + const int items = st->ItemsCount(); + if (items <= 0) { + return; + } + + if (endIdxInGroup < items - K_LAST_INDEX_OFFSET) { + return; + } + + if (st->loading || st->lastTriggeredTot == st->total) { + return; + } + + st->lastTriggeredTot = st->total; + + if (st->NoMore()) { + const int count = st->RenderCount(); + const int idx = std::max(K_MIN_INDEX, count - K_LAST_INDEX_OFFSET); + list->ScrollToIndex(idx); + return; + } + + st->loading = true; + ToggleLoadingFooter(st, true); + AppendTailBatch(st, K_LOAD_BATCH); + ToggleLoadingFooter(st, false); + st->loading = false; +} + +inline void WireVisibleChange(const std::shared_ptr &st, const std::shared_ptr &list) +{ + using V = ListMaker::VisibleContentChange; + list->RegisterOnVisibleContentChange([st, list](const V &ev) { + const int32_t endIdx = ev.EndOnItem() ? ev.endItemIndex : -1; + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnVisibleContentChange endIdxInGroup=%{public}d", + endIdx); + OnVisibleChangeCore(st, list, endIdx); + }); +} + +static void PrependNewData(const std::shared_ptr &st) +{ + std::vector news; + news.reserve(K_REFRESH_PREPEND_COUNT); + for (int i = 0; i < K_REFRESH_PREPEND_COUNT; ++i) { + news.emplace_back(std::string("New Item ") + std::to_string(i)); + } + + const bool hadFooter = st->showLoadingFooter; + ToggleLoadingFooter(st, false); + + st->data.insert(st->data.begin(), news.begin(), news.end()); + st->adapter->InsertRange(0, K_REFRESH_PREPEND_COUNT); + + st->total = std::min(st->total + K_REFRESH_PREPEND_COUNT, K_MAX_ITEMS); + st->lastTriggeredTot = -1; + + if (hadFooter) { + ToggleLoadingFooter(st, true); + } + + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, + "OnRefresh prepend=%{public}d total=%{public}d", + K_REFRESH_PREPEND_COUNT, st->total); +} + +/** + * 刷新行为 + */ +static void WireRefreshBehavior(const std::shared_ptr &st, + const std::shared_ptr &refresh) +{ + static std::atomic_bool sRefreshing{false}; + + refresh->RegisterOnRefresh([st, refresh]() { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnRefresh triggered"); + + if (sRefreshing.exchange(true)) { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnRefresh ignored: busy"); + return; + } + + using Clock = std::chrono::steady_clock; + const auto t0 = Clock::now(); + + if (st && !st->NoMore()) { + PrependNewData(st); + } + + const int elapsedMs = + static_cast(std::chrono::duration_cast(Clock::now() - t0).count()); + const int delay = std::max(0, K_MIN_REFRESH_MS - elapsedMs); + + Utils::PostDelayedTask(delay, [refresh]() { + refresh->SetRefreshing(false); + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnRefresh complete"); + }); + + sRefreshing = false; + }); +} + +inline void WireRefreshLogs(const std::shared_ptr &refresh) +{ + refresh->RegisterOnOffsetChange([](float offsetVp) { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnOffsetChange offsetVp=%{public}f", offsetVp); + }); + refresh->RegisterOnStateChange([](int32_t state) { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnStateChange state=%{public}d", state); + }); +} + +/** + * Build + */ +static std::shared_ptr Build() +{ + // 根节点 + std::shared_ptr root = MakeRoot(); + + // 分组 + 列表 + std::shared_ptr group = std::make_shared(); + std::shared_ptr list = MakeList(group); + + // 刷新容器 + std::shared_ptr refresh = MakeRefresh(list); + + // 状态 + std::shared_ptr st = std::make_shared(); + st->data.reserve(K_MAX_ITEMS); + for (int i = 0; i < K_INIT_COUNT; ++i) { + st->data.emplace_back(std::to_string(i)); + } + st->total = K_INIT_COUNT; + + // 适配器 + st->adapter = MakeListAdapter(st); + group->SetLazyAdapter(st->adapter); + + // 行为绑定 + WireReachEnd(st, list); + WireVisibleChange(st, list); + WireRefreshBehavior(st, refresh); + WireRefreshLogs(refresh); + + // 组装 + root->AddChild(refresh); + + // keep alive + GetKeepAliveContainer().emplace_back(root); + GetKeepAliveContainer().emplace_back(refresh); + GetKeepAliveContainer().emplace_back(list); + GetKeepAliveContainer().emplace_back(group); + static std::vector> g_states; + g_states.emplace_back(st); + + return root; +} +} // namespace + +ArkUI_NodeHandle RefreshMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + std::shared_ptr root = Build(); + if (root && root->GetHandle() != nullptr) { + Utils::SetAttributeFloat32(api, root->GetHandle(), NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, root->GetHandle()); + } + + return page; +} \ No newline at end of file diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.h new file mode 100644 index 000000000..e592dc5fc --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/RefreshMaker.h @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef REFRESH_MAKER_H +#define REFRESH_MAKER_H + +#include +#include + +#include +#include + +#include "ScrollableNode.h" + +inline constexpr int32_t K_REFRESH_OFFSET_DATA_INDEX = 0; +inline constexpr int32_t K_REFRESH_STATE_DATA_INDEX = 0; + +class RefreshMaker : public BaseNode { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + RefreshMaker() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_REFRESH)), + registrar_(nodeApi_, GetHandle()) + { + nodeApi_->addNodeEventReceiver(GetHandle(), StaticEvent); + } + + // ===== 属性(组件范围)===== + void SetRefreshing(bool refreshing) + { + ArkUI_NumberValue v[]{{.i32 = refreshing ? 1 : 0}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_REFRESHING, &item); + } + + void SetHeaderContent(const std::shared_ptr &header) + { + if (header == nullptr) { + nodeApi_->resetAttribute(GetHandle(), NODE_REFRESH_CONTENT); + header_.reset(); + return; + } + ArkUI_AttributeItem item{nullptr, 0, nullptr, header->GetHandle()}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_CONTENT, &item); + header_ = header; + } + + void SetPullDownRatio(float ratio) + { + ArkUI_NumberValue v[]{{.f32 = ratio}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_PULL_DOWN_RATIO, &item); + } + + void SetRefreshOffset(float vp) + { + ArkUI_NumberValue v[]{{.f32 = vp}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_OFFSET, &item); + } + + void SetPullToRefresh(bool enable) + { + ArkUI_NumberValue v[]{{.i32 = enable ? 1 : 0}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_PULL_TO_REFRESH, &item); + } + + void SetMaxPullDownDistance(float vp) + { + ArkUI_NumberValue v[]{{.f32 = vp}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_MAX_PULL_DOWN_DISTANCE, &item); + } + + void AttachChild(const std::shared_ptr &child) + { + if (child != nullptr) { + AddChild(child); + } + } + + // ===== 事件注册 ===== + void RegisterOnRefresh(const std::function &cb) + { + onRefresh_ = cb; + registrar_.RegisterEvent(NODE_REFRESH_ON_REFRESH, this); + } + + void RegisterOnOffsetChange(const std::function &cb) + { + onOffsetChange_ = cb; + registrar_.RegisterEvent(NODE_REFRESH_ON_OFFSET_CHANGE, this); + } + + void RegisterOnStateChange(const std::function &cb) + { + onStateChange_ = cb; + registrar_.RegisterEvent(NODE_REFRESH_STATE_CHANGE, this); + } + +private: + static void StaticEvent(ArkUI_NodeEvent *ev) + { + if (ev == nullptr) { + return; + } + auto *self = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(ev)); + if (self == nullptr) { + return; + } + self->OnNodeEvent(ev); + } + + void OnNodeEvent(ArkUI_NodeEvent *ev) override + { + const int32_t type = OH_ArkUI_NodeEvent_GetEventType(ev); + ArkUI_NodeComponentEvent *ce = OH_ArkUI_NodeEvent_GetNodeComponentEvent(ev); + + switch (type) { + case NODE_REFRESH_ON_REFRESH: + if (onRefresh_) { + onRefresh_(); + } + break; + case NODE_REFRESH_ON_OFFSET_CHANGE: + if (ce != nullptr && onOffsetChange_) { + onOffsetChange_(ce->data[K_REFRESH_OFFSET_DATA_INDEX].f32); + } + break; + case NODE_REFRESH_STATE_CHANGE: + if (ce != nullptr && onStateChange_) { + onStateChange_(ce->data[K_REFRESH_STATE_DATA_INDEX].i32); + } + break; + default: + break; + } + } + +private: + std::shared_ptr header_; + std::function onRefresh_; + std::function onOffsetChange_; + std::function onStateChange_; + + Utils::NodeEventRegistrar registrar_; +}; + +#endif // REFRESH_MAKER_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.cpp new file mode 100644 index 000000000..02c64c18a --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.cpp @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollMaker.h" + +#include +#ifndef LOG_TAG +#define LOG_TAG "ScrollMaker" +#endif + +// ===== 业务常量 ===== +namespace { +constexpr float K_WIDTH_PERCENT_FULL = 1.0f; +constexpr float K_HEIGHT_PERCENT_FULL = 1.0f; +constexpr uint32_t K_PAGE_BG = 0xFFFFFF00U; // Yellow + +constexpr uint32_t K_COLOR_BROWN = 0xFFA52A2AU; +constexpr uint32_t K_COLOR_BLUE = 0xFF0000FFU; +constexpr uint32_t K_COLOR_GREEN = 0xFF008000U; + +constexpr float K_TEXT_SIZE = 100.0f; + +// 其他行为参数 +constexpr float K_FRICTION = 0.9f; +constexpr float K_FLING_LIMIT = 12000.0f; + +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; + +// 事件数据数组索引常量 +constexpr int32_t K_PRIMARY_DELTA_DATA_INDEX = 0; +constexpr int32_t K_SECONDARY_DELTA_DATA_INDEX = 1; +constexpr int32_t K_AREA_HEIGHT_FALLBACK_DATA_INDEX = 6; +constexpr int32_t K_AREA_HEIGHT_PRIMARY_DATA_INDEX = 7; +} // namespace + +// ===== 全局状态 ===== +static ArkUI_NativeNodeAPI_1 *gApi{nullptr}; +static std::shared_ptr gScroll; +static ArkUI_NodeHandle gRoot{nullptr}; +static ArkUI_NodeHandle gPageTop{nullptr}; +static ArkUI_NodeHandle gPageMid{nullptr}; +static ArkUI_NodeHandle gPageBot{nullptr}; + +static float g_selfH{0.0f}; +static float g_yOffset{0.0f}; + +// ===== 工具:页面创建与布局 ===== +/** + * 创建页面节点 + * @param bg 背景色 + * @param label 标签文本 + * @return 页面节点句柄 + */ +static ArkUI_NodeHandle CreatePage(uint32_t bg, const char *label) +{ + if (gApi == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = gApi->createNode(ARKUI_NODE_STACK); + Utils::SetAttributeFloat32(gApi, page, NODE_WIDTH_PERCENT, K_WIDTH_PERCENT_FULL); + Utils::SetAttributeFloat32(gApi, page, NODE_HEIGHT_PERCENT, K_HEIGHT_PERCENT_FULL); + Utils::SetAttributeUInt32(gApi, page, NODE_BACKGROUND_COLOR, bg); + + ArkUI_NodeHandle text = gApi->createNode(ARKUI_NODE_TEXT); + Utils::SetTextContent(gApi, text, label); + Utils::SetAttributeFloat32(gApi, text, NODE_FONT_SIZE, K_TEXT_SIZE); + + gApi->addChild(page, text); + return page; +} + +/** + * 根据索引获取偏移量 + * @param index 索引值 + * @return 偏移量 + */ +static inline float GetOffsetByIndex(int index) +{ + float off = g_yOffset - static_cast(index) * g_selfH; + if (off > 1.5f * g_selfH) { + off -= 3.0f * g_selfH; + } else if (off < -1.5f * g_selfH) { + off += 3.0f * g_selfH; + } + return off; +} + +static void ApplyTranslate(ArkUI_NodeHandle n, float ty) +{ + ArkUI_NumberValue vx{.f32 = 0.0f}; + ArkUI_NumberValue vy{.f32 = ty}; + ArkUI_NumberValue vz{.f32 = 0.0f}; + + ArkUI_NumberValue v[] = {vx, vy, vz}; + ArkUI_AttributeItem it{v, 3}; + + gApi->setAttribute(n, NODE_TRANSLATE, &it); +} + +static void SyncAllTranslate() +{ + if (gApi == nullptr) { + return; + } + ApplyTranslate(gPageTop, -GetOffsetByIndex(-1)); + ApplyTranslate(gPageMid, -GetOffsetByIndex(0)); + ApplyTranslate(gPageBot, -GetOffsetByIndex(1)); +} + +// ===== 事件读取辅助 ===== +static inline float ReadDeltaPrimary(ArkUI_NodeComponentEvent *ce) { return ce->data[K_PRIMARY_DELTA_DATA_INDEX].f32; } +static inline float ReadDeltaSecondary(ArkUI_NodeComponentEvent *ce) +{ + return ce->data[K_SECONDARY_DELTA_DATA_INDEX].f32; +} + +// ===== 事件处理 ===== +static void OnAreaChange(ArkUI_NodeComponentEvent *ce) +{ + if (ce == nullptr) { + return; + } + + float h = ce->data[K_AREA_HEIGHT_PRIMARY_DATA_INDEX].f32; + if (h <= 0.0f) { + h = ce->data[K_AREA_HEIGHT_FALLBACK_DATA_INDEX].f32; + } + + if (h > 0.0f && std::fabs(h - g_selfH) > 0.5f) { + g_selfH = h; + SyncAllTranslate(); + } +} + +static void ApplyScrollDelta(float delta) +{ + if (std::fabs(delta) <= 1e-5f) { + return; + } + + g_yOffset += delta; + + const float period = 3.0f * g_selfH; + if (g_selfH > 0.0f && period > 0.0f) { + while (g_yOffset < 0.0f) { + g_yOffset += period; + } + while (g_yOffset >= period) { + g_yOffset -= period; + } + } + + SyncAllTranslate(); +} + +static void OnScrollFrameBegin(ArkUI_NodeComponentEvent *ce) +{ + if (ce == nullptr) { + return; + } + + float delta = ReadDeltaPrimary(ce); + if (std::fabs(delta) < 1e-5f) { + delta = ReadDeltaSecondary(ce); + } + + ApplyScrollDelta(delta); + + ce->data[K_PRIMARY_DELTA_DATA_INDEX].f32 = 0.0f; + ce->data[K_SECONDARY_DELTA_DATA_INDEX].f32 = 0.0f; +} + +static void OnScroll(ArkUI_NodeComponentEvent *ce) +{ + if (ce == nullptr) { + return; + } + float delta = ReadDeltaPrimary(ce); + if (std::fabs(delta) < 1e-5f) { + delta = ReadDeltaSecondary(ce); + } + ApplyScrollDelta(delta); +} + +static void OnSimpleLog(const char *tag) +{ + OH_LOG_Print(LOG_APP, LOG_DEBUG, K_LOG_DOMAIN, LOG_TAG, "%{public}s", tag); +} + +static bool HandleCoreScrollEvents(int32_t type, ArkUI_NodeComponentEvent *ce) +{ + switch (type) { + case NODE_SCROLL_EVENT_ON_SCROLL_FRAME_BEGIN: + OnScrollFrameBegin(ce); + return true; + case NODE_SCROLL_EVENT_ON_WILL_SCROLL: + OnSimpleLog("OnWillScroll"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL: + OnScroll(ce); + return true; + case NODE_SCROLL_EVENT_ON_DID_SCROLL: + OnSimpleLog("OnDidScroll"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL_START: + OnSimpleLog("OnScrollStart"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL_STOP: + OnSimpleLog("OnScrollStop"); + return true; + default: + return false; + } +} + +static bool HandleBoundaryEvents(int32_t type, ArkUI_NodeComponentEvent * /*ce*/) +{ + switch (type) { + case NODE_SCROLL_EVENT_ON_REACH_START: + OnSimpleLog("OnReachStart"); + return true; + case NODE_SCROLL_EVENT_ON_REACH_END: + OnSimpleLog("OnReachEnd"); + return true; + case NODE_SCROLL_EVENT_ON_WILL_STOP_DRAGGING: + OnSimpleLog("OnWillStopDragging"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL_EDGE: + OnSimpleLog("OnScrollEdge"); + return true; + default: + return false; + } +} + +static bool HandleZoomEvents(int32_t type, ArkUI_NodeComponentEvent * /*ce*/) +{ + switch (type) { + case NODE_SCROLL_EVENT_ON_ZOOM_START: + OnSimpleLog("OnZoomStart"); + return true; + case NODE_SCROLL_EVENT_ON_ZOOM_STOP: + OnSimpleLog("OnZoomStop"); + return true; + case NODE_SCROLL_EVENT_ON_DID_ZOOM: + OnSimpleLog("OnDidZoom"); + return true; + default: + return false; + } +} + +static void HandleNodeEvent(ArkUI_NodeEvent *ev) +{ + if (ev == nullptr) { + return; + } + + int32_t type = OH_ArkUI_NodeEvent_GetEventType(ev); + ArkUI_NodeComponentEvent *ce = OH_ArkUI_NodeEvent_GetNodeComponentEvent(ev); + + if (type == NODE_EVENT_ON_AREA_CHANGE) { + OnAreaChange(ce); + return; + } + + if (HandleCoreScrollEvents(type, ce)) { + return; + } + if (HandleBoundaryEvents(type, ce)) { + return; + } + if (HandleZoomEvents(type, ce)) { + return; + } +} + +// ===== 组装构建 ===== +static void SetupScroll() +{ + gScroll = std::make_shared(); + + gScroll->SetWidthPercent(K_WIDTH_PERCENT_FULL); + gScroll->SetHeightPercent(K_HEIGHT_PERCENT_FULL); + gScroll->SetBackgroundColor(K_PAGE_BG); + + gScroll->SetDirection(ARKUI_SCROLL_DIRECTION_VERTICAL); + gScroll->SetScrollBarDisplayMode(ARKUI_SCROLL_BAR_DISPLAY_MODE_OFF); + gScroll->SetScrollBarWidth(2.0f); + gScroll->SetScrollBarColor(0x66000000U); + gScroll->SetScrollBarMargin(0.0f, 0.0f, 0.0f, 0.0f); + + gScroll->SetEnableScrollInteraction(true); + gScroll->SetFriction(K_FRICTION); + gScroll->SetClipContent(true); + gScroll->SetPageEnabled(false); + gScroll->SetBackToTopEnabled(false); + gScroll->SetFadingEdge(0.0f); + gScroll->SetFlingSpeedLimit(K_FLING_LIMIT); + gScroll->SetEdgeEffect(ARKUI_EDGE_EFFECT_SPRING, true, + static_cast(ARKUI_EFFECT_EDGE_START | ARKUI_EFFECT_EDGE_END)); +} + +static void SetupRootAndPages() +{ + gRoot = gApi->createNode(ARKUI_NODE_STACK); + Utils::SetAttributeFloat32(gApi, gRoot, NODE_WIDTH_PERCENT, K_WIDTH_PERCENT_FULL); + Utils::SetAttributeFloat32(gApi, gRoot, NODE_HEIGHT_PERCENT, K_HEIGHT_PERCENT_FULL); + + gPageTop = CreatePage(K_COLOR_BROWN, "3"); + gPageMid = CreatePage(K_COLOR_BLUE, "1"); + gPageBot = CreatePage(K_COLOR_GREEN, "2"); + + gApi->addChild(gRoot, gPageTop); + gApi->addChild(gRoot, gPageMid); + gApi->addChild(gRoot, gPageBot); + + gScroll->AddChild(gRoot); +} + +static void RegisterEvents() +{ + gApi->addNodeEventReceiver(gScroll->GetScroll(), &HandleNodeEvent); + gApi->addNodeEventReceiver(gRoot, &HandleNodeEvent); + gApi->registerNodeEvent(gRoot, NODE_EVENT_ON_AREA_CHANGE, 0, nullptr); +} + +/** + * 构建滚动视图 + * @return 滚动节点句柄 + */ +static ArkUI_NodeHandle Build() +{ + gApi = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + if (gApi == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "GetNativeNodeAPI failed"); + return nullptr; + } + + SetupScroll(); + SetupRootAndPages(); + RegisterEvents(); + SyncAllTranslate(); + + return gScroll->GetScroll(); +} + +ArkUI_NodeHandle ScrollMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + ArkUI_NodeHandle scroll = Build(); + if (scroll != nullptr) { + Utils::SetAttributeFloat32(api, scroll, NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, scroll); + } + + return page; +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.h new file mode 100644 index 000000000..2b91527b9 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollMaker.h @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCROLL_MAKER_H +#define SCROLL_MAKER_H + +#include +#include + +#include +#include +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" + +/** + * @brief ArkUI Scroll 节点轻封装 + */ +class ScrollMaker { +public: + using OnScrollState = std::function; + using OnWillScroll = std::function; + using OnDidScroll = std::function; + + void SetOnScrollStateChanged(OnScrollState cb) { onState_ = std::move(cb); } + void SetOnWillScroll(OnWillScroll cb) { onWill_ = std::move(cb); } + void SetOnDidScroll(OnDidScroll cb) { onDid_ = std::move(cb); } + + static ArkUI_NodeHandle CreateNativeNode(); + +public: + ScrollMaker() + { + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api_); + scroll_ = api_->createNode(ARKUI_NODE_SCROLL); + api_->addNodeEventReceiver(scroll_, StaticEvent); + scrollGuard_.Bind(api_, scroll_, this, SCROLL_EVT_ALL); + } + + ~ScrollMaker() + { + scrollGuard_.Release(); + scroll_ = nullptr; + api_ = nullptr; + } + + // ===== 通用布局/外观 ===== + void SetWidthPercent(float percent) { Utils::SetAttributeFloat32(api_, scroll_, NODE_WIDTH_PERCENT, percent); } + void SetHeightPercent(float percent) { Utils::SetAttributeFloat32(api_, scroll_, NODE_HEIGHT_PERCENT, percent); } + void SetBackgroundColor(uint32_t color) { Utils::SetAttributeUInt32(api_, scroll_, NODE_BACKGROUND_COLOR, color); } + + void SetDirection(ArkUI_ScrollDirection direction) + { + ArkUI_NumberValue v{.i32 = static_cast(direction)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_SCROLL_DIRECTION, &it); + } + + void SetScrollBarDisplayMode(ArkUI_ScrollBarDisplayMode displayMode) + { + ArkUI_NumberValue v{.i32 = static_cast(displayMode)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_DISPLAY_MODE, &it); + } + + void SetScrollBarWidth(float w) + { + ArkUI_NumberValue v{.f32 = w}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_WIDTH, &it); + } + + void SetScrollBarColor(uint32_t argb) + { + ArkUI_NumberValue v{.u32 = argb}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_COLOR, &it); + } + + /** @brief 滚动条外边距:top,right,bottom,left(单位:vp) */ + void SetScrollBarMargin(float top, float right, float bottom, float left) + { + ArkUI_NumberValue vTop{.f32 = top}; + ArkUI_NumberValue vRight{.f32 = right}; + ArkUI_NumberValue vBottom{.f32 = bottom}; + ArkUI_NumberValue vLeft{.f32 = left}; + ArkUI_NumberValue v[] = {vTop, vRight, vBottom, vLeft}; + ArkUI_AttributeItem it{v, 4}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_MARGIN, &it); + } + + void SetEnableScrollInteraction(bool enable) + { + ArkUI_NumberValue v{.i32 = enable ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_ENABLE_SCROLL_INTERACTION, &it); + } + + void SetFriction(float friction) { Utils::SetAttributeFloat32(api_, scroll_, NODE_SCROLL_FRICTION, friction); } + + void SetNestedMode(ArkUI_ScrollNestedMode mode) + { + ArkUI_NumberValue v{.i32 = static_cast(mode)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_NESTED_SCROLL, &it); + } + + void SetScrollEdge(ArkUI_ScrollEdge edge) + { + ArkUI_NumberValue v{.i32 = static_cast(edge)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_EDGE, &it); + } + + void SetContentClipMode(ArkUI_ContentClipMode clipMode) + { + ArkUI_NumberValue v{.i32 = static_cast(clipMode)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_CLIP_CONTENT, &it); + } + + void SetClipContent(bool clip) + { + ArkUI_NumberValue v{.i32 = clip ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_CLIP_CONTENT, &it); + } + + void SetPageEnabled(bool enable) + { + ArkUI_NumberValue v{.i32 = enable ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_PAGE, &it); + } + + void SetBackToTopEnabled(bool enable) + { + ArkUI_NumberValue v{.i32 = enable ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BACK_TO_TOP, &it); + } + + // 偏移/尺寸/By/Fling/渐隐/限速 + void SetOffset(float x, float y) + { + ArkUI_NumberValue vx{.f32 = x}; + ArkUI_NumberValue vy{.f32 = y}; + ArkUI_NumberValue v[] = {vx, vy}; + ArkUI_AttributeItem it{v, 2}; + api_->setAttribute(scroll_, NODE_SCROLL_OFFSET, &it); + } + + void SetSize(float w, float h) + { + ArkUI_NumberValue vw{.f32 = w}; + ArkUI_NumberValue vh{.f32 = h}; + ArkUI_NumberValue v[] = {vw, vh}; + ArkUI_AttributeItem it{v, 2}; + api_->setAttribute(scroll_, NODE_SCROLL_SIZE, &it); + } + + void ScrollBy(float dx, float dy) + { + ArkUI_NumberValue vx{.f32 = dx}; + ArkUI_NumberValue vy{.f32 = dy}; + ArkUI_NumberValue v[] = {vx, vy}; + ArkUI_AttributeItem it{v, 2}; + api_->setAttribute(scroll_, NODE_SCROLL_BY, &it); + } + + void Fling(float velocity) + { + ArkUI_NumberValue v{.f32 = velocity}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_FLING, &it); + } + + void SetFadingEdge(float len) + { + ArkUI_NumberValue v{.f32 = len}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_FADING_EDGE, &it); + } + + void SetFlingSpeedLimit(float limit) + { + ArkUI_NumberValue v{.f32 = limit}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_FLING_SPEED_LIMIT, &it); + } + + /** + * @brief 设置 Scroll 组件的边缘效果(越界回弹/发光等)。 + * @param effect [IN] 边缘效果类型,取值为 ArkUI_EdgeEffect(如 ARKUI_EDGE_EFFECT_NONE、ARKUI_EDGE_EFFECT_SPRING)。 + * @param enableWhenContentSmaller [IN] 当内容尺寸小于组件本身时是否仍启用边缘效果(1 启用,0 禁用)。 + * @param edge [IN] 生效方向,ArkUI_EffectEdge,可位或组合(如 ARKUI_EFFECT_EDGE_START | ARKUI_EFFECT_EDGE_END)。 + * @note 对应属性:NODE_SCROLL_EDGE_EFFECT;ArkUI_AttributeItem.value + * 含义:[0].i32=effect,[1].i32=enableWhenContentSmaller,[2].i32=edge。 + */ + void SetEdgeEffect(ArkUI_EdgeEffect effect, bool enableWhenContentSmaller, ArkUI_EffectEdge edge) + { + ArkUI_NumberValue v[3]; + v[0].i32 = static_cast(effect); // [0] 效果类型 + v[1].i32 = enableWhenContentSmaller ? 1 : 0; // [1] 小内容是否启用 + v[2].i32 = static_cast(edge); // [2] 生效边(位掩码) + + ArkUI_AttributeItem it{v, 3}; + api_->setAttribute(scroll_, NODE_SCROLL_EDGE_EFFECT, &it); + } + + void SetSnapAlign(ArkUI_ScrollSnapAlign align) + { + ArkUI_NumberValue v{.i32 = static_cast(align)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_SNAP, &it); + } + + // ===== 子节点 ===== + void AddChild(ArkUI_NodeHandle child) + { + if (child != nullptr) { + api_->addChild(scroll_, child); + } + } + + ArkUI_NodeHandle GetScroll() const { return scroll_; } + +private: + inline void HandleScrollStateChanged(const ArkUI_NodeComponentEvent *comp) + { + if (!onState_) { + return; + } + auto state = static_cast(comp->data[0].i32); + onState_(state); + } + + inline void HandleWillScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onWill_) { + return; + } + float dx = comp->data[0].f32; + float dy = comp->data[1].f32; + auto state = static_cast(comp->data[2].i32); + auto src = static_cast(comp->data[3].i32); + onWill_(dx, dy, state, src); + } + + inline void HandleDidScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onDid_) { + return; + } + float dx = comp->data[0].f32; + float dy = comp->data[1].f32; + auto state = static_cast(comp->data[2].i32); + onDid_(dx, dy, state); + } + + inline void HandleListWillScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onWill_) { + return; + } + float dy = comp->data[0].f32; + auto state = static_cast(comp->data[1].i32); + auto src = static_cast(comp->data[2].i32); + onWill_(0.0f, dy, state, src); + } + + inline void HandleOnWillScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onWill_) { + return; + } + float d = comp->data[0].f32; // 单轴按约定复用 dy + auto state = static_cast(comp->data[1].i32); + auto src = static_cast(comp->data[2].i32); + onWill_(0.0f, d, state, src); + } + + static void StaticEvent(ArkUI_NodeEvent *ev) + { + if (!ev) { + return; + } + auto *self = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(ev)); + if (!self) { + return; + } + + const auto *comp = OH_ArkUI_NodeEvent_GetNodeComponentEvent(ev); + if (!comp) { + return; + } + + switch (OH_ArkUI_NodeEvent_GetEventType(ev)) { + case NODE_SWIPER_EVENT_ON_SCROLL_STATE_CHANGED: + self->HandleScrollStateChanged(comp); + break; + case NODE_SCROLL_EVENT_ON_WILL_SCROLL: + self->HandleWillScroll(comp); + break; + case NODE_SCROLL_EVENT_ON_DID_SCROLL: + self->HandleDidScroll(comp); + break; + case NODE_LIST_ON_WILL_SCROLL: + self->HandleListWillScroll(comp); + break; + case NODE_ON_WILL_SCROLL: + self->HandleOnWillScroll(comp); + break; + default: + break; + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_{nullptr}; + ArkUI_NodeHandle scroll_{nullptr}; + ScrollEventGuard scrollGuard_{}; + + OnScrollState onState_{}; + OnWillScroll onWill_{}; + OnDidScroll onDid_{}; +}; +#endif // SCROLL_MAKER_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableEvent.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableEvent.h new file mode 100644 index 000000000..611599124 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableEvent.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCROLL_EVENT_H +#define SCROLL_EVENT_H + +#include +#include + +/** + * 滚动事件掩码 + */ +enum : uint32_t { + SCROLL_EVT_FRAME_BEGIN = 1u << 0, + SCROLL_EVT_START = 1u << 1, + SCROLL_EVT_STOP = 1u << 2, + SCROLL_EVT_REACH_START = 1u << 3, + SCROLL_EVT_REACH_END = 1u << 4, + SCROLL_EVT_WILL_STOP_DRAG = 1u << 5, + SCROLL_EVT_ALL = SCROLL_EVT_FRAME_BEGIN | SCROLL_EVT_START | SCROLL_EVT_STOP | SCROLL_EVT_REACH_START | + SCROLL_EVT_REACH_END | SCROLL_EVT_WILL_STOP_DRAG +}; + +/** + * 事件 map + */ +struct ScrollEvtMap { + uint32_t bit; + ArkUI_NodeEventType evt; +}; + +inline constexpr ScrollEvtMap K_SCROLL_EVT_MAP[] = { + {SCROLL_EVT_FRAME_BEGIN, NODE_SCROLL_EVENT_ON_SCROLL_FRAME_BEGIN}, + {SCROLL_EVT_START, NODE_SCROLL_EVENT_ON_SCROLL_START}, + {SCROLL_EVT_STOP, NODE_SCROLL_EVENT_ON_SCROLL_STOP}, + {SCROLL_EVT_REACH_START, NODE_SCROLL_EVENT_ON_REACH_START}, + {SCROLL_EVT_REACH_END, NODE_SCROLL_EVENT_ON_REACH_END}, + {SCROLL_EVT_WILL_STOP_DRAG, NODE_SCROLL_EVENT_ON_WILL_STOP_DRAGGING}, +}; + +template inline void ForEachScrollEvt(uint32_t mask, F &&fn) +{ + for (const auto &m : K_SCROLL_EVT_MAP) { + if (mask & m.bit) { + fn(m.evt); + } + } +} + +/** + * 批量注册事件 + */ +inline void RegisterScrollEvents(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, void *userData, + uint32_t mask = SCROLL_EVT_ALL) +{ + if (!api || !node) { + return; + } + ForEachScrollEvt(mask, [&](ArkUI_NodeEventType evt) { api->registerNodeEvent(node, evt, 0, userData); }); +} + +/** + * 批量取消注册事件 + */ +inline void UnregisterScrollEvents(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, uint32_t mask = SCROLL_EVT_ALL) +{ + if (!api || !node) { + return; + } + ForEachScrollEvt(mask, [&](ArkUI_NodeEventType evt) { api->unregisterNodeEvent(node, evt); }); +} + +/** + * 滚动事件自动注册/注销 + */ +class ScrollEventGuard { +public: + ScrollEventGuard() = default; + + ScrollEventGuard(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, void *userData, uint32_t mask = SCROLL_EVT_ALL) + { + Bind(api, node, userData, mask); + } + + void Bind(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, void *userData, uint32_t mask = SCROLL_EVT_ALL) + { + api_ = api; + node_ = node; + user_ = userData; + mask_ = mask; + RegisterScrollEvents(api_, node_, user_, mask_); + armed_ = true; + } + + void Release() + { + if (armed_) { + UnregisterScrollEvents(api_, node_, mask_); + armed_ = false; + } + } + + ~ScrollEventGuard() + { + if (armed_) { + UnregisterScrollEvents(api_, node_, mask_); + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_ = nullptr; + ArkUI_NodeHandle node_ = nullptr; + void *user_ = nullptr; + uint32_t mask_ = SCROLL_EVT_ALL; + bool armed_ = false; +}; + +#endif // SCROLL_EVENT_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableNode.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableNode.h new file mode 100644 index 000000000..795cfdd88 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableNode.h @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ARKUINODE_H +#define ARKUINODE_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ScrollableUtils.h" + +class NodeApiInstance { +public: + static NodeApiInstance *GetInstance() + { + static NodeApiInstance instance; + return &instance; + } + ArkUI_NativeNodeAPI_1 *GetNativeNodeAPI() const { return nodeApi_; } + +private: + NodeApiInstance() { OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, nodeApi_); } + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + + NodeApiInstance(const NodeApiInstance &) = delete; + NodeApiInstance &operator=(const NodeApiInstance &) = delete; +}; + +class BaseNode : public std::enable_shared_from_this { +public: + explicit BaseNode(ArkUI_NodeHandle handle) + : nodeApi_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()), nodeHandle_(handle) + { + if (!Utils::IsNotNull(nodeApi_) || !Utils::IsNotNull(nodeHandle_)) { + return; + } + RegisterClickEvent(); + } + + virtual ~BaseNode() + { + UnregisterClickEvent(); + ClearChildren(); + nodeHandle_ = nullptr; + } + + BaseNode(const BaseNode &) = delete; + BaseNode &operator=(const BaseNode &) = delete; + + ArkUI_NodeHandle GetHandle() const { return nodeHandle_; } + + void AddChild(const std::shared_ptr &child) + { + if (!Utils::IsNotNull(child)) { + return; + } + children_.push_back(child); + nodeApi_->addChild(nodeHandle_, child->GetHandle()); + } + + void RemoveChild(const std::shared_ptr &child) + { + if (!Utils::IsNotNull(child)) { + return; + } + auto it = std::find(children_.begin(), children_.end(), child); + if (it != children_.end()) { + nodeApi_->removeChild(nodeHandle_, child->GetHandle()); + children_.erase(it); + } + } + + // ---------------- 通用属性 ---------------- + + void SetWidth(float width) { Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_WIDTH, width); } + void SetHeight(float height) { Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_HEIGHT, height); } + void SetWidthPercent(float percent) + { + Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_WIDTH_PERCENT, percent); + } + void SetHeightPercent(float percent) + { + Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_HEIGHT_PERCENT, percent); + } + void SetSize(float w, float h) { Utils::SetSize(nodeApi_, nodeHandle_, w, h); } + void SetSizePercent(float wp, float hp) { Utils::SetSizePercent(nodeApi_, nodeHandle_, wp, hp); } + void SetFullSize() { Utils::SetFullSize(nodeApi_, nodeHandle_); } + + void SetBackgroundColor(uint32_t color) { Utils::SetBackgroundColor(nodeApi_, nodeHandle_, color); } + + virtual void SetTransparentBackground() final { Utils::SetTransparentBackground(nodeApi_, nodeHandle_); } + + void SetOpacity(float opacity) + { + if (!Utils::ValidateApiAndNode(nodeApi_, nodeHandle_, "BaseNode::SetOpacity")) { + return; + } + Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_OPACITY, opacity); + } + + // ---------------- 事件 ---------------- + + void RegisterOnClick(const std::function &callback) { onClickCallback_ = callback; } + +protected: + virtual void OnNodeEvent(ArkUI_NodeEvent *event) + { + if (OH_ArkUI_NodeEvent_GetEventType(event) == NODE_ON_CLICK && onClickCallback_) { + onClickCallback_(event); + } + } + + static void StaticEventReceiver(ArkUI_NodeEvent *event) + { + auto *self = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(event)); + if (Utils::IsNotNull(self)) { + self->OnNodeEvent(event); + } + } + +private: + void RegisterClickEvent() + { + if (Utils::IsNotNull(nodeApi_) && Utils::IsNotNull(nodeHandle_)) { + nodeApi_->registerNodeEvent(nodeHandle_, NODE_ON_CLICK, 0, this); + hasClickEventRegistered_ = true; + } + } + + void UnregisterClickEvent() + { + if (Utils::IsNotNull(nodeApi_) && Utils::IsNotNull(nodeHandle_) && hasClickEventRegistered_) { + nodeApi_->unregisterNodeEvent(nodeHandle_, NODE_ON_CLICK); + hasClickEventRegistered_ = false; + } + } + + void ClearChildren() { children_.clear(); } + +protected: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + ArkUI_NodeHandle nodeHandle_ = nullptr; + std::list> children_; + std::function onClickCallback_; + bool hasClickEventRegistered_ = false; +}; + +// 保活容器 +template inline std::vector> &GetKeepAliveContainer() +{ + static std::vector> keepAliveContainer; + return keepAliveContainer; +} + +#endif // COMMON_ARKUINODE_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.cpp new file mode 100644 index 000000000..753b99070 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.cpp @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ScrollableUtils.h" + +#include +#include +#include + +namespace Utils { + +// ------------------------------ +// 常量 +// ------------------------------ +namespace { +constexpr int K_EDGES = 4; +constexpr int K_EDGE_TOP = 0; +constexpr int K_EDGE_RIGHT = 1; +constexpr int K_EDGE_BOTTOM = 2; +constexpr int K_EDGE_LEFT = 3; + +constexpr int K_ENABLED = 1; + +constexpr float K_PERCENT_FULL = 1.0f; + +constexpr uint32_t K_COLOR_TRANSPARENT = 0x00000000U; +constexpr float K_DEFAULT_SCROLL_BAR_WIDTH = 4.0f; +constexpr uint32_t K_DEFAULT_SCROLL_BAR_COLOR = 0x66000000U; +constexpr float K_DEFAULT_SCROLL_FRICTION = 0.015f; +constexpr int32_t K_DEFAULT_FADING_EDGE = 12; + +// 属性数组的最大容量 +constexpr int K_MAX_ATTR_VALUES = 8; + +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; + +struct NodeAttrTarget { + ArkUI_NativeNodeAPI_1 *api; + ArkUI_NodeHandle node; + ArkUI_NodeAttributeType attr; + const char *debugName; +}; +} // namespace + +// ------------------------------ +// 判空 / 校验 +// ------------------------------ +bool IsValidRange(int32_t start, int32_t end, int32_t count) +{ + if (!IsValidIndex(start, count)) { + return false; + } + if (end < start) { + return false; + } + if (end > count) { + return false; + } + return true; +} + +bool ValidateApiAndNode(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, const char *functionName) +{ + if (!IsNotNull(api)) { + if (functionName != nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "%{public}s: api is null", functionName); + } + return false; + } + if (!IsNotNull(node)) { + if (functionName != nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "%{public}s: node is null", functionName); + } + return false; + } + return true; +} + +// ------------------------------ +// 通用属性设置 +// ------------------------------ +void SetAttributeFloat32(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, float value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeFloat32")) { + return; + } + ArkUI_NumberValue numberValue{}; + numberValue.f32 = value; + + ArkUI_AttributeItem item{&numberValue, 1}; + api->setAttribute(node, attr, &item); +} + +void SetAttributeUInt32(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, uint32_t value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeUInt32")) { + return; + } + ArkUI_NumberValue numberValue{}; + numberValue.u32 = value; + + ArkUI_AttributeItem item{&numberValue, 1}; + api->setAttribute(node, attr, &item); +} + +void SetAttributeInt32(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, int32_t value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeInt32")) { + return; + } + ArkUI_NumberValue numberValue{}; + numberValue.i32 = value; + + ArkUI_AttributeItem item{&numberValue, 1}; + api->setAttribute(node, attr, &item); +} + +void SetAttributeString(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, + const char *value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeString")) { + return; + } + if (!IsNotNull(value)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "SetAttributeString: value is null"); + return; + } + + ArkUI_AttributeItem item{nullptr, 0, value}; + api->setAttribute(node, attr, &item); +} + +// 设置 float 数组属性 +static void ApplyFloatArrayAttribute(const NodeAttrTarget &target, const float *values, int count) +{ + if (!ValidateApiAndNode(target.api, target.node, target.debugName)) { + return; + } + if (values == nullptr || count <= 0) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "%{public}s: invalid values", + target.debugName ? target.debugName : "ApplyFloatArrayAttribute"); + return; + } + + std::array buf{}; + int capped = count; + if (capped > static_cast(buf.size())) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "%{public}s: count too large=%{public}d", + target.debugName ? target.debugName : "ApplyFloatArrayAttribute", count); + capped = static_cast(buf.size()); + } + for (int i = 0; i < capped; ++i) { + buf[static_cast(i)].f32 = values[i]; + } + + ArkUI_AttributeItem item{buf.data(), static_cast(capped)}; + target.api->setAttribute(target.node, target.attr, &item); +} + +// ------------------------------ +// 尺寸 / 背景 +// ------------------------------ +void SetSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, float width, float height) +{ + SetAttributeFloat32(api, node, NODE_WIDTH, width); + SetAttributeFloat32(api, node, NODE_HEIGHT, height); +} + +void SetSizePercent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, float widthPercent, float heightPercent) +{ + SetAttributeFloat32(api, node, NODE_WIDTH_PERCENT, widthPercent); + SetAttributeFloat32(api, node, NODE_HEIGHT_PERCENT, heightPercent); +} + +void SetFullSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) +{ + SetSizePercent(api, node, K_PERCENT_FULL, K_PERCENT_FULL); +} + +void SetBackgroundColor(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, uint32_t color) +{ + SetAttributeUInt32(api, node, NODE_BACKGROUND_COLOR, color); +} + +void SetTransparentBackground(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) +{ + SetBackgroundColor(api, node, K_COLOR_TRANSPARENT); +} + +void SetPadding(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, const Padding &padding) +{ + const float vals[K_EDGES] = {padding.top, padding.right, padding.bottom, padding.left}; + ApplyFloatArrayAttribute(NodeAttrTarget{api, node, NODE_PADDING, "SetPadding"}, vals, K_EDGES); +} + +// ------------------------------ +// 文本 +// ------------------------------ +ArkUI_NodeHandle CreateTextNode(ArkUI_NativeNodeAPI_1 *api, const char *text) +{ + if (!IsNotNull(api) || !IsNotNull(text)) { + return nullptr; + } + + ArkUI_NodeHandle textNode = api->createNode(ARKUI_NODE_TEXT); + if (!IsNotNull(textNode)) { + return nullptr; + } + + SetAttributeString(api, textNode, NODE_TEXT_CONTENT, text); + return textNode; +} + +void SetTextStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle textNode, float fontSize, uint32_t fontColor, + int32_t textAlign) +{ + if (!ValidateApiAndNode(api, textNode, "SetTextStyle")) { + return; + } + SetAttributeFloat32(api, textNode, NODE_FONT_SIZE, fontSize); + SetAttributeUInt32(api, textNode, NODE_FONT_COLOR, fontColor); + SetAttributeInt32(api, textNode, NODE_TEXT_ALIGN, textAlign); +} + +void SetTextContent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle textNode, const char *text) +{ + SetAttributeString(api, textNode, NODE_TEXT_CONTENT, text); +} + +// ------------------------------ +// 滚动 +// ------------------------------ +void SetScrollBarStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, bool visible, float width, uint32_t color) +{ + if (!ValidateApiAndNode(api, node, "SetScrollBarStyle")) { + return; + } + + int32_t displayMode = visible ? ARKUI_SCROLL_BAR_DISPLAY_MODE_AUTO : ARKUI_SCROLL_BAR_DISPLAY_MODE_OFF; + SetAttributeInt32(api, node, NODE_SCROLL_BAR_DISPLAY_MODE, displayMode); + if (visible) { + SetAttributeFloat32(api, node, NODE_SCROLL_BAR_WIDTH, width); + SetAttributeUInt32(api, node, NODE_SCROLL_BAR_COLOR, color); + } +} + +void SetDefaultScrollStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) +{ + if (!ValidateApiAndNode(api, node, "SetDefaultScrollStyle")) { + return; + } + SetScrollBarStyle(api, node, true, K_DEFAULT_SCROLL_BAR_WIDTH, K_DEFAULT_SCROLL_BAR_COLOR); + SetAttributeInt32(api, node, NODE_SCROLL_EDGE_EFFECT, ARKUI_EDGE_EFFECT_SPRING); + SetAttributeInt32(api, node, NODE_SCROLL_ENABLE_SCROLL_INTERACTION, K_ENABLED); + SetAttributeFloat32(api, node, NODE_SCROLL_FRICTION, K_DEFAULT_SCROLL_FRICTION); + SetAttributeInt32(api, node, NODE_SCROLL_NESTED_SCROLL, ARKUI_SCROLL_NESTED_MODE_SELF_FIRST); + SetAttributeInt32(api, node, NODE_SCROLL_FADING_EDGE, K_DEFAULT_FADING_EDGE); +} + +// ------------------------------ +// 异步任务 +// ------------------------------ +void PostDelayedTask(int32_t delayMs, std::function task) +{ + if (!task) { + return; + } + + int32_t safeDelayMs = delayMs; + if (safeDelayMs < 0) { + safeDelayMs = 0; + } + + std::thread worker([safeDelayMs, fn = std::move(task)]() mutable { + if (safeDelayMs > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(safeDelayMs)); + } + fn(); + }); + worker.detach(); +} + +// ------------------------------ +// N-API 工具 +// ------------------------------ +std::string GetStringFromNapi(napi_env env, napi_value value) +{ + if (!IsNotNull(env) || !IsNotNull(value)) { + return std::string(); + } + + size_t length = 0; + napi_status s0 = napi_get_value_string_utf8(env, value, nullptr, 0, &length); + if (s0 != napi_ok) { + return std::string(); + } + + std::string result(length, '\0'); + size_t written = 0; + napi_status s1 = napi_get_value_string_utf8(env, value, result.data(), length + 1, &written); + if (s1 != napi_ok) { + return std::string(); + } + + return result; +} + +bool IsNapiArray(napi_env env, napi_value value) +{ + if (!IsNotNull(env) || !IsNotNull(value)) { + return false; + } + + bool isArray = false; + napi_status status = napi_is_array(env, value, &isArray); + if (status != napi_ok) { + return false; + } + + return isArray; +} + +ArkUI_NodeContentHandle GetNodeContentFromNapi(napi_env env, napi_callback_info info) +{ + if (!IsNotNull(env) || !IsNotNull(info)) { + return nullptr; + } + + size_t argc = 1; + napi_value arg0 = nullptr; + napi_status st = napi_get_cb_info(env, info, &argc, &arg0, nullptr, nullptr); + if (st != napi_ok || argc < 1) { + return nullptr; + } + + ArkUI_NodeContentHandle content = nullptr; + OH_ArkUI_GetNodeContentFromNapiValue(env, arg0, &content); + return content; +} + +void AddNodeToContent(ArkUI_NodeContentHandle content, ArkUI_NodeHandle node) +{ + if (!IsNotNull(content) || !IsNotNull(node)) { + return; + } + OH_ArkUI_NodeContent_AddNode(content, node); +} + +// ------------------------------ +// NodeEventRegistrar +// ------------------------------ +NodeEventRegistrar::NodeEventRegistrar(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) + : nodeApi_(api), nodeHandle_(node) +{ + if (!ValidateApiAndNode(api, node, "NodeEventRegistrar::Constructor")) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "NodeEventRegistrar: invalid api or node"); + } +} + +NodeEventRegistrar::~NodeEventRegistrar() +{ + if (!IsNotNull(nodeApi_) || !IsNotNull(nodeHandle_)) { + return; + } + for (ArkUI_NodeEventType eventType : registeredEventTypes_) { + nodeApi_->unregisterNodeEvent(nodeHandle_, eventType); + } + registeredEventTypes_.clear(); +} + +void NodeEventRegistrar::RegisterEvent(ArkUI_NodeEventType eventType, void *userData) +{ + if (!ValidateApiAndNode(nodeApi_, nodeHandle_, "NodeEventRegistrar::RegisterEvent")) { + return; + } + nodeApi_->registerNodeEvent(nodeHandle_, eventType, 0, userData); + registeredEventTypes_.push_back(eventType); +} + +void NodeEventRegistrar::RegisterMultipleEvents(std::initializer_list eventTypes, void *userData) +{ + for (ArkUI_NodeEventType eventType : eventTypes) { + RegisterEvent(eventType, userData); + } +} + +// ------------------------------ +// AdapterEventRegistrar +// ------------------------------ +AdapterEventRegistrar::AdapterEventRegistrar(ArkUI_NodeAdapterHandle adapter, void *userData, + void (*callback)(ArkUI_NodeAdapterEvent *)) + : adapterHandle_(adapter) +{ + if (!IsNotNull(adapterHandle_)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "AdapterEventRegistrar: adapter is null"); + return; + } + if (!IsNotNull(callback)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "AdapterEventRegistrar: callback is null"); + return; + } + + int32_t result = OH_ArkUI_NodeAdapter_RegisterEventReceiver(adapterHandle_, userData, callback); + if (result != 0) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, + "AdapterEventRegistrar: failed to register event receiver, result=%{public}d", result); + } +} + +AdapterEventRegistrar::~AdapterEventRegistrar() +{ + if (IsNotNull(adapterHandle_)) { + OH_ArkUI_NodeAdapter_UnregisterEventReceiver(adapterHandle_); + } +} + +} // namespace Utils diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.h new file mode 100644 index 000000000..42815bf4c --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/ScrollableUtils.h @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCROLL_UTILS_H +#define SCROLL_UTILS_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#ifndef LOG_TAG +#define LOG_TAG "ScrollUtils" +#endif + +namespace Utils { + +// ============================== +// 判空 / 校验 +// ============================== +template +inline bool IsNotNull(const T *ptr) +{ + return ptr != nullptr; +} + +template +inline bool IsNotNull(const std::shared_ptr &ptr) +{ + return ptr != nullptr; +} + +template +inline bool IsNotNull(T *ptr) +{ + return ptr != nullptr; +} + +inline bool IsValidIndex(int32_t index, int32_t count) +{ + return (index >= 0) && (index < count); +} + +inline bool IsValidRange(int32_t start, int32_t end, int32_t count); + +bool ValidateApiAndNode(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + const char *functionName = nullptr); + +template +constexpr int ArrSize(const T (&arr)[N]) noexcept +{ + return static_cast(N); +} + +// ============================== +// 尺寸 / 背景 +// ============================== + +struct Padding { + float top {0.f}; + float right {0.f}; + float bottom {0.f}; + float left {0.f}; + + static Padding All(float v) { return Padding{v, v, v, v}; } + static Padding Symmetric(float vertical, float horizontal) + { + return Padding{vertical, horizontal, vertical, horizontal}; + } + static Padding Only(float t, float r, float b, float l) + { + return Padding{t, r, b, l}; + } +}; + +void SetSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, float width, float height); + +void SetSizePercent(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + float widthPercent, + float heightPercent); + +void SetFullSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + +void SetBackgroundColor(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, uint32_t color); + +void SetTransparentBackground(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + +void SetPadding(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, const Padding &padding); + +// ============================== +// 通用属性设置 +// ============================== +void SetAttributeFloat32(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + float value); + +void SetAttributeUInt32(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + uint32_t value); + +void SetAttributeInt32(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + int32_t value); + +void SetAttributeString(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + const char *value); + +// ============================== +// 文本节点工具 +// ============================== +ArkUI_NodeHandle CreateTextNode(ArkUI_NativeNodeAPI_1 *api, const char *text); + +void SetTextStyle(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle textNode, + float fontSize, + uint32_t fontColor, + int32_t textAlign); + +void SetTextContent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle textNode, const char *text); + +// ============================== +// 滚动相关 +// ============================== +void SetScrollBarStyle(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + bool visible, + float width, + uint32_t color); + +void SetDefaultScrollStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + +// ============================== +// 异步任务 +// ============================== +void PostDelayedTask(int32_t delayMs, std::function task); + +// ============================== +// N-API 工具 +// ============================== +std::string GetStringFromNapi(napi_env env, napi_value value); + +bool IsNapiArray(napi_env env, napi_value value); + +ArkUI_NodeContentHandle GetNodeContentFromNapi(napi_env env, napi_callback_info info); + +void AddNodeToContent(ArkUI_NodeContentHandle content, ArkUI_NodeHandle node); + +// ============================== +// 节点事件注册器 +// ============================== +class NodeEventRegistrar { +public: + NodeEventRegistrar(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + ~NodeEventRegistrar(); + + void RegisterEvent(ArkUI_NodeEventType eventType, void *userData); + void RegisterMultipleEvents(std::initializer_list eventTypes, void *userData); + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + ArkUI_NodeHandle nodeHandle_ = nullptr; + std::vector registeredEventTypes_; +}; + +// ============================== +// Adapter 事件接收器 +// ============================== +class AdapterEventRegistrar { +public: + AdapterEventRegistrar(ArkUI_NodeAdapterHandle adapter, + void *userData, + void (*callback)(ArkUI_NodeAdapterEvent *)); + ~AdapterEventRegistrar(); + +private: + ArkUI_NodeAdapterHandle adapterHandle_ = nullptr; +}; + +} // namespace Utils + +#endif // SCROLL_UTILS_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.cpp new file mode 100644 index 000000000..bac997064 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.cpp @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ArkUINodeAdapter.h" +#include "WaterFlowMaker.h" +#include "WaterFlowSection.h" + +namespace { +constexpr char K_ITEM_TITLE_PREFIX[] = "FlowItem "; +constexpr float K_FONT_SIZE = 16.0f; + +constexpr float K_MAIN_A = 120.0f; +constexpr float K_MAIN_B = 160.0f; +constexpr float K_MAIN_C = 100.0f; + +constexpr float K_WATER_FLOW_W = 400.0f; +constexpr float K_WATER_FLOW_H = 600.0f; + +constexpr int K_INIT_RESERVE = 200; +constexpr int K_INIT_SEED = 100; + +constexpr int32_t K_CROSS_COUNT = 2; +constexpr float K_COLUMN_GAP = 10.0f; +constexpr float K_ROW_GAP = 10.0f; +constexpr int32_t K_MARGIN_TOP = 12, K_MARGIN_RIGHT = 12, K_MARGIN_BOTTOM = 12, K_MARGIN_LEFT = 12; + +constexpr float K_WIDTH_PERCENT_FULL = 1.0f; +constexpr bool K_SYNC_LOAD = true; +constexpr int32_t K_CACHED_ITEM_COUNT = 24; +constexpr float K_ITEM_MAIN_MIN = 80.0f; +constexpr float K_ITEM_MAIN_MAX = 220.0f; + +constexpr int32_t K_LAYOUT_DIRECTION_RTL = 1; +constexpr int32_t K_LAYOUT_MODE_WATER_FLOW = 1; + +constexpr char K_COLUMN_TEMPLATE[] = "1fr 1fr"; +constexpr char K_ROW_TEMPLATE[] = "auto"; + +constexpr int32_t K_SCROLL_TO_INDEX = 5; +constexpr int32_t K_SCROLL_ALIGN_CENTER = static_cast(ARKUI_SCROLL_ALIGNMENT_CENTER); + +constexpr int K_AUTO_THRESHOLD = 20; +constexpr int K_AUTO_BATCH = 100; +constexpr int K_AUTO_MAX_ITEMS = 100000; + +constexpr float K_MAIN_SEQ[] = {K_MAIN_A, K_MAIN_B, K_MAIN_C}; +constexpr size_t K_MAIN_SEQ_COUNT = sizeof(K_MAIN_SEQ) / sizeof(K_MAIN_SEQ[0]); +constexpr uint32_t K_PALETTE[] = {0xFF6A5ACD, 0xFF00FFFF, 0xFF00FF7F, 0xFFDA70D6, 0xFFFFC0CB}; +constexpr size_t K_PALETTE_COUNT = sizeof(K_PALETTE) / sizeof(K_PALETTE[0]); +} // namespace + +static std::shared_ptr gNode; +static std::shared_ptr gAdapter; +static std::shared_ptr gSection; +static std::vector gItems; + +struct AutoAppendConfig { + bool enabled = true; + int threshold = K_AUTO_THRESHOLD; + int batch = K_AUTO_BATCH; + int maxItems = K_AUTO_MAX_ITEMS; + bool appending = false; + int lastSizeTriggered = -1; +}; +static AutoAppendConfig g_auto; + +/** + * 根据索引获取主轴尺寸 + * @param idx 索引值 + * @return 主轴尺寸 + */ +static inline float MainSizeByIndex(int32_t idx) +{ + if (K_MAIN_SEQ_COUNT == 0U) { + return K_MAIN_C; + } + + const size_t i = static_cast(idx) % K_MAIN_SEQ_COUNT; + return K_MAIN_SEQ[i]; +} + +/** + * 根据索引获取颜色 + * @param index 索引值 + * @return 颜色值 + */ +static inline uint32_t ColorByIndex(int index) +{ + if (K_PALETTE_COUNT == 0U) { + return 0xFFFFFFFFU; + } + + const size_t i = static_cast(index) % K_PALETTE_COUNT; + return K_PALETTE[i]; +} + +static inline void BindText(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle flowItem, int32_t index) +{ + ArkUI_NodeHandle text = api->getFirstChild(flowItem); + if (!text) { + return; + } + + Utils::SetTextContent(api, text, gItems[static_cast(index)].c_str()); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_FONT_SIZE); +} + +/** + * 方法描述 + * @param total 参数描述 + * @return 返回值描述 + */ +static inline bool ReachedBoundary(int total) +{ + return (total >= g_auto.maxItems) || (total == g_auto.lastSizeTriggered); +} + +static inline bool ShouldAppend(int32_t index, int total) +{ + return g_auto.enabled && !g_auto.appending && !ReachedBoundary(total) && index >= (total - g_auto.threshold); +} + +static void AppendBatchInternal(int addCount) +{ + if (!gAdapter || !gSection || !gNode || addCount <= 0) { + return; + } + + const int32_t start = static_cast(gItems.size()); + gItems.reserve(gItems.size() + static_cast(addCount)); + + for (int i = 0; i < addCount; ++i) { + gItems.emplace_back(std::string(K_ITEM_TITLE_PREFIX) + std::to_string(start + i)); + } + + gAdapter->InsertRange(start, addCount); + + const int32_t newCount = static_cast(gItems.size()); + OH_ArkUI_WaterFlowSectionOption_SetItemCount(gSection->GetSectionOptions(), 0, newCount); + + gNode->SetSection(gSection); + g_auto.lastSizeTriggered = static_cast(gItems.size()); +} + +static void MaybeAppendOnTail(int32_t index) +{ + const int total = static_cast(gItems.size()); + if (!ShouldAppend(index, total)) { + return; + } + + g_auto.appending = true; + const int remain = g_auto.maxItems - total; + const int toAdd = remain > 0 ? std::min(g_auto.batch, remain) : 0; + AppendBatchInternal(toAdd); + g_auto.appending = false; +} + +static int32_t AdapterGetTotalCount() { return static_cast(gItems.size()); } + +static uint64_t AdapterGetStableId(int32_t i) +{ + const std::string &key = gItems[static_cast(i)]; + return static_cast(std::hash{}(key)); +} + +static ArkUI_NodeHandle AdapterOnCreate(ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) +{ + if (!api) { + return nullptr; + } + + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_FLOW_ITEM); + api->addChild(item, text); + return item; +} + +static void AdapterOnBind(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index) +{ + Utils::SetAttributeFloat32(api, item, NODE_HEIGHT, MainSizeByIndex(index)); + Utils::SetAttributeFloat32(api, item, NODE_WIDTH_PERCENT, K_WIDTH_PERCENT_FULL); + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, ColorByIndex(index)); + BindText(api, item, index); + MaybeAppendOnTail(index); +} + +static ArkUINodeAdapter::Callbacks MakeCallbacks() +{ + ArkUINodeAdapter::Callbacks cb{}; + cb.getTotalCount = &AdapterGetTotalCount; + cb.getStableId = &AdapterGetStableId; + cb.onCreate = &AdapterOnCreate; + cb.onBind = &AdapterOnBind; + cb.onRecycle = nullptr; + return cb; +} + +static ArkUI_NodeHandle CreateFooter() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (!api) { + return nullptr; + } + + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + Utils::SetTextContent(api, text, "到底啦…"); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, 14.0f); + + ArkUI_NodeHandle footer = api->createNode(ARKUI_NODE_FLOW_ITEM); + Utils::SetAttributeFloat32(api, footer, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, footer, NODE_HEIGHT, 48.0f); + Utils::SetAttributeUInt32(api, footer, NODE_BACKGROUND_COLOR, 0x11000000U); + api->addChild(footer, text); + return footer; +} + +static void SetupSection() +{ + ArkUI_Margin margin{K_MARGIN_TOP, K_MARGIN_RIGHT, K_MARGIN_BOTTOM, K_MARGIN_LEFT}; + + SingleSectionParams params{}; + params.itemCount = static_cast(gItems.size()); + params.crossCount = K_CROSS_COUNT; + params.colGap = K_COLUMN_GAP; + params.rowGap = K_ROW_GAP; + params.margin = margin; + params.getMainSizeByIndex = &MainSizeByIndex; + params.userData = nullptr; + params.getMainSizeByIndexWithUserData = nullptr; + + gNode->SetSingleSection(params); + gSection = gNode->GetWaterFlowSection(); +} + +static void SetupNodeAndAdapter() +{ + gNode = std::make_shared(); + gNode->SetHeight(K_WATER_FLOW_H); + gNode->SetWidth(K_WATER_FLOW_W); + gNode->SetScrollCommon(); + gNode->SetLayoutDirection(K_LAYOUT_DIRECTION_RTL); + gNode->SetColumnTemplate(K_COLUMN_TEMPLATE); + gNode->SetRowTemplate(K_ROW_TEMPLATE); + gNode->SetGaps(K_COLUMN_GAP, K_ROW_GAP); + gNode->SetCachedCount(K_CACHED_ITEM_COUNT); + gNode->SetItemConstraintSize(K_ITEM_MAIN_MIN, K_ITEM_MAIN_MAX); + gNode->SetLayoutMode(ARKUI_WATER_FLOW_LAYOUT_MODE_SLIDING_WINDOW); + gNode->SetSyncLoad(K_SYNC_LOAD); + + SetupSection(); + + if (ArkUI_NodeHandle footer = CreateFooter()) { + gNode->SetFooter(footer); + } + + gAdapter = std::make_shared(); + gAdapter->SetCallbacks(MakeCallbacks()); + gNode->SetLazyAdapter(gAdapter); + gAdapter->ReloadAllItems(); +} + +static void InitData() +{ + gItems.clear(); + gItems.reserve(static_cast(K_INIT_RESERVE)); + for (int i = 0; i < K_INIT_SEED; ++i) { + gItems.emplace_back(std::string(K_ITEM_TITLE_PREFIX) + std::to_string(i)); + } +} + +static ArkUI_NodeHandle BuildWaterFlow() +{ + InitData(); + SetupNodeAndAdapter(); + return gNode->GetWaterFlow(); +} + +ArkUI_NodeHandle WaterFlowMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (!api) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (!page) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + ArkUI_NodeHandle waterflow = BuildWaterFlow(); + if (waterflow) { + Utils::SetAttributeFloat32(api, waterflow, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, waterflow, NODE_LAYOUT_WEIGHT, 1.0f); + + api->addChild(page, waterflow); + } + return page; +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.h new file mode 100644 index 000000000..1edee671e --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowMaker.h @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef WATERFLOW_MAKER_H +#define WATERFLOW_MAKER_H + +#include +#include +#include +#include +#include + +#include "ArkUINodeAdapter.h" +#include "WaterFlowSection.h" +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" + +#ifndef LOG_TAG +#define LOG_TAG "WaterFlowMaker" +#endif + +// ===== 业务常量 ===== +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; +constexpr float K_DEFAULT_SCROLL_BAR_WIDTH = 4.0f; +constexpr uint32_t K_DEFAULT_SCROLL_BAR_COLOR = 0x66000000U; +constexpr float K_DEFAULT_SCROLL_FRICTION = 0.12f; +constexpr float K_DEFAULT_FLING_SPEED_LIMIT = 2800.0f; +// 渐隐边缘尺寸 +constexpr int32_t K_DEFAULT_FADING_EDGE = 12; +// 单分组索引 +constexpr int32_t K_SINGLE_SECTION_INDEX = 0; + +// ---- 新增:单分组配置结构体 ---- +struct SingleSectionParams { + int32_t itemCount = 0; + int32_t crossCount = 1; + float colGap = 0.0f; + float rowGap = 0.0f; + ArkUI_Margin margin{}; // {top,right,bottom,left} + + // 必填:按 index 计算主轴尺寸的回调 + float (*getMainSizeByIndex)(int32_t) = nullptr; + + // 可选:带 userData 的回调 + void *userData = nullptr; + float (*getMainSizeByIndexWithUserData)(int32_t, void *) = nullptr; +}; + +class WaterFlowMaker { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + WaterFlowMaker() + { + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api_); + waterFlow_ = api_->createNode(ARKUI_NODE_WATER_FLOW); + api_->addNodeEventReceiver(waterFlow_, StaticEvent); + + scrollGuard_.Bind(api_, waterFlow_, this, SCROLL_EVT_ALL); + + OH_LOG_Print(LOG_APP, LOG_INFO, K_LOG_DOMAIN, LOG_TAG, "WaterFlowNode created"); + } + + ~WaterFlowMaker() + { + scrollGuard_.Release(); + + adapter_.reset(); + section_.reset(); + waterFlow_ = nullptr; + } + + // ---- Size ---- + void SetWidth(float width) { Utils::SetAttributeFloat32(api_, waterFlow_, NODE_WIDTH, width); } + + void SetHeight(float height) { Utils::SetAttributeFloat32(api_, waterFlow_, NODE_HEIGHT, height); } + + // ---- Adapter ---- + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (adapter == nullptr) { + return; + } + ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetAdapter()}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_NODE_ADAPTER, &item); + adapter_ = adapter; + } + + // ---- Section ---- + void SetSection(const std::shared_ptr §ion) + { + if (section == nullptr) { + return; + } + auto *opts = section->GetSectionOptions(); + if (opts == nullptr) { + return; + } + ArkUI_NumberValue start[] = {{.i32 = K_SINGLE_SECTION_INDEX}}; + ArkUI_AttributeItem item{start, 1, nullptr, opts}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_SECTION_OPTION, &item); + section_ = section; + } + + // ---- Layout / template / gap ---- + void SetLayoutDirection(int32_t direction) + { + ArkUI_NumberValue v[] = {{.i32 = direction}}; + ArkUI_AttributeItem item{v, 1}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_LAYOUT_DIRECTION, &item); + } + + void SetColumnTemplate(const char *tpl) + { + ArkUI_AttributeItem item{nullptr, 0, tpl, nullptr}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_COLUMN_TEMPLATE, &item); + } + + void SetRowTemplate(const char *tpl) + { + ArkUI_AttributeItem item{nullptr, 0, tpl, nullptr}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_ROW_TEMPLATE, &item); + } + + void SetGaps(float colGap, float rowGap) + { + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_WATER_FLOW_COLUMN_GAP, colGap); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_WATER_FLOW_ROW_GAP, rowGap); + } + + // ---- Cache / scroll / mode ---- + void SetCachedCount(int32_t count) + { + Utils::SetAttributeInt32(api_, waterFlow_, NODE_WATER_FLOW_CACHED_COUNT, count); + } + + void SetFooter(ArkUI_NodeHandle footer) + { + ArkUI_AttributeItem item{nullptr, 0, nullptr, footer}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_FOOTER, &item); + } + + void ScrollToIndex(int32_t index, int32_t align) + { + ArkUI_NumberValue v[] = {{.i32 = index}, {.i32 = align}}; + ArkUI_AttributeItem item{v, 2}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_SCROLL_TO_INDEX, &item); + } + + void SetItemConstraintSize(float mainMin, float mainMax) + { + ArkUI_NumberValue v[] = {{.f32 = mainMin}, {.f32 = mainMax}}; + ArkUI_AttributeItem item{v, 2}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_ITEM_CONSTRAINT_SIZE, &item); + } + + void SetLayoutMode(ArkUI_WaterFlowLayoutMode mode) + { + Utils::SetAttributeInt32(api_, waterFlow_, NODE_WATER_FLOW_LAYOUT_MODE, static_cast(mode)); + } + + void SetSyncLoad(bool enabled) + { + Utils::SetAttributeInt32(api_, waterFlow_, NODE_WATER_FLOW_SYNC_LOAD, enabled ? 1 : 0); + } + + void SetScrollFriction(float f) { Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FRICTION, f); } + + void SetFlingSpeedLimit(float limit) + { + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FLING_SPEED_LIMIT, limit); + } + + // ---- 通用滚动外观/行为预设 ---- + void SetScrollCommon() + { + // 滚动条外观 + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_BAR_DISPLAY_MODE, + static_cast(ARKUI_SCROLL_BAR_DISPLAY_MODE_AUTO)); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_BAR_WIDTH, K_DEFAULT_SCROLL_BAR_WIDTH); + Utils::SetAttributeUInt32(api_, waterFlow_, NODE_SCROLL_BAR_COLOR, K_DEFAULT_SCROLL_BAR_COLOR); + + // 交互与摩擦 + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_ENABLE_SCROLL_INTERACTION, 1); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FRICTION, K_DEFAULT_SCROLL_FRICTION); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FLING_SPEED_LIMIT, K_DEFAULT_FLING_SPEED_LIMIT); + + // 嵌套滚动策略 & 渐隐边缘 + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_NESTED_SCROLL, + static_cast(ARKUI_SCROLL_NESTED_MODE_SELF_FIRST)); + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_FADING_EDGE, K_DEFAULT_FADING_EDGE); + } + + ArkUI_NodeHandle GetWaterFlow() const { return waterFlow_; } + + std::shared_ptr GetWaterFlowSection() const { return section_; } + + void SetSingleSection(const SingleSectionParams &p) + { + if (!ValidateSingleSectionParams(p)) { + return; + } + + EnsureSectionSized(1); + + SectionOption opt = BuildSectionOption(p.itemCount, p.crossCount, p.colGap, p.rowGap, p.margin); + opt.onGetItemMainSizeByIndex = p.getMainSizeByIndex; + opt.userData = p.userData; + + ApplySectionOption(opt); + RegisterUserDataCallbackIfNeeded(p.userData, p.getMainSizeByIndexWithUserData); + SetSection(section_); + } + +private: + static void StaticEvent(ArkUI_NodeEvent *ev) { (void)ev; } + + static bool ValidateSingleSectionParams(const SingleSectionParams &p) + { + if (p.getMainSizeByIndex == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "getMainSizeByIndex is null"); + return false; + } + if (p.itemCount < 0 || p.crossCount <= 0) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, + "invalid counts: itemCount=%{public}d, crossCount=%{public}d", p.itemCount, p.crossCount); + return false; + } + return true; + } + + void EnsureSectionSized(int32_t sectionCount) + { + if (section_ == nullptr) { + section_ = std::make_shared(); + } + section_->Resize(sectionCount); + } + + SectionOption BuildSectionOption(int32_t itemCount, int32_t crossCount, float colGap, float rowGap, + ArkUI_Margin margin) const + { + SectionOption opt{}; + opt.itemsCount = itemCount; + opt.crossCount = crossCount; + opt.columnsGap = colGap; + opt.rowsGap = rowGap; + opt.margin = margin; + return opt; + } + + void ApplySectionOption(const SectionOption &opt) + { + section_->SetSection(section_->GetSectionOptions(), K_SINGLE_SECTION_INDEX, opt); + } + + void RegisterUserDataCallbackIfNeeded(void *userData, float (*cb)(int32_t, void *)) + { + if (userData != nullptr && cb != nullptr) { + OH_ArkUI_WaterFlowSectionOption_RegisterGetItemMainSizeCallbackByIndexWithUserData( + section_->GetSectionOptions(), K_SINGLE_SECTION_INDEX, userData, cb); + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_ = nullptr; + ArkUI_NodeHandle waterFlow_ = nullptr; + std::shared_ptr section_ = nullptr; + std::shared_ptr adapter_; + + ScrollEventGuard scrollGuard_; +}; + +#endif // WATERFLOW_MAKER_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowSection.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowSection.h new file mode 100644 index 000000000..7352f04e5 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/WaterFlowSection.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef WATERFLOW_SECTION_H +#define WATERFLOW_SECTION_H + +#include + +struct SectionOption { + int32_t itemsCount = 0; + int32_t crossCount = 1; + float columnsGap = 0.0f; + float rowsGap = 0.0f; + ArkUI_Margin margin{0, 0, 0, 0}; // {top, right, bottom, left} + float (*onGetItemMainSizeByIndex)(int32_t) = nullptr; + void *userData = nullptr; +}; + +class WaterFlowSection { +public: + WaterFlowSection() : sectionOptions_(OH_ArkUI_WaterFlowSectionOption_Create()) {} + + ~WaterFlowSection() + { + OH_ArkUI_WaterFlowSectionOption_Dispose(sectionOptions_); + sectionOptions_ = nullptr; + } + + void Resize(int32_t size) { OH_ArkUI_WaterFlowSectionOption_SetSize(sectionOptions_, size); } + + int32_t Size() const { return OH_ArkUI_WaterFlowSectionOption_GetSize(sectionOptions_); } + + void SetSection(ArkUI_WaterFlowSectionOption *opts, int32_t index, const SectionOption &s) + { + if (opts == nullptr) { + return; + } + OH_ArkUI_WaterFlowSectionOption_SetItemCount(opts, index, s.itemsCount); + OH_ArkUI_WaterFlowSectionOption_SetCrossCount(opts, index, s.crossCount); + OH_ArkUI_WaterFlowSectionOption_SetColumnGap(opts, index, s.columnsGap); + OH_ArkUI_WaterFlowSectionOption_SetRowGap(opts, index, s.rowsGap); + OH_ArkUI_WaterFlowSectionOption_SetMargin(opts, index, s.margin.top, s.margin.right, s.margin.bottom, + s.margin.left); + if (s.onGetItemMainSizeByIndex != nullptr) { + OH_ArkUI_WaterFlowSectionOption_RegisterGetItemMainSizeCallbackByIndex(opts, index, + s.onGetItemMainSizeByIndex); + } + } + + SectionOption GetSection(ArkUI_WaterFlowSectionOption *opts, int32_t index) + { + SectionOption s{}; + if (opts == nullptr) { + return s; + } + + s.itemsCount = OH_ArkUI_WaterFlowSectionOption_GetItemCount(opts, index); + s.crossCount = OH_ArkUI_WaterFlowSectionOption_GetCrossCount(opts, index); + s.columnsGap = OH_ArkUI_WaterFlowSectionOption_GetColumnGap(opts, index); + s.rowsGap = OH_ArkUI_WaterFlowSectionOption_GetRowGap(opts, index); + s.margin = OH_ArkUI_WaterFlowSectionOption_GetMargin(opts, index); + + return s; + } + + ArkUI_WaterFlowSectionOption *GetSectionOptions() const { return sectionOptions_; } + +private: + ArkUI_WaterFlowSectionOption *sectionOptions_ = nullptr; +}; + +#endif // WATERFLOW_SECTION_H diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.cpp index 1914f9c6f..0fd9fee57 100644 --- a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.cpp +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.cpp @@ -14,10 +14,15 @@ */ #include "manager.h" +#include "GridMaker.h" #include "TextMaker.h" #include "SwiperMaker.h" #include "AccessibilityMaker.h" #include "EmbeddedComponentMaker.h" +#include "WaterFlowMaker.h" +#include "ScrollMaker.h" +#include "RefreshMaker.h" +#include "ListMaker.h" #include "baseUtils.h" #include "napi/native_api.h" #include @@ -27,120 +32,114 @@ #include namespace ConstIde { - const uint32_t NUMBER_0 = 0; - const uint32_t NUMBER_1 = 1; - const uint32_t NUMBER_2 = 2; - const uint32_t MARGIN_NUMBER_30 = 30; -} +const uint32_t NUMBER_0 = 0; +const uint32_t NUMBER_1 = 1; +const uint32_t NUMBER_2 = 2; +const uint32_t MARGIN_NUMBER_30 = 30; +constexpr const char *K_LOG_DOMAIN = "Manager"; +} // namespace ConstIde Manager Manager::manager_; -ArkUI_NativeNodeAPI_1 *Manager::nodeAPI_ = reinterpret_cast( +ArkUI_NativeNodeAPI_1 *Manager::nodeAPI_ = reinterpret_cast( OH_ArkUI_QueryModuleInterfaceByName(ARKUI_NATIVE_NODE, "ArkUI_NativeNodeAPI_1")); -napi_value Manager::CreateSwiperNativeNode(napi_env env, napi_callback_info info) +template +static napi_value CreateNativeNode(napi_env env, napi_callback_info info, const char *who, MakeNodeFn makeNodeFn) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, ConstIde::K_LOG_DOMAIN, "%{public}s BEGIN", who); + if ((env == nullptr) || (info == nullptr)) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, ConstIde::K_LOG_DOMAIN, "%{public}s env or info is null", + who); return nullptr; } - size_t argCnt = ConstIde::NUMBER_1; + + size_t argc = ConstIde::NUMBER_1; napi_value args[ConstIde::NUMBER_1] = {nullptr}; - if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); + napi_status st = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (st != napi_ok || argc < ConstIde::NUMBER_1) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, ConstIde::K_LOG_DOMAIN, "%{public}s napi_get_cb_info failed", + who); + return nullptr; } ArkUI_NodeContentHandle nodeContentHandle = nullptr; - OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); - - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); - if (nodeAPI_ != nullptr) { - if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { - ArkUI_NodeHandle testNode = SwiperMaker::createSwiperPage(); - OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); - } + if (nodeContentHandle == nullptr) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, ConstIde::K_LOG_DOMAIN, + "%{public}s nodeContentHandle is null", who); + return nullptr; } - return nullptr; -} -napi_value Manager::CreateNativeTextNode(napi_env env, napi_callback_info info) -{ - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); - if ((env == nullptr) || (info == nullptr)) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, ConstIde::K_LOG_DOMAIN, "%{public}s after GetNodeContent", who); + + if (Manager::nodeAPI_ == nullptr) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, ConstIde::K_LOG_DOMAIN, "%{public}s nodeAPI_ is null", who); return nullptr; } - size_t argCnt = ConstIde::NUMBER_1; - napi_value args[ConstIde::NUMBER_1] = {nullptr}; - if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); + + // 构建具体节点 & 挂载 + ArkUI_NodeHandle page = makeNodeFn(); + if (page == nullptr) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, ConstIde::K_LOG_DOMAIN, "%{public}s makeNodeFn return null", + who); + return nullptr; } - ArkUI_NodeContentHandle nodeContentHandle = nullptr; + OH_ArkUI_NodeContent_AddNode(nodeContentHandle, page); + return nullptr; +} - OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); +napi_value Manager::CreateSwiperNativeNode(napi_env env, napi_callback_info info) +{ + return CreateNativeNode(env, info, "CreateNativeSwiperNode", + []() -> ArkUI_NodeHandle { return SwiperMaker::createSwiperPage(); }); +} - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); - if (nodeAPI_ != nullptr) { - if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { - ArkUI_NodeHandle testNode = TextMaker::createTextPage(); - OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); - } - } - return nullptr; +napi_value Manager::CreateNativeTextNode(napi_env env, napi_callback_info info) +{ + return CreateNativeNode(env, info, "CreateNativeTextNode", + []() -> ArkUI_NodeHandle { return TextMaker::createTextPage(); }); } napi_value Manager::CreateNativeAccessibilityNode(napi_env env, napi_callback_info info) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); - if ((env == nullptr) || (info == nullptr)) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); - return nullptr; - } - size_t argCnt = ConstIde::NUMBER_1; - napi_value args[ConstIde::NUMBER_1] = {nullptr}; - if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); - } - - ArkUI_NodeContentHandle nodeContentHandle = nullptr; + return CreateNativeNode(env, info, "CreateNativeAccessibilityNode", + []() -> ArkUI_NodeHandle { return AccessibilityMaker::CreateNativeNode(); }); +} - OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); +napi_value Manager::CreateNativeEmbeddedComponentNode(napi_env env, napi_callback_info info) +{ + return CreateNativeNode(env, info, "CreateNativeEmbeddedComponentNode", + []() -> ArkUI_NodeHandle { return EmbeddedComponentMaker::CreateNativeNode(); }); +} - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); - if (nodeAPI_ != nullptr) { - if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { - ArkUI_NodeHandle testNode = AccessibilityMaker::CreateNativeNode(); - OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); - } - } - return nullptr; +napi_value Manager::CreateWaterFlowNativeNode(napi_env env, napi_callback_info info) +{ + return CreateNativeNode(env, info, "CreateWaterFlowNativeNode", + []() -> ArkUI_NodeHandle { return WaterFlowMaker::CreateNativeNode(); }); } -napi_value Manager::CreateNativeEmbeddedComponentNode(napi_env env, napi_callback_info info) +napi_value Manager::CreateGridNativeNode(napi_env env, napi_callback_info info) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); - if ((env == nullptr) || (info == nullptr)) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); - return nullptr; - } - size_t argCnt = ConstIde::NUMBER_1; - napi_value args[ConstIde::NUMBER_1] = {nullptr}; - if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); - } + return CreateNativeNode(env, info, "CreateGridNativeNode", + []() -> ArkUI_NodeHandle { return GridMaker::CreateNativeNode(); }); +} - ArkUI_NodeContentHandle nodeContentHandle = nullptr; +napi_value Manager::CreateScrollNativeNode(napi_env env, napi_callback_info info) +{ + return CreateNativeNode(env, info, "CreateScrollNativeNode", + []() -> ArkUI_NodeHandle { return ScrollMaker::CreateNativeNode(); }); +} - OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); +napi_value Manager::CreateRefreshNativeNode(napi_env env, napi_callback_info info) +{ + return CreateNativeNode(env, info, "CreateRefreshNativeNode", + []() -> ArkUI_NodeHandle { return RefreshMaker::CreateNativeNode(); }); +} - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); - if (nodeAPI_ != nullptr) { - if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { - ArkUI_NodeHandle testNode = EmbeddedComponentMaker::CreateNativeNode(); - OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); - } - } - return nullptr; +napi_value Manager::CreateListNativeNode(napi_env env, napi_callback_info info) +{ + return CreateNativeNode(env, info, "CreateListNativeNode", + []() -> ArkUI_NodeHandle { return ListMaker::CreateNativeNode(); }); } \ No newline at end of file diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.h b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.h index 2cd15649f..72588261c 100644 --- a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.h +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/manager.h @@ -35,6 +35,11 @@ public: static napi_value CreateSwiperNativeNode(napi_env env, napi_callback_info info); static napi_value CreateNativeAccessibilityNode(napi_env env, napi_callback_info info); static napi_value CreateNativeEmbeddedComponentNode(napi_env env, napi_callback_info info); + static napi_value CreateWaterFlowNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateGridNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateScrollNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateRefreshNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateListNativeNode(napi_env env, napi_callback_info info); private: static Manager manager_; diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/napi_init.cpp b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/napi_init.cpp index 7673f9911..41af55408 100644 --- a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/napi_init.cpp +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/napi_init.cpp @@ -34,6 +34,16 @@ static napi_value Init(napi_env env, napi_value exports) napi_default, nullptr}, {"createNativeEmbeddedComponentNode", nullptr, Manager::CreateNativeEmbeddedComponentNode, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"createWaterFlowNativeNode", nullptr, Manager::CreateWaterFlowNativeNode, nullptr, nullptr, nullptr, + napi_default, nullptr}, + {"createGridNativeNode", nullptr, Manager::CreateGridNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + {"createScrollNativeNode", nullptr, Manager::CreateScrollNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + {"createRefreshNativeNode", nullptr, Manager::CreateRefreshNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + {"createListNativeNode", nullptr, Manager::CreateListNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, // 参考新增其他createNative方法和Maker类 }; @@ -55,4 +65,7 @@ static napi_module demoModule = { .reserved = {0}, }; -extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); } \ No newline at end of file +extern "C" __attribute__((constructor)) void RegisterEntryModule(void) +{ + napi_module_register(&demoModule); +} \ No newline at end of file diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/types/libentry/Index.d.ts b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/types/libentry/Index.d.ts index ebd6298cc..c85182287 100644 --- a/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/types/libentry/Index.d.ts +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/cpp/types/libentry/Index.d.ts @@ -16,4 +16,9 @@ export const createNativeTextNode: (content: Object) =>void; export const createSwiperNativeNode: (content: Object) =>void; export const createNativeAccessibilityNode: (content: Object) => void; -export const createNativeEmbeddedComponentNode: (content: Object) => void; \ No newline at end of file +export const createNativeEmbeddedComponentNode: (content: Object) => void; +export const createWaterFlowNativeNode: (content: Object) => void; +export const createGridNativeNode: (content: Object) => void; +export const createScrollNativeNode: (content: Object) => void; +export const createRefreshNativeNode: (content: Object) => void; +export const createListNativeNode: (content: Object) => void; \ No newline at end of file diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/Index.ets b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/Index.ets index c37dc0830..0d390309b 100644 --- a/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/Index.ets +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/Index.ets @@ -27,9 +27,13 @@ struct MenuIndex { [ { title: 'Text', url: 'pages/page_text' }, { title: 'Swiper', url: 'pages/page_swiper' }, - { title: 'add your router', url: '' }, { title: 'accessibility', url: 'pages/page_accessibility' }, { title: 'embedded component', url: 'pages/page_embedded_component' }, + { title: 'WaterFlow', url: 'pages/page_waterflow' }, + { title: 'Grid', url: 'pages/page_grid' }, + { title: 'Scroll', url: 'pages/page_scroll' }, + { title: 'Refresh', url: 'pages/page_refresh' }, + { title: 'List', url: 'pages/page_list' }, ]; build() { diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_grid.ets b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_grid.ets new file mode 100644 index 000000000..91c26abb9 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_grid.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createGridNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_list.ets b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_list.ets new file mode 100644 index 000000000..52a9dc01c --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_list.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createListNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_refresh.ets b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_refresh.ets new file mode 100644 index 000000000..73f028421 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_refresh.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createRefreshNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_scroll.ets b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_scroll.ets new file mode 100644 index 000000000..fe8e5a35c --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_scroll.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createScrollNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_waterflow.ets b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_waterflow.ets new file mode 100644 index 000000000..8cc63eea5 --- /dev/null +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/ets/pages/page_waterflow.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createWaterFlowNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeNodeBaseSample/entry/src/main/resources/base/profile/main_pages.json b/ArkUIKit/NativeNodeBaseSample/entry/src/main/resources/base/profile/main_pages.json index 4b3bc0ef9..e96c6ab43 100644 --- a/ArkUIKit/NativeNodeBaseSample/entry/src/main/resources/base/profile/main_pages.json +++ b/ArkUIKit/NativeNodeBaseSample/entry/src/main/resources/base/profile/main_pages.json @@ -4,6 +4,11 @@ "pages/page_text", "pages/page_swiper", "pages/page_accessibility", - "pages/page_embedded_component" + "pages/page_embedded_component", + "pages/page_waterflow", + "pages/page_grid", + "pages/page_scroll", + "pages/page_refresh", + "pages/page_list" ] } diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUIBaseNode.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUIBaseNode.h index 9931c7d72..6c354f653 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUIBaseNode.h +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUIBaseNode.h @@ -20,7 +20,6 @@ #include #include -#include "manager.h" #include "NativeModule.h" namespace NativeModule { diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUINodeAdapter.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUINodeAdapter.h new file mode 100644 index 000000000..560cae8e3 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ArkUINodeAdapter.h @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ARKUINODEADAPTER_H +#define ARKUINODEADAPTER_H + +#include +#include +#include + +#include +#include +#include + +#include "ScrollableUtils.h" + +/** + * 通用 NodeAdapter 封装 + */ +struct NodeAdapterCallbacks { + std::function getTotalCount; + std::function getStableId; + std::function onCreate; + std::function onBind; + std::function onRecycle; +}; + +class ArkUINodeAdapter { +public: + using Callbacks = NodeAdapterCallbacks; + + ArkUINodeAdapter() : placeholderNodeType_(kInvalidNodeType) { InitializeApiAndAdapter(); } + + explicit ArkUINodeAdapter(int32_t placeholderNodeType) : placeholderNodeType_(placeholderNodeType) + { + InitializeApiAndAdapter(); + } + + ~ArkUINodeAdapter() + { + ClearNodeCache(); + OH_ArkUI_NodeAdapter_UnregisterEventReceiver(adapterHandle_); + OH_ArkUI_NodeAdapter_Dispose(adapterHandle_); + adapterHandle_ = nullptr; + } + + ArkUI_NodeAdapterHandle GetAdapter() const { return adapterHandle_; } + + void SetPlaceholderType(int32_t placeholderNodeType) { placeholderNodeType_ = placeholderNodeType; } + + void EnsurePlaceholderTypeOr(int32_t fallbackNodeType) + { + if (placeholderNodeType_ < 0) { + placeholderNodeType_ = fallbackNodeType; + } + } + + void SetCallbacks(const NodeAdapterCallbacks &callbacks) + { + callbacks_ = callbacks; + SynchronizeItemCount(GetTotalItemCount()); + } + + // ======================================== + // 数据变动通知接口 + // ======================================== + void ReloadAllItems() { OH_ArkUI_NodeAdapter_ReloadAllItems(adapterHandle_); } + + void InsertRange(int32_t index, int32_t count) + { + if (count <= 0) { + return; + } + int32_t validIndex = ClampIndexToRange(index, GetTotalItemCount()); + OH_ArkUI_NodeAdapter_InsertItem(adapterHandle_, validIndex, count); + SynchronizeItemCount(GetTotalItemCount()); + } + + void RemoveRange(int32_t index, int32_t count) + { + if (count <= 0) { + return; + } + if (!Utils::IsValidIndex(index, GetTotalItemCount())) { + return; + } + OH_ArkUI_NodeAdapter_RemoveItem(adapterHandle_, index, count); + SynchronizeItemCount(GetTotalItemCount()); + } + +protected: + // ======================================== + // 事件分发处理 + // ======================================== + static void OnStaticEvent(ArkUI_NodeAdapterEvent *event) + { + auto *self = reinterpret_cast(OH_ArkUI_NodeAdapterEvent_GetUserData(event)); + if (Utils::IsNotNull(self)) { + self->OnEvent(event); + } + } + + void OnEvent(ArkUI_NodeAdapterEvent *event) + { + const int32_t eventType = OH_ArkUI_NodeAdapterEvent_GetType(event); + switch (eventType) { + case NODE_ADAPTER_EVENT_ON_GET_NODE_ID: { + HandleGetNodeId(event); + break; + } + case NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER: { + HandleAddNodeToAdapter(event); + break; + } + case NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER: { + HandleRemoveNodeFromAdapter(event); + break; + } + default: { + break; + } + } + } + +private: + // ======================================== + // 私有常量和工具方法 + // ======================================== + static constexpr int32_t kInvalidNodeType = -1; + + void InitializeApiAndAdapter() + { + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, nodeApi_); + adapterHandle_ = OH_ArkUI_NodeAdapter_Create(); + OH_ArkUI_NodeAdapter_RegisterEventReceiver(adapterHandle_, this, &ArkUINodeAdapter::OnStaticEvent); + SynchronizeItemCount(GetTotalItemCount()); + } + + void ClearNodeCache() + { + while (!nodeCache_.empty()) { + nodeCache_.pop(); + } + } + + int32_t GetTotalItemCount() const + { + if (callbacks_.getTotalCount) { + return callbacks_.getTotalCount(); + } + return 0; + } + + int32_t ClampIndexToRange(int32_t index, int32_t maxCount) const + { + if (index < 0) { + return 0; + } + if (index > maxCount) { + return maxCount; + } + return index; + } + + void SynchronizeItemCount(int32_t count) + { + OH_ArkUI_NodeAdapter_SetTotalNodeCount(adapterHandle_, static_cast(count)); + } + + ArkUI_NodeHandle PopFromCacheOrCreate(int32_t index) + { + if (!nodeCache_.empty()) { + ArkUI_NodeHandle handle = nodeCache_.top(); + nodeCache_.pop(); + return handle; + } + if (callbacks_.onCreate) { + return callbacks_.onCreate(nodeApi_, index); + } + const ArkUI_NodeType nodeType = (placeholderNodeType_ >= 0) ? static_cast(placeholderNodeType_) + : ARKUI_NODE_LIST_ITEM; + return nodeApi_->createNode(nodeType); + } + + // ======================================== + // 事件处理实现 + // ======================================== + void HandleGetNodeId(ArkUI_NodeAdapterEvent *event) + { + const int32_t index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event); + uint64_t nodeId = static_cast(index); + + if (Utils::IsValidIndex(index, GetTotalItemCount()) && callbacks_.getStableId) { + nodeId = callbacks_.getStableId(index); + } + OH_ArkUI_NodeAdapterEvent_SetNodeId(event, nodeId); + } + + void HandleAddNodeToAdapter(ArkUI_NodeAdapterEvent *event) + { + const int32_t index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event); + ArkUI_NodeHandle item = PopFromCacheOrCreate(index); + + if (callbacks_.onBind && Utils::IsValidIndex(index, GetTotalItemCount())) { + callbacks_.onBind(nodeApi_, item, index); + } + OH_ArkUI_NodeAdapterEvent_SetItem(event, item); + } + + void HandleRemoveNodeFromAdapter(ArkUI_NodeAdapterEvent *event) + { + ArkUI_NodeHandle node = OH_ArkUI_NodeAdapterEvent_GetRemovedNode(event); + if (!Utils::IsNotNull(node)) { + return; + } + + if (callbacks_.onRecycle) { + callbacks_.onRecycle(nodeApi_, node); + } + nodeCache_.push(node); + } + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + ArkUI_NodeAdapterHandle adapterHandle_ = nullptr; + NodeAdapterCallbacks callbacks_; + std::stack nodeCache_; + int32_t placeholderNodeType_ = kInvalidNodeType; +}; + +#endif // ARKUINODEADAPTER_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/CMakeLists.txt b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/CMakeLists.txt index 52e7b19fc..41bdb9046 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/CMakeLists.txt +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/CMakeLists.txt @@ -1,20 +1,36 @@ # the minimum version of CMake. -cmake_minimum_required(VERSION 3.5.0) +cmake_minimum_required(VERSION 3.28.0) project(native_type_sample) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +if(DEFINED PACKAGE_FIND_FILE) + include(${PACKAGE_FIND_FILE}) +endif() + include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/include) -add_library(entry SHARED napi_init.cpp manager.cpp baseUtils.cpp SwiperMaker.cpp TextMaker.cpp AccessibilityMaker.cpp EmbeddedComponentMaker.cpp) - -find_library(hilog-lib hilog_ndk.z) - -find_library(libace-lib ace_ndk.z) - -find_library(libnapi-lib ace_napi.z) - - -target_link_libraries(entry PUBLIC ${hilog-lib} ${libace-lib} ${libnapi-lib}) -target_link_libraries(entry PUBLIC ${libace-lib} libace_napi.z.so libnative_drawing.so libhilog_ndk.z.so libability_base_want.so) \ No newline at end of file +file(GLOB_RECURSE ENTRY_SOURCES CONFIGURE_DEPENDS + "${NATIVERENDER_ROOT_PATH}/napi_init.cpp" + "${NATIVERENDER_ROOT_PATH}/*.cpp" +) + +add_library(entry SHARED ${ENTRY_SOURCES}) + +find_library( + # Sets the name of the path variable. + hilog-lib + libace-lib + libnapi-lib + ace_napi.z +) +target_link_libraries(entry PUBLIC + libace_ndk.z.so + libace_napi.z.so + libhilog_ndk.z.so + libnative_drawing.so + libability_base_want.so +) \ No newline at end of file diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.cpp new file mode 100644 index 000000000..c2b310efc --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +#include +#include +#include +#include // snprintf +#include +#include + +#include +#include + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" +#include "GridMaker.h" + +namespace { +// ===== 布局与样式常量 ===== +constexpr char K_ROWS_TEMPLATE[] = "auto"; +constexpr char K_COLUMNS_TEMPLATE[] = "1fr 1fr"; +constexpr float K_COLUMNS_GAP = 10.0f; +constexpr float K_ROWS_GAP = 15.0f; +constexpr uint32_t K_ITEM_BG_COLOR = 0xFFF1F3F5U; + +constexpr uint32_t K_GRID_CACHED_COUNT = 32; +constexpr bool K_GRID_SYNC_LOAD = true; +constexpr ArkUI_FocusWrapMode K_FOCUS_WRAP_MODE = ARKUI_FOCUS_WRAP_MODE_DEFAULT; + +constexpr int K_ITEM_COUNT = 60; +constexpr float K_ITEM_HEIGHT = 72.0f; +constexpr int K_GRID_INDEX_WIDTH = 2; +} // namespace + +// ---------- 生成数据:Grid01 ~ Grid60 ---------- +static std::vector MakeServicesData(size_t count = K_ITEM_COUNT) +{ + std::vector out; + out.reserve(count); + for (size_t i = 1; i <= count; ++i) { + std::ostringstream oss; + oss << "Grid" << std::setw(K_GRID_INDEX_WIDTH) << std::setfill('0') << i; + out.emplace_back(oss.str()); + } + return out; +} + +// ---------- 配置 Grid 外观/交互 ---------- +static void ConfigureGrid(const std::shared_ptr &grid) +{ + grid->SetWidthPercent(1.0f); + grid->SetDefaultScrollStyle(); + grid->SetColumnsTemplate(K_COLUMNS_TEMPLATE); + grid->SetCachedCount(K_GRID_CACHED_COUNT); + grid->SetFocusWrapMode(K_FOCUS_WRAP_MODE); + grid->SetSyncLoad(K_GRID_SYNC_LOAD); + grid->SetColumnsGap(K_COLUMNS_GAP); + grid->SetRowsGap(K_ROWS_GAP); +} + +// ---------- 适配器回调(创建/绑定) ---------- +static ArkUI_NodeHandle GridCreateItem(ArkUI_NativeNodeAPI_1 *api) +{ + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_GRID_ITEM); + api->addChild(item, text); + return item; +} + +static void GridBindItem(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index, + const std::shared_ptr> &data) +{ + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + Utils::SetAttributeFloat32(api, item, NODE_HEIGHT, K_ITEM_HEIGHT); + + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!text) { + return; + } + + const int32_t n = static_cast(data->size()); + const char *s = (index >= 0 && index < n) ? (*data)[static_cast(index)].c_str() : ""; + Utils::SetTextContent(api, text, s); +} + +// ---------- 构建 Adapter ---------- +static std::shared_ptr MakeGridAdapter(const std::shared_ptr> &data) +{ + auto adapter = std::make_shared(); + adapter->EnsurePlaceholderTypeOr(static_cast(ARKUI_NODE_GRID_ITEM)); + + ArkUINodeAdapter::Callbacks cb{}; + cb.getTotalCount = [data]() -> int32_t { return static_cast(data->size()); }; + cb.getStableId = [data](int32_t i) -> uint64_t { + const int32_t n = static_cast(data->size()); + if (i >= 0 && i < n) { + return static_cast(std::hash{}((*data)[static_cast(i)])); + } + return static_cast(i); + }; + cb.onCreate = [](ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) -> ArkUI_NodeHandle { return GridCreateItem(api); }; + cb.onBind = [data](ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index) { + GridBindItem(api, item, index, data); + }; + + adapter->SetCallbacks(cb); + return adapter; +} + +// ---------- 整体构建 Grid ---------- +static std::shared_ptr BuildGrid() +{ + auto grid = std::make_shared(); + ConfigureGrid(grid); + + auto data = std::make_shared>(MakeServicesData(K_ITEM_COUNT)); + auto adapter = MakeGridAdapter(data); + grid->SetLazyAdapter(adapter); + adapter->ReloadAllItems(); + GetKeepAliveContainer().emplace_back(grid); + return grid; +} + +ArkUI_NodeHandle GridMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + // 根容器全屏 + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + // 构建 Grid + std::shared_ptr grid = BuildGrid(); + if (grid && grid->GetHandle() != nullptr) { + Utils::SetAttributeFloat32(api, grid->GetHandle(), NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, grid->GetHandle()); + } + + return page; +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.h new file mode 100644 index 000000000..dd437a3dd --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/GridMaker.h @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GRID_NODE_H +#define GRID_NODE_H + +#include + +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" +#include "ArkUINodeAdapter.h" + +class GridMaker : public BaseNode { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + GridMaker() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_GRID)), + nodeApi_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()) + { + if (!Utils::IsNotNull(nodeApi_) || !Utils::IsNotNull(GetHandle())) { + return; + } + + nodeApi_->addNodeEventReceiver(GetHandle(), StaticEventReceiver); + scrollEventGuard_.Bind(nodeApi_, GetHandle(), this, SCROLL_EVT_ALL); + } + + ~GridMaker() override + { + scrollEventGuard_.Release(); + nodeAdapter_.reset(); + } + + // ======================================== + // 尺寸设置接口 + // ======================================== + void SetGridSize(float width, float height) + { + SetSize(width, height); + } + + void SetGridSizePercent(float widthPercent, float heightPercent) + { + SetSizePercent(widthPercent, heightPercent); + } + + // ======================================== + // 模板和间距设置 + // ======================================== + void SetRowsTemplate(const char *rowsTemplate) + { + Utils::SetAttributeString(nodeApi_, GetHandle(), NODE_GRID_ROW_TEMPLATE, rowsTemplate); + } + + void SetColumnsTemplate(const char *columnsTemplate) + { + Utils::SetAttributeString(nodeApi_, GetHandle(), NODE_GRID_COLUMN_TEMPLATE, columnsTemplate); + } + + void SetColumnsGap(float gap) + { + Utils::SetAttributeFloat32(nodeApi_, GetHandle(), NODE_GRID_COLUMN_GAP, gap); + } + + void SetRowsGap(float gap) + { + Utils::SetAttributeFloat32(nodeApi_, GetHandle(), NODE_GRID_ROW_GAP, gap); + } + + // ======================================== + // 行为和性能设置 + // ======================================== + void SetCachedCount(uint32_t count) + { + Utils::SetAttributeUInt32(nodeApi_, GetHandle(), NODE_GRID_CACHED_COUNT, count); + } + + void SetFocusWrapMode(ArkUI_FocusWrapMode mode) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_GRID_FOCUS_WRAP_MODE, static_cast(mode)); + } + + void SetSyncLoad(bool enabled) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_GRID_SYNC_LOAD, enabled ? 1 : 0); + } + + void SetDefaultScrollStyle() + { + Utils::SetDefaultScrollStyle(nodeApi_, GetHandle()); + } + + // ======================================== + // 适配器设置 + // ======================================== + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (!Utils::IsNotNull(adapter)) { + return; + } + ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetAdapter()}; + nodeApi_->setAttribute(GetHandle(), NODE_GRID_NODE_ADAPTER, &item); + nodeAdapter_ = adapter; + } + +protected: + void OnNodeEvent(ArkUI_NodeEvent *event) override + { + BaseNode::OnNodeEvent(event); + } + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + std::shared_ptr nodeAdapter_; + ScrollEventGuard scrollEventGuard_; +}; + +#endif // GRID_NODE_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemGroup.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemGroup.h new file mode 100644 index 000000000..014ebd987 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemGroup.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIST_ITEM_GROUP_H +#define LIST_ITEM_GROUP_H + +#include +#include +#include + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" + +/** 轻量封装:分组节点,仅提供示例所需 API */ +class ListItemGroupNode : public BaseNode { +public: + ListItemGroupNode() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_LIST_ITEM_GROUP)), + api_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()) + { + } + + ~ListItemGroupNode() override + { + if (!api_) { + return; + } + + // 清空 adapter + if (adapter_) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_NODE_ADAPTER, &it); + adapter_.reset(); + } + + // 清空 header / footer —— 传 nullptr 给同一属性即可 + if (header_) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_HEADER, &it); + header_.reset(); + } + if (footer_) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_FOOTER, &it); + footer_.reset(); + } + } + + // 设置/清空 Header:传 nullptr 即清空 + void SetHeader(const std::shared_ptr &header) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, header ? header->GetHandle() : nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_HEADER, &it); + header_ = header; + } + + // 设置/清空 Footer:传 nullptr 即清空 + void SetFooter(const std::shared_ptr &footer) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, footer ? footer->GetHandle() : nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_FOOTER, &it); + footer_ = footer; + } + + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (!adapter) { + ArkUI_AttributeItem it{nullptr, 0, nullptr, nullptr}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_NODE_ADAPTER, &it); + adapter_.reset(); + return; + } + adapter->EnsurePlaceholderTypeOr(static_cast(ARKUI_NODE_LIST_ITEM)); + ArkUI_AttributeItem it{nullptr, 0, nullptr, adapter->GetAdapter()}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_NODE_ADAPTER, &it); + adapter_ = adapter; + } + + // 可选:分组 divider + void SetDivider(float widthPx) + { + ArkUI_NumberValue v{.f32 = widthPx}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_SET_DIVIDER, &it); + } + + // 可选:分组 children main size + void SetChildrenMainSizeOption(ArkUI_ListChildrenMainSize *opt) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, opt}; + api_->setAttribute(GetHandle(), NODE_LIST_ITEM_GROUP_CHILDREN_MAIN_SIZE, &it); + } + +private: + ArkUI_NativeNodeAPI_1 *api_ = nullptr; + std::shared_ptr adapter_; + std::shared_ptr header_; + std::shared_ptr footer_; +}; + +#endif // LIST_ITEM_GROUP_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemSwipe.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemSwipe.h new file mode 100644 index 000000000..33e8f06d0 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListItemSwipe.h @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIST_ITEM_SWIPE_H +#define LIST_ITEM_SWIPE_H + +#include + +#include +#include +#include + +#ifndef LOG_TAG +#define LOG_TAG "ListItemSwipe" +#endif + +/** + * 轻量封装:为 ARKUI_NODE_LIST_ITEM 配置 Swipe Action(左右动作区、阈值、回调等) + */ +class ListItemSwipe { +public: + explicit ListItemSwipe(ArkUI_NativeNodeAPI_1 *api) : api_(api) {} + ~ListItemSwipe() + { + if (option_) { + OH_ArkUI_ListItemSwipeActionOption_Dispose(option_); + option_ = nullptr; + } + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_Dispose(startItem_); + startItem_ = nullptr; + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_Dispose(endItem_); + endItem_ = nullptr; + } + } + + // ====== 构建左右动作区 ====== + ListItemSwipe &BuildStartArea(const std::function &builder) + { + EnsureOption(); + if (!startItem_) { + startItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + ArkUI_NodeHandle node = builder ? builder(api_) : nullptr; + OH_ArkUI_ListItemSwipeActionItem_SetContent(startItem_, node); + OH_ArkUI_ListItemSwipeActionOption_SetStart(option_, startItem_); + return *this; + } + + ListItemSwipe &BuildEndArea(const std::function &builder) + { + EnsureOption(); + if (!endItem_) { + endItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + ArkUI_NodeHandle node = builder ? builder(api_) : nullptr; + OH_ArkUI_ListItemSwipeActionItem_SetContent(endItem_, node); + OH_ArkUI_ListItemSwipeActionOption_SetEnd(option_, endItem_); + return *this; + } + + // ====== 阈值(长距离删除阈值) ====== + ListItemSwipe &SetActionAreaDistance(float distance) + { + EnsureStartEnd(); + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetActionAreaDistance(startItem_, distance); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetActionAreaDistance(endItem_, distance); + } + return *this; + } + + float GetActionAreaDistanceStart() const + { + if (startItem_) { + return OH_ArkUI_ListItemSwipeActionItem_GetActionAreaDistance(startItem_); + } + return -1.0f; + } + + float GetActionAreaDistanceEnd() const + { + if (endItem_) { + return OH_ArkUI_ListItemSwipeActionItem_GetActionAreaDistance(endItem_); + } + return -1.0f; + } + + // ====== 进入/退出/触发/状态变化 ====== + ListItemSwipe &OnEnter(const std::function &cb) + { + EnsureStartEnd(); + enter_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionArea(startItem_, &ThunkEnter); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionArea(endItem_, &ThunkEnter); + } + return *this; + } + + ListItemSwipe &OnExit(const std::function &cb) + { + EnsureStartEnd(); + exit_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionArea(startItem_, &ThunkExit); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionArea(endItem_, &ThunkExit); + } + return *this; + } + + ListItemSwipe &OnAction(const std::function &cb) + { + EnsureStartEnd(); + action_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnAction(startItem_, &ThunkAction); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnAction(endItem_, &ThunkAction); + } + return *this; + } + + ListItemSwipe &OnStateChange(const std::function &cb) + { + EnsureStartEnd(); + state_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChange(startItem_, &ThunkState); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChange(endItem_, &ThunkState); + } + return *this; + } + + ListItemSwipe &OnEnterWithUserData(const std::function &cb) + { + EnsureStartEnd(); + enterUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionAreaWithUserData(startItem_, this, &ThunkEnterUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnEnterActionAreaWithUserData(endItem_, this, &ThunkEnterUD); + } + return *this; + } + + ListItemSwipe &OnExitWithUserData(const std::function &cb) + { + EnsureStartEnd(); + exitUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionAreaWithUserData(startItem_, this, &ThunkExitUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnExitActionAreaWithUserData(endItem_, this, &ThunkExitUD); + } + return *this; + } + + ListItemSwipe &OnActionWithUserData(const std::function &cb) + { + EnsureStartEnd(); + actionUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnActionWithUserData(startItem_, this, &ThunkActionUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnActionWithUserData(endItem_, this, &ThunkActionUD); + } + return *this; + } + + ListItemSwipe &OnStateChangeWithUserData(const std::function &cb) + { + EnsureStartEnd(); + stateUD_ = cb; + if (startItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChangeWithUserData(startItem_, this, &ThunkStateUD); + } + if (endItem_) { + OH_ArkUI_ListItemSwipeActionItem_SetOnStateChangeWithUserData(endItem_, this, &ThunkStateUD); + } + return *this; + } + + // ====== Edge Effect / Offset 回调 ====== + ListItemSwipe &SetEdgeEffect(int edgeEffect /*ArkUI_ListItemSwipeEdgeEffect*/) + { + EnsureOption(); + OH_ArkUI_ListItemSwipeActionOption_SetEdgeEffect( + option_, static_cast(edgeEffect)); + return *this; + } + + int GetEdgeEffect() const + { + if (option_) { + return OH_ArkUI_ListItemSwipeActionOption_GetEdgeEffect(option_); + } + return -1; + } + + ListItemSwipe &OnOffsetChange(const std::function &cb) + { + EnsureOption(); + offset_ = cb; + OH_ArkUI_ListItemSwipeActionOption_SetOnOffsetChange(option_, &ThunkOffset); + return *this; + } + + ListItemSwipe &OnOffsetChangeWithUserData(const std::function &cb) + { + EnsureOption(); + offsetUD_ = cb; + OH_ArkUI_ListItemSwipeActionOption_SetOnOffsetChangeWithUserData(option_, this, &ThunkOffsetUD); + return *this; + } + + // ====== 挂载到指定 LIST_ITEM 节点 ====== + void AttachToListItem(ArkUI_NodeHandle listItem) + { + if (!api_ || !listItem) { + return; + } + EnsureOption(); + ArkUI_AttributeItem it{nullptr, 0, nullptr, option_}; + api_->setAttribute(listItem, NODE_LIST_ITEM_SWIPE_ACTION, &it); + + (void)GetActionAreaDistanceStart(); + (void)GetActionAreaDistanceEnd(); + (void)GetEdgeEffect(); + } + +private: + void EnsureOption() + { + if (!option_) { + option_ = OH_ArkUI_ListItemSwipeActionOption_Create(); + } + } + + void EnsureStartEnd() + { + EnsureOption(); + if (!startItem_) { + startItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + if (!endItem_) { + endItem_ = OH_ArkUI_ListItemSwipeActionItem_Create(); + } + OH_ArkUI_ListItemSwipeActionOption_SetStart(option_, startItem_); + OH_ArkUI_ListItemSwipeActionOption_SetEnd(option_, endItem_); + } + + static void ThunkEnter() {} + static void ThunkExit() {} + static void ThunkAction() {} + static void ThunkState(ArkUI_ListItemSwipeActionState state) { (void)state; } + static void ThunkOffset(float offset) { (void)offset; } + + static void ThunkEnterUD(void *ud) + { + if (auto *self = static_cast(ud); self && self->enterUD_) { + self->enterUD_(ud); + } + } + + static void ThunkExitUD(void *ud) + { + if (auto *self = static_cast(ud); self && self->exitUD_) { + self->exitUD_(ud); + } + } + + static void ThunkActionUD(void *ud) + { + if (auto *self = static_cast(ud); self && self->actionUD_) { + self->actionUD_(ud); + } + } + + static void ThunkStateUD(ArkUI_ListItemSwipeActionState state, void *ud) + { + if (auto *self = static_cast(ud); self && self->stateUD_) { + self->stateUD_(static_cast(state), ud); + } + if (auto *self2 = static_cast(ud); self2 && self2->state_) { + self2->state_(static_cast(state)); + } + } + + static void ThunkOffsetUD(float offset, void *ud) + { + if (auto *self = static_cast(ud); self && self->offsetUD_) { + self->offsetUD_(offset, ud); + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_{nullptr}; + + ArkUI_ListItemSwipeActionItem *startItem_{nullptr}; + ArkUI_ListItemSwipeActionItem *endItem_{nullptr}; + ArkUI_ListItemSwipeActionOption *option_{nullptr}; + + // 无 userData 回调 + std::function enter_; + std::function exit_; + std::function action_; + std::function state_; + std::function offset_; + + // 带 userData 回调 + std::function enterUD_; + std::function exitUD_; + std::function actionUD_; + std::function stateUD_; + std::function offsetUD_; +}; + +#endif // LIST_ITEM_SWIPE_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.cpp new file mode 100644 index 000000000..6f219fdfb --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.cpp @@ -0,0 +1,618 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "ScrollableUtils.h" + +#ifndef LOG_TAG +#define LOG_TAG "ListMaker" +#endif + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" +#include "ListMaker.h" +#include "ListItemGroup.h" +#include "ListItemSwipe.h" + +// ===== 常量定义 ===== +namespace { +constexpr int K_ALPHABET_COUNT = 26; +constexpr int K_INDEX_ITEM_HEIGHT = 22; +constexpr int32_t K_GROUP_FIRST_ITEM_INDEX = 0; +constexpr int32_t K_FIRST_GROUP_INDEX = 0; + +constexpr float K_LIST_WIDTH_PERCENT = 0.82f; +constexpr float K_FULL_PERCENT = 1.0f; +constexpr float K_LIST_SPACE = 8.0f; + +constexpr float K_ITEM_FONT_SIZE = 16.0f; +constexpr float K_INDEX_FONT_SIZE = 14.0f; +constexpr float K_ROW_HEIGHT = 80.0f; + +constexpr float K_DELETE_HEIGHT = 64.0f; +constexpr float K_DELETE_WIDTH = 88.0f; +constexpr float K_SWIPE_ACTION_DISTANCE = 96.0f; + +constexpr float K_INDEX_BAR_WIDTH = 56.0f; +constexpr float K_INDEX_BAR_PAD_TOP = 0.0f; +constexpr float K_INDEX_BAR_PAD_RIGHT = 0.0f; +constexpr float K_INDEX_BAR_PAD_BOTTOM = 0.0f; +constexpr float K_INDEX_BAR_PAD_LEFT = 8.0f; + +constexpr float K_HEADER_HEIGHT = 40.0f; +constexpr float K_FOOTER_HEIGHT = 28.0f; + +constexpr uint32_t K_COLOR_WHITE = 0xFFFFFFFFU; +constexpr uint32_t K_COLOR_BLACK = 0xFF000000U; +constexpr uint32_t K_COLOR_DELETE_BG = 0xFFE53935U; +constexpr uint32_t K_COLOR_INDEX_ACTIVE = 0xFF003366U; +constexpr uint32_t K_COLOR_INDEX_INACTIVE = 0xFF222222U; +constexpr uint32_t K_COLOR_INDEX_ACTIVE_BG = 0xFFE0F0FFU; +constexpr uint32_t K_COLOR_INDEX_INACTIVE_BG = 0x00000000U; + +constexpr uint32_t K_COLOR_HEADER_BG = 0xFFEFEFEFU; +constexpr uint32_t K_COLOR_FOOTER_BG = 0xFFF7F7F7U; + +constexpr int K_EDGE_EFFECT_NONE = 0; + +constexpr const char *K_DELETE_TEXT = "Delete"; +constexpr const char *K_FOOTER_TEXT = "—— 已到底 ——"; +constexpr const char *K_INVALID_TEXT = ""; + +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; +} // namespace + +// ===== 数据源 ===== +static const char *const NAMES_A[] = {"Alice", "Andrew", "Amy", "Aaron", "安娜", "安琪", "爱华", "阿明"}; +static const char *const NAMES_B[] = {"Ben", "Bella", "Brian", "Brandon", "博文", "斌", "白雪", "彬彬"}; +static const char *const NAMES_C[] = {"Chris", "Charlotte", "Cindy", "Caleb", "晨曦", "承泽", "楚怡", "春燕"}; +static const char *const NAMES_D[] = {"Daniel", "David", "Diana", "Dylan", "大伟", "东旭", "德华", "丹妮"}; +static const char *const NAMES_E[] = {"Emma", "Ethan", "Emily", "Eric", "恩泽", "恩雅", "尔雅", "恩宁"}; +static const char *const NAMES_F[] = {"Frank", "Fiona", "Felix", "Fred", "方圆", "芙蓉", "芳怡", "飞扬"}; +static const char *const NAMES_G[] = {"Grace", "George", "Gavin", "Gloria", "国强", "国华", "光耀", "桂英"}; +static const char *const NAMES_H[] = {"Henry", "Hannah", "Helen", "Harry", "海峰", "红梅", "宏伟", "浩然"}; +static const char *const NAMES_I[] = {"Isaac", "Ice", "Ian", "Isabella", "一涵", "一诺", "怡君", "依琳"}; +static const char *const NAMES_J[] = {"Jack", "James", "Jason", "Julia", "佳怡", "建国", "靖雯", "俊杰"}; +static const char *const NAMES_K[] = {"Kevin", "Kate", "Kelly", "Kyle", "可欣", "可可", "昆明", "康宁"}; +static const char *const NAMES_L[] = {"Lucas", "Leo", "Lily", "Lauren", "丽丽", "丽华", "立国", "林涛"}; +static const char *const NAMES_M[] = {"Michael", "Mary", "Mark", "Molly", "美玲", "明慧", "明杰", "梦瑶"}; +static const char *const NAMES_N[] = {"Nancy", "Nathan", "Nick", "Nora", "楠楠", "宁静", "娜娜", "乃文"}; +static const char *const NAMES_O[] = {"Oliver", "Olivia", "Owen", "Oscar", "欧阳娜", "欧莉", "欧阳晨", "欧文"}; +static const char *const NAMES_P[] = {"Peter", "Paul", "Philip", "Penny", "佩琪", "佩华", "平安", "鹏飞"}; +static const char *const NAMES_Q[] = {"Quentin", "Queenie", "Quinn", "Quincy", "琪琳", "倩倩", "清华", "强辉"}; +static const char *const NAMES_R[] = {"Robert", "Rachel", "Ryan", "Ruby", "荣辉", "若曦", "瑞雪", "日新"}; +static const char *const NAMES_S[] = {"Steven", "Susan", "Sarah", "Simon", "思远", "素芳", "诗涵", "少华"}; +static const char *const NAMES_T[] = {"Thomas", "Tony", "Tina", "Taylor", "天宇", "婷怡", "涛涛", "同辉"}; +static const char *const NAMES_U[] = {"Ulysses", "Uma", "Ulrich", "Ursula", "宇航", "宇轩", "宇宁", "宇泽"}; +static const char *const NAMES_V[] = {"Victor", "Victoria", "Vincent", "Vivian", "薇薇", "维娜", "维德", "维琪"}; +static const char *const NAMES_W[] = {"William", "Wendy", "Walter", "Willow", "伟强", "文静", "文博", "卫东"}; +static const char *const NAMES_X[] = {"Xavier", "Xander", "Xenia", "Xiomara", "晓明", "欣怡", "旭东", "霞"}; +static const char *const NAMES_Y[] = {"Yvonne", "Yolanda", "Yara", "Yvette", "怡然", "颖颖", "逸飞", "毅然"}; +static const char *const NAMES_Z[] = {"Zoe", "Zachary", "Zane", "Zara", "紫琪", "志强", "梓涵", "泽宇"}; + +struct GroupNames { + char letter; + const char *const *arr; + int size; +}; + +static const GroupNames GROUPS[] = {{'A', NAMES_A, Utils::ArrSize(NAMES_A)}, {'B', NAMES_B, Utils::ArrSize(NAMES_B)}, + {'C', NAMES_C, Utils::ArrSize(NAMES_C)}, {'D', NAMES_D, Utils::ArrSize(NAMES_D)}, + {'E', NAMES_E, Utils::ArrSize(NAMES_E)}, {'F', NAMES_F, Utils::ArrSize(NAMES_F)}, + {'G', NAMES_G, Utils::ArrSize(NAMES_G)}, {'H', NAMES_H, Utils::ArrSize(NAMES_H)}, + {'I', NAMES_I, Utils::ArrSize(NAMES_I)}, {'J', NAMES_J, Utils::ArrSize(NAMES_J)}, + {'K', NAMES_K, Utils::ArrSize(NAMES_K)}, {'L', NAMES_L, Utils::ArrSize(NAMES_L)}, + {'M', NAMES_M, Utils::ArrSize(NAMES_M)}, {'N', NAMES_N, Utils::ArrSize(NAMES_N)}, + {'O', NAMES_O, Utils::ArrSize(NAMES_O)}, {'P', NAMES_P, Utils::ArrSize(NAMES_P)}, + {'Q', NAMES_Q, Utils::ArrSize(NAMES_Q)}, {'R', NAMES_R, Utils::ArrSize(NAMES_R)}, + {'S', NAMES_S, Utils::ArrSize(NAMES_S)}, {'T', NAMES_T, Utils::ArrSize(NAMES_T)}, + {'U', NAMES_U, Utils::ArrSize(NAMES_U)}, {'V', NAMES_V, Utils::ArrSize(NAMES_V)}, + {'W', NAMES_W, Utils::ArrSize(NAMES_W)}, {'X', NAMES_X, Utils::ArrSize(NAMES_X)}, + {'Y', NAMES_Y, Utils::ArrSize(NAMES_Y)}, {'Z', NAMES_Z, Utils::ArrSize(NAMES_Z)}}; + +// ===== 交互上下文 ===== +struct ClickCtx { + ArkUI_NativeNodeAPI_1 *api{nullptr}; + std::shared_ptr> items; + std::weak_ptr adapter; + int index{-1}; + uint64_t stableId{0}; + ArkUI_NodeHandle itemHandle{nullptr}; +}; + +static std::unordered_map> s_btnCtx; +static std::unordered_map s_itemToDeleteBtn; + +// ===== 工具函数 ===== +static int FindIndexByStableId(const std::vector &items, uint64_t sid) +{ + const int n = static_cast(items.size()); + for (int i = 0; i < n; ++i) { + uint64_t v = static_cast(std::hash{}(items[static_cast(i)])); + if (v == sid) { + return i; + } + } + return -1; +} + +static int ClampFallbackIndex(int fallback, int n) +{ + if (n <= 0) { + return -1; + } + int idx = fallback; + if (idx < 0) { + idx = 0; + } + if (idx >= n) { + idx = n - 1; + } + return idx; +} + +// ===== 静态事件处理 ===== +static void StaticDeleteBtnEvent(ArkUI_NodeEvent *ev) +{ + if (ev == nullptr) { + return; + } + if (OH_ArkUI_NodeEvent_GetEventType(ev) != NODE_ON_CLICK) { + return; + } + + ClickCtx *ctx = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(ev)); + if (ctx == nullptr || ctx->api == nullptr) { + return; + } + std::shared_ptr> items = ctx->items; + if (!items) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete skip: items is null"); + return; + } + + int idx = FindIndexByStableId(*items, ctx->stableId); + if (idx < 0) { + idx = ClampFallbackIndex(ctx->index, static_cast(items->size())); + if (idx < 0) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete skip: no valid index"); + return; + } + } + + const int n = static_cast(items->size()); + if (idx < 0 || idx >= n) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete skip: invalid idx=%d, size=%d", idx, n); + return; + } + + std::string deletedItem = (*items)[static_cast(idx)]; + items->erase(items->begin() + idx); + + if (auto ad = ctx->adapter.lock()) { + ad->RemoveRange(idx, 1); + OH_LOG_Print(LOG_APP, LOG_INFO, K_LOG_DOMAIN, LOG_TAG, "Deleted item [%s] at index=%d, remaining=%d", + deletedItem.c_str(), idx, static_cast(items->size())); + } else { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "Delete success but adapter is null"); + } +} + +// ===== 视图构造工具 ===== +static std::shared_ptr MakeText(const char *s, float h, uint32_t bg) +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + std::shared_ptr node = std::make_shared(text); + + Utils::SetTextContent(api, text, s); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_ITEM_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, h); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, h); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeUInt32(api, text, NODE_BACKGROUND_COLOR, bg); + return node; +} + +static void SetIndexTextStyle(ArkUI_NodeHandle text, int h, bool active) +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_INDEX_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, static_cast(h)); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, static_cast(h)); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeUInt32(api, text, NODE_FONT_COLOR, active ? K_COLOR_INDEX_ACTIVE : K_COLOR_INDEX_INACTIVE); + Utils::SetAttributeUInt32(api, text, NODE_BACKGROUND_COLOR, + active ? K_COLOR_INDEX_ACTIVE_BG : K_COLOR_INDEX_INACTIVE_BG); +} + +struct IndexState { + std::vector> letters; + int selected; + std::vector groupVisible; + IndexState() : selected(-1) {} +}; + +static void UpdateIndexHighlight(const std::shared_ptr &st, int idx) +{ + if (!st) { + return; + } + if (st->selected == idx) { + return; + } + + int prev = st->selected; + if (prev >= 0 && prev < static_cast(st->letters.size())) { + ArkUI_NodeHandle prevHandle = st->letters[static_cast(prev)]->GetHandle(); + SetIndexTextStyle(prevHandle, K_INDEX_ITEM_HEIGHT, false); + } + if (idx >= 0 && idx < static_cast(st->letters.size())) { + ArkUI_NodeHandle nowHandle = st->letters[static_cast(idx)]->GetHandle(); + SetIndexTextStyle(nowHandle, K_INDEX_ITEM_HEIGHT, true); + st->selected = idx; + } +} + +static std::shared_ptr BuildRightIndexColumn(const std::shared_ptr &list, + const std::shared_ptr &st) +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle colHandle = api->createNode(ARKUI_NODE_COLUMN); + std::shared_ptr col = std::make_shared(colHandle); + + col->SetWidth(K_INDEX_BAR_WIDTH); + col->SetHeightPercent(K_FULL_PERCENT); + + Utils::SetPadding( + api, col->GetHandle(), + Utils::Padding::Only(K_INDEX_BAR_PAD_TOP, K_INDEX_BAR_PAD_RIGHT, K_INDEX_BAR_PAD_BOTTOM, K_INDEX_BAR_PAD_LEFT)); + + st->letters.reserve(K_ALPHABET_COUNT); + for (int i = 0; i < K_ALPHABET_COUNT; ++i) { + char label[2]; + label[0] = static_cast('A' + i); + label[1] = '\0'; + + ArkUI_NodeHandle textHandle = api->createNode(ARKUI_NODE_TEXT); + std::shared_ptr t = std::make_shared(textHandle); + + Utils::SetTextContent(api, textHandle, label); + SetIndexTextStyle(textHandle, K_INDEX_ITEM_HEIGHT, false); + + int group = i; + t->RegisterOnClick([list, st, i, group](ArkUI_NodeEvent *) { + UpdateIndexHighlight(st, i); + list->ScrollToIndexInGroup(group, 0); + }); + + col->AddChild(t); + st->letters.emplace_back(t); + } + return col; +} + +static std::shared_ptr ApplyListSafeProps() +{ + std::shared_ptr list = std::make_shared(); + list->SetWidthPercent(K_LIST_WIDTH_PERCENT); + list->SetHeightPercent(K_FULL_PERCENT); + list->SetScrollBarState(true); + list->SetClipContent(true); + list->SetSpace(K_LIST_SPACE); + list->SetNestedScrollMode(ListMaker::kNestedScrollParentFirst); + return list; +} + +static ArkUI_NodeHandle CreateDeleteButton(ArkUI_NativeNodeAPI_1 *api) +{ + ArkUI_NodeHandle t = api->createNode(ARKUI_NODE_TEXT); + Utils::SetTextContent(api, t, K_DELETE_TEXT); + Utils::SetAttributeFloat32(api, t, NODE_FONT_SIZE, K_INDEX_FONT_SIZE); + Utils::SetAttributeFloat32(api, t, NODE_HEIGHT, K_DELETE_HEIGHT); + Utils::SetAttributeFloat32(api, t, NODE_TEXT_LINE_HEIGHT, K_DELETE_HEIGHT); + Utils::SetAttributeInt32(api, t, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeUInt32(api, t, NODE_BACKGROUND_COLOR, K_COLOR_DELETE_BG); + Utils::SetAttributeUInt32(api, t, NODE_FONT_COLOR, K_COLOR_WHITE); + Utils::SetAttributeFloat32(api, t, NODE_WIDTH, K_DELETE_WIDTH); + return t; +} + +static void SetupSwipeForListItem(ArkUI_NodeHandle item, ArkUI_NativeNodeAPI_1 *api, + const std::shared_ptr> &items, + const std::weak_ptr &adapterWeak) +{ + static std::vector> s_swipeKeep; + + std::unique_ptr swipe = std::make_unique(api); + + auto makeDeleteBtn = [api, items, adapterWeak, item](ArkUI_NativeNodeAPI_1 *apiInner) -> ArkUI_NodeHandle { + ArkUI_NodeHandle btn = CreateDeleteButton(apiInner); + if (!btn) { + return nullptr; + } + + // 1) 创建时立即建立上下文 + std::shared_ptr ctx = std::make_shared(); + ctx->api = api; + ctx->items = items; + ctx->adapter = adapterWeak; + ctx->itemHandle = item; + + s_itemToDeleteBtn[item] = btn; + s_btnCtx[btn] = ctx; + + // 2) 双路径注册(两个 API 都试一次,避免实例不一致) + apiInner->addNodeEventReceiver(btn, &StaticDeleteBtnEvent); + apiInner->registerNodeEvent(btn, NODE_ON_CLICK, 0, ctx.get()); + + // 保险:用外层 api 再注册一层(有的环境两个指针其实等价,但这样更稳) + api->addNodeEventReceiver(btn, &StaticDeleteBtnEvent); + api->registerNodeEvent(btn, NODE_ON_CLICK, 0, ctx.get()); + + // 3) 父项兜底注册(在 item 上也注册 CLICK) + // 点击冒泡到 item 时,我们在 StaticDeleteBtnEvent 里根据目标句柄再判断 + api->addNodeEventReceiver(item, &StaticDeleteBtnEvent); + api->registerNodeEvent(item, NODE_ON_CLICK, 0, ctx.get()); + + return btn; + }; + + swipe->BuildEndArea(makeDeleteBtn) + .SetActionAreaDistance(K_SWIPE_ACTION_DISTANCE) + .SetEdgeEffect(K_EDGE_EFFECT_NONE) + .OnEnter([] {}) + .OnExit([] {}) + .OnAction([] {}) + .OnStateChange([](int /*st*/) {}) + .OnOffsetChange([](float /*off*/) {}) + .OnEnterWithUserData([](void *) {}) + .OnExitWithUserData([](void *) {}) + .OnActionWithUserData([](void *) {}) + .OnStateChangeWithUserData([](int /*st*/, void *) {}) + .OnOffsetChangeWithUserData([](float /*off*/, void *) {}); + + swipe->AttachToListItem(item); + s_swipeKeep.emplace_back(std::move(swipe)); +} + +static ArkUI_NodeHandle CreateListItemWithSwipe(ArkUI_NativeNodeAPI_1 *api, + const std::shared_ptr> &items, + const std::weak_ptr &adapterWeak) +{ + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_LIST_ITEM); + api->addChild(item, text); + + Utils::SetAttributeFloat32(api, item, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, K_FULL_PERCENT); + + SetupSwipeForListItem(item, api, items, adapterWeak); + return item; +} + +static void BindListItemContent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index, + const std::shared_ptr> &items, + const std::weak_ptr &adapterWeak) +{ + if (!Utils::ValidateApiAndNode(api, item, "BindListItemContent")) { + return; + } + if (!Utils::IsNotNull(items)) { + return; + } + + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!Utils::IsNotNull(text)) { + return; + } + + const int32_t n = static_cast(items->size()); + const bool valid = Utils::IsValidIndex(index, n); + const char *content = valid ? (*items)[static_cast(index)].c_str() : K_INVALID_TEXT; + + Utils::SetTextContent(api, text, content); + Utils::SetBackgroundColor(api, item, K_COLOR_WHITE); + Utils::SetTextStyle(api, text, K_ITEM_FONT_SIZE, K_COLOR_BLACK, ARKUI_TEXT_ALIGNMENT_CENTER); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, K_ROW_HEIGHT); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, K_ROW_HEIGHT); + + // 更新删除按钮上下文 + auto itBtn = s_itemToDeleteBtn.find(item); + if (itBtn == s_itemToDeleteBtn.end()) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, + "BindListItemContent: item not found in s_itemToDeleteBtn"); + return; + } + + auto itCtx = s_btnCtx.find(itBtn->second); + if (itCtx == s_btnCtx.end() || !itCtx->second) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "BindListItemContent: context not found for button"); + return; + } + + ClickCtx &ctx = *itCtx->second; + ctx.index = index; + ctx.items = items; // 更新到当前 Group 的 items + ctx.adapter = adapterWeak; // 更新到当前 Group 的 adapter + + if (valid) { + ctx.stableId = static_cast(std::hash{}((*items)[static_cast(index)])); + } + + OH_LOG_Print(LOG_APP, LOG_DEBUG, K_LOG_DOMAIN, LOG_TAG, + "BindListItemContent: updated context for index=%d, content=%s", index, content); +} + +static NodeAdapterCallbacks CreateGroupCallbacks(const std::shared_ptr> &items) +{ + NodeAdapterCallbacks cb{}; + cb.getTotalCount = [items]() -> int32_t { return static_cast(items->size()); }; + cb.getStableId = [items](int32_t i) -> uint64_t { + const int32_t n = static_cast(items->size()); + if (i >= 0 && i < n) { + return static_cast(std::hash{}((*items)[static_cast(i)])); + } + return static_cast(i); + }; + return cb; +} + +static std::shared_ptr> CreateGroupItemsData(const GroupNames &gn) +{ + std::shared_ptr> items = std::make_shared>(); + items->reserve(static_cast(gn.size)); + for (int i = 0; i < gn.size; ++i) { + items->emplace_back(gn.arr[i]); + } + return items; +} + +static void SetupGroupHeaderAndFooter(std::shared_ptr &group, const GroupNames &gn, bool isLast) +{ + char title[16]{}; + title[0] = gn.letter; + title[1] = '\0'; + + std::shared_ptr header = MakeText(title, K_HEADER_HEIGHT, K_COLOR_HEADER_BG); + group->SetHeader(header); + + if (isLast) { + std::shared_ptr footer = MakeText(K_FOOTER_TEXT, K_FOOTER_HEIGHT, K_COLOR_FOOTER_BG); + group->SetFooter(footer); + } +} + +// 修改后的 CreateGroupAdapterCallbacks 函数 +static NodeAdapterCallbacks CreateGroupAdapterCallbacks(const std::shared_ptr> &items, + std::shared_ptr adapter) +{ + NodeAdapterCallbacks cb = CreateGroupCallbacks(items); + + cb.onCreate = [items, adapterWeak = std::weak_ptr(adapter)]( + ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) -> ArkUI_NodeHandle { + return CreateListItemWithSwipe(api, items, adapterWeak); + }; + + cb.onBind = [items, adapterWeak = std::weak_ptr(adapter)](ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle item, int32_t index) { + BindListItemContent(api, item, index, items, adapterWeak); + }; + + return cb; +} + +static std::shared_ptr BuildGroup(int gIndex, bool isLast, + const std::shared_ptr & /*st*/) +{ + const GroupNames &gn = GROUPS[static_cast(gIndex)]; + std::shared_ptr group = std::make_shared(); + + SetupGroupHeaderAndFooter(group, gn, isLast); + + std::shared_ptr> items = CreateGroupItemsData(gn); + std::shared_ptr adapter = + std::make_shared(static_cast(ARKUI_NODE_LIST_ITEM)); + NodeAdapterCallbacks cb = CreateGroupAdapterCallbacks(items, adapter); + + adapter->SetCallbacks(cb); + group->SetLazyAdapter(adapter); + return group; +} + +/** + * 创建按字母索引的懒加载列表 + */ +std::shared_ptr CreateLazyTextListExample() +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle rootHandle = api->createNode(ARKUI_NODE_ROW); + std::shared_ptr root = std::make_shared(rootHandle); + + root->SetWidthPercent(K_FULL_PERCENT); + root->SetHeightPercent(K_FULL_PERCENT); + + std::shared_ptr list = ApplyListSafeProps(); + std::shared_ptr st = std::make_shared(); + st->groupVisible.assign(K_ALPHABET_COUNT, false); + + for (int g = 0; g < K_ALPHABET_COUNT; ++g) { + bool isLast = (g == K_ALPHABET_COUNT - 1); + std::shared_ptr grp = BuildGroup(g, isLast, st); + list->AddChild(std::static_pointer_cast(grp)); + } + + std::shared_ptr right = BuildRightIndexColumn(list, st); + list->RegisterOnScrollIndex([st](int32_t first, int32_t last) { + int idx = first; + if (last == K_ALPHABET_COUNT - 1) { + idx = last; + } + if (idx < 0) + return; + if (idx >= K_ALPHABET_COUNT) + idx = K_ALPHABET_COUNT - 1; + UpdateIndexHighlight(st, idx); + }); + + list->RegisterOnReachEnd([st]() { UpdateIndexHighlight(st, K_ALPHABET_COUNT - 1); }); + + UpdateIndexHighlight(st, K_FIRST_GROUP_INDEX); + + root->AddChild(list); + root->AddChild(right); + + static std::vector> g_keepAlive; + g_keepAlive.emplace_back(root); + return root; +} + +ArkUI_NodeHandle ListMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + std::shared_ptr root = CreateLazyTextListExample(); + if (root && root->GetHandle() != nullptr) { + Utils::SetAttributeFloat32(api, root->GetHandle(), NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, root->GetHandle()); + } + + return page; +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.h new file mode 100644 index 000000000..9442f57cb --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ListMaker.h @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LIST_MAKER_H +#define LIST_MAKER_H + +#include +#include + +#include +#include +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" +#include "ArkUINodeAdapter.h" + +#ifndef LOG_TAG +#define LOG_TAG "ListMaker" +#endif + +class ListMaker : public BaseNode { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + // 嵌套滚动模式常量 + static constexpr int32_t kNestedScrollParentFirst = 0; + static constexpr int32_t kNestedScrollChildFirst = 1; + static constexpr int32_t kNestedScrollSelfFirst = 2; + + // 组件事件数据数组索引常量 + static constexpr int32_t kScrollIndexFirstDataIndex = 0; + static constexpr int32_t kScrollIndexLastDataIndex = 3; + + static constexpr int32_t kVisibleChangeFirstChildDataIndex = 0; + static constexpr int32_t kVisibleChangeStartAreaDataIndex = 1; + static constexpr int32_t kVisibleChangeStartIndexDataIndex = 2; + static constexpr int32_t kVisibleChangeLastChildDataIndex = 3; + static constexpr int32_t kVisibleChangeEndAreaDataIndex = 4; + static constexpr int32_t kVisibleChangeEndIndexDataIndex = 5; + + static constexpr int32_t kScrollFrameBeginDataIndex = 0; + + static constexpr uint32_t kColorTransparent = 0x00000000U; + + // —— 可视内容变化事件数据 —— // + struct VisibleContentChange { + int32_t firstChildIndex = -1; // 可视区域首个“子组件”(item/header/footer)索引 + ArkUI_ListItemGroupArea startArea = ARKUI_LIST_ITEM_GROUP_AREA_OUTSIDE; // 起点区 + int32_t startItemIndex = -1; // 若起点不是 item,则为 -1 + + int32_t lastChildIndex = -1; // 可视区域最后一个“子组件”索引 + ArkUI_ListItemGroupArea endArea = ARKUI_LIST_ITEM_GROUP_AREA_OUTSIDE; // 终点区 + int32_t endItemIndex = -1; // 若终点不是 item,则为 -1 + + bool StartOnItem() const { return startArea == ARKUI_LIST_ITEM_SWIPE_AREA_ITEM && startItemIndex >= 0; } + bool EndOnItem() const { return endArea == ARKUI_LIST_ITEM_SWIPE_AREA_ITEM && endItemIndex >= 0; } + }; + +public: + ListMaker() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_LIST)), + nodeApi_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()) + { + if (!Utils::IsNotNull(nodeApi_) || !Utils::IsNotNull(GetHandle())) { + return; + } + + nodeApi_->addNodeEventReceiver(GetHandle(), &ListMaker::StaticEventReceiver); + const uint32_t scrollEventMask = SCROLL_EVT_FRAME_BEGIN | SCROLL_EVT_REACH_END; + scrollEventGuard_.Bind(nodeApi_, GetHandle(), this, scrollEventMask); + } + + ~ListMaker() override + { + scrollEventGuard_.Release(); + + if (!Utils::IsNotNull(nodeApi_)) { + return; + } + UnregisterSpecificEvents(); + ResetListAdapter(); + CleanupChildrenMainSizeOption(); + } + + // ======================================== + // 通用属性设置接口 + // ======================================== + void SetClipContent(bool clipEnabled) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_CLIP_CONTENT, clipEnabled ? 1 : 0); + } + + void SetEdgeEffectSpring(bool springEnabled) + { + int32_t effectValue = springEnabled ? ARKUI_EDGE_EFFECT_SPRING : ARKUI_EDGE_EFFECT_NONE; + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_EDGE_EFFECT, effectValue); + } + + void SetScrollBarVisible(bool visible) + { + int32_t displayMode = visible ? ARKUI_SCROLL_BAR_DISPLAY_MODE_ON : ARKUI_SCROLL_BAR_DISPLAY_MODE_OFF; + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_BAR_DISPLAY_MODE, displayMode); + } + + void SetItemSpacing(float spacing) { Utils::SetAttributeFloat32(nodeApi_, GetHandle(), NODE_LIST_SPACE, spacing); } + + void SetScrollBarState(bool visible) { SetScrollBarVisible(visible); } + void SetSpace(float spacing) { SetItemSpacing(spacing); } + + void SetNestedScrollMode(int32_t mode) + { + Utils::SetAttributeInt32(nodeApi_, GetHandle(), NODE_SCROLL_NESTED_SCROLL, mode); + } + + // ======================================== + // 滚动控制接口 + // ======================================== + void ScrollToIndex(int32_t index) { ScrollToIndex(index, false, ARKUI_SCROLL_ALIGNMENT_START); } + + void ScrollToIndex(int32_t index, bool smooth, ArkUI_ScrollAlignment align) + { + ArkUI_NumberValue v[3]; + v[0].i32 = index; // value[0] + v[1].i32 = smooth ? 1 : 0; // value[1] (optional) + v[2].i32 = static_cast(align); // value[2] (optional) + + ArkUI_AttributeItem it{v, 3}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SCROLL_TO_INDEX, &it); + } + + void ScrollToIndexInGroup(int32_t groupIndex, int32_t itemIndex) + { + ArkUI_NumberValue values[] = {{.i32 = groupIndex}, {.i32 = itemIndex}}; + ArkUI_AttributeItem item{values, 2}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SCROLL_TO_INDEX_IN_GROUP, &item); + } + + // ======================================== + // 适配器设置接口 + // ======================================== + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (!Utils::IsNotNull(adapter)) { + nodeApi_->resetAttribute(GetHandle(), NODE_LIST_NODE_ADAPTER); + listAdapter_.reset(); + return; + } + adapter->EnsurePlaceholderTypeOr(static_cast(ARKUI_NODE_LIST_ITEM)); + ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetAdapter()}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_NODE_ADAPTER, &item); + listAdapter_ = adapter; + } + + // —— 扩展属性 —— // + void SetDirection(ArkUI_Axis axis) + { + ArkUI_NumberValue v0{}; + v0.i32 = static_cast(axis); + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_DIRECTION, &it); + } + + void SetSticky(ArkUI_StickyStyle sticky) + { + ArkUI_NumberValue v0{}; + v0.i32 = static_cast(sticky); + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_STICKY, &it); + } + + void SetCachedCount(int32_t count) + { + ArkUI_NumberValue v0{}; + v0.i32 = count; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_CACHED_COUNT, &it); + } + + void SetInitialIndex(int32_t index) + { + ArkUI_NumberValue v0{}; + v0.i32 = index; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_INITIAL_INDEX, &it); + } + + void SetDivider(float widthPx) + { + ArkUI_NumberValue v0{}; + v0.f32 = widthPx; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_DIVIDER, &it); + } + + void SetAlignListItem(ArkUI_ListItemAlignment align) + { + ArkUI_NumberValue v0{}; + v0.i32 = static_cast(align); + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_ALIGN_LIST_ITEM, &it); + } + + void SetChildrenMainSizeOption(ArkUI_ListChildrenMainSize *opt) + { + ArkUI_AttributeItem it{nullptr, 0, nullptr, opt}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_CHILDREN_MAIN_SIZE, &it); + childrenMainSizeOption_ = opt; + } + + void SetFocusWrapMode(int mode) + { + ArkUI_NumberValue v0{}; + v0.i32 = mode; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_FOCUS_WRAP_MODE, &it); + } + + void SetMaintainVisibleContentPosition(bool on) + { + ArkUI_NumberValue v0{}; + v0.i32 = on ? 1 : 0; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_MAINTAIN_VISIBLE_CONTENT_POSITION, &it); + } + + void SetStackFromEnd(bool on) + { + ArkUI_NumberValue v0{}; + v0.i32 = on ? 1 : 0; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_STACK_FROM_END, &it); + } + + void SetSyncLoad(bool on) + { + ArkUI_NumberValue v0{}; + v0.i32 = on ? 1 : 0; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SYNC_LOAD, &it); + } + + void SetScrollSnapAlign(int align /*ARKUI_SCROLL_SNAP_ALIGN_**/) + { + ArkUI_NumberValue v0{}; + v0.i32 = align; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_SCROLL_SNAP_ALIGN, &it); + } + + void SetLanes(int lanes) + { + ArkUI_NumberValue v0{}; + v0.i32 = lanes; + ArkUI_AttributeItem it{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_LIST_LANES, &it); + } + + void SetContentOffsets(float startPx, float endPx) + { + ArkUI_NumberValue v0{}; + v0.f32 = startPx; + ArkUI_AttributeItem it0{&v0, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_SCROLL_CONTENT_START_OFFSET, &it0); + + ArkUI_NumberValue v1{}; + v1.f32 = endPx; + ArkUI_AttributeItem it1{&v1, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_SCROLL_CONTENT_END_OFFSET, &it1); + } + + // ======================================== + // 事件注册接口 + // ======================================== + void RegisterOnScrollIndex(const std::function &callback) + { + onScrollIndexCallback_ = callback; + if (!isScrollIndexEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_INDEX, 0, this); + isScrollIndexEventRegistered_ = true; + } + } + + // 可视区域变化 + void RegisterOnVisibleContentChange(const std::function &callback) + { + onVisibleChangeCallback_ = callback; + if (!isVisibleChangeEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_VISIBLE_CONTENT_CHANGE, 0, this); + isVisibleChangeEventRegistered_ = true; + } + } + + void RegisterOnWillScroll(const std::function &callback) + { + onWillScrollCallback_ = callback; + if (!isWillScrollEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_WILL_SCROLL, 0, this); + isWillScrollEventRegistered_ = true; + } + } + + void RegisterOnDidScroll(const std::function &callback) + { + onDidScrollCallback_ = callback; + if (!isDidScrollEventRegistered_) { + nodeApi_->registerNodeEvent(GetHandle(), NODE_LIST_ON_DID_SCROLL, 0, this); + isDidScrollEventRegistered_ = true; + } + } + + void RegisterOnReachEnd(const std::function &callback) { onReachEndCallback_ = callback; } + + void RegisterOnScrollFrameBegin(const std::function &callback) + { + onScrollFrameBeginCallback_ = callback; + } + +protected: + void OnNodeEvent(ArkUI_NodeEvent *event) override + { + BaseNode::OnNodeEvent(event); + + ArkUI_NodeComponentEvent *componentEvent = OH_ArkUI_NodeEvent_GetNodeComponentEvent(event); + if (!Utils::IsNotNull(componentEvent)) { + return; + } + + int32_t eventType = OH_ArkUI_NodeEvent_GetEventType(event); + HandleSpecificListEvent(eventType, componentEvent); + } + +private: + void UnregisterSpecificEvents() + { + if (isScrollIndexEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_INDEX); + } + if (isVisibleChangeEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_SCROLL_VISIBLE_CONTENT_CHANGE); + } + if (isWillScrollEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_WILL_SCROLL); + } + if (isDidScrollEventRegistered_) { + nodeApi_->unregisterNodeEvent(GetHandle(), NODE_LIST_ON_DID_SCROLL); + } + } + + void ResetListAdapter() + { + if (Utils::IsNotNull(listAdapter_)) { + nodeApi_->resetAttribute(GetHandle(), NODE_LIST_NODE_ADAPTER); + listAdapter_.reset(); + } + } + + void CleanupChildrenMainSizeOption() + { + if (Utils::IsNotNull(childrenMainSizeOption_)) { + nodeApi_->resetAttribute(GetHandle(), NODE_LIST_CHILDREN_MAIN_SIZE); + OH_ArkUI_ListChildrenMainSizeOption_Dispose(childrenMainSizeOption_); + childrenMainSizeOption_ = nullptr; + } + } + + void OnScrollIndexEvt(const ArkUI_NodeComponentEvent *ev) + { + if (!onScrollIndexCallback_) { + return; + } + const int32_t firstIndex = ev->data[kScrollIndexFirstDataIndex].i32; + const int32_t lastIndex = ev->data[kScrollIndexLastDataIndex].i32; + onScrollIndexCallback_(firstIndex, lastIndex); + } + + void OnVisibleChangeEvt(const ArkUI_NodeComponentEvent *ev) + { + if (!onVisibleChangeCallback_) { + return; + } + VisibleContentChange v{}; + v.firstChildIndex = ev->data[kVisibleChangeFirstChildDataIndex].i32; + v.startArea = static_cast(ev->data[kVisibleChangeStartAreaDataIndex].i32); + v.startItemIndex = ev->data[kVisibleChangeStartIndexDataIndex].i32; + v.lastChildIndex = ev->data[kVisibleChangeLastChildDataIndex].i32; + v.endArea = static_cast(ev->data[kVisibleChangeEndAreaDataIndex].i32); + v.endItemIndex = ev->data[kVisibleChangeEndIndexDataIndex].i32; + onVisibleChangeCallback_(v); + } + + void OnReachEndEvt() + { + if (onReachEndCallback_) { + onReachEndCallback_(); + } + } + + void OnScrollFrameBeginEvt(const ArkUI_NodeComponentEvent *ev) + { + if (onScrollFrameBeginCallback_) { + onScrollFrameBeginCallback_(ev->data[kScrollFrameBeginDataIndex].f32); + } + } + + void OnWillScrollEvt() + { + if (onWillScrollCallback_) { + onWillScrollCallback_(); + } + } + + void OnDidScrollEvt() + { + if (onDidScrollCallback_) { + onDidScrollCallback_(); + } + } + + void HandleSpecificListEvent(int32_t eventType, ArkUI_NodeComponentEvent *ev) + { + switch (eventType) { + case NODE_LIST_ON_SCROLL_INDEX: + OnScrollIndexEvt(ev); + break; + case NODE_LIST_ON_SCROLL_VISIBLE_CONTENT_CHANGE: + OnVisibleChangeEvt(ev); + break; + case NODE_SCROLL_EVENT_ON_REACH_END: + OnReachEndEvt(); + break; + case NODE_SCROLL_EVENT_ON_SCROLL_FRAME_BEGIN: + OnScrollFrameBeginEvt(ev); + break; + case NODE_LIST_ON_WILL_SCROLL: + OnWillScrollEvt(); + break; + case NODE_LIST_ON_DID_SCROLL: + OnDidScrollEvt(); + break; + default: + break; + } + } + + ArkUI_ListChildrenMainSize *EnsureChildrenMainSizeOption() + { + if (!childrenMainSizeOption_) { + auto *opt = OH_ArkUI_ListChildrenMainSizeOption_Create(); + SetChildrenMainSizeOption(opt); + } + return childrenMainSizeOption_; + } + + void ChildrenMainSizeSetDefault(float mainSize) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_SetDefaultMainSize(opt, mainSize); + } + + float ChildrenMainSizeGetDefault() const + { + if (!childrenMainSizeOption_) { + return 0.0f; + } + return OH_ArkUI_ListChildrenMainSizeOption_GetDefaultMainSize(childrenMainSizeOption_); + } + + void ChildrenMainSizeResize(int32_t size) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_Resize(opt, size); + } + + void ChildrenMainSizeSplice(int32_t index, int32_t deleteCount, int32_t addCount) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_Splice(opt, index, deleteCount, addCount); + } + + void ChildrenMainSizeUpdate(int32_t index, float mainSize) + { + auto *opt = EnsureChildrenMainSizeOption(); + if (!opt) { + return; + } + OH_ArkUI_ListChildrenMainSizeOption_UpdateSize(opt, index, mainSize); + } + + float ChildrenMainSizeGet(int32_t index) const + { + if (!childrenMainSizeOption_) { + return 0.0f; + } + return OH_ArkUI_ListChildrenMainSizeOption_GetMainSize(childrenMainSizeOption_, index); + } + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + std::shared_ptr listAdapter_; + ArkUI_ListChildrenMainSize *childrenMainSizeOption_ = nullptr; + + // 事件回调函数 + std::function onScrollIndexCallback_; + std::function onVisibleChangeCallback_; + std::function onReachEndCallback_; + std::function onScrollFrameBeginCallback_; + std::function onWillScrollCallback_; + std::function onDidScrollCallback_; + + // 事件注册状态 + bool isScrollIndexEventRegistered_ = false; + bool isVisibleChangeEventRegistered_ = false; + bool isWillScrollEventRegistered_ = false; + bool isDidScrollEventRegistered_ = false; + + ScrollEventGuard scrollEventGuard_; +}; + +#endif // LIST_MAKER_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.cpp new file mode 100644 index 000000000..be12a4216 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.cpp @@ -0,0 +1,617 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ListMaker.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#ifndef LOG_TAG +#define LOG_TAG "RefreshMaker" +#endif + +#include "ScrollableNode.h" +#include "ArkUINodeAdapter.h" +#include "ListItemGroup.h" +#include "ListItemSwipe.h" +#include "RefreshMaker.h" + +// ================= 业务常量 ================= +namespace { + +constexpr uint32_t K_COLOR_PAGE_BG = 0xFFF1F3F5U; +constexpr uint32_t K_ITEM_BG_COLOR = 0xFFFFFFFFU; +constexpr const char* K_LOADING_TEXT = "加载中…"; +constexpr float K_ITEM_FONT_SIZE = 16.0f; + +constexpr int K_INIT_COUNT = 10; +constexpr int K_LOAD_BATCH = 10; +constexpr int K_MAX_ITEMS = 100; +constexpr int K_REFRESH_PREPEND_COUNT = 5; +constexpr int K_MIN_REFRESH_MS = 350; + +constexpr float K_WIDTH_PERCENT_FULL = 1.0f; +constexpr float K_HEIGHT_PERCENT_FULL = 1.0f; +constexpr float K_LIST_SPACE = 12.0f; + +constexpr float K_REFRESH_OFFSET_VP = 64.0f; +constexpr float K_MAX_PULL_VP = 128.0f; +constexpr float K_PULL_DOWN_RATIO = 0.6f; + +constexpr bool K_LIST_CLIP_CONTENT = true; +constexpr bool K_LIST_EDGE_SPRING = true; +constexpr bool K_LIST_SCROLLBAR_VISIBLE = false; + +constexpr int K_MIN_INDEX = 0; +constexpr int K_LAST_INDEX_OFFSET = 1; + +// 样式 +constexpr float K_ROW_HEIGHT = 80.0f; +constexpr float K_FOOTER_HEIGHT = 64.0f; + +constexpr int64_t K_FOOTER_STABLE_ID = -16; + +} // namespace + +namespace { +constexpr int RET_OK = 0; + +inline bool Ok(int rc, const char* what) +{ + if (rc != RET_OK) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, + "%{public}s failed rc=%{public}d", what, rc); + return false; + } + return true; +} +} // namespace + +// ================= 状态 ================= +namespace { + +struct RefreshListState { + std::shared_ptr adapter; + std::vector data; + bool showLoadingFooter = false; + + int total = K_INIT_COUNT; // 逻辑总数(与 data.size() 同步增长) + int lastTriggeredTot = -1; + bool loading = false; + + bool NoMore() const { return total >= K_MAX_ITEMS; } + + int ItemsCount() const { return static_cast(data.size()); } + int RenderCount() const { return ItemsCount() + (showLoadingFooter ? 1 : 0); } +}; + +} // namespace + +// ================= 业务工具 ================= +namespace { + +/** + * 添加加载状态尾部 + * @param st 刷新列表状态 + */ +inline void AddLoadingFooter(const std::shared_ptr &st) +{ + const int footerIndex = st->ItemsCount(); + st->adapter->InsertRange(footerIndex, 1); + st->showLoadingFooter = true; +} + +/** + * 移除加载状态尾部 + * @param st 刷新列表状态 + */ +inline void RemoveLoadingFooter(const std::shared_ptr &st) +{ + const int footerIndex = st->ItemsCount(); + st->adapter->RemoveRange(footerIndex, 1); + st->showLoadingFooter = false; +} + +/** + * 切换加载状态尾部显示 + * @param st 刷新列表状态 + * @param on 是否显示 + */ +static void ToggleLoadingFooter(const std::shared_ptr &st, bool on) +{ + if (!st || !st->adapter) { + return; + } + + if (on && !st->showLoadingFooter) { + AddLoadingFooter(st); + } else if (!on && st->showLoadingFooter) { + RemoveLoadingFooter(st); + } +} + +/** + * 末尾追加批量数据 + * @param st 刷新列表状态 + * @param add 添加数量 + */ +static void AppendTailBatch(const std::shared_ptr &st, int add) +{ + if (!st || !st->adapter) { + return; + } + + const int base = st->total; + const int addClamped = std::min(add, K_MAX_ITEMS - base); + if (addClamped <= 0) { + return; + } + + // 先去掉 footer,再插入数据,再恢复 footer(避免 index 混淆) + const bool hadFooter = st->showLoadingFooter; + ToggleLoadingFooter(st, false); + + st->data.reserve(st->data.size() + static_cast(addClamped)); + for (int i = 0; i < addClamped; ++i) { + st->data.emplace_back(std::to_string(base + i)); + } + st->adapter->InsertRange(base, addClamped); + + st->total += addClamped; + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, + "AppendTailBatch add=%{public}d total=%{public}d", addClamped, st->total); + + if (hadFooter) { + ToggleLoadingFooter(st, true); + } +} + +} // namespace + +// ================= 适配器回调(通用 ArkUINodeAdapter) ================= +namespace { + +/** + * 创建列表项 + * @param api 原生节点API接口 + * @return 列表项节点句柄 + */ +static ArkUI_NodeHandle CreateListItem(ArkUI_NativeNodeAPI_1 *api) +{ + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + if (text == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "create TEXT node null"); + return nullptr; + } + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_LIST_ITEM); + if (item == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "create LIST_ITEM node null"); + return nullptr; + } + + if (int rc = api->addChild(item, text); !Ok(rc, "addChild(item,text)")) { + return nullptr; + } + + Utils::SetAttributeFloat32(api, item, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + + // 让文本也占满宽度 + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, 1.0f); + return item; +} + +/** + * 绑定为普通列表项 + * @param api 原生节点API接口 + * @param item 列表项节点句柄 + * @param txt 文本内容 + */ +static void BindAsNormal(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, const char *txt) +{ + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!text) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "BindAsNormal getFirstChild null"); + return; + } + + // 背景放在 item 上 + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + + // 文本占满宽度 + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, 1.0f); + + Utils::SetTextContent(api, text, txt); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_ITEM_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, K_ROW_HEIGHT); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, K_ROW_HEIGHT); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); +} + +/** + * 绑定为加载尾部项 + * @param api 原生节点API接口 + * @param item 列表项节点句柄 + */ +static void BindAsFooter(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item) +{ + ArkUI_NodeHandle text = api->getFirstChild(item); + if (!text) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "BindAsFooter getFirstChild null"); + return; + } + + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, K_ITEM_BG_COLOR); + Utils::SetAttributeFloat32(api, text, NODE_WIDTH_PERCENT, 1.0f); + + Utils::SetTextContent(api, text, K_LOADING_TEXT); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_ITEM_FONT_SIZE); + Utils::SetAttributeFloat32(api, text, NODE_HEIGHT, K_FOOTER_HEIGHT); + Utils::SetAttributeFloat32(api, text, NODE_TEXT_LINE_HEIGHT, K_FOOTER_HEIGHT); + Utils::SetAttributeInt32(api, text, NODE_TEXT_ALIGN, ARKUI_TEXT_ALIGNMENT_CENTER); +} + +/** + * 创建节点适配器回调 + * @param st 刷新列表状态 + * @return 节点适配器回调结构 + */ +static NodeAdapterCallbacks MakeCallbacks(const std::shared_ptr &st) +{ + NodeAdapterCallbacks cb{}; + + cb.getTotalCount = [st]() -> int32_t { + return st ? st->RenderCount() : 0; + }; + + cb.getStableId = [st](int32_t i) -> uint64_t { + if (!st) { + return static_cast(i); + } + const int items = st->ItemsCount(); + if (st->showLoadingFooter && i == items) { + return static_cast(K_FOOTER_STABLE_ID); + } + if (i >= 0 && i < items) { + return static_cast(std::hash{}(st->data[static_cast(i)])); + } + return static_cast(i); + }; + + cb.onCreate = [](ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) -> ArkUI_NodeHandle { + return CreateListItem(api); + }; + + cb.onBind = [st](ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index) { + if (!st) { + return; + } + + const int items = st->ItemsCount(); + if (st->showLoadingFooter && index == items) { + BindAsFooter(api, item); + return; + } + if (index >= 0 && index < items) { + BindAsNormal(api, item, st->data[static_cast(index)].c_str()); + return; + } + BindAsNormal(api, item, ""); + }; + + return cb; +} + +/** + * 创建 ArkUINodeAdapter + */ +static std::shared_ptr MakeListAdapter(const std::shared_ptr &st) +{ + std::shared_ptr adapter = + std::make_shared(static_cast(ARKUI_NODE_LIST_ITEM)); + adapter->SetCallbacks(MakeCallbacks(st)); + return adapter; +} + +} // namespace + +// ================= 视图拼装 ================= +namespace { + +/** + * 创建根节点 + * @return 根节点智能指针 + */ +static std::shared_ptr MakeRoot() +{ + ArkUI_NativeNodeAPI_1 *api = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + ArkUI_NodeHandle h = api->createNode(ARKUI_NODE_COLUMN); + + std::shared_ptr root = std::make_shared(h); + root->SetWidthPercent(K_WIDTH_PERCENT_FULL); + root->SetHeightPercent(K_HEIGHT_PERCENT_FULL); + Utils::SetAttributeUInt32(api, root->GetHandle(), NODE_BACKGROUND_COLOR, K_COLOR_PAGE_BG); + return root; +} + +static void ConfigureList(const std::shared_ptr &list) +{ + list->SetWidthPercent(K_WIDTH_PERCENT_FULL); + list->SetHeightPercent(K_HEIGHT_PERCENT_FULL); + list->SetTransparentBackground(); + list->SetClipContent(K_LIST_CLIP_CONTENT); + list->SetEdgeEffectSpring(K_LIST_EDGE_SPRING); + list->SetScrollBarState(K_LIST_SCROLLBAR_VISIBLE); + list->SetSpace(K_LIST_SPACE); + list->SetNestedScrollMode(ListMaker::kNestedScrollParentFirst); +} + +/** + * 创建列表节点 + * @param group 列表项组节点 + * @return 列表节点智能指针 + */ +inline std::shared_ptr MakeList(const std::shared_ptr &group) +{ + std::shared_ptr list = std::make_shared(); + ConfigureList(list); + list->AddChild(std::static_pointer_cast(group)); + return list; +} + +/** + * 创建刷新节点 + * @param list 列表节点 + * @return 刷新节点智能指针 + */ +static std::shared_ptr MakeRefresh(const std::shared_ptr &list) +{ + std::shared_ptr refresh = std::make_shared(); + refresh->AttachChild(list); + refresh->SetTransparentBackground(); + refresh->SetPullToRefresh(true); + refresh->SetRefreshOffset(K_REFRESH_OFFSET_VP); + refresh->SetPullDownRatio(K_PULL_DOWN_RATIO); + refresh->SetMaxPullDownDistance(K_MAX_PULL_VP); + return refresh; +} + +// 触底加载 +static void WireReachEnd(const std::shared_ptr &st, const std::shared_ptr &list) +{ + list->RegisterOnReachEnd([st, list]() { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnReachEnd triggered"); + + if (!st || st->loading || st->lastTriggeredTot == st->total) { + return; + } + st->lastTriggeredTot = st->total; + + if (st->NoMore()) { + const int count = st->RenderCount(); + const int idx = std::max(K_MIN_INDEX, count - K_LAST_INDEX_OFFSET); + list->ScrollToIndex(idx); + return; + } + + st->loading = true; + ToggleLoadingFooter(st, true); + AppendTailBatch(st, K_LOAD_BATCH); + ToggleLoadingFooter(st, false); + st->loading = false; + }); +} + +static void OnVisibleChangeCore(const std::shared_ptr &st, + const std::shared_ptr &list, + int32_t endIdxInGroup) +{ + if (!st) { + return; + } + + const int items = st->ItemsCount(); + if (items <= 0) { + return; + } + + if (endIdxInGroup < items - K_LAST_INDEX_OFFSET) { + return; + } + + if (st->loading || st->lastTriggeredTot == st->total) { + return; + } + + st->lastTriggeredTot = st->total; + + if (st->NoMore()) { + const int count = st->RenderCount(); + const int idx = std::max(K_MIN_INDEX, count - K_LAST_INDEX_OFFSET); + list->ScrollToIndex(idx); + return; + } + + st->loading = true; + ToggleLoadingFooter(st, true); + AppendTailBatch(st, K_LOAD_BATCH); + ToggleLoadingFooter(st, false); + st->loading = false; +} + +inline void WireVisibleChange(const std::shared_ptr &st, const std::shared_ptr &list) +{ + using V = ListMaker::VisibleContentChange; + list->RegisterOnVisibleContentChange([st, list](const V &ev) { + const int32_t endIdx = ev.EndOnItem() ? ev.endItemIndex : -1; + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnVisibleContentChange endIdxInGroup=%{public}d", + endIdx); + OnVisibleChangeCore(st, list, endIdx); + }); +} + +static void PrependNewData(const std::shared_ptr &st) +{ + std::vector news; + news.reserve(K_REFRESH_PREPEND_COUNT); + for (int i = 0; i < K_REFRESH_PREPEND_COUNT; ++i) { + news.emplace_back(std::string("New Item ") + std::to_string(i)); + } + + const bool hadFooter = st->showLoadingFooter; + ToggleLoadingFooter(st, false); + + st->data.insert(st->data.begin(), news.begin(), news.end()); + st->adapter->InsertRange(0, K_REFRESH_PREPEND_COUNT); + + st->total = std::min(st->total + K_REFRESH_PREPEND_COUNT, K_MAX_ITEMS); + st->lastTriggeredTot = -1; + + if (hadFooter) { + ToggleLoadingFooter(st, true); + } + + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, + "OnRefresh prepend=%{public}d total=%{public}d", + K_REFRESH_PREPEND_COUNT, st->total); +} + +/** + * 刷新行为 + */ +static void WireRefreshBehavior(const std::shared_ptr &st, + const std::shared_ptr &refresh) +{ + static std::atomic_bool sRefreshing{false}; + + refresh->RegisterOnRefresh([st, refresh]() { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnRefresh triggered"); + + if (sRefreshing.exchange(true)) { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnRefresh ignored: busy"); + return; + } + + using Clock = std::chrono::steady_clock; + const auto t0 = Clock::now(); + + if (st && !st->NoMore()) { + PrependNewData(st); + } + + const int elapsedMs = + static_cast(std::chrono::duration_cast(Clock::now() - t0).count()); + const int delay = std::max(0, K_MIN_REFRESH_MS - elapsedMs); + + Utils::PostDelayedTask(delay, [refresh]() { + refresh->SetRefreshing(false); + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnRefresh complete"); + }); + + sRefreshing = false; + }); +} + +inline void WireRefreshLogs(const std::shared_ptr &refresh) +{ + refresh->RegisterOnOffsetChange([](float offsetVp) { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnOffsetChange offsetVp=%{public}f", offsetVp); + }); + refresh->RegisterOnStateChange([](int32_t state) { + OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, LOG_TAG, "OnStateChange state=%{public}d", state); + }); +} + +/** + * Build + */ +static std::shared_ptr Build() +{ + // 根节点 + std::shared_ptr root = MakeRoot(); + + // 分组 + 列表 + std::shared_ptr group = std::make_shared(); + std::shared_ptr list = MakeList(group); + + // 刷新容器 + std::shared_ptr refresh = MakeRefresh(list); + + // 状态 + std::shared_ptr st = std::make_shared(); + st->data.reserve(K_MAX_ITEMS); + for (int i = 0; i < K_INIT_COUNT; ++i) { + st->data.emplace_back(std::to_string(i)); + } + st->total = K_INIT_COUNT; + + // 适配器 + st->adapter = MakeListAdapter(st); + group->SetLazyAdapter(st->adapter); + + // 行为绑定 + WireReachEnd(st, list); + WireVisibleChange(st, list); + WireRefreshBehavior(st, refresh); + WireRefreshLogs(refresh); + + // 组装 + root->AddChild(refresh); + + // keep alive + GetKeepAliveContainer().emplace_back(root); + GetKeepAliveContainer().emplace_back(refresh); + GetKeepAliveContainer().emplace_back(list); + GetKeepAliveContainer().emplace_back(group); + static std::vector> g_states; + g_states.emplace_back(st); + + return root; +} +} // namespace + +ArkUI_NodeHandle RefreshMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + std::shared_ptr root = Build(); + if (root && root->GetHandle() != nullptr) { + Utils::SetAttributeFloat32(api, root->GetHandle(), NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, root->GetHandle()); + } + + return page; +} \ No newline at end of file diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.h new file mode 100644 index 000000000..e592dc5fc --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/RefreshMaker.h @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef REFRESH_MAKER_H +#define REFRESH_MAKER_H + +#include +#include + +#include +#include + +#include "ScrollableNode.h" + +inline constexpr int32_t K_REFRESH_OFFSET_DATA_INDEX = 0; +inline constexpr int32_t K_REFRESH_STATE_DATA_INDEX = 0; + +class RefreshMaker : public BaseNode { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + RefreshMaker() + : BaseNode(NodeApiInstance::GetInstance()->GetNativeNodeAPI()->createNode(ARKUI_NODE_REFRESH)), + registrar_(nodeApi_, GetHandle()) + { + nodeApi_->addNodeEventReceiver(GetHandle(), StaticEvent); + } + + // ===== 属性(组件范围)===== + void SetRefreshing(bool refreshing) + { + ArkUI_NumberValue v[]{{.i32 = refreshing ? 1 : 0}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_REFRESHING, &item); + } + + void SetHeaderContent(const std::shared_ptr &header) + { + if (header == nullptr) { + nodeApi_->resetAttribute(GetHandle(), NODE_REFRESH_CONTENT); + header_.reset(); + return; + } + ArkUI_AttributeItem item{nullptr, 0, nullptr, header->GetHandle()}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_CONTENT, &item); + header_ = header; + } + + void SetPullDownRatio(float ratio) + { + ArkUI_NumberValue v[]{{.f32 = ratio}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_PULL_DOWN_RATIO, &item); + } + + void SetRefreshOffset(float vp) + { + ArkUI_NumberValue v[]{{.f32 = vp}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_OFFSET, &item); + } + + void SetPullToRefresh(bool enable) + { + ArkUI_NumberValue v[]{{.i32 = enable ? 1 : 0}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_PULL_TO_REFRESH, &item); + } + + void SetMaxPullDownDistance(float vp) + { + ArkUI_NumberValue v[]{{.f32 = vp}}; + ArkUI_AttributeItem item{v, 1}; + nodeApi_->setAttribute(GetHandle(), NODE_REFRESH_MAX_PULL_DOWN_DISTANCE, &item); + } + + void AttachChild(const std::shared_ptr &child) + { + if (child != nullptr) { + AddChild(child); + } + } + + // ===== 事件注册 ===== + void RegisterOnRefresh(const std::function &cb) + { + onRefresh_ = cb; + registrar_.RegisterEvent(NODE_REFRESH_ON_REFRESH, this); + } + + void RegisterOnOffsetChange(const std::function &cb) + { + onOffsetChange_ = cb; + registrar_.RegisterEvent(NODE_REFRESH_ON_OFFSET_CHANGE, this); + } + + void RegisterOnStateChange(const std::function &cb) + { + onStateChange_ = cb; + registrar_.RegisterEvent(NODE_REFRESH_STATE_CHANGE, this); + } + +private: + static void StaticEvent(ArkUI_NodeEvent *ev) + { + if (ev == nullptr) { + return; + } + auto *self = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(ev)); + if (self == nullptr) { + return; + } + self->OnNodeEvent(ev); + } + + void OnNodeEvent(ArkUI_NodeEvent *ev) override + { + const int32_t type = OH_ArkUI_NodeEvent_GetEventType(ev); + ArkUI_NodeComponentEvent *ce = OH_ArkUI_NodeEvent_GetNodeComponentEvent(ev); + + switch (type) { + case NODE_REFRESH_ON_REFRESH: + if (onRefresh_) { + onRefresh_(); + } + break; + case NODE_REFRESH_ON_OFFSET_CHANGE: + if (ce != nullptr && onOffsetChange_) { + onOffsetChange_(ce->data[K_REFRESH_OFFSET_DATA_INDEX].f32); + } + break; + case NODE_REFRESH_STATE_CHANGE: + if (ce != nullptr && onStateChange_) { + onStateChange_(ce->data[K_REFRESH_STATE_DATA_INDEX].i32); + } + break; + default: + break; + } + } + +private: + std::shared_ptr header_; + std::function onRefresh_; + std::function onOffsetChange_; + std::function onStateChange_; + + Utils::NodeEventRegistrar registrar_; +}; + +#endif // REFRESH_MAKER_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.cpp new file mode 100644 index 000000000..02c64c18a --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.cpp @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollMaker.h" + +#include +#ifndef LOG_TAG +#define LOG_TAG "ScrollMaker" +#endif + +// ===== 业务常量 ===== +namespace { +constexpr float K_WIDTH_PERCENT_FULL = 1.0f; +constexpr float K_HEIGHT_PERCENT_FULL = 1.0f; +constexpr uint32_t K_PAGE_BG = 0xFFFFFF00U; // Yellow + +constexpr uint32_t K_COLOR_BROWN = 0xFFA52A2AU; +constexpr uint32_t K_COLOR_BLUE = 0xFF0000FFU; +constexpr uint32_t K_COLOR_GREEN = 0xFF008000U; + +constexpr float K_TEXT_SIZE = 100.0f; + +// 其他行为参数 +constexpr float K_FRICTION = 0.9f; +constexpr float K_FLING_LIMIT = 12000.0f; + +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; + +// 事件数据数组索引常量 +constexpr int32_t K_PRIMARY_DELTA_DATA_INDEX = 0; +constexpr int32_t K_SECONDARY_DELTA_DATA_INDEX = 1; +constexpr int32_t K_AREA_HEIGHT_FALLBACK_DATA_INDEX = 6; +constexpr int32_t K_AREA_HEIGHT_PRIMARY_DATA_INDEX = 7; +} // namespace + +// ===== 全局状态 ===== +static ArkUI_NativeNodeAPI_1 *gApi{nullptr}; +static std::shared_ptr gScroll; +static ArkUI_NodeHandle gRoot{nullptr}; +static ArkUI_NodeHandle gPageTop{nullptr}; +static ArkUI_NodeHandle gPageMid{nullptr}; +static ArkUI_NodeHandle gPageBot{nullptr}; + +static float g_selfH{0.0f}; +static float g_yOffset{0.0f}; + +// ===== 工具:页面创建与布局 ===== +/** + * 创建页面节点 + * @param bg 背景色 + * @param label 标签文本 + * @return 页面节点句柄 + */ +static ArkUI_NodeHandle CreatePage(uint32_t bg, const char *label) +{ + if (gApi == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = gApi->createNode(ARKUI_NODE_STACK); + Utils::SetAttributeFloat32(gApi, page, NODE_WIDTH_PERCENT, K_WIDTH_PERCENT_FULL); + Utils::SetAttributeFloat32(gApi, page, NODE_HEIGHT_PERCENT, K_HEIGHT_PERCENT_FULL); + Utils::SetAttributeUInt32(gApi, page, NODE_BACKGROUND_COLOR, bg); + + ArkUI_NodeHandle text = gApi->createNode(ARKUI_NODE_TEXT); + Utils::SetTextContent(gApi, text, label); + Utils::SetAttributeFloat32(gApi, text, NODE_FONT_SIZE, K_TEXT_SIZE); + + gApi->addChild(page, text); + return page; +} + +/** + * 根据索引获取偏移量 + * @param index 索引值 + * @return 偏移量 + */ +static inline float GetOffsetByIndex(int index) +{ + float off = g_yOffset - static_cast(index) * g_selfH; + if (off > 1.5f * g_selfH) { + off -= 3.0f * g_selfH; + } else if (off < -1.5f * g_selfH) { + off += 3.0f * g_selfH; + } + return off; +} + +static void ApplyTranslate(ArkUI_NodeHandle n, float ty) +{ + ArkUI_NumberValue vx{.f32 = 0.0f}; + ArkUI_NumberValue vy{.f32 = ty}; + ArkUI_NumberValue vz{.f32 = 0.0f}; + + ArkUI_NumberValue v[] = {vx, vy, vz}; + ArkUI_AttributeItem it{v, 3}; + + gApi->setAttribute(n, NODE_TRANSLATE, &it); +} + +static void SyncAllTranslate() +{ + if (gApi == nullptr) { + return; + } + ApplyTranslate(gPageTop, -GetOffsetByIndex(-1)); + ApplyTranslate(gPageMid, -GetOffsetByIndex(0)); + ApplyTranslate(gPageBot, -GetOffsetByIndex(1)); +} + +// ===== 事件读取辅助 ===== +static inline float ReadDeltaPrimary(ArkUI_NodeComponentEvent *ce) { return ce->data[K_PRIMARY_DELTA_DATA_INDEX].f32; } +static inline float ReadDeltaSecondary(ArkUI_NodeComponentEvent *ce) +{ + return ce->data[K_SECONDARY_DELTA_DATA_INDEX].f32; +} + +// ===== 事件处理 ===== +static void OnAreaChange(ArkUI_NodeComponentEvent *ce) +{ + if (ce == nullptr) { + return; + } + + float h = ce->data[K_AREA_HEIGHT_PRIMARY_DATA_INDEX].f32; + if (h <= 0.0f) { + h = ce->data[K_AREA_HEIGHT_FALLBACK_DATA_INDEX].f32; + } + + if (h > 0.0f && std::fabs(h - g_selfH) > 0.5f) { + g_selfH = h; + SyncAllTranslate(); + } +} + +static void ApplyScrollDelta(float delta) +{ + if (std::fabs(delta) <= 1e-5f) { + return; + } + + g_yOffset += delta; + + const float period = 3.0f * g_selfH; + if (g_selfH > 0.0f && period > 0.0f) { + while (g_yOffset < 0.0f) { + g_yOffset += period; + } + while (g_yOffset >= period) { + g_yOffset -= period; + } + } + + SyncAllTranslate(); +} + +static void OnScrollFrameBegin(ArkUI_NodeComponentEvent *ce) +{ + if (ce == nullptr) { + return; + } + + float delta = ReadDeltaPrimary(ce); + if (std::fabs(delta) < 1e-5f) { + delta = ReadDeltaSecondary(ce); + } + + ApplyScrollDelta(delta); + + ce->data[K_PRIMARY_DELTA_DATA_INDEX].f32 = 0.0f; + ce->data[K_SECONDARY_DELTA_DATA_INDEX].f32 = 0.0f; +} + +static void OnScroll(ArkUI_NodeComponentEvent *ce) +{ + if (ce == nullptr) { + return; + } + float delta = ReadDeltaPrimary(ce); + if (std::fabs(delta) < 1e-5f) { + delta = ReadDeltaSecondary(ce); + } + ApplyScrollDelta(delta); +} + +static void OnSimpleLog(const char *tag) +{ + OH_LOG_Print(LOG_APP, LOG_DEBUG, K_LOG_DOMAIN, LOG_TAG, "%{public}s", tag); +} + +static bool HandleCoreScrollEvents(int32_t type, ArkUI_NodeComponentEvent *ce) +{ + switch (type) { + case NODE_SCROLL_EVENT_ON_SCROLL_FRAME_BEGIN: + OnScrollFrameBegin(ce); + return true; + case NODE_SCROLL_EVENT_ON_WILL_SCROLL: + OnSimpleLog("OnWillScroll"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL: + OnScroll(ce); + return true; + case NODE_SCROLL_EVENT_ON_DID_SCROLL: + OnSimpleLog("OnDidScroll"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL_START: + OnSimpleLog("OnScrollStart"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL_STOP: + OnSimpleLog("OnScrollStop"); + return true; + default: + return false; + } +} + +static bool HandleBoundaryEvents(int32_t type, ArkUI_NodeComponentEvent * /*ce*/) +{ + switch (type) { + case NODE_SCROLL_EVENT_ON_REACH_START: + OnSimpleLog("OnReachStart"); + return true; + case NODE_SCROLL_EVENT_ON_REACH_END: + OnSimpleLog("OnReachEnd"); + return true; + case NODE_SCROLL_EVENT_ON_WILL_STOP_DRAGGING: + OnSimpleLog("OnWillStopDragging"); + return true; + case NODE_SCROLL_EVENT_ON_SCROLL_EDGE: + OnSimpleLog("OnScrollEdge"); + return true; + default: + return false; + } +} + +static bool HandleZoomEvents(int32_t type, ArkUI_NodeComponentEvent * /*ce*/) +{ + switch (type) { + case NODE_SCROLL_EVENT_ON_ZOOM_START: + OnSimpleLog("OnZoomStart"); + return true; + case NODE_SCROLL_EVENT_ON_ZOOM_STOP: + OnSimpleLog("OnZoomStop"); + return true; + case NODE_SCROLL_EVENT_ON_DID_ZOOM: + OnSimpleLog("OnDidZoom"); + return true; + default: + return false; + } +} + +static void HandleNodeEvent(ArkUI_NodeEvent *ev) +{ + if (ev == nullptr) { + return; + } + + int32_t type = OH_ArkUI_NodeEvent_GetEventType(ev); + ArkUI_NodeComponentEvent *ce = OH_ArkUI_NodeEvent_GetNodeComponentEvent(ev); + + if (type == NODE_EVENT_ON_AREA_CHANGE) { + OnAreaChange(ce); + return; + } + + if (HandleCoreScrollEvents(type, ce)) { + return; + } + if (HandleBoundaryEvents(type, ce)) { + return; + } + if (HandleZoomEvents(type, ce)) { + return; + } +} + +// ===== 组装构建 ===== +static void SetupScroll() +{ + gScroll = std::make_shared(); + + gScroll->SetWidthPercent(K_WIDTH_PERCENT_FULL); + gScroll->SetHeightPercent(K_HEIGHT_PERCENT_FULL); + gScroll->SetBackgroundColor(K_PAGE_BG); + + gScroll->SetDirection(ARKUI_SCROLL_DIRECTION_VERTICAL); + gScroll->SetScrollBarDisplayMode(ARKUI_SCROLL_BAR_DISPLAY_MODE_OFF); + gScroll->SetScrollBarWidth(2.0f); + gScroll->SetScrollBarColor(0x66000000U); + gScroll->SetScrollBarMargin(0.0f, 0.0f, 0.0f, 0.0f); + + gScroll->SetEnableScrollInteraction(true); + gScroll->SetFriction(K_FRICTION); + gScroll->SetClipContent(true); + gScroll->SetPageEnabled(false); + gScroll->SetBackToTopEnabled(false); + gScroll->SetFadingEdge(0.0f); + gScroll->SetFlingSpeedLimit(K_FLING_LIMIT); + gScroll->SetEdgeEffect(ARKUI_EDGE_EFFECT_SPRING, true, + static_cast(ARKUI_EFFECT_EDGE_START | ARKUI_EFFECT_EDGE_END)); +} + +static void SetupRootAndPages() +{ + gRoot = gApi->createNode(ARKUI_NODE_STACK); + Utils::SetAttributeFloat32(gApi, gRoot, NODE_WIDTH_PERCENT, K_WIDTH_PERCENT_FULL); + Utils::SetAttributeFloat32(gApi, gRoot, NODE_HEIGHT_PERCENT, K_HEIGHT_PERCENT_FULL); + + gPageTop = CreatePage(K_COLOR_BROWN, "3"); + gPageMid = CreatePage(K_COLOR_BLUE, "1"); + gPageBot = CreatePage(K_COLOR_GREEN, "2"); + + gApi->addChild(gRoot, gPageTop); + gApi->addChild(gRoot, gPageMid); + gApi->addChild(gRoot, gPageBot); + + gScroll->AddChild(gRoot); +} + +static void RegisterEvents() +{ + gApi->addNodeEventReceiver(gScroll->GetScroll(), &HandleNodeEvent); + gApi->addNodeEventReceiver(gRoot, &HandleNodeEvent); + gApi->registerNodeEvent(gRoot, NODE_EVENT_ON_AREA_CHANGE, 0, nullptr); +} + +/** + * 构建滚动视图 + * @return 滚动节点句柄 + */ +static ArkUI_NodeHandle Build() +{ + gApi = NodeApiInstance::GetInstance()->GetNativeNodeAPI(); + if (gApi == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "GetNativeNodeAPI failed"); + return nullptr; + } + + SetupScroll(); + SetupRootAndPages(); + RegisterEvents(); + SyncAllTranslate(); + + return gScroll->GetScroll(); +} + +ArkUI_NodeHandle ScrollMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (api == nullptr) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (page == nullptr) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + ArkUI_NodeHandle scroll = Build(); + if (scroll != nullptr) { + Utils::SetAttributeFloat32(api, scroll, NODE_LAYOUT_WEIGHT, 1.0f); + api->addChild(page, scroll); + } + + return page; +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.h new file mode 100644 index 000000000..2b91527b9 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollMaker.h @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCROLL_MAKER_H +#define SCROLL_MAKER_H + +#include +#include + +#include +#include +#include + +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" + +/** + * @brief ArkUI Scroll 节点轻封装 + */ +class ScrollMaker { +public: + using OnScrollState = std::function; + using OnWillScroll = std::function; + using OnDidScroll = std::function; + + void SetOnScrollStateChanged(OnScrollState cb) { onState_ = std::move(cb); } + void SetOnWillScroll(OnWillScroll cb) { onWill_ = std::move(cb); } + void SetOnDidScroll(OnDidScroll cb) { onDid_ = std::move(cb); } + + static ArkUI_NodeHandle CreateNativeNode(); + +public: + ScrollMaker() + { + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api_); + scroll_ = api_->createNode(ARKUI_NODE_SCROLL); + api_->addNodeEventReceiver(scroll_, StaticEvent); + scrollGuard_.Bind(api_, scroll_, this, SCROLL_EVT_ALL); + } + + ~ScrollMaker() + { + scrollGuard_.Release(); + scroll_ = nullptr; + api_ = nullptr; + } + + // ===== 通用布局/外观 ===== + void SetWidthPercent(float percent) { Utils::SetAttributeFloat32(api_, scroll_, NODE_WIDTH_PERCENT, percent); } + void SetHeightPercent(float percent) { Utils::SetAttributeFloat32(api_, scroll_, NODE_HEIGHT_PERCENT, percent); } + void SetBackgroundColor(uint32_t color) { Utils::SetAttributeUInt32(api_, scroll_, NODE_BACKGROUND_COLOR, color); } + + void SetDirection(ArkUI_ScrollDirection direction) + { + ArkUI_NumberValue v{.i32 = static_cast(direction)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_SCROLL_DIRECTION, &it); + } + + void SetScrollBarDisplayMode(ArkUI_ScrollBarDisplayMode displayMode) + { + ArkUI_NumberValue v{.i32 = static_cast(displayMode)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_DISPLAY_MODE, &it); + } + + void SetScrollBarWidth(float w) + { + ArkUI_NumberValue v{.f32 = w}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_WIDTH, &it); + } + + void SetScrollBarColor(uint32_t argb) + { + ArkUI_NumberValue v{.u32 = argb}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_COLOR, &it); + } + + /** @brief 滚动条外边距:top,right,bottom,left(单位:vp) */ + void SetScrollBarMargin(float top, float right, float bottom, float left) + { + ArkUI_NumberValue vTop{.f32 = top}; + ArkUI_NumberValue vRight{.f32 = right}; + ArkUI_NumberValue vBottom{.f32 = bottom}; + ArkUI_NumberValue vLeft{.f32 = left}; + ArkUI_NumberValue v[] = {vTop, vRight, vBottom, vLeft}; + ArkUI_AttributeItem it{v, 4}; + api_->setAttribute(scroll_, NODE_SCROLL_BAR_MARGIN, &it); + } + + void SetEnableScrollInteraction(bool enable) + { + ArkUI_NumberValue v{.i32 = enable ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_ENABLE_SCROLL_INTERACTION, &it); + } + + void SetFriction(float friction) { Utils::SetAttributeFloat32(api_, scroll_, NODE_SCROLL_FRICTION, friction); } + + void SetNestedMode(ArkUI_ScrollNestedMode mode) + { + ArkUI_NumberValue v{.i32 = static_cast(mode)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_NESTED_SCROLL, &it); + } + + void SetScrollEdge(ArkUI_ScrollEdge edge) + { + ArkUI_NumberValue v{.i32 = static_cast(edge)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_EDGE, &it); + } + + void SetContentClipMode(ArkUI_ContentClipMode clipMode) + { + ArkUI_NumberValue v{.i32 = static_cast(clipMode)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_CLIP_CONTENT, &it); + } + + void SetClipContent(bool clip) + { + ArkUI_NumberValue v{.i32 = clip ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_CLIP_CONTENT, &it); + } + + void SetPageEnabled(bool enable) + { + ArkUI_NumberValue v{.i32 = enable ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_PAGE, &it); + } + + void SetBackToTopEnabled(bool enable) + { + ArkUI_NumberValue v{.i32 = enable ? 1 : 0}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_BACK_TO_TOP, &it); + } + + // 偏移/尺寸/By/Fling/渐隐/限速 + void SetOffset(float x, float y) + { + ArkUI_NumberValue vx{.f32 = x}; + ArkUI_NumberValue vy{.f32 = y}; + ArkUI_NumberValue v[] = {vx, vy}; + ArkUI_AttributeItem it{v, 2}; + api_->setAttribute(scroll_, NODE_SCROLL_OFFSET, &it); + } + + void SetSize(float w, float h) + { + ArkUI_NumberValue vw{.f32 = w}; + ArkUI_NumberValue vh{.f32 = h}; + ArkUI_NumberValue v[] = {vw, vh}; + ArkUI_AttributeItem it{v, 2}; + api_->setAttribute(scroll_, NODE_SCROLL_SIZE, &it); + } + + void ScrollBy(float dx, float dy) + { + ArkUI_NumberValue vx{.f32 = dx}; + ArkUI_NumberValue vy{.f32 = dy}; + ArkUI_NumberValue v[] = {vx, vy}; + ArkUI_AttributeItem it{v, 2}; + api_->setAttribute(scroll_, NODE_SCROLL_BY, &it); + } + + void Fling(float velocity) + { + ArkUI_NumberValue v{.f32 = velocity}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_FLING, &it); + } + + void SetFadingEdge(float len) + { + ArkUI_NumberValue v{.f32 = len}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_FADING_EDGE, &it); + } + + void SetFlingSpeedLimit(float limit) + { + ArkUI_NumberValue v{.f32 = limit}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_FLING_SPEED_LIMIT, &it); + } + + /** + * @brief 设置 Scroll 组件的边缘效果(越界回弹/发光等)。 + * @param effect [IN] 边缘效果类型,取值为 ArkUI_EdgeEffect(如 ARKUI_EDGE_EFFECT_NONE、ARKUI_EDGE_EFFECT_SPRING)。 + * @param enableWhenContentSmaller [IN] 当内容尺寸小于组件本身时是否仍启用边缘效果(1 启用,0 禁用)。 + * @param edge [IN] 生效方向,ArkUI_EffectEdge,可位或组合(如 ARKUI_EFFECT_EDGE_START | ARKUI_EFFECT_EDGE_END)。 + * @note 对应属性:NODE_SCROLL_EDGE_EFFECT;ArkUI_AttributeItem.value + * 含义:[0].i32=effect,[1].i32=enableWhenContentSmaller,[2].i32=edge。 + */ + void SetEdgeEffect(ArkUI_EdgeEffect effect, bool enableWhenContentSmaller, ArkUI_EffectEdge edge) + { + ArkUI_NumberValue v[3]; + v[0].i32 = static_cast(effect); // [0] 效果类型 + v[1].i32 = enableWhenContentSmaller ? 1 : 0; // [1] 小内容是否启用 + v[2].i32 = static_cast(edge); // [2] 生效边(位掩码) + + ArkUI_AttributeItem it{v, 3}; + api_->setAttribute(scroll_, NODE_SCROLL_EDGE_EFFECT, &it); + } + + void SetSnapAlign(ArkUI_ScrollSnapAlign align) + { + ArkUI_NumberValue v{.i32 = static_cast(align)}; + ArkUI_AttributeItem it{&v, 1}; + api_->setAttribute(scroll_, NODE_SCROLL_SNAP, &it); + } + + // ===== 子节点 ===== + void AddChild(ArkUI_NodeHandle child) + { + if (child != nullptr) { + api_->addChild(scroll_, child); + } + } + + ArkUI_NodeHandle GetScroll() const { return scroll_; } + +private: + inline void HandleScrollStateChanged(const ArkUI_NodeComponentEvent *comp) + { + if (!onState_) { + return; + } + auto state = static_cast(comp->data[0].i32); + onState_(state); + } + + inline void HandleWillScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onWill_) { + return; + } + float dx = comp->data[0].f32; + float dy = comp->data[1].f32; + auto state = static_cast(comp->data[2].i32); + auto src = static_cast(comp->data[3].i32); + onWill_(dx, dy, state, src); + } + + inline void HandleDidScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onDid_) { + return; + } + float dx = comp->data[0].f32; + float dy = comp->data[1].f32; + auto state = static_cast(comp->data[2].i32); + onDid_(dx, dy, state); + } + + inline void HandleListWillScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onWill_) { + return; + } + float dy = comp->data[0].f32; + auto state = static_cast(comp->data[1].i32); + auto src = static_cast(comp->data[2].i32); + onWill_(0.0f, dy, state, src); + } + + inline void HandleOnWillScroll(const ArkUI_NodeComponentEvent *comp) + { + if (!onWill_) { + return; + } + float d = comp->data[0].f32; // 单轴按约定复用 dy + auto state = static_cast(comp->data[1].i32); + auto src = static_cast(comp->data[2].i32); + onWill_(0.0f, d, state, src); + } + + static void StaticEvent(ArkUI_NodeEvent *ev) + { + if (!ev) { + return; + } + auto *self = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(ev)); + if (!self) { + return; + } + + const auto *comp = OH_ArkUI_NodeEvent_GetNodeComponentEvent(ev); + if (!comp) { + return; + } + + switch (OH_ArkUI_NodeEvent_GetEventType(ev)) { + case NODE_SWIPER_EVENT_ON_SCROLL_STATE_CHANGED: + self->HandleScrollStateChanged(comp); + break; + case NODE_SCROLL_EVENT_ON_WILL_SCROLL: + self->HandleWillScroll(comp); + break; + case NODE_SCROLL_EVENT_ON_DID_SCROLL: + self->HandleDidScroll(comp); + break; + case NODE_LIST_ON_WILL_SCROLL: + self->HandleListWillScroll(comp); + break; + case NODE_ON_WILL_SCROLL: + self->HandleOnWillScroll(comp); + break; + default: + break; + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_{nullptr}; + ArkUI_NodeHandle scroll_{nullptr}; + ScrollEventGuard scrollGuard_{}; + + OnScrollState onState_{}; + OnWillScroll onWill_{}; + OnDidScroll onDid_{}; +}; +#endif // SCROLL_MAKER_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableEvent.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableEvent.h new file mode 100644 index 000000000..611599124 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableEvent.h @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCROLL_EVENT_H +#define SCROLL_EVENT_H + +#include +#include + +/** + * 滚动事件掩码 + */ +enum : uint32_t { + SCROLL_EVT_FRAME_BEGIN = 1u << 0, + SCROLL_EVT_START = 1u << 1, + SCROLL_EVT_STOP = 1u << 2, + SCROLL_EVT_REACH_START = 1u << 3, + SCROLL_EVT_REACH_END = 1u << 4, + SCROLL_EVT_WILL_STOP_DRAG = 1u << 5, + SCROLL_EVT_ALL = SCROLL_EVT_FRAME_BEGIN | SCROLL_EVT_START | SCROLL_EVT_STOP | SCROLL_EVT_REACH_START | + SCROLL_EVT_REACH_END | SCROLL_EVT_WILL_STOP_DRAG +}; + +/** + * 事件 map + */ +struct ScrollEvtMap { + uint32_t bit; + ArkUI_NodeEventType evt; +}; + +inline constexpr ScrollEvtMap K_SCROLL_EVT_MAP[] = { + {SCROLL_EVT_FRAME_BEGIN, NODE_SCROLL_EVENT_ON_SCROLL_FRAME_BEGIN}, + {SCROLL_EVT_START, NODE_SCROLL_EVENT_ON_SCROLL_START}, + {SCROLL_EVT_STOP, NODE_SCROLL_EVENT_ON_SCROLL_STOP}, + {SCROLL_EVT_REACH_START, NODE_SCROLL_EVENT_ON_REACH_START}, + {SCROLL_EVT_REACH_END, NODE_SCROLL_EVENT_ON_REACH_END}, + {SCROLL_EVT_WILL_STOP_DRAG, NODE_SCROLL_EVENT_ON_WILL_STOP_DRAGGING}, +}; + +template inline void ForEachScrollEvt(uint32_t mask, F &&fn) +{ + for (const auto &m : K_SCROLL_EVT_MAP) { + if (mask & m.bit) { + fn(m.evt); + } + } +} + +/** + * 批量注册事件 + */ +inline void RegisterScrollEvents(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, void *userData, + uint32_t mask = SCROLL_EVT_ALL) +{ + if (!api || !node) { + return; + } + ForEachScrollEvt(mask, [&](ArkUI_NodeEventType evt) { api->registerNodeEvent(node, evt, 0, userData); }); +} + +/** + * 批量取消注册事件 + */ +inline void UnregisterScrollEvents(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, uint32_t mask = SCROLL_EVT_ALL) +{ + if (!api || !node) { + return; + } + ForEachScrollEvt(mask, [&](ArkUI_NodeEventType evt) { api->unregisterNodeEvent(node, evt); }); +} + +/** + * 滚动事件自动注册/注销 + */ +class ScrollEventGuard { +public: + ScrollEventGuard() = default; + + ScrollEventGuard(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, void *userData, uint32_t mask = SCROLL_EVT_ALL) + { + Bind(api, node, userData, mask); + } + + void Bind(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, void *userData, uint32_t mask = SCROLL_EVT_ALL) + { + api_ = api; + node_ = node; + user_ = userData; + mask_ = mask; + RegisterScrollEvents(api_, node_, user_, mask_); + armed_ = true; + } + + void Release() + { + if (armed_) { + UnregisterScrollEvents(api_, node_, mask_); + armed_ = false; + } + } + + ~ScrollEventGuard() + { + if (armed_) { + UnregisterScrollEvents(api_, node_, mask_); + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_ = nullptr; + ArkUI_NodeHandle node_ = nullptr; + void *user_ = nullptr; + uint32_t mask_ = SCROLL_EVT_ALL; + bool armed_ = false; +}; + +#endif // SCROLL_EVENT_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableNode.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableNode.h new file mode 100644 index 000000000..4249a2b5a --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableNode.h @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCROLL_NODE_H +#define SCROLL_NODE_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ScrollableUtils.h" + +class NodeApiInstance { +public: + static NodeApiInstance *GetInstance() + { + static NodeApiInstance instance; + return &instance; + } + ArkUI_NativeNodeAPI_1 *GetNativeNodeAPI() const { return nodeApi_; } + +private: + NodeApiInstance() { OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, nodeApi_); } + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + + NodeApiInstance(const NodeApiInstance &) = delete; + NodeApiInstance &operator=(const NodeApiInstance &) = delete; +}; + +class BaseNode : public std::enable_shared_from_this { +public: + explicit BaseNode(ArkUI_NodeHandle handle) + : nodeApi_(NodeApiInstance::GetInstance()->GetNativeNodeAPI()), nodeHandle_(handle) + { + if (!Utils::IsNotNull(nodeApi_) || !Utils::IsNotNull(nodeHandle_)) { + return; + } + RegisterClickEvent(); + } + + virtual ~BaseNode() + { + UnregisterClickEvent(); + ClearChildren(); + nodeHandle_ = nullptr; + } + + BaseNode(const BaseNode &) = delete; + BaseNode &operator=(const BaseNode &) = delete; + + ArkUI_NodeHandle GetHandle() const { return nodeHandle_; } + + void AddChild(const std::shared_ptr &child) + { + if (!Utils::IsNotNull(child)) { + return; + } + children_.push_back(child); + nodeApi_->addChild(nodeHandle_, child->GetHandle()); + } + + void RemoveChild(const std::shared_ptr &child) + { + if (!Utils::IsNotNull(child)) { + return; + } + auto it = std::find(children_.begin(), children_.end(), child); + if (it != children_.end()) { + nodeApi_->removeChild(nodeHandle_, child->GetHandle()); + children_.erase(it); + } + } + + // ---------------- 通用属性 ---------------- + + void SetWidth(float width) { Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_WIDTH, width); } + void SetHeight(float height) { Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_HEIGHT, height); } + void SetWidthPercent(float percent) + { + Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_WIDTH_PERCENT, percent); + } + void SetHeightPercent(float percent) + { + Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_HEIGHT_PERCENT, percent); + } + void SetSize(float w, float h) { Utils::SetSize(nodeApi_, nodeHandle_, w, h); } + void SetSizePercent(float wp, float hp) { Utils::SetSizePercent(nodeApi_, nodeHandle_, wp, hp); } + void SetFullSize() { Utils::SetFullSize(nodeApi_, nodeHandle_); } + + void SetBackgroundColor(uint32_t color) { Utils::SetBackgroundColor(nodeApi_, nodeHandle_, color); } + + virtual void SetTransparentBackground() final { Utils::SetTransparentBackground(nodeApi_, nodeHandle_); } + + void SetOpacity(float opacity) + { + if (!Utils::ValidateApiAndNode(nodeApi_, nodeHandle_, "BaseNode::SetOpacity")) { + return; + } + Utils::SetAttributeFloat32(nodeApi_, nodeHandle_, NODE_OPACITY, opacity); + } + + // ---------------- 事件 ---------------- + + void RegisterOnClick(const std::function &callback) { onClickCallback_ = callback; } + +protected: + virtual void OnNodeEvent(ArkUI_NodeEvent *event) + { + if (OH_ArkUI_NodeEvent_GetEventType(event) == NODE_ON_CLICK && onClickCallback_) { + onClickCallback_(event); + } + } + + static void StaticEventReceiver(ArkUI_NodeEvent *event) + { + auto *self = reinterpret_cast(OH_ArkUI_NodeEvent_GetUserData(event)); + if (Utils::IsNotNull(self)) { + self->OnNodeEvent(event); + } + } + +private: + void RegisterClickEvent() + { + if (Utils::IsNotNull(nodeApi_) && Utils::IsNotNull(nodeHandle_)) { + nodeApi_->registerNodeEvent(nodeHandle_, NODE_ON_CLICK, 0, this); + hasClickEventRegistered_ = true; + } + } + + void UnregisterClickEvent() + { + if (Utils::IsNotNull(nodeApi_) && Utils::IsNotNull(nodeHandle_) && hasClickEventRegistered_) { + nodeApi_->unregisterNodeEvent(nodeHandle_, NODE_ON_CLICK); + hasClickEventRegistered_ = false; + } + } + + void ClearChildren() { children_.clear(); } + +protected: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + ArkUI_NodeHandle nodeHandle_ = nullptr; + std::list> children_; + std::function onClickCallback_; + bool hasClickEventRegistered_ = false; +}; + +// 保活容器 +template inline std::vector> &GetKeepAliveContainer() +{ + static std::vector> keepAliveContainer; + return keepAliveContainer; +} + +#endif // COMMON_SCROLL_NODE_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.cpp new file mode 100644 index 000000000..753b99070 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.cpp @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ScrollableUtils.h" + +#include +#include +#include + +namespace Utils { + +// ------------------------------ +// 常量 +// ------------------------------ +namespace { +constexpr int K_EDGES = 4; +constexpr int K_EDGE_TOP = 0; +constexpr int K_EDGE_RIGHT = 1; +constexpr int K_EDGE_BOTTOM = 2; +constexpr int K_EDGE_LEFT = 3; + +constexpr int K_ENABLED = 1; + +constexpr float K_PERCENT_FULL = 1.0f; + +constexpr uint32_t K_COLOR_TRANSPARENT = 0x00000000U; +constexpr float K_DEFAULT_SCROLL_BAR_WIDTH = 4.0f; +constexpr uint32_t K_DEFAULT_SCROLL_BAR_COLOR = 0x66000000U; +constexpr float K_DEFAULT_SCROLL_FRICTION = 0.015f; +constexpr int32_t K_DEFAULT_FADING_EDGE = 12; + +// 属性数组的最大容量 +constexpr int K_MAX_ATTR_VALUES = 8; + +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; + +struct NodeAttrTarget { + ArkUI_NativeNodeAPI_1 *api; + ArkUI_NodeHandle node; + ArkUI_NodeAttributeType attr; + const char *debugName; +}; +} // namespace + +// ------------------------------ +// 判空 / 校验 +// ------------------------------ +bool IsValidRange(int32_t start, int32_t end, int32_t count) +{ + if (!IsValidIndex(start, count)) { + return false; + } + if (end < start) { + return false; + } + if (end > count) { + return false; + } + return true; +} + +bool ValidateApiAndNode(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, const char *functionName) +{ + if (!IsNotNull(api)) { + if (functionName != nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "%{public}s: api is null", functionName); + } + return false; + } + if (!IsNotNull(node)) { + if (functionName != nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "%{public}s: node is null", functionName); + } + return false; + } + return true; +} + +// ------------------------------ +// 通用属性设置 +// ------------------------------ +void SetAttributeFloat32(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, float value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeFloat32")) { + return; + } + ArkUI_NumberValue numberValue{}; + numberValue.f32 = value; + + ArkUI_AttributeItem item{&numberValue, 1}; + api->setAttribute(node, attr, &item); +} + +void SetAttributeUInt32(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, uint32_t value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeUInt32")) { + return; + } + ArkUI_NumberValue numberValue{}; + numberValue.u32 = value; + + ArkUI_AttributeItem item{&numberValue, 1}; + api->setAttribute(node, attr, &item); +} + +void SetAttributeInt32(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, int32_t value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeInt32")) { + return; + } + ArkUI_NumberValue numberValue{}; + numberValue.i32 = value; + + ArkUI_AttributeItem item{&numberValue, 1}; + api->setAttribute(node, attr, &item); +} + +void SetAttributeString(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, ArkUI_NodeAttributeType attr, + const char *value) +{ + if (!ValidateApiAndNode(api, node, "SetAttributeString")) { + return; + } + if (!IsNotNull(value)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "SetAttributeString: value is null"); + return; + } + + ArkUI_AttributeItem item{nullptr, 0, value}; + api->setAttribute(node, attr, &item); +} + +// 设置 float 数组属性 +static void ApplyFloatArrayAttribute(const NodeAttrTarget &target, const float *values, int count) +{ + if (!ValidateApiAndNode(target.api, target.node, target.debugName)) { + return; + } + if (values == nullptr || count <= 0) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "%{public}s: invalid values", + target.debugName ? target.debugName : "ApplyFloatArrayAttribute"); + return; + } + + std::array buf{}; + int capped = count; + if (capped > static_cast(buf.size())) { + OH_LOG_Print(LOG_APP, LOG_WARN, K_LOG_DOMAIN, LOG_TAG, "%{public}s: count too large=%{public}d", + target.debugName ? target.debugName : "ApplyFloatArrayAttribute", count); + capped = static_cast(buf.size()); + } + for (int i = 0; i < capped; ++i) { + buf[static_cast(i)].f32 = values[i]; + } + + ArkUI_AttributeItem item{buf.data(), static_cast(capped)}; + target.api->setAttribute(target.node, target.attr, &item); +} + +// ------------------------------ +// 尺寸 / 背景 +// ------------------------------ +void SetSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, float width, float height) +{ + SetAttributeFloat32(api, node, NODE_WIDTH, width); + SetAttributeFloat32(api, node, NODE_HEIGHT, height); +} + +void SetSizePercent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, float widthPercent, float heightPercent) +{ + SetAttributeFloat32(api, node, NODE_WIDTH_PERCENT, widthPercent); + SetAttributeFloat32(api, node, NODE_HEIGHT_PERCENT, heightPercent); +} + +void SetFullSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) +{ + SetSizePercent(api, node, K_PERCENT_FULL, K_PERCENT_FULL); +} + +void SetBackgroundColor(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, uint32_t color) +{ + SetAttributeUInt32(api, node, NODE_BACKGROUND_COLOR, color); +} + +void SetTransparentBackground(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) +{ + SetBackgroundColor(api, node, K_COLOR_TRANSPARENT); +} + +void SetPadding(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, const Padding &padding) +{ + const float vals[K_EDGES] = {padding.top, padding.right, padding.bottom, padding.left}; + ApplyFloatArrayAttribute(NodeAttrTarget{api, node, NODE_PADDING, "SetPadding"}, vals, K_EDGES); +} + +// ------------------------------ +// 文本 +// ------------------------------ +ArkUI_NodeHandle CreateTextNode(ArkUI_NativeNodeAPI_1 *api, const char *text) +{ + if (!IsNotNull(api) || !IsNotNull(text)) { + return nullptr; + } + + ArkUI_NodeHandle textNode = api->createNode(ARKUI_NODE_TEXT); + if (!IsNotNull(textNode)) { + return nullptr; + } + + SetAttributeString(api, textNode, NODE_TEXT_CONTENT, text); + return textNode; +} + +void SetTextStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle textNode, float fontSize, uint32_t fontColor, + int32_t textAlign) +{ + if (!ValidateApiAndNode(api, textNode, "SetTextStyle")) { + return; + } + SetAttributeFloat32(api, textNode, NODE_FONT_SIZE, fontSize); + SetAttributeUInt32(api, textNode, NODE_FONT_COLOR, fontColor); + SetAttributeInt32(api, textNode, NODE_TEXT_ALIGN, textAlign); +} + +void SetTextContent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle textNode, const char *text) +{ + SetAttributeString(api, textNode, NODE_TEXT_CONTENT, text); +} + +// ------------------------------ +// 滚动 +// ------------------------------ +void SetScrollBarStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, bool visible, float width, uint32_t color) +{ + if (!ValidateApiAndNode(api, node, "SetScrollBarStyle")) { + return; + } + + int32_t displayMode = visible ? ARKUI_SCROLL_BAR_DISPLAY_MODE_AUTO : ARKUI_SCROLL_BAR_DISPLAY_MODE_OFF; + SetAttributeInt32(api, node, NODE_SCROLL_BAR_DISPLAY_MODE, displayMode); + if (visible) { + SetAttributeFloat32(api, node, NODE_SCROLL_BAR_WIDTH, width); + SetAttributeUInt32(api, node, NODE_SCROLL_BAR_COLOR, color); + } +} + +void SetDefaultScrollStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) +{ + if (!ValidateApiAndNode(api, node, "SetDefaultScrollStyle")) { + return; + } + SetScrollBarStyle(api, node, true, K_DEFAULT_SCROLL_BAR_WIDTH, K_DEFAULT_SCROLL_BAR_COLOR); + SetAttributeInt32(api, node, NODE_SCROLL_EDGE_EFFECT, ARKUI_EDGE_EFFECT_SPRING); + SetAttributeInt32(api, node, NODE_SCROLL_ENABLE_SCROLL_INTERACTION, K_ENABLED); + SetAttributeFloat32(api, node, NODE_SCROLL_FRICTION, K_DEFAULT_SCROLL_FRICTION); + SetAttributeInt32(api, node, NODE_SCROLL_NESTED_SCROLL, ARKUI_SCROLL_NESTED_MODE_SELF_FIRST); + SetAttributeInt32(api, node, NODE_SCROLL_FADING_EDGE, K_DEFAULT_FADING_EDGE); +} + +// ------------------------------ +// 异步任务 +// ------------------------------ +void PostDelayedTask(int32_t delayMs, std::function task) +{ + if (!task) { + return; + } + + int32_t safeDelayMs = delayMs; + if (safeDelayMs < 0) { + safeDelayMs = 0; + } + + std::thread worker([safeDelayMs, fn = std::move(task)]() mutable { + if (safeDelayMs > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(safeDelayMs)); + } + fn(); + }); + worker.detach(); +} + +// ------------------------------ +// N-API 工具 +// ------------------------------ +std::string GetStringFromNapi(napi_env env, napi_value value) +{ + if (!IsNotNull(env) || !IsNotNull(value)) { + return std::string(); + } + + size_t length = 0; + napi_status s0 = napi_get_value_string_utf8(env, value, nullptr, 0, &length); + if (s0 != napi_ok) { + return std::string(); + } + + std::string result(length, '\0'); + size_t written = 0; + napi_status s1 = napi_get_value_string_utf8(env, value, result.data(), length + 1, &written); + if (s1 != napi_ok) { + return std::string(); + } + + return result; +} + +bool IsNapiArray(napi_env env, napi_value value) +{ + if (!IsNotNull(env) || !IsNotNull(value)) { + return false; + } + + bool isArray = false; + napi_status status = napi_is_array(env, value, &isArray); + if (status != napi_ok) { + return false; + } + + return isArray; +} + +ArkUI_NodeContentHandle GetNodeContentFromNapi(napi_env env, napi_callback_info info) +{ + if (!IsNotNull(env) || !IsNotNull(info)) { + return nullptr; + } + + size_t argc = 1; + napi_value arg0 = nullptr; + napi_status st = napi_get_cb_info(env, info, &argc, &arg0, nullptr, nullptr); + if (st != napi_ok || argc < 1) { + return nullptr; + } + + ArkUI_NodeContentHandle content = nullptr; + OH_ArkUI_GetNodeContentFromNapiValue(env, arg0, &content); + return content; +} + +void AddNodeToContent(ArkUI_NodeContentHandle content, ArkUI_NodeHandle node) +{ + if (!IsNotNull(content) || !IsNotNull(node)) { + return; + } + OH_ArkUI_NodeContent_AddNode(content, node); +} + +// ------------------------------ +// NodeEventRegistrar +// ------------------------------ +NodeEventRegistrar::NodeEventRegistrar(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node) + : nodeApi_(api), nodeHandle_(node) +{ + if (!ValidateApiAndNode(api, node, "NodeEventRegistrar::Constructor")) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "NodeEventRegistrar: invalid api or node"); + } +} + +NodeEventRegistrar::~NodeEventRegistrar() +{ + if (!IsNotNull(nodeApi_) || !IsNotNull(nodeHandle_)) { + return; + } + for (ArkUI_NodeEventType eventType : registeredEventTypes_) { + nodeApi_->unregisterNodeEvent(nodeHandle_, eventType); + } + registeredEventTypes_.clear(); +} + +void NodeEventRegistrar::RegisterEvent(ArkUI_NodeEventType eventType, void *userData) +{ + if (!ValidateApiAndNode(nodeApi_, nodeHandle_, "NodeEventRegistrar::RegisterEvent")) { + return; + } + nodeApi_->registerNodeEvent(nodeHandle_, eventType, 0, userData); + registeredEventTypes_.push_back(eventType); +} + +void NodeEventRegistrar::RegisterMultipleEvents(std::initializer_list eventTypes, void *userData) +{ + for (ArkUI_NodeEventType eventType : eventTypes) { + RegisterEvent(eventType, userData); + } +} + +// ------------------------------ +// AdapterEventRegistrar +// ------------------------------ +AdapterEventRegistrar::AdapterEventRegistrar(ArkUI_NodeAdapterHandle adapter, void *userData, + void (*callback)(ArkUI_NodeAdapterEvent *)) + : adapterHandle_(adapter) +{ + if (!IsNotNull(adapterHandle_)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "AdapterEventRegistrar: adapter is null"); + return; + } + if (!IsNotNull(callback)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "AdapterEventRegistrar: callback is null"); + return; + } + + int32_t result = OH_ArkUI_NodeAdapter_RegisterEventReceiver(adapterHandle_, userData, callback); + if (result != 0) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, + "AdapterEventRegistrar: failed to register event receiver, result=%{public}d", result); + } +} + +AdapterEventRegistrar::~AdapterEventRegistrar() +{ + if (IsNotNull(adapterHandle_)) { + OH_ArkUI_NodeAdapter_UnregisterEventReceiver(adapterHandle_); + } +} + +} // namespace Utils diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.h new file mode 100644 index 000000000..42815bf4c --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/ScrollableUtils.h @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SCROLL_UTILS_H +#define SCROLL_UTILS_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#ifndef LOG_TAG +#define LOG_TAG "ScrollUtils" +#endif + +namespace Utils { + +// ============================== +// 判空 / 校验 +// ============================== +template +inline bool IsNotNull(const T *ptr) +{ + return ptr != nullptr; +} + +template +inline bool IsNotNull(const std::shared_ptr &ptr) +{ + return ptr != nullptr; +} + +template +inline bool IsNotNull(T *ptr) +{ + return ptr != nullptr; +} + +inline bool IsValidIndex(int32_t index, int32_t count) +{ + return (index >= 0) && (index < count); +} + +inline bool IsValidRange(int32_t start, int32_t end, int32_t count); + +bool ValidateApiAndNode(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + const char *functionName = nullptr); + +template +constexpr int ArrSize(const T (&arr)[N]) noexcept +{ + return static_cast(N); +} + +// ============================== +// 尺寸 / 背景 +// ============================== + +struct Padding { + float top {0.f}; + float right {0.f}; + float bottom {0.f}; + float left {0.f}; + + static Padding All(float v) { return Padding{v, v, v, v}; } + static Padding Symmetric(float vertical, float horizontal) + { + return Padding{vertical, horizontal, vertical, horizontal}; + } + static Padding Only(float t, float r, float b, float l) + { + return Padding{t, r, b, l}; + } +}; + +void SetSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, float width, float height); + +void SetSizePercent(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + float widthPercent, + float heightPercent); + +void SetFullSize(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + +void SetBackgroundColor(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, uint32_t color); + +void SetTransparentBackground(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + +void SetPadding(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node, const Padding &padding); + +// ============================== +// 通用属性设置 +// ============================== +void SetAttributeFloat32(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + float value); + +void SetAttributeUInt32(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + uint32_t value); + +void SetAttributeInt32(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + int32_t value); + +void SetAttributeString(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + ArkUI_NodeAttributeType attr, + const char *value); + +// ============================== +// 文本节点工具 +// ============================== +ArkUI_NodeHandle CreateTextNode(ArkUI_NativeNodeAPI_1 *api, const char *text); + +void SetTextStyle(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle textNode, + float fontSize, + uint32_t fontColor, + int32_t textAlign); + +void SetTextContent(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle textNode, const char *text); + +// ============================== +// 滚动相关 +// ============================== +void SetScrollBarStyle(ArkUI_NativeNodeAPI_1 *api, + ArkUI_NodeHandle node, + bool visible, + float width, + uint32_t color); + +void SetDefaultScrollStyle(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + +// ============================== +// 异步任务 +// ============================== +void PostDelayedTask(int32_t delayMs, std::function task); + +// ============================== +// N-API 工具 +// ============================== +std::string GetStringFromNapi(napi_env env, napi_value value); + +bool IsNapiArray(napi_env env, napi_value value); + +ArkUI_NodeContentHandle GetNodeContentFromNapi(napi_env env, napi_callback_info info); + +void AddNodeToContent(ArkUI_NodeContentHandle content, ArkUI_NodeHandle node); + +// ============================== +// 节点事件注册器 +// ============================== +class NodeEventRegistrar { +public: + NodeEventRegistrar(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle node); + ~NodeEventRegistrar(); + + void RegisterEvent(ArkUI_NodeEventType eventType, void *userData); + void RegisterMultipleEvents(std::initializer_list eventTypes, void *userData); + +private: + ArkUI_NativeNodeAPI_1 *nodeApi_ = nullptr; + ArkUI_NodeHandle nodeHandle_ = nullptr; + std::vector registeredEventTypes_; +}; + +// ============================== +// Adapter 事件接收器 +// ============================== +class AdapterEventRegistrar { +public: + AdapterEventRegistrar(ArkUI_NodeAdapterHandle adapter, + void *userData, + void (*callback)(ArkUI_NodeAdapterEvent *)); + ~AdapterEventRegistrar(); + +private: + ArkUI_NodeAdapterHandle adapterHandle_ = nullptr; +}; + +} // namespace Utils + +#endif // SCROLL_UTILS_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.cpp new file mode 100644 index 000000000..439cd55ba --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.cpp @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#include "ArkUINode.h" +#include "ScrollableUtils.h" +#include "ArkUINodeAdapter.h" +#include "WaterFlowMaker.h" +#include "WaterFlowSection.h" + +namespace { +constexpr char K_ITEM_TITLE_PREFIX[] = "FlowItem "; +constexpr float K_FONT_SIZE = 16.0f; + +constexpr float K_MAIN_A = 120.0f; +constexpr float K_MAIN_B = 160.0f; +constexpr float K_MAIN_C = 100.0f; + +constexpr float K_WATER_FLOW_W = 400.0f; +constexpr float K_WATER_FLOW_H = 600.0f; + +constexpr int K_INIT_RESERVE = 200; +constexpr int K_INIT_SEED = 100; + +constexpr int32_t K_CROSS_COUNT = 2; +constexpr float K_COLUMN_GAP = 10.0f; +constexpr float K_ROW_GAP = 10.0f; +constexpr int32_t K_MARGIN_TOP = 12, K_MARGIN_RIGHT = 12, K_MARGIN_BOTTOM = 12, K_MARGIN_LEFT = 12; + +constexpr float K_WIDTH_PERCENT_FULL = 1.0f; +constexpr bool K_SYNC_LOAD = true; +constexpr int32_t K_CACHED_ITEM_COUNT = 24; +constexpr float K_ITEM_MAIN_MIN = 80.0f; +constexpr float K_ITEM_MAIN_MAX = 220.0f; + +constexpr int32_t K_LAYOUT_DIRECTION_RTL = 1; +constexpr int32_t K_LAYOUT_MODE_WATER_FLOW = 1; + +constexpr char K_COLUMN_TEMPLATE[] = "1fr 1fr"; +constexpr char K_ROW_TEMPLATE[] = "auto"; + +constexpr int32_t K_SCROLL_TO_INDEX = 5; +constexpr int32_t K_SCROLL_ALIGN_CENTER = static_cast(ARKUI_SCROLL_ALIGNMENT_CENTER); + +constexpr int K_AUTO_THRESHOLD = 20; +constexpr int K_AUTO_BATCH = 100; +constexpr int K_AUTO_MAX_ITEMS = 100000; + +constexpr float K_MAIN_SEQ[] = {K_MAIN_A, K_MAIN_B, K_MAIN_C}; +constexpr size_t K_MAIN_SEQ_COUNT = sizeof(K_MAIN_SEQ) / sizeof(K_MAIN_SEQ[0]); +constexpr uint32_t K_PALETTE[] = {0xFF6A5ACD, 0xFF00FFFF, 0xFF00FF7F, 0xFFDA70D6, 0xFFFFC0CB}; +constexpr size_t K_PALETTE_COUNT = sizeof(K_PALETTE) / sizeof(K_PALETTE[0]); +} // namespace + +static std::shared_ptr gNode; +static std::shared_ptr gAdapter; +static std::shared_ptr gSection; +static std::vector gItems; + +struct AutoAppendConfig { + bool enabled = true; + int threshold = K_AUTO_THRESHOLD; + int batch = K_AUTO_BATCH; + int maxItems = K_AUTO_MAX_ITEMS; + bool appending = false; + int lastSizeTriggered = -1; +}; +static AutoAppendConfig g_auto; + +/** + * 根据索引获取主轴尺寸 + * @param idx 索引值 + * @return 主轴尺寸 + */ +static inline float MainSizeByIndex(int32_t idx) +{ + if (K_MAIN_SEQ_COUNT == 0U) { + return K_MAIN_C; + } + + const size_t i = static_cast(idx) % K_MAIN_SEQ_COUNT; + return K_MAIN_SEQ[i]; +} + +/** + * 根据索引获取颜色 + * @param index 索引值 + * @return 颜色值 + */ +static inline uint32_t ColorByIndex(int index) +{ + if (K_PALETTE_COUNT == 0U) { + return 0xFFFFFFFFU; + } + + const size_t i = static_cast(index) % K_PALETTE_COUNT; + return K_PALETTE[i]; +} + +static inline void BindText(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle flowItem, int32_t index) +{ + ArkUI_NodeHandle text = api->getFirstChild(flowItem); + if (!text) { + return; + } + + Utils::SetTextContent(api, text, gItems[static_cast(index)].c_str()); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, K_FONT_SIZE); +} + +/** + * 方法描述 + * @param total 参数描述 + * @return 返回值描述 + */ +static inline bool ReachedBoundary(int total) +{ + return (total >= g_auto.maxItems) || (total == g_auto.lastSizeTriggered); +} + +static inline bool ShouldAppend(int32_t index, int total) +{ + return g_auto.enabled && !g_auto.appending && !ReachedBoundary(total) && index >= (total - g_auto.threshold); +} + +static void AppendBatchInternal(int addCount) +{ + if (!gAdapter || !gSection || !gNode || addCount <= 0) { + return; + } + + const int32_t start = static_cast(gItems.size()); + gItems.reserve(gItems.size() + static_cast(addCount)); + + for (int i = 0; i < addCount; ++i) { + gItems.emplace_back(std::string(K_ITEM_TITLE_PREFIX) + std::to_string(start + i)); + } + + gAdapter->InsertRange(start, addCount); + + const int32_t newCount = static_cast(gItems.size()); + OH_ArkUI_WaterFlowSectionOption_SetItemCount(gSection->GetSectionOptions(), 0, newCount); + + gNode->SetSection(gSection); + g_auto.lastSizeTriggered = static_cast(gItems.size()); +} + +static void MaybeAppendOnTail(int32_t index) +{ + const int total = static_cast(gItems.size()); + if (!ShouldAppend(index, total)) { + return; + } + + g_auto.appending = true; + const int remain = g_auto.maxItems - total; + const int toAdd = remain > 0 ? std::min(g_auto.batch, remain) : 0; + AppendBatchInternal(toAdd); + g_auto.appending = false; +} + +static int32_t AdapterGetTotalCount() { return static_cast(gItems.size()); } + +static uint64_t AdapterGetStableId(int32_t i) +{ + const std::string &key = gItems[static_cast(i)]; + return static_cast(std::hash{}(key)); +} + +static ArkUI_NodeHandle AdapterOnCreate(ArkUI_NativeNodeAPI_1 *api, int32_t /*index*/) +{ + if (!api) { + return nullptr; + } + + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + ArkUI_NodeHandle item = api->createNode(ARKUI_NODE_FLOW_ITEM); + api->addChild(item, text); + return item; +} + +static void AdapterOnBind(ArkUI_NativeNodeAPI_1 *api, ArkUI_NodeHandle item, int32_t index) +{ + Utils::SetAttributeFloat32(api, item, NODE_HEIGHT, MainSizeByIndex(index)); + Utils::SetAttributeFloat32(api, item, NODE_WIDTH_PERCENT, K_WIDTH_PERCENT_FULL); + Utils::SetAttributeUInt32(api, item, NODE_BACKGROUND_COLOR, ColorByIndex(index)); + BindText(api, item, index); + MaybeAppendOnTail(index); +} + +static ArkUINodeAdapter::Callbacks MakeCallbacks() +{ + ArkUINodeAdapter::Callbacks cb{}; + cb.getTotalCount = &AdapterGetTotalCount; + cb.getStableId = &AdapterGetStableId; + cb.onCreate = &AdapterOnCreate; + cb.onBind = &AdapterOnBind; + cb.onRecycle = nullptr; + return cb; +} + +static ArkUI_NodeHandle CreateFooter() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (!api) { + return nullptr; + } + + ArkUI_NodeHandle text = api->createNode(ARKUI_NODE_TEXT); + Utils::SetTextContent(api, text, "到底啦…"); + Utils::SetAttributeFloat32(api, text, NODE_FONT_SIZE, 14.0f); + + ArkUI_NodeHandle footer = api->createNode(ARKUI_NODE_FLOW_ITEM); + Utils::SetAttributeFloat32(api, footer, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, footer, NODE_HEIGHT, 48.0f); + Utils::SetAttributeUInt32(api, footer, NODE_BACKGROUND_COLOR, 0x11000000U); + api->addChild(footer, text); + return footer; +} + +static void SetupSection() +{ + ArkUI_Margin margin{K_MARGIN_TOP, K_MARGIN_RIGHT, K_MARGIN_BOTTOM, K_MARGIN_LEFT}; + + SingleSectionParams params{}; + params.itemCount = static_cast(gItems.size()); + params.crossCount = K_CROSS_COUNT; + params.colGap = K_COLUMN_GAP; + params.rowGap = K_ROW_GAP; + params.margin = margin; + params.getMainSizeByIndex = &MainSizeByIndex; + params.userData = nullptr; + params.getMainSizeByIndexWithUserData = nullptr; + + gNode->SetSingleSection(params); + gSection = gNode->GetWaterFlowSection(); +} + +static void SetupNodeAndAdapter() +{ + gNode = std::make_shared(); + gNode->SetHeight(K_WATER_FLOW_H); + gNode->SetWidth(K_WATER_FLOW_W); + gNode->SetScrollCommon(); + gNode->SetLayoutDirection(K_LAYOUT_DIRECTION_RTL); + gNode->SetColumnTemplate(K_COLUMN_TEMPLATE); + gNode->SetRowTemplate(K_ROW_TEMPLATE); + gNode->SetGaps(K_COLUMN_GAP, K_ROW_GAP); + gNode->SetCachedCount(K_CACHED_ITEM_COUNT); + gNode->SetItemConstraintSize(K_ITEM_MAIN_MIN, K_ITEM_MAIN_MAX); + gNode->SetLayoutMode(ARKUI_WATER_FLOW_LAYOUT_MODE_SLIDING_WINDOW); + gNode->SetSyncLoad(K_SYNC_LOAD); + + SetupSection(); + + if (ArkUI_NodeHandle footer = CreateFooter()) { + gNode->SetFooter(footer); + } + + gAdapter = std::make_shared(); + gAdapter->SetCallbacks(MakeCallbacks()); + gNode->SetLazyAdapter(gAdapter); + gAdapter->ReloadAllItems(); +} + +static void InitData() +{ + gItems.clear(); + gItems.reserve(static_cast(K_INIT_RESERVE)); + for (int i = 0; i < K_INIT_SEED; ++i) { + gItems.emplace_back(std::string(K_ITEM_TITLE_PREFIX) + std::to_string(i)); + } +} + +static ArkUI_NodeHandle BuildWaterFlow() +{ + InitData(); + SetupNodeAndAdapter(); + return gNode->GetWaterFlow(); +} + +ArkUI_NodeHandle WaterFlowMaker::CreateNativeNode() +{ + ArkUI_NativeNodeAPI_1 *api = nullptr; + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api); + if (!api) { + return nullptr; + } + + ArkUI_NodeHandle page = api->createNode(ARKUI_NODE_COLUMN); + if (!page) { + return nullptr; + } + Utils::SetAttributeFloat32(api, page, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, page, NODE_HEIGHT_PERCENT, 1.0f); + + ArkUI_NodeHandle waterflow = BuildWaterFlow(); + if (waterflow) { + Utils::SetAttributeFloat32(api, waterflow, NODE_WIDTH_PERCENT, 1.0f); + Utils::SetAttributeFloat32(api, waterflow, NODE_LAYOUT_WEIGHT, 1.0f); + + api->addChild(page, waterflow); + } + return page; +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.h new file mode 100644 index 000000000..1edee671e --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowMaker.h @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef WATERFLOW_MAKER_H +#define WATERFLOW_MAKER_H + +#include +#include +#include +#include +#include + +#include "ArkUINodeAdapter.h" +#include "WaterFlowSection.h" +#include "ScrollableNode.h" +#include "ScrollableUtils.h" +#include "ScrollableEvent.h" + +#ifndef LOG_TAG +#define LOG_TAG "WaterFlowMaker" +#endif + +// ===== 业务常量 ===== +constexpr unsigned int K_LOG_DOMAIN = 0xFF00; +constexpr float K_DEFAULT_SCROLL_BAR_WIDTH = 4.0f; +constexpr uint32_t K_DEFAULT_SCROLL_BAR_COLOR = 0x66000000U; +constexpr float K_DEFAULT_SCROLL_FRICTION = 0.12f; +constexpr float K_DEFAULT_FLING_SPEED_LIMIT = 2800.0f; +// 渐隐边缘尺寸 +constexpr int32_t K_DEFAULT_FADING_EDGE = 12; +// 单分组索引 +constexpr int32_t K_SINGLE_SECTION_INDEX = 0; + +// ---- 新增:单分组配置结构体 ---- +struct SingleSectionParams { + int32_t itemCount = 0; + int32_t crossCount = 1; + float colGap = 0.0f; + float rowGap = 0.0f; + ArkUI_Margin margin{}; // {top,right,bottom,left} + + // 必填:按 index 计算主轴尺寸的回调 + float (*getMainSizeByIndex)(int32_t) = nullptr; + + // 可选:带 userData 的回调 + void *userData = nullptr; + float (*getMainSizeByIndexWithUserData)(int32_t, void *) = nullptr; +}; + +class WaterFlowMaker { +public: + static ArkUI_NodeHandle CreateNativeNode(); + + WaterFlowMaker() + { + OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, api_); + waterFlow_ = api_->createNode(ARKUI_NODE_WATER_FLOW); + api_->addNodeEventReceiver(waterFlow_, StaticEvent); + + scrollGuard_.Bind(api_, waterFlow_, this, SCROLL_EVT_ALL); + + OH_LOG_Print(LOG_APP, LOG_INFO, K_LOG_DOMAIN, LOG_TAG, "WaterFlowNode created"); + } + + ~WaterFlowMaker() + { + scrollGuard_.Release(); + + adapter_.reset(); + section_.reset(); + waterFlow_ = nullptr; + } + + // ---- Size ---- + void SetWidth(float width) { Utils::SetAttributeFloat32(api_, waterFlow_, NODE_WIDTH, width); } + + void SetHeight(float height) { Utils::SetAttributeFloat32(api_, waterFlow_, NODE_HEIGHT, height); } + + // ---- Adapter ---- + void SetLazyAdapter(const std::shared_ptr &adapter) + { + if (adapter == nullptr) { + return; + } + ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetAdapter()}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_NODE_ADAPTER, &item); + adapter_ = adapter; + } + + // ---- Section ---- + void SetSection(const std::shared_ptr §ion) + { + if (section == nullptr) { + return; + } + auto *opts = section->GetSectionOptions(); + if (opts == nullptr) { + return; + } + ArkUI_NumberValue start[] = {{.i32 = K_SINGLE_SECTION_INDEX}}; + ArkUI_AttributeItem item{start, 1, nullptr, opts}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_SECTION_OPTION, &item); + section_ = section; + } + + // ---- Layout / template / gap ---- + void SetLayoutDirection(int32_t direction) + { + ArkUI_NumberValue v[] = {{.i32 = direction}}; + ArkUI_AttributeItem item{v, 1}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_LAYOUT_DIRECTION, &item); + } + + void SetColumnTemplate(const char *tpl) + { + ArkUI_AttributeItem item{nullptr, 0, tpl, nullptr}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_COLUMN_TEMPLATE, &item); + } + + void SetRowTemplate(const char *tpl) + { + ArkUI_AttributeItem item{nullptr, 0, tpl, nullptr}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_ROW_TEMPLATE, &item); + } + + void SetGaps(float colGap, float rowGap) + { + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_WATER_FLOW_COLUMN_GAP, colGap); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_WATER_FLOW_ROW_GAP, rowGap); + } + + // ---- Cache / scroll / mode ---- + void SetCachedCount(int32_t count) + { + Utils::SetAttributeInt32(api_, waterFlow_, NODE_WATER_FLOW_CACHED_COUNT, count); + } + + void SetFooter(ArkUI_NodeHandle footer) + { + ArkUI_AttributeItem item{nullptr, 0, nullptr, footer}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_FOOTER, &item); + } + + void ScrollToIndex(int32_t index, int32_t align) + { + ArkUI_NumberValue v[] = {{.i32 = index}, {.i32 = align}}; + ArkUI_AttributeItem item{v, 2}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_SCROLL_TO_INDEX, &item); + } + + void SetItemConstraintSize(float mainMin, float mainMax) + { + ArkUI_NumberValue v[] = {{.f32 = mainMin}, {.f32 = mainMax}}; + ArkUI_AttributeItem item{v, 2}; + api_->setAttribute(waterFlow_, NODE_WATER_FLOW_ITEM_CONSTRAINT_SIZE, &item); + } + + void SetLayoutMode(ArkUI_WaterFlowLayoutMode mode) + { + Utils::SetAttributeInt32(api_, waterFlow_, NODE_WATER_FLOW_LAYOUT_MODE, static_cast(mode)); + } + + void SetSyncLoad(bool enabled) + { + Utils::SetAttributeInt32(api_, waterFlow_, NODE_WATER_FLOW_SYNC_LOAD, enabled ? 1 : 0); + } + + void SetScrollFriction(float f) { Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FRICTION, f); } + + void SetFlingSpeedLimit(float limit) + { + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FLING_SPEED_LIMIT, limit); + } + + // ---- 通用滚动外观/行为预设 ---- + void SetScrollCommon() + { + // 滚动条外观 + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_BAR_DISPLAY_MODE, + static_cast(ARKUI_SCROLL_BAR_DISPLAY_MODE_AUTO)); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_BAR_WIDTH, K_DEFAULT_SCROLL_BAR_WIDTH); + Utils::SetAttributeUInt32(api_, waterFlow_, NODE_SCROLL_BAR_COLOR, K_DEFAULT_SCROLL_BAR_COLOR); + + // 交互与摩擦 + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_ENABLE_SCROLL_INTERACTION, 1); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FRICTION, K_DEFAULT_SCROLL_FRICTION); + Utils::SetAttributeFloat32(api_, waterFlow_, NODE_SCROLL_FLING_SPEED_LIMIT, K_DEFAULT_FLING_SPEED_LIMIT); + + // 嵌套滚动策略 & 渐隐边缘 + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_NESTED_SCROLL, + static_cast(ARKUI_SCROLL_NESTED_MODE_SELF_FIRST)); + Utils::SetAttributeInt32(api_, waterFlow_, NODE_SCROLL_FADING_EDGE, K_DEFAULT_FADING_EDGE); + } + + ArkUI_NodeHandle GetWaterFlow() const { return waterFlow_; } + + std::shared_ptr GetWaterFlowSection() const { return section_; } + + void SetSingleSection(const SingleSectionParams &p) + { + if (!ValidateSingleSectionParams(p)) { + return; + } + + EnsureSectionSized(1); + + SectionOption opt = BuildSectionOption(p.itemCount, p.crossCount, p.colGap, p.rowGap, p.margin); + opt.onGetItemMainSizeByIndex = p.getMainSizeByIndex; + opt.userData = p.userData; + + ApplySectionOption(opt); + RegisterUserDataCallbackIfNeeded(p.userData, p.getMainSizeByIndexWithUserData); + SetSection(section_); + } + +private: + static void StaticEvent(ArkUI_NodeEvent *ev) { (void)ev; } + + static bool ValidateSingleSectionParams(const SingleSectionParams &p) + { + if (p.getMainSizeByIndex == nullptr) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, "getMainSizeByIndex is null"); + return false; + } + if (p.itemCount < 0 || p.crossCount <= 0) { + OH_LOG_Print(LOG_APP, LOG_ERROR, K_LOG_DOMAIN, LOG_TAG, + "invalid counts: itemCount=%{public}d, crossCount=%{public}d", p.itemCount, p.crossCount); + return false; + } + return true; + } + + void EnsureSectionSized(int32_t sectionCount) + { + if (section_ == nullptr) { + section_ = std::make_shared(); + } + section_->Resize(sectionCount); + } + + SectionOption BuildSectionOption(int32_t itemCount, int32_t crossCount, float colGap, float rowGap, + ArkUI_Margin margin) const + { + SectionOption opt{}; + opt.itemsCount = itemCount; + opt.crossCount = crossCount; + opt.columnsGap = colGap; + opt.rowsGap = rowGap; + opt.margin = margin; + return opt; + } + + void ApplySectionOption(const SectionOption &opt) + { + section_->SetSection(section_->GetSectionOptions(), K_SINGLE_SECTION_INDEX, opt); + } + + void RegisterUserDataCallbackIfNeeded(void *userData, float (*cb)(int32_t, void *)) + { + if (userData != nullptr && cb != nullptr) { + OH_ArkUI_WaterFlowSectionOption_RegisterGetItemMainSizeCallbackByIndexWithUserData( + section_->GetSectionOptions(), K_SINGLE_SECTION_INDEX, userData, cb); + } + } + +private: + ArkUI_NativeNodeAPI_1 *api_ = nullptr; + ArkUI_NodeHandle waterFlow_ = nullptr; + std::shared_ptr section_ = nullptr; + std::shared_ptr adapter_; + + ScrollEventGuard scrollGuard_; +}; + +#endif // WATERFLOW_MAKER_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowSection.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowSection.h new file mode 100644 index 000000000..7352f04e5 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/WaterFlowSection.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef WATERFLOW_SECTION_H +#define WATERFLOW_SECTION_H + +#include + +struct SectionOption { + int32_t itemsCount = 0; + int32_t crossCount = 1; + float columnsGap = 0.0f; + float rowsGap = 0.0f; + ArkUI_Margin margin{0, 0, 0, 0}; // {top, right, bottom, left} + float (*onGetItemMainSizeByIndex)(int32_t) = nullptr; + void *userData = nullptr; +}; + +class WaterFlowSection { +public: + WaterFlowSection() : sectionOptions_(OH_ArkUI_WaterFlowSectionOption_Create()) {} + + ~WaterFlowSection() + { + OH_ArkUI_WaterFlowSectionOption_Dispose(sectionOptions_); + sectionOptions_ = nullptr; + } + + void Resize(int32_t size) { OH_ArkUI_WaterFlowSectionOption_SetSize(sectionOptions_, size); } + + int32_t Size() const { return OH_ArkUI_WaterFlowSectionOption_GetSize(sectionOptions_); } + + void SetSection(ArkUI_WaterFlowSectionOption *opts, int32_t index, const SectionOption &s) + { + if (opts == nullptr) { + return; + } + OH_ArkUI_WaterFlowSectionOption_SetItemCount(opts, index, s.itemsCount); + OH_ArkUI_WaterFlowSectionOption_SetCrossCount(opts, index, s.crossCount); + OH_ArkUI_WaterFlowSectionOption_SetColumnGap(opts, index, s.columnsGap); + OH_ArkUI_WaterFlowSectionOption_SetRowGap(opts, index, s.rowsGap); + OH_ArkUI_WaterFlowSectionOption_SetMargin(opts, index, s.margin.top, s.margin.right, s.margin.bottom, + s.margin.left); + if (s.onGetItemMainSizeByIndex != nullptr) { + OH_ArkUI_WaterFlowSectionOption_RegisterGetItemMainSizeCallbackByIndex(opts, index, + s.onGetItemMainSizeByIndex); + } + } + + SectionOption GetSection(ArkUI_WaterFlowSectionOption *opts, int32_t index) + { + SectionOption s{}; + if (opts == nullptr) { + return s; + } + + s.itemsCount = OH_ArkUI_WaterFlowSectionOption_GetItemCount(opts, index); + s.crossCount = OH_ArkUI_WaterFlowSectionOption_GetCrossCount(opts, index); + s.columnsGap = OH_ArkUI_WaterFlowSectionOption_GetColumnGap(opts, index); + s.rowsGap = OH_ArkUI_WaterFlowSectionOption_GetRowGap(opts, index); + s.margin = OH_ArkUI_WaterFlowSectionOption_GetMargin(opts, index); + + return s; + } + + ArkUI_WaterFlowSectionOption *GetSectionOptions() const { return sectionOptions_; } + +private: + ArkUI_WaterFlowSectionOption *sectionOptions_ = nullptr; +}; + +#endif // WATERFLOW_SECTION_H diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.cpp index cdf33a45d..fc54722cf 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.cpp +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.cpp @@ -16,10 +16,15 @@ #include "manager.h" #include "ArkUIAnimationNode.h" #include "ArkUIVisualEffectsNode.h" +#include "GridMaker.h" +#include "ListMaker.h" +#include "RefreshMaker.h" +#include "ScrollMaker.h" #include "SwiperMaker.h" #include "TextMaker.h" #include "AccessibilityMaker.h" #include "EmbeddedComponentMaker.h" +#include "WaterFlowMaker.h" #include "baseUtils.h" #include "napi/native_api.h" #include @@ -279,4 +284,139 @@ napi_value Manager::CreateNativeEmbeddedComponentNode(napi_env env, napi_callbac } } return nullptr; +} + +napi_value Manager::CreateWaterFlowNativeNode(napi_env env, napi_callback_info info) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); + if ((env == nullptr) || (info == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); + return nullptr; + } + size_t argCnt = ConstIde::NUMBER_1; + napi_value args[ConstIde::NUMBER_1] = {nullptr}; + if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); + } + + ArkUI_NodeContentHandle nodeContentHandle = nullptr; + + OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); + + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); + if (nodeAPI_ != nullptr) { + if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { + ArkUI_NodeHandle testNode = WaterFlowMaker::CreateNativeNode(); + OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); + } + } + return nullptr; +} + +napi_value Manager::CreateGridNativeNode(napi_env env, napi_callback_info info) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); + if ((env == nullptr) || (info == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); + return nullptr; + } + size_t argCnt = ConstIde::NUMBER_1; + napi_value args[ConstIde::NUMBER_1] = {nullptr}; + if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); + } + + ArkUI_NodeContentHandle nodeContentHandle = nullptr; + + OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); + + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); + if (nodeAPI_ != nullptr) { + if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { + ArkUI_NodeHandle testNode = GridMaker::CreateNativeNode(); + OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); + } + } + return nullptr; +} + +napi_value Manager::CreateScrollNativeNode(napi_env env, napi_callback_info info) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); + if ((env == nullptr) || (info == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); + return nullptr; + } + size_t argCnt = ConstIde::NUMBER_1; + napi_value args[ConstIde::NUMBER_1] = {nullptr}; + if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); + } + + ArkUI_NodeContentHandle nodeContentHandle = nullptr; + + OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); + + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); + if (nodeAPI_ != nullptr) { + if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { + ArkUI_NodeHandle testNode = ScrollMaker::CreateNativeNode(); + OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); + } + } + return nullptr; +} + +napi_value Manager::CreateRefreshNativeNode(napi_env env, napi_callback_info info) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); + if ((env == nullptr) || (info == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); + return nullptr; + } + size_t argCnt = ConstIde::NUMBER_1; + napi_value args[ConstIde::NUMBER_1] = {nullptr}; + if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); + } + + ArkUI_NodeContentHandle nodeContentHandle = nullptr; + + OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); + + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); + if (nodeAPI_ != nullptr) { + if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { + ArkUI_NodeHandle testNode = RefreshMaker::CreateNativeNode(); + OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); + } + } + return nullptr; +} + +napi_value Manager::CreateListNativeNode(napi_env env, napi_callback_info info) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode BEGIN"); + if ((env == nullptr) || (info == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode env or info is null"); + return nullptr; + } + size_t argCnt = ConstIde::NUMBER_1; + napi_value args[ConstIde::NUMBER_1] = {nullptr}; + if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "CreateNativeNode napi_get_cb_info failed"); + } + + ArkUI_NodeContentHandle nodeContentHandle = nullptr; + + OH_ArkUI_GetNodeContentFromNapiValue(env, args[ConstIde::NUMBER_0], &nodeContentHandle); + + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Manager", "OH_ArkUI_GetBasicNodeAPI after"); + if (nodeAPI_ != nullptr) { + if (nodeAPI_->createNode != nullptr && nodeAPI_->addChild != nullptr) { + ArkUI_NodeHandle testNode = ListMaker::CreateNativeNode(); + OH_ArkUI_NodeContent_AddNode(nodeContentHandle, testNode); + } + } + return nullptr; } \ No newline at end of file diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.h b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.h index a7fdba11a..2ae8f0f8c 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.h +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/manager.h @@ -66,6 +66,11 @@ public: static napi_value CreateNativeTextNode(napi_env env, napi_callback_info info); static napi_value CreateNativeAccessibilityNode(napi_env env, napi_callback_info info); static napi_value CreateNativeEmbeddedComponentNode(napi_env env, napi_callback_info info); + static napi_value CreateWaterFlowNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateGridNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateScrollNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateRefreshNativeNode(napi_env env, napi_callback_info info); + static napi_value CreateListNativeNode(napi_env env, napi_callback_info info); private: static Manager manager_; diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/napi_init.cpp b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/napi_init.cpp index 36b4ed8f1..6b677977e 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/napi_init.cpp +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/napi_init.cpp @@ -16,9 +16,9 @@ #include "manager.h" #include - EXTERN_C_START -static napi_value Init(napi_env env, napi_value exports) { +static napi_value Init(napi_env env, napi_value exports) +{ OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Init", "Init begins"); if ((env == nullptr) || (exports == nullptr)) { OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Init", "env or exports is null"); @@ -43,11 +43,25 @@ static napi_value Init(napi_env env, napi_value exports) { {"createNativeRootVisualEffects3", nullptr, createNativeRootVisualEffects3, nullptr, nullptr, nullptr, napi_default, nullptr}, {"destroyNativeRoot", nullptr, DestroyNativeRoot, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"createNativeTextNode", nullptr, Manager::CreateNativeTextNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + {"createNativeSwiperNode", nullptr, Manager::CreateNativeSwiperNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, {"createNativeAccessibilityNode", nullptr, Manager::CreateNativeAccessibilityNode, nullptr, nullptr, nullptr, napi_default, nullptr}, {"createNativeEmbeddedComponentNode", nullptr, Manager::CreateNativeEmbeddedComponentNode, nullptr, nullptr, nullptr, napi_default, nullptr}, - // 参考Swiper新增其他createNative方法和Maker类 + {"createWaterFlowNativeNode", nullptr, Manager::CreateWaterFlowNativeNode, nullptr, nullptr, nullptr, + napi_default, nullptr}, + {"createGridNativeNode", nullptr, Manager::CreateGridNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + {"createScrollNativeNode", nullptr, Manager::CreateScrollNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + {"createRefreshNativeNode", nullptr, Manager::CreateRefreshNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + {"createListNativeNode", nullptr, Manager::CreateListNativeNode, nullptr, nullptr, nullptr, napi_default, + nullptr}, + // 参考新增其他createNative方法和Maker类 }; if (napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc) != napi_ok) { diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/types/libentry/Index.d.ts b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/types/libentry/Index.d.ts index a2f8dd020..e79315809 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/cpp/types/libentry/Index.d.ts +++ b/ArkUIKit/NativeTypeSample/entry/src/main/cpp/types/libentry/Index.d.ts @@ -25,4 +25,9 @@ export const createNativeRootVisualEffects2: (content: Object) => void; export const createNativeRootVisualEffects3: (content: Object) => void; export const destroyNativeRoot: () => void; export const createNativeAccessibilityNode: (content: Object) => void; -export const createNativeEmbeddedComponentNode: (content: Object) => void; \ No newline at end of file +export const createNativeEmbeddedComponentNode: (content: Object) => void; +export const createWaterFlowNativeNode: (content: Object) => void; +export const createGridNativeNode: (content: Object) => void; +export const createScrollNativeNode: (content: Object) => void; +export const createRefreshNativeNode: (content: Object) => void; +export const createListNativeNode: (content: Object) => void; \ No newline at end of file diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/Index.ets b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/Index.ets index 9ec5e22ca..409ee829c 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/Index.ets +++ b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/Index.ets @@ -25,12 +25,17 @@ interface ListCategories { struct MenuIndex { private items: ListCategories[] = [ - { title: 'Swiper', url: 'pages/page_swiper' }, + { title: 'Swiper', url: 'pages/page_swiper' }, { title: 'text', url: 'pages/page_text' }, { title: 'Animation', url: 'pages/page_animation' }, { title: 'Visual Effects', url: 'pages/page_visual_effects' }, { title: 'accessibility', url: 'pages/page_accessibility' }, { title: 'embedded component', url: 'pages/page_embedded_component' }, + { title: 'WaterFlow', url: 'pages/page_waterflow' }, + { title: 'Grid', url: 'pages/page_grid' }, + { title: 'Scroll', url: 'pages/page_scroll' }, + { title: 'Refresh', url: 'pages/page_refresh' }, + { title: 'List', url: 'pages/page_list' }, ]; build() { diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_grid.ets b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_grid.ets new file mode 100644 index 000000000..91c26abb9 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_grid.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createGridNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_list.ets b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_list.ets new file mode 100644 index 000000000..52a9dc01c --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_list.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createListNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_refresh.ets b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_refresh.ets new file mode 100644 index 000000000..73f028421 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_refresh.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createRefreshNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_scroll.ets b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_scroll.ets new file mode 100644 index 000000000..fe8e5a35c --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_scroll.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createScrollNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_waterflow.ets b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_waterflow.ets new file mode 100644 index 000000000..8cc63eea5 --- /dev/null +++ b/ArkUIKit/NativeTypeSample/entry/src/main/ets/pages/page_waterflow.ets @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Napi from 'libentry.so'; +import { NodeContent } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + private nodeContent = new NodeContent(); + aboutToAppear() { + // 通过C-API创建节点,并添加到管理器nodeContent上 + Napi.createWaterFlowNativeNode(this.nodeContent); + } + build() { + Column() { + ContentSlot(this.nodeContent) + } + .width('100%') + .height('100%') + } +} diff --git a/ArkUIKit/NativeTypeSample/entry/src/main/resources/base/profile/main_pages.json b/ArkUIKit/NativeTypeSample/entry/src/main/resources/base/profile/main_pages.json index d75958ede..e96c6ab43 100644 --- a/ArkUIKit/NativeTypeSample/entry/src/main/resources/base/profile/main_pages.json +++ b/ArkUIKit/NativeTypeSample/entry/src/main/resources/base/profile/main_pages.json @@ -1,9 +1,14 @@ { "src": [ "pages/Index", - "pages/page_swiper", "pages/page_text", + "pages/page_swiper", "pages/page_accessibility", - "pages/page_embedded_component" + "pages/page_embedded_component", + "pages/page_waterflow", + "pages/page_grid", + "pages/page_scroll", + "pages/page_refresh", + "pages/page_list" ] } -- Gitee