diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..2072f06d9e7273fe8733d996f289ebb0f7c9caeb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "entry/src/main/cpp/libboundscheck"] + path = entry/src/main/cpp/libboundscheck + url = https://gitee.com/openeuler/libboundscheck.git diff --git a/README.md b/README.md index bc105f1d14184138e00440c30513b22fbb5240cd..c65c9c9242f91b5860f932cebff6ae1b60e3d527 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,104 @@ -# 多API版本兼容示例 +# 实现多API版本兼容 -### 介绍 -通过@ohos.deviceInfo(设备信息)模块deviceInfo.sdkApiVersion和deviceInfo.distributionOSApiVersion获取系统SDK版本,以Text、Scroll和HdsActionBar组件在API 18和API 20的兼容为例,介绍多版本适配的实现方法。在Native侧,通过deviceinfo的OH_GetSdkApiVersion获取系统SDK版本,以Button组件为例介绍多版本适配的实现方法。 +## 项目介绍 +本项目演示在 HarmonyOS 多版本环境下,如何在同一套代码中实现 API 能力探测与优雅降级,进而实现优雅的 API 兼容策略,覆盖 ArkTS层与Native层两类典型场景: -### 工程目录 +1. **ArkTS(UI 层)**:通过 `deviceInfo.sdkApiVersion` / `deviceInfo.distributionOSApiVersion` 判断系统版本,选择高版本新特性或低版本替代实现。 +2. **Native(NAPI/C++ 层)**: + - 通过 `dlopen + dlsym` 对动态库符号进行探测,判断某个系统 API 是否存在,从而避免低版本调用导致崩溃。 + - 通过OH_GetDistributionOSApiVersion()获取ISV发行版系统api版本并判断系统版本,选择高版本新特性或低版本特性替代实现。 + +## 效果图预览 +| 首页 | 滚动场景 | 音频音量场景 | +| :------------------------------------------: | :------------------------------------------: | :------------------------------------------: | +| | | | + +## 使用说明 + +1. 使用git clone下载该项目代码,并将整个应用示例工程导入DevEco Studio。 + +2. 打开Terminal终端,使用下面的命令初始化并更新引用的所有子模块: + + ``` + git submodule update --init --recursive + ``` + +3. 执行编译构建,安装运行后,即可在设备上查看应用示例运行效果,以及进行相关调试。 + +## 工程目录 ``` -|──entry/src/main/cpp -| |──classdef -| | |──include -| | | |──ArkUIBaseNode.h // 组件树操作的基类 -| | | |──ArkUINode.h // 通用组件的封装 -| | | |──ArkUIButtonNode.h // 实现按钮组件的封装类 -| | | └──NativeModuleInstance.h // ArkUI在Native侧模块的封装接口 -| | └──src -| | | |──ArkUIBaseNode.cpp // 组件树操作的基类 -| | | |──ArkUINode.cpp // 通用组件的封装 -| | | |──ArkUIButtonNode.cpp // 实现按钮组件的封装类 -| | | └──NativeModuleInstance.cpp // ArkUI在Native侧模块的封装接口 -| |──function -| | |──include -| | | |──IntegratingWithArkts.h // 接入ArkTS界面 -| | | └──NativeEntry.h // 管理Native组件生命周期 -| | └──src -| | | └──IntegratingWithArkts.cpp // 接入ArkTS界面 -| └──types -| | └──libentry -| | | |──Index.d.ts // Native侧接口导出声明文件 -| | | └──oh-package.json5 -| |──CMakeLists.txt // cmake配置文件 -| └──napi_init.cpp // 接口映射、模块注册 +├──entry/src/main/cpp +│ ├──classdef +│ │ ├──include +│ │ │ ├──ArkUIBaseNode.h // 组件树操作的基类 +│ │ │ ├──ArkUINode.h // 通用组件的封装 +│ │ │ ├──ArkUIButtonNode.h // 实现按钮组件的封装类 +│ │ │ └──NativeModuleInstance.h // ArkUI在Native侧模块的封装接口 +│ │ └──src +│ │ ├──ArkUIBaseNode.cpp // 组件树操作的基类 +│ │ ├──ArkUINode.cpp // 通用组件的封装 +│ │ ├──ArkUIButtonNode.cpp // 实现按钮组件的封装类 +│ │ └──NativeModuleInstance.cpp // ArkUI在Native侧模块的封装接口 +│ ├──function +│ │ ├──include +│ │ │ ├──IntegratingWithArkts.h // 接入ArkTS界面 +│ │ │ └──NativeEntry.h // 管理Native组件生命周期 +│ │ └──src +│ │ └──IntegratingWithArkts.cpp // 接入ArkTS界面 +│ ├──libboundscheck // libboundscheck三方库 +│ └──types +│ │ └──libentry +│ │ ├──Index.d.ts // Native侧接口导出声明文件 +│ │ └──oh-package.json5 +│ ├──CMakeLists.txt // cmake配置文件 +│ └──napi_init.cpp // 接口映射、模块注册 ├──entry/src/main/ets // 代码区 -│ ├──components // 自定义组件 -│ │ ├──ActionBarAdapter.ets // HdsActionBar组件版本兼容示例 -│ │ ├──NativeButtonAdapter.ets // Button组件版本兼容示例 -│ │ ├──ScrollComponentAdapter.ets // Scroll组件版本兼容示例 -│ │ └──TextComponentAdapter.ets // Text组件版本兼容示例 +│ ├──contants +│ │ └──CommonConstants.ets // 常量类 │ ├──entryability │ │ └──EntryAbility.ets // 程序入口类 │ ├──entrybackupability │ │ └──EntryBackupAbility.ets // 应用数据备份和恢复类 -│ └──pages -│ └──Index.ets // 应用主界面 +│ ├──pages +│ │ ├──ActionBarScene.ets // HdsActionBar组件版本兼容示例展示页 +│ │ ├──AudioVolumeScene.ets // 音频音量场景兼容示例展示页 +│ │ ├──ButtonDisplayScene.ets // Button组件版本兼容示例展示页 +│ │ ├──Index.ets // 首页 +│ │ ├──MarqueeDisplayScene.ets // 走马灯场景兼容性示例展示页 +│ │ └──ScrollScene.ets // 滚动场景兼容示例展示页 +│ └──utils +│ ├──CommonUtils.ets // 通用工具类 +│ └──Logger.ets // 日志工具类 +└──entry/src/main/resources // 应用资源目录 ``` -### 使用说明 -安装应用之后,进入首页。 +## 实现说明 +本项目采用三类兼容策略:**版本判断**、**能力探测**和**安全装配**,确保应用在不同HarmonyOS版本上优雅运行。 + +### ArkTS层兼容 + +- **API版本分支**:通过`deviceInfo.sdkApiVersion`或`deviceInfo.distributionOSApiVersion`判断,选择高版本API或降级实现 + - *ActionBar示例*:API 6.0.0+(60000)使用HdsActionBar,低版本用基础组件模拟 + - *Text/Marquee*:仅在API 18+启用跑马灯效果 +- **特性按需装配**:Scroll组件通过"特性规格表"动态注册API 12/18/20的不同能力,使用WeakSet避免重复绑定 -### 实现说明 -* 通过@ohos.deviceInfo(设备信息)模块deviceInfo.sdkApiVersion和deviceInfo.distributionOSApiVersion属性来获取当前设备SDK版本,然后和目标版本进行比对。 -* 以Text组件的marqueeOptions属性使用为例来展示API18的兼容,实现了跑马灯效果 -* 以Scroll组件的maxZoomScale、minZoomScale和enableBouncesZoom为例子来展示API20的兼容,实现了图片缩放效果。 -* 以HdsActionBar组件的使用例子来展示API20的兼容,实现可以展开和收起的ActionBar效果。 -* 以Native侧的Button组件为例,展示API20的兼容。 +### Native层兼容 -### 相关权限 +- **版本阈值判断**:使用`OH_GetDistributionOSApiVersion()`与预设阈值(如50101对应5.1.1)选择不同UI枚举值 +- **动态符号探测**:通过`dlopen/dlsym`检测系统库符号是否存在,避免低版本调用崩溃 +- **严格生命周期管理**:Native节点创建与销毁严格配对,防止资源泄漏 + +## 相关权限 不涉及 -### 约束与限制 +## 模块依赖 + +- 依赖[libboundscheck](https://gitee.com/openeuler/libboundscheck)三方库 + +## 约束与限制 1. 本示例仅支持标准系统上运行,支持设备:华为手机。 -2. HarmonyOS系统:HarmonyOS 6.0.0 Beta2及以上。 -3. DevEco Studio版本:DevEco Studio 6.0.0 Beta2及以上。 -4. HarmonyOS SDK版本:HarmonyOS 6.0.0 Beta2 SDK及以上。 +2. HarmonyOS系统:HarmonyOS 6.0.0及以上。 +3. DevEco Studio版本:DevEco Studio 6.0.0及以上。 +4. HarmonyOS SDK版本:HarmonyOS 6.0.0 SDK及以上。 diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt index eccba73b47b604f39f0a2a63da2655f0459cb91d..6f805c547909c6d077b354d20cffabfb73090614 100644 --- a/entry/src/main/cpp/CMakeLists.txt +++ b/entry/src/main/cpp/CMakeLists.txt @@ -9,17 +9,26 @@ if(DEFINED PACKAGE_FIND_FILE) include(${PACKAGE_FIND_FILE}) endif() +# ----------------------- +# Third party library path +# ----------------------- +set(BOUNDSCHECK_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libboundscheck") + include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/classdef/include - ${NATIVERENDER_ROOT_PATH}/function/include) + ${NATIVERENDER_ROOT_PATH}/function/include + ${BOUNDSCHECK_SRC_DIR}/include) file(GLOB BASECLADD_SOURCES "./classdef/src/*.cpp") file(GLOB FUNCTION_SOURCES "./function/src/*.cpp") +file(GLOB_RECURSE SOURCE_FILES "${BOUNDSCHECK_SRC_DIR}/src/*.c") + add_library(entry SHARED napi_init.cpp ${BASECLADD_SOURCES} - ${FUNCTION_SOURCES}) + ${FUNCTION_SOURCES} + ${SOURCE_FILES}) target_link_libraries(entry PUBLIC libace_napi.z.so libace_ndk.z.so libhilog_ndk.z.so libnative_drawing.so libuv.so libdeviceinfo_ndk.z.so) \ No newline at end of file diff --git a/entry/src/main/cpp/classdef/include/ArkUIBaseNode.h b/entry/src/main/cpp/classdef/include/ArkUIBaseNode.h index 6e7549ea8a031bc30187a74ae03b2a92a6c920b3..841165dd8313e95d597952737dbb14513acbc125 100644 --- a/entry/src/main/cpp/classdef/include/ArkUIBaseNode.h +++ b/entry/src/main/cpp/classdef/include/ArkUIBaseNode.h @@ -24,11 +24,19 @@ namespace NativeModule { class ArkUIBaseNode { public: - explicit ArkUIBaseNode(ArkUI_NodeHandle handle) - : handle_(handle), nativeModule_(NativeModuleInstance::GetInstance()->GetNativeNodeAPI()) {} + explicit ArkUIBaseNode(ArkUI_NodeHandle handle) : handle_(handle) + { + auto instance = NativeModuleInstance::GetInstance(); + if (instance != nullptr) { + nativeModule_ = instance->GetNativeNodeAPI(); + } + } virtual ~ArkUIBaseNode() { + if (nativeModule_ == nullptr) { + return; + } // Encapsulate destructor to implement the function of removing child nodes. if (!children_.empty()) { for (const auto &child : children_) { @@ -42,8 +50,6 @@ public: void AddChild(const std::shared_ptr &child); - std::list> GetChildren(); - ArkUI_NodeHandle GetHandle() const; protected: diff --git a/entry/src/main/cpp/classdef/include/ArkUIButtonNode.h b/entry/src/main/cpp/classdef/include/ArkUIButtonNode.h index a1804d7c6eea8e5ecdd16b6bb9e2f89d6c599c2f..0532cad9d788cd1b45705f6b7b359d11379690c0 100644 --- a/entry/src/main/cpp/classdef/include/ArkUIButtonNode.h +++ b/entry/src/main/cpp/classdef/include/ArkUIButtonNode.h @@ -23,8 +23,7 @@ class ArkUIButtonNode : public ArkUINode { public: ArkUIButtonNode() : ArkUINode((NativeModuleInstance::GetInstance()->GetNativeNodeAPI())->createNode(ARKUI_NODE_BUTTON)) {} - - // Text attribute NDK interface encapsulation. + void SetFontSize(float fontSize); void SetFontColor(uint32_t color); @@ -37,4 +36,4 @@ public: }; } // namespace NativeModule -#endif // NDKCREATEUI_ARKUITEXTNODE_H \ No newline at end of file +#endif // NDKCREATEUI_ARKUI_BUTTON_H \ No newline at end of file diff --git a/entry/src/main/cpp/classdef/include/NativeModuleInstance.h b/entry/src/main/cpp/classdef/include/NativeModuleInstance.h index da2059e6bb4372144e6056ce4d8800bc6a6bfe5a..24b4650f97aa4565d25012774dc36913ac591155 100644 --- a/entry/src/main/cpp/classdef/include/NativeModuleInstance.h +++ b/entry/src/main/cpp/classdef/include/NativeModuleInstance.h @@ -29,13 +29,12 @@ public: return &instance; } - NativeModuleInstance() - { + NativeModuleInstance() { // Retrieve the function pointer structure object of the NDK interface for subsequent operations. OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, arkUINativeNodeApi_); - if (!arkUINativeNodeApi_) { - return; - } + // Recovery mechanisms and error handling should be triggered here based on actual scenarios when initialization + // fails; this is for demonstration purposes only. + assert(arkUINativeNodeApi_); } // Expose to other modules for use. ArkUI_NativeNodeAPI_1 *GetNativeNodeAPI(); diff --git a/entry/src/main/cpp/classdef/src/ArkUIBaseNode.cpp b/entry/src/main/cpp/classdef/src/ArkUIBaseNode.cpp index dbe80dbaa620bcceb4889464087db24ec3420068..c90c566aa979cff46e84192ebe955e6b667d01f0 100644 --- a/entry/src/main/cpp/classdef/src/ArkUIBaseNode.cpp +++ b/entry/src/main/cpp/classdef/src/ArkUIBaseNode.cpp @@ -22,7 +22,5 @@ void ArkUIBaseNode::AddChild(const std::shared_ptr &child) OnAddChild(child); } -std::list> ArkUIBaseNode::GetChildren() { return children_; } - ArkUI_NodeHandle ArkUIBaseNode::GetHandle() const { return handle_; } } // namespace NativeModule \ No newline at end of file diff --git a/entry/src/main/cpp/classdef/src/ArkUIButtonNode.cpp b/entry/src/main/cpp/classdef/src/ArkUIButtonNode.cpp index 758f4bfdc1608985acf72f80e0a09ea2d50e945d..33d9701f390070b98302f6e7c9f4994d551ce477 100644 --- a/entry/src/main/cpp/classdef/src/ArkUIButtonNode.cpp +++ b/entry/src/main/cpp/classdef/src/ArkUIButtonNode.cpp @@ -15,6 +15,7 @@ #include #include "ArkUIButtonNode.h" +#include "securec.h" namespace NativeModule { // Text attribute NDK interface encapsulation. @@ -37,7 +38,9 @@ void ArkUIButtonNode::SetFontColor(uint32_t color) void ArkUIButtonNode::SetTextContent(const std::string &content) { assert(handle_); - ArkUI_AttributeItem item = {nullptr, 0, content.c_str()}; + char* contentPtr = new char[content.length() + 1]; + strcpy_s(contentPtr, content.length() + 1, content.c_str()); + ArkUI_AttributeItem item = {nullptr, 0, contentPtr}; nativeModule_->setAttribute(handle_, NODE_BUTTON_LABEL, &item); } @@ -54,7 +57,7 @@ void ArkUIButtonNode::SetButtonType(int32_t buttonType) assert(handle_); ArkUI_NumberValue value[] = {{.i32 = buttonType}}; ArkUI_AttributeItem item = {value, 1}; - nativeModule_->setAttribute(handle_, NODE_BUTTON_TYPE , &item); + nativeModule_->setAttribute(handle_, NODE_BUTTON_TYPE, &item); } // [End button_api] } // namespace NativeModule \ No newline at end of file diff --git a/entry/src/main/cpp/classdef/src/ArkUINode.cpp b/entry/src/main/cpp/classdef/src/ArkUINode.cpp index 832b3d2979012a70a6e8cb757baf8aca333f308a..e8d480fc613c18020e8f40e0d7fb88022d66c8e2 100644 --- a/entry/src/main/cpp/classdef/src/ArkUINode.cpp +++ b/entry/src/main/cpp/classdef/src/ArkUINode.cpp @@ -18,7 +18,9 @@ namespace NativeModule { void ArkUINode::SetWidth(float width) { - assert(handle_); + if (!handle_) { + throw std::runtime_error("handle_ is null!"); + } ArkUI_NumberValue value[] = {{.f32 = width}}; ArkUI_AttributeItem item = {value, 1}; nativeModule_->setAttribute(handle_, NODE_WIDTH, &item); @@ -26,7 +28,9 @@ void ArkUINode::SetWidth(float width) void ArkUINode::SetPercentWidth(float percent) { - assert(handle_); + if (!handle_) { + throw std::runtime_error("handle_ is null!"); + } ArkUI_NumberValue value[] = {{.f32 = percent}}; ArkUI_AttributeItem item = {value, 1}; nativeModule_->setAttribute(handle_, NODE_WIDTH_PERCENT, &item); @@ -34,7 +38,9 @@ void ArkUINode::SetPercentWidth(float percent) void ArkUINode::SetHeight(float height) { - assert(handle_); + if (!handle_) { + throw std::runtime_error("handle_ is null!"); + } ArkUI_NumberValue value[] = {{.f32 = height}}; ArkUI_AttributeItem item = {value, 1}; nativeModule_->setAttribute(handle_, NODE_HEIGHT, &item); @@ -42,7 +48,9 @@ void ArkUINode::SetHeight(float height) void ArkUINode::SetPercentHeight(float percent) { - assert(handle_); + if (!handle_) { + throw std::runtime_error("handle_ is null!"); + } ArkUI_NumberValue value[] = {{.f32 = percent}}; ArkUI_AttributeItem item = {value, 1}; nativeModule_->setAttribute(handle_, NODE_HEIGHT_PERCENT, &item); @@ -50,7 +58,9 @@ void ArkUINode::SetPercentHeight(float percent) void ArkUINode::SetBackgroundColor(uint32_t color) { - assert(handle_); + if (!handle_) { + throw std::runtime_error("handle_ is null!"); + } ArkUI_NumberValue value[] = {{.u32 = color}}; ArkUI_AttributeItem item = {value, 1}; nativeModule_->setAttribute(handle_, NODE_BACKGROUND_COLOR, &item); @@ -59,9 +69,11 @@ void ArkUINode::SetBackgroundColor(uint32_t color) // Handle general events. void ArkUINode::RegisterOnClick(const std::function &onClick) { - assert(handle_); + if (!handle_) { + throw std::runtime_error("handle_ is null!"); + } onClick_ = onClick; - // 注册点击事件。 + // Register click event nativeModule_->registerNodeEvent(handle_, NODE_ON_CLICK, 0, nullptr); } @@ -112,11 +124,14 @@ void ArkUINode::ProcessNodeEvent(ArkUI_NodeEvent *event) case NODE_TOUCH_EVENT: { if (onTouch_) { auto *uiInputEvent = OH_ArkUI_NodeEvent_GetInputEvent(event); - float x = OH_ArkUI_PointerEvent_GetX(uiInputEvent); - float y = OH_ArkUI_PointerEvent_GetY(uiInputEvent); - auto type = OH_ArkUI_UIInputEvent_GetAction(uiInputEvent); - onTouch_(type, x, y); + if (uiInputEvent != nullptr) { + float x = OH_ArkUI_PointerEvent_GetX(uiInputEvent); + float y = OH_ArkUI_PointerEvent_GetY(uiInputEvent); + auto type = OH_ArkUI_UIInputEvent_GetAction(uiInputEvent); + onTouch_(type, x, y); + } } + break; } case NODE_EVENT_ON_DISAPPEAR: { if (onDisappear_) { @@ -139,16 +154,22 @@ void ArkUINode::ProcessNodeEvent(ArkUI_NodeEvent *event) void ArkUINode::OnAddChild(const std::shared_ptr &child) { - nativeModule_->addChild(handle_, child->GetHandle()); + if (child) { + nativeModule_->addChild(handle_, child->GetHandle()); + } } void ArkUINode::OnRemoveChild(const std::shared_ptr &child) { - nativeModule_->removeChild(handle_, child->GetHandle()); + if (child) { + nativeModule_->removeChild(handle_, child->GetHandle()); + } } void ArkUINode::OnInsertChild(const std::shared_ptr &child, int32_t index) { - nativeModule_->insertChildAt(handle_, child->GetHandle(), index); + if (child) { + nativeModule_->insertChildAt(handle_, child->GetHandle(), index); + } } } // namespace NativeModule \ No newline at end of file diff --git a/entry/src/main/cpp/function/include/IntegratingWithArkts.h b/entry/src/main/cpp/function/include/IntegratingWithArkts.h index edcecdfc05d114870a175dc86cb4c965b87f3a48..a1e90258af82e0bb1a7e61a0a855edd7546ad8e5 100644 --- a/entry/src/main/cpp/function/include/IntegratingWithArkts.h +++ b/entry/src/main/cpp/function/include/IntegratingWithArkts.h @@ -21,14 +21,15 @@ #include "ArkUIBaseNode.h" namespace NativeModule { -constexpr int32_t List_NUM = 30; -constexpr int32_t TEXT_FONTSIZE = 16; -constexpr int32_t TEXT_HEIGHT = 100; +constexpr int32_t TEXT_FONT_SIZE = 16; +constexpr int32_t TEXT_HEIGHT = 40; napi_value CreateButtonNativeRoot(napi_env env, napi_callback_info info); napi_value DestroyButtonNativeRoot(napi_env env, napi_callback_info info); +napi_value CheckAudioVolumeManagerExists(napi_env env, napi_callback_info info); + std::shared_ptr CreateButtonExample(); } // namespace NativeModule diff --git a/entry/src/main/cpp/function/include/NativeEntry.h b/entry/src/main/cpp/function/include/NativeEntry.h index c744e84d0aa5b423206a045cc79727e1b2320030..5236a7d4f1d8e565c6d4bac86c81f284b49726db 100644 --- a/entry/src/main/cpp/function/include/NativeEntry.h +++ b/entry/src/main/cpp/function/include/NativeEntry.h @@ -28,6 +28,9 @@ public: void SetRootNode(const std::shared_ptr &baseNode) { + if (!handle_) { + return; + } root_ = baseNode; // Add Native components to NodeContent for mounting display. OH_ArkUI_NodeContent_AddNode(handle_, root_->GetHandle()); @@ -35,6 +38,9 @@ public: void DisposeRootNode() { + if (root_ == nullptr) { + return; + } // Uninstall components from NodeContent and destroy native components. OH_ArkUI_NodeContent_RemoveNode(handle_, root_->GetHandle()); root_.reset(); diff --git a/entry/src/main/cpp/function/src/IntegratingWithArkts.cpp b/entry/src/main/cpp/function/src/IntegratingWithArkts.cpp index 37704cd6497504a37c32ea401985868dab229787..dd37bc85d3d6f1701b79fd1664f4fb6da74b2eb3 100644 --- a/entry/src/main/cpp/function/src/IntegratingWithArkts.cpp +++ b/entry/src/main/cpp/function/src/IntegratingWithArkts.cpp @@ -15,25 +15,44 @@ #include #include +#include +#include +#include +#include #include "ArkUIButtonNode.h" #include "NativeEntry.h" +#include "hilog/log.h" +#include "napi/native_api.h" #include "IntegratingWithArkts.h" -#include + +#undef LOG_DOMAIN +#undef LOG_TAG +#define LOG_DOMAIN 0x3200 +#define LOG_TAG "APILevelAdapt" namespace NativeModule { NativeEntry nativeEntry; +constexpr int MIN_API_VERSION_5_1_1 = 50101; napi_value CreateButtonNativeRoot(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value args[1] = {nullptr}; - - // Get parameters passed in from JS. napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + // Check if the number of arguments is 1 + if (argc != 1) { + napi_throw_error(env, nullptr, "Expected 1 argument"); + return nullptr; + } // Get NodeContent. ArkUI_NodeContentHandle contentHandle; - OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &contentHandle); + int32_t resultCode = OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &contentHandle); + if (resultCode != ARKUI_ERROR_CODE_NO_ERROR) { + napi_throw_error(env, nullptr, "Failed to get node content from Napi value"); + return nullptr; + } nativeEntry.SetContentHandle(contentHandle); // Create a text list. @@ -49,15 +68,15 @@ std::shared_ptr CreateButtonExample() auto textNode = std::make_shared(); textNode->SetTextContent(std::string("Hello World")); // [StartExclude button_api] - textNode->SetFontSize(TEXT_FONTSIZE); + textNode->SetFontSize(TEXT_FONT_SIZE); textNode->SetPercentWidth(1); - textNode->SetHeight(40); + textNode->SetHeight(TEXT_HEIGHT); textNode->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER); // [EndExclude button_api] // Regarding the proprietary interfaces of HarmonyOS, specifically the interfaces marked as since M.F.S(N). // Compatibility judgment, the value corresponding to version 5.1.1(19) is 50101, // which is derived from the new interface's since field 5*10000 + 1*100 + 1. - if (OH_GetDistributionOSApiVersion() >= 50101) { + if (OH_GetDistributionOSApiVersion() >= MIN_API_VERSION_5_1_1) { textNode->SetButtonType(ARKUI_BUTTON_ROUNDED_RECTANGLE); } else { textNode->SetButtonType(ARKUI_BUTTON_TYPE_CAPSULE); @@ -68,7 +87,63 @@ std::shared_ptr CreateButtonExample() napi_value DestroyButtonNativeRoot(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_value args[1] = {nullptr}; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (argc != 0) { + napi_throw_error(env, nullptr, "Expected 0 argument"); + return nullptr; + } nativeEntry.DisposeRootNode(); return nullptr; } + +/** + * Checks whether the OH_AudioManager_GetAudioVolumeManager function exists in the dynamic library. + * This function dynamically loads the location library and attempts to retrieve + * the specified function symbol. The result indicates the availability of the + * location API on the current system. + */ +napi_value CheckAudioVolumeManagerExists(napi_env env, napi_callback_info info) +{ + size_t argc = 0; + napi_value args[1] = {nullptr}; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + if (argc != 0) { + napi_throw_error(env, nullptr, "Expected 0 argument"); + return nullptr; + } + // Handle to the dynamically loaded library + void *handle = NULL; + // Function pointer declaration for the target API + OH_AudioCommon_Result (*OH_AudioManager_GetAudioVolumeManager_Test)(OH_AudioVolumeManager **); + // Initialize function pointer to NULL + OH_AudioManager_GetAudioVolumeManager_Test = NULL; + // Flag indicating whether the function exists + bool isExisted = false; + + // Attempt to dynamically load the location library + handle = dlopen("libohaudio.so", RTLD_LAZY); + if (handle != NULL) { + // Retrieve the address of the OH_AudioManager_GetAudioVolumeManager function from the library + OH_AudioManager_GetAudioVolumeManager_Test = + (OH_AudioCommon_Result(*)(OH_AudioVolumeManager **))dlsym(handle, "OH_AudioManager_GetAudioVolumeManager"); + + // Set flag based on whether the function symbol was successfully found + isExisted = OH_AudioManager_GetAudioVolumeManager_Test != NULL; + + // Close the library handle to release resources + dlclose(handle); + } else { + const char* error = dlerror(); + if (error != NULL) { + OH_LOG_INFO(LOG_APP, "Failed to load library: %s", error); + } else { + OH_LOG_INFO(LOG_APP, "Failed to load library: unknown error"); + } + } + napi_value result = NULL; + napi_get_boolean(env, isExisted, &result); + return result; +} } // namespace NativeModule \ No newline at end of file diff --git a/entry/src/main/cpp/libboundscheck b/entry/src/main/cpp/libboundscheck new file mode 160000 index 0000000000000000000000000000000000000000..1ae16ab92de4884eacea211dcd1989af95dae79b --- /dev/null +++ b/entry/src/main/cpp/libboundscheck @@ -0,0 +1 @@ +Subproject commit 1ae16ab92de4884eacea211dcd1989af95dae79b diff --git a/entry/src/main/cpp/napi_init.cpp b/entry/src/main/cpp/napi_init.cpp index 3dbe3f3497de8d125588977a17708c3d5218cf30..476ddcbc6970b23aa38b8f6304244ed93005c121 100644 --- a/entry/src/main/cpp/napi_init.cpp +++ b/entry/src/main/cpp/napi_init.cpp @@ -21,10 +21,12 @@ EXTERN_C_START void RegisterListComponents(napi_env env, napi_value exports) { napi_property_descriptor desc[] = { - {"CreateButtonNativeRoot", nullptr, NativeModule::CreateButtonNativeRoot, nullptr, nullptr, nullptr, + {"createButtonNativeRoot", nullptr, NativeModule::CreateButtonNativeRoot, nullptr, nullptr, nullptr, napi_default, nullptr}, - {"DestroyButtonNativeRoot", nullptr, NativeModule::DestroyButtonNativeRoot, nullptr, nullptr, nullptr, + {"destroyButtonNativeRoot", nullptr, NativeModule::DestroyButtonNativeRoot, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"checkAudioVolumeManagerExists", nullptr, NativeModule::CheckAudioVolumeManagerExists, nullptr, nullptr, + nullptr, napi_default, nullptr}, }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); } diff --git a/entry/src/main/cpp/types/libentry/Index.d.ts b/entry/src/main/cpp/types/libentry/Index.d.ts index 213acecd0d498a8be395609608dff52345f77203..84af24cc516c54a8288fdf089062661e6b0987dd 100644 --- a/entry/src/main/cpp/types/libentry/Index.d.ts +++ b/entry/src/main/cpp/types/libentry/Index.d.ts @@ -15,6 +15,21 @@ import { NodeContent } from "@kit.ArkUI"; -export const CreateButtonNativeRoot: (content: NodeContent) => void; +/** + * Creates a native root node for a button component in the ArkUI framework. + * @param content - Entity Encapsulation of Node Content + * @returns void + */ +export const createButtonNativeRoot: (content: NodeContent) => void; + +/** + * Destroys the native root node for a button component and releases associated resources. + * @returns void + */ +export const destroyButtonNativeRoot: () => void; -export const DestroyButtonNativeRoot: () => void; \ No newline at end of file +/** + * Checks whether the OH_AudioManager_GetAudioVolumeManager function exists in the dynamic library. + * @returns void + */ +export const checkAudioVolumeManagerExists: () => boolean; \ No newline at end of file diff --git a/entry/src/main/ets/components/ActionBarAdapter.ets b/entry/src/main/ets/components/ActionBarAdapter.ets deleted file mode 100644 index 35a310373fefadd8d2d6cd804aaa51bdc38530c8..0000000000000000000000000000000000000000 --- a/entry/src/main/ets/components/ActionBarAdapter.ets +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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 { HdsActionBar, ActionBarButton, ActionBarStyle } from '@kit.UIDesignKit'; -import { deviceInfo } from '@kit.BasicServicesKit'; - -@Component -export struct ActionBarAdapter { - @State isExpand: boolean = true; - @State isPrimaryIconChanged: boolean = false; - // [Start action_bar_api] - build() { - Column() { - // Regarding the proprietary interfaces of HarmonyOS, specifically the interfaces marked as since M.F.S(N). - // Compatibility judgment, the value corresponding to version 6.0.0(20) is 60000, - // which is derived from the new interface's since field 6*10000 + 0*100 + 0. - if (deviceInfo.distributionOSApiVersion >= 60000) { - // Component that calls the API of version 6.0.0(20) - HdsActionBar({ - startButtons: [new ActionBarButton({ - baseIcon: $r('sys.symbol.stopwatch_fill') - })], - endButtons: [new ActionBarButton({ - baseIcon: $r('sys.symbol.mic_fill') - })], - // [StartExclude action_bar_api] - primaryButton: new ActionBarButton({ - baseIcon: $r('sys.symbol.plus'), - altIcon: $r('sys.symbol.play_fill'), - onClick: () => { - this.isExpand = !this.isExpand; - this.isPrimaryIconChanged = !this.isPrimaryIconChanged; - } - }), - actionBarStyle: new ActionBarStyle({ - isPrimaryIconChanged: this.isPrimaryIconChanged - }), - isExpand: this.isExpand!! - // [EndExclude action_bar_api] - }) - } else { - // Downgrading plan - Row({ space: 25 }) { - // [StartExclude action_bar_api] - if (this.isExpand) { - Button({ type: ButtonType.Circle }) { - SymbolGlyph($r('sys.symbol.stopwatch_fill')) - .fontSize(24) - .fontColor([$r('sys.color.font_secondary')]) - } - .aspectRatio(1) - .height(45) - .backgroundColor($r('sys.color.background_secondary')) - .margin({ left: 10 }) - } - - Button({ type: ButtonType.Circle }) { - SymbolGlyph(this.isExpand ? $r('sys.symbol.plus') : $r('sys.symbol.play_fill')) - .fontSize(24) - .fontColor([$r('sys.color.white')]) - } - .aspectRatio(1) - .height(55) - .backgroundColor($r('sys.color.brand')) - .onClick(() => { - this.isExpand = !this.isExpand; - }) - - if (this.isExpand) { - Button({ type: ButtonType.Circle }) { - SymbolGlyph($r('sys.symbol.mic_fill')) - .fontSize(24) - .fontColor([$r('sys.color.font_secondary')]) - } - .aspectRatio(1) - .height(45) - .backgroundColor($r('sys.color.background_secondary')) - .margin({ right: 10 }) - } - // [EndExclude action_bar_api] - } - // [StartExclude action_bar_api] - .backgroundColor($r('sys.color.background_primary')) - .borderRadius(30) - // [EndExclude action_bar_api] - } - } - // [StartExclude action_bar_api] - .width('100%') - .justifyContent(FlexAlign.Center) - .alignItems(HorizontalAlign.Center) - // [EndExclude action_bar_api] - } - // [End action_bar_api] -} \ No newline at end of file diff --git a/entry/src/main/ets/components/ScrollComponentAdapter.ets b/entry/src/main/ets/contants/CommonConstants.ets similarity index 41% rename from entry/src/main/ets/components/ScrollComponentAdapter.ets rename to entry/src/main/ets/contants/CommonConstants.ets index a33bad4a85e62612a730e25caf1a0a85511639cb..793c4fc13756d0f30912723d9baec0967625198d 100644 --- a/entry/src/main/ets/components/ScrollComponentAdapter.ets +++ b/entry/src/main/ets/contants/CommonConstants.ets @@ -13,34 +13,44 @@ * limitations under the License. */ -import { deviceInfo } from '@kit.BasicServicesKit'; - -class MyModifier implements AttributeModifier { - applyNormalAttribute(instance: ScrollAttribute): void { - // Judgment is made by the api version information of deviceInfo - if (deviceInfo.sdkApiVersion >= 20) { - // To adapt to the Scroll scaling property, you can set the minimum and maximum scaling ratios - instance.maxZoomScale(1.2) - instance.minZoomScale(0.5) - instance.enableBouncesZoom(true) - } - } +/** + * Route type define + */ +export interface Route { + title: ResourceStr; + to: string; } +export const ACTION_BAR_SCENE_NAME: string = 'ActionBarScene'; + +export const BUTTON_DISPLAY_SCENE_NAME: string = 'ButtonDisplayScene'; +export const SCROLL_SCENE_NAME: string = 'ScrollScene'; -@Component -export struct ScrollComponentAdapter { - adaptModifier: MyModifier = new MyModifier(); +export const MARQUEE_DISPLAY_SCENE_NAME: string = 'MarqueeDisplayScene'; - build() { - Scroll() { - // 'app.media.startIcon' is just an example, please replace the actual image. - Image($r('app.media.startIcon')) - .width('80%') - } - .scrollable(ScrollDirection.FREE) - .width('80%') - .attributeModifier(this.adaptModifier) +export const AUDIO_VOLUME_SCENE_NAME: string = 'AudioVolumeScene'; + +export const ROUTES: Route[] = [ + { + title: $r('app.string.action_bar_scene'), + to: ACTION_BAR_SCENE_NAME + }, + { + title: $r('app.string.button_display_scene'), + to: BUTTON_DISPLAY_SCENE_NAME + }, + { + title: $r('app.string.scroll_scene'), + to: SCROLL_SCENE_NAME + }, + { + title: $r('app.string.marquee_display_scene'), + to: MARQUEE_DISPLAY_SCENE_NAME + }, + { + title: $r('app.string.audio_volume_scene'), + to: AUDIO_VOLUME_SCENE_NAME } -} +] + diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index d3ee54d2beea61d38866762868bd056d3e39e3c9..4c7be41246b50b10a8f8543f48833a6bc0079597 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -16,10 +16,13 @@ import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { window } from '@kit.ArkUI'; +import { Logger } from '../utils/Logger'; const DOMAIN = 0x0000; export default class EntryAbility extends UIAbility { + private windowClass: window.Window | null = null; + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); @@ -38,6 +41,16 @@ export default class EntryAbility extends UIAbility { hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); return; } + windowStage.getMainWindow().then((windowObj) => { + this.windowClass = windowObj; + try { + AppStorage.setOrCreate('uiContext', this.windowClass.getUIContext()) + } catch (error) { + Logger.error(`Failed to obtain a UIContext instance.`) + } + }).catch(() => { + Logger.error(`Failed to obtain the main window of this window stage.`) + }) hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); }); } diff --git a/entry/src/main/ets/pages/ActionBarScene.ets b/entry/src/main/ets/pages/ActionBarScene.ets new file mode 100644 index 0000000000000000000000000000000000000000..40a5d830bfc665069711d6b772a8233bb214a9aa --- /dev/null +++ b/entry/src/main/ets/pages/ActionBarScene.ets @@ -0,0 +1,121 @@ +/* + * 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 { ActionBarButton, ActionBarStyle, HdsActionBar } from '@kit.UIDesignKit'; +import { deviceInfo } from '@kit.BasicServicesKit'; + +@Builder +export function ActionBarSceneBuilder() { + ActionBarScene() +} + +@Component +struct ActionBarScene { + @State isExpand: boolean = true; + @State isPrimaryIconChanged: boolean = false; + + aboutToAppear(): void { + } + + build() { + // [Start action_bar_api] + NavDestination() { + Column() { + // Regarding the proprietary interfaces of HarmonyOS, specifically the interfaces marked as since M.F.S(N). + // Compatibility judgment, the value corresponding to version 6.0.0(20) is 60000, + // which is derived from the new interface's since field 6*10000 + 0*100 + 0. + if (deviceInfo.distributionOSApiVersion >= 60000) { + // Component that calls the API of version 6.0.0(20) + HdsActionBar({ + startButtons: [new ActionBarButton({ + baseIcon: $r('sys.symbol.stopwatch_fill') + })], + endButtons: [new ActionBarButton({ + baseIcon: $r('sys.symbol.mic_fill') + })], + // [StartExclude action_bar_api] + primaryButton: new ActionBarButton({ + baseIcon: $r('sys.symbol.plus'), + altIcon: $r('sys.symbol.play_fill'), + onClick: () => { + this.isExpand = !this.isExpand; + this.isPrimaryIconChanged = !this.isPrimaryIconChanged; + } + }), + actionBarStyle: new ActionBarStyle({ + isPrimaryIconChanged: this.isPrimaryIconChanged + }), + isExpand: this.isExpand + // [EndExclude action_bar_api] + }) + } else { + // Downgrading plan + Row({ space: 25 }) { + // [StartExclude action_bar_api] + if (this.isExpand) { + Button({ type: ButtonType.Circle }) { + SymbolGlyph($r('sys.symbol.stopwatch_fill')) + .fontSize(24) + .fontColor([$r('sys.color.font_secondary')]) + } + .aspectRatio(1) + .height(45) + .backgroundColor($r('sys.color.background_secondary')) + .margin({ left: 10 }) + } + + Button({ type: ButtonType.Circle }) { + SymbolGlyph(this.isExpand ? $r('sys.symbol.plus') : $r('sys.symbol.play_fill')) + .fontSize(24) + .fontColor([$r('sys.color.white')]) + } + .aspectRatio(1) + .height(55) + .backgroundColor($r('sys.color.brand')) + .onClick(() => { + this.isExpand = !this.isExpand; + }) + + if (this.isExpand) { + Button({ type: ButtonType.Circle }) { + SymbolGlyph($r('sys.symbol.mic_fill')) + .fontSize(24) + .fontColor([$r('sys.color.font_secondary')]) + } + .aspectRatio(1) + .height(45) + .backgroundColor($r('sys.color.background_secondary')) + .margin({ right: 10 }) + } + // [EndExclude action_bar_api] + } + // [StartExclude action_bar_api] + .backgroundColor($r('sys.color.background_primary')) + .borderRadius(30) + // [EndExclude action_bar_api] + } + } + // [StartExclude action_bar_api] + .width('100%') + .height('100%') + .justifyContent(FlexAlign.End) + .alignItems(HorizontalAlign.Center) + // [EndExclude action_bar_api] + } + .title($r('app.string.action_bar_scene')) + .backgroundColor($r('app.color.common_backgroundColor')) + // [End action_bar_api] + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/AudioVolumeScene.ets b/entry/src/main/ets/pages/AudioVolumeScene.ets new file mode 100644 index 0000000000000000000000000000000000000000..def383de9356dba7e6f27b98eb89ff1669986bcb --- /dev/null +++ b/entry/src/main/ets/pages/AudioVolumeScene.ets @@ -0,0 +1,49 @@ +/* + * 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 testNapi from 'libentry.so'; +import CommonUtils from '../utils/CommonUtils'; + +@Builder +export function AudioVolumeSceneBuilder() { + AudioVolumeScene() +} + +@Builder +function AudioVolumeScene() { + NavDestination() { + Column() { + Row() { + Button($r('app.string.audio_volume_button_label')) + .width('100%') + .onClick(() => { + const isExisted = testNapi.checkAudioVolumeManagerExists(); + const ctx = AppStorage.get('uiContext') as UIContext | undefined; + CommonUtils.showToast(ctx, isExisted ? $r('app.string.support_label') : $r('app.string.unsupport_label')); + }) + } + .width('100%') + } + .justifyContent(FlexAlign.End) + .padding({ + left: 16, + right: 16 + }) + .width('100%') + .height('100%') + } + .title($r('app.string.audio_volume_scene')) + .backgroundColor($r('app.color.common_backgroundColor')) +} \ No newline at end of file diff --git a/entry/src/main/ets/components/NativeButtonAdapter.ets b/entry/src/main/ets/pages/ButtonDisplayScene.ets similarity index 57% rename from entry/src/main/ets/components/NativeButtonAdapter.ets rename to entry/src/main/ets/pages/ButtonDisplayScene.ets index fd99dd4386e36c9dc8be3642375c128318600904..03856b4d493d12a4f1aa3a7ae99e46aa65536127 100644 --- a/entry/src/main/ets/components/NativeButtonAdapter.ets +++ b/entry/src/main/ets/pages/ButtonDisplayScene.ets @@ -16,27 +16,40 @@ import { NodeContent } from '@kit.ArkUI'; import nativeNode from 'libentry.so'; +@Builder +export function ButtonDisplaySceneBuilder() { + ButtonDisplayScene() +} @Component -export struct NativeButtonAdapter { +struct ButtonDisplayScene { private rootSlot = new NodeContent(); aboutToAppear(): void { - nativeNode.CreateButtonNativeRoot(this.rootSlot); + nativeNode.createButtonNativeRoot(this.rootSlot); } aboutToDisappear(): void { - nativeNode.DestroyButtonNativeRoot(); + nativeNode.destroyButtonNativeRoot(); } build() { - Column() { - Row() { - // Bind NodeContent and ContentSlot placeholder components. - ContentSlot(this.rootSlot) + NavDestination() { + Column() { + Row() { + // Bind NodeContent and ContentSlot placeholder components. + ContentSlot(this.rootSlot) + } } + .justifyContent(FlexAlign.End) + .padding({ + left: 16, + right: 16 + }) + .width('100%') + .height('100%') } - .width('100%') - .height('30%') + .title($r('app.string.button_display_scene')) + .backgroundColor($r('app.color.common_backgroundColor')) } -} +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 90d1e6d188728732bf8b0c2d1f13ee700ea168fc..2754c69050b29ff604e106199e575f66a5a22cfb 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -13,22 +13,55 @@ * limitations under the License. */ -import { ScrollComponentAdapter } from '../components/ScrollComponentAdapter'; -import { TextComponentAdapter } from '../components/TextComponentAdapter'; -import { ActionBarAdapter } from '../components/ActionBarAdapter'; -import { NativeButtonAdapter } from '../components/NativeButtonAdapter'; +import { Route, ROUTES } from '../contants/CommonConstants'; @Entry @Component struct Index { + private routes: Route[] = ROUTES; + private pageInfos: NavPathStack = new NavPathStack(); build() { - Column() { - ScrollComponentAdapter() - TextComponentAdapter() - ActionBarAdapter() - NativeButtonAdapter() + Navigation(this.pageInfos) { + List() { + ForEach(this.routes, (item: Route, index: number) => { + ListItem() { + Column() { + Row() { + Text(item.title) + .width('91.1%') + .height(48) + .fontWeight(FontWeight.Medium) + .fontSize('16fp') + SymbolGlyph($r('sys.symbol.chevron_forward')) + .fontSize($r('sys.float.padding_level12')) + .fontColor([$r('sys.color.font_tertiary')]) + } + .padding({ left: 12 }) + .onClick(() => { + this.pageInfos.pushPath({ + name: item.to + }) + }) + if (this.routes.length - 1 !== index) { + Divider() + .strokeWidth(0.5) + .color('#0D000000') + .width('93%') + } + } + } + .height(48) + }, (item: Route, index: number) => JSON.stringify(item) + index) + } + .borderRadius(15) + .width('91.1%') + .height(48 * this.routes.length) + .backgroundColor(Color.White) } + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .height('100%') .width('100%') + .title($r('app.string.index_label')) + .backgroundColor($r('app.color.common_backgroundColor')) } } diff --git a/entry/src/main/ets/components/TextComponentAdapter.ets b/entry/src/main/ets/pages/MarqueeDisplayScene.ets similarity index 64% rename from entry/src/main/ets/components/TextComponentAdapter.ets rename to entry/src/main/ets/pages/MarqueeDisplayScene.ets index b28c8efbdb66dcdf568921d8cb05e8ebd2b95473..6ee165e19b2536cd6d38414a1c5e68fc16877ff3 100644 --- a/entry/src/main/ets/components/TextComponentAdapter.ets +++ b/entry/src/main/ets/pages/MarqueeDisplayScene.ets @@ -13,7 +13,7 @@ * limitations under the License. */ -import { deviceInfo } from "@kit.BasicServicesKit"; +import { deviceInfo } from '@kit.BasicServicesKit'; class MyModifier implements AttributeModifier { applyNormalAttribute(instance: TextAttribute): void { @@ -26,16 +26,26 @@ class MyModifier implements AttributeModifier { } } -@Component -export struct TextComponentAdapter { - private message: string = 'This a a simple Code of Text Marquee'; - private adaptModifier: MyModifier = new MyModifier(); +@Builder +export function MarqueeDisplaySceneBuilder() { + MarqueeDisplayScene() +} - build() { - Text(this.message) - .fontSize(24) - .height(200) - .width('100%') - .attributeModifier(this.adaptModifier) +@Builder +function MarqueeDisplayScene() { + NavDestination() { + Column() { + Row() { + Text('This is a simple Code of Text Marquee.') + .fontSize(24) + .height(200) + .width('100%') + .attributeModifier(new MyModifier()) + } + } + .width('100%') + .height('100%') } -} + .title($r('app.string.marquee_display_scene')) + .backgroundColor($r('app.color.common_backgroundColor')) +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/ScrollScene.ets b/entry/src/main/ets/pages/ScrollScene.ets new file mode 100644 index 0000000000000000000000000000000000000000..5f7d4bef722132b8376a3a8c216d92719b709b0d --- /dev/null +++ b/entry/src/main/ets/pages/ScrollScene.ets @@ -0,0 +1,189 @@ +/* + * 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 { deviceInfo } from '@kit.BasicServicesKit'; +import CommonUtils from '../utils/CommonUtils'; +import { Logger } from '../utils/Logger'; + +// Type definition for feature installer function that applies specific features to a ScrollAttribute instance +type FeatureInstaller = (instance: ScrollAttribute) => void; + +// Interface defining a feature specification with minimum API version requirement and installer function +interface FeatureSpec { + minVersion: number; + installer: FeatureInstaller; +} + +const ctx = AppStorage.get('uiContext') as UIContext | undefined; +// Registry to track whether onDidScroll feature has been installed for a ScrollAttribute instance +// Using WeakSet to avoid memory leaks and allow garbage collection of unused instances +const didScrollRegistry = new WeakSet(); +// Registry to track whether onScrollEdge feature has been installed for a ScrollAttribute instance +const scrollEdgeRegistry = new WeakSet(); +// Registry to track whether zoom features have been installed for a ScrollAttribute instance +const zoomRegistry = new WeakSet(); +// Registry to ensure the toast is shown only once per fling cycle +const flingToastRegistry = new WeakMap(); + +// Array of feature specifications organized by minimum API version requirements +const FEATURE_SPECS: ReadonlyArray = [ + { + minVersion: 12, + installer: (instance: ScrollAttribute) => { + // Skip installation if this feature is already registered for the instance + if (didScrollRegistry.has(instance)) { + return; + } + // API 12+ supports `onDidScroll` callback for tracking scroll events + instance.onDidScroll((_xOffset: number, _yOffset: number, scrollState: ScrollState) => { + const hasToastShown = flingToastRegistry.get(instance) ?? false; + if (scrollState === ScrollState.Fling) { + if (!hasToastShown) { + CommonUtils.showToast(ctx, $r('app.string.fling_prompt_dialog_tip')); + flingToastRegistry.set(instance, true); + } + } else if (hasToastShown) { + // Reset the flag when leaving the fling state so future fling actions can trigger the toast again + flingToastRegistry.set(instance, false); + } + }); + // Mark this instance as having the feature installed to prevent duplicate registration + didScrollRegistry.add(instance); + } + }, + { + minVersion: 18, + installer: (instance: ScrollAttribute) => { + // Skip installation if this feature is already registered for the instance + if (scrollEdgeRegistry.has(instance)) { + return; + } + // API 18+ enhances scroll edge detection capabilities + instance.onScrollEdge((side: Edge) => { + if (side === Edge.Top) { + CommonUtils.showToast(ctx, $r('app.string.scroll_to_top_tip')); + } else if (side === Edge.Bottom) { + CommonUtils.showToast(ctx, $r('app.string.scroll_to_bottom_tip')); + } + }); + // Mark this instance as having the feature installed to prevent duplicate registration + scrollEdgeRegistry.add(instance); + } + }, + { + minVersion: 20, + installer: (instance: ScrollAttribute) => { + // Skip installation if this feature is already registered for the instance + if (zoomRegistry.has(instance)) { + return; + } + // API 20+ introduces zoom capabilities for scrollable content + instance.maxZoomScale(2); + instance.minZoomScale(1); + instance.enableBouncesZoom(true); + + // Register zoom event callbacks + instance.onDidZoom((scale: number) => { + Logger.info(`onDidZoom:${scale}`); + }); + instance.onZoomStart(() => { + CommonUtils.showToast(ctx, $r('app.string.zoom_start_trigger_tip')); + }); + instance.onZoomStop(() => { + CommonUtils.showToast(ctx, $r('app.string.zoom_stop_trigger_tip')); + }); + + // Mark this instance as having the feature installed to prevent duplicate registration + zoomRegistry.add(instance); + } + } +]; + +// Custom attribute modifier that dynamically applies features based on device API version +class MyModifier implements AttributeModifier { + applyNormalAttribute(instance: ScrollAttribute): void { + // Get current device SDK API version + const sdkVersion: number = deviceInfo.sdkApiVersion ?? 0; + // Apply all compatible features based on device API version + FEATURE_SPECS.forEach(feature => { + if (sdkVersion >= feature.minVersion) { + feature.installer(instance); + } + }); + } +} + +@Builder +export function ScrollSceneBuilder() { + ScrollScene() +} + +@Component +struct ScrollScene { + // Instance of our custom modifier to apply version-adaptive features + private adaptModifier: MyModifier = new MyModifier(); + // Determine scroll direction based on API version: + // Use FREE direction on API 20+ (supports zoom), otherwise use VERTICAL + private readonly scrollDirection: ScrollDirection = + (deviceInfo.sdkApiVersion ?? 0) >= 20 ? ScrollDirection.FREE : ScrollDirection.Vertical; + // Sample data array for rendering scrollable content + private arr: number[] = []; + + aboutToAppear(): void { + for (let i = 1; i <= 12; i++) { + this.arr.push(i); + } + } + + build() { + NavDestination() { + Column() { + Scroll() { + Column() { + ForEach(this.arr, (item: number, index: number) => { + Row() { + if (index % 2 === 0) { + Text(item.toString()) + .fontSize(16) + .textAlign(TextAlign.Center) + } else { + Image($r('app.media.daily_video')) + .objectFit(ImageFit.Cover) + } + } + .width('90%') + .height(150) + .backgroundColor(0xFFFFFF) + .borderRadius(15) + .clip(true) + .justifyContent(FlexAlign.Center) + .margin({ top: 10 }) + }, (item: number, index: number) => `${index}_${item}`) + } + .width('100%') + } + .scrollable(this.scrollDirection) + .scrollBar(BarState.Off) + .width('100%') + .attributeModifier(this.adaptModifier) + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + } + .width('100%') + .height('100%') + } + .title($r('app.string.scroll_scene')) + .backgroundColor($r('app.color.common_backgroundColor')) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/CommonUtils.ets b/entry/src/main/ets/utils/CommonUtils.ets new file mode 100644 index 0000000000000000000000000000000000000000..8fab8e0b10c3a448dff5d6fa12713d8000d1b2c1 --- /dev/null +++ b/entry/src/main/ets/utils/CommonUtils.ets @@ -0,0 +1,37 @@ +/* + * 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 { Logger } from './Logger'; + +/** + * Common utility helpers shared across the project. + */ +export default class CommonUtils { + public static showToast(ctx: UIContext | undefined, curMsg: ResourceStr) { + const uiContext = ctx ?? AppStorage.get('uiContext') as UIContext | undefined; + if (!uiContext) { + Logger.warn('UIContext is not available'); + return; + } + try { + uiContext?.getPromptAction().showToast({ + message: curMsg, + duration: 1500 + }); + } catch (error) { + Logger.error(`Failed to open toast dialog, error info: ${JSON.stringify(error)}`); + } + } +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/Logger.ets b/entry/src/main/ets/utils/Logger.ets new file mode 100644 index 0000000000000000000000000000000000000000..5b50f8509ed45ac59d2c95b318af8d0508f99328 --- /dev/null +++ b/entry/src/main/ets/utils/Logger.ets @@ -0,0 +1,60 @@ +/* + * 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 { hilog } from '@kit.PerformanceAnalysisKit'; + +/** + * Logger utility to centralize log formatting and output. + */ +export class Logger { + // Log domain to distinguish module source. + private static readonly domain: number = 0xFF00; + // Log prefix for quick issue localization. + private static readonly prefix: string = 'APILevelAdapt'; + // Log format template. + private static format: string = '%{public}s'; + + /** + * Print a debug level log. + * @param args Debug message content. + */ + static debug(...args: string[]): void { + hilog.debug(Logger.domain, Logger.prefix, Logger.format, ...args); + } + + /** + * Print an info level log. + * @param args Information content. + */ + static info(...args: string[]): void { + hilog.info(Logger.domain, Logger.prefix, Logger.format, ...args); + } + + /** + * Print a warning level log. + * @param args Warning content. + */ + static warn(...args: string[]): void { + hilog.warn(Logger.domain, Logger.prefix, Logger.format, ...args); + } + + /** + * Print an error level log. + * @param args Error content. + */ + static error(...args: string[]): void { + hilog.error(Logger.domain, Logger.prefix, Logger.format, ...args); + } +} \ No newline at end of file diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index 53024e8f020cc0824592631ba6924108c8028f0f..5a4061626e928c13c5c8a51f20235e1d47549a40 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -10,6 +10,7 @@ "deliveryWithInstall": true, "installationFree": false, "pages": "$profile:main_pages", + "routerMap": "$profile:router_map", "abilities": [ { "name": "EntryAbility", diff --git a/entry/src/main/resources/base/element/color.json b/entry/src/main/resources/base/element/color.json index 3c712962da3c2751c2b9ddb53559afcbd2b54a02..886bf7b59aa42889d6182fcb662536cdb1135d41 100644 --- a/entry/src/main/resources/base/element/color.json +++ b/entry/src/main/resources/base/element/color.json @@ -3,6 +3,10 @@ { "name": "start_window_background", "value": "#FFFFFF" + }, + { + "name": "common_backgroundColor", + "value": "#F1F3F5" } ] } \ No newline at end of file diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index 720747a71938e8c0e7c70cc781227de15f759c29..a8d5e7d0e12fe8a2941d4c6311570d8342def594 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -11,6 +11,62 @@ { "name": "EntryAbility_label", "value": "APILevelAdapt" + }, + { + "name": "action_bar_scene", + "value": "Action Bar Scene" + }, + { + "name": "button_display_scene", + "value": "Button Display Scene" + }, + { + "name": "scroll_scene", + "value": "Scroll Scene" + }, + { + "name": "marquee_display_scene", + "value": "Marquee Display Scene" + }, + { + "name": "audio_volume_scene", + "value": "Audio Volume Scene" + }, + { + "name": "index_label", + "value": "Multi-API Version Compatibility Sample" + }, + { + "name": "audio_volume_button_label", + "value": "Check volume manager support" + }, + { + "name": "support_label", + "value": "Supported" + }, + { + "name": "unsupport_label", + "value": "Not Supported" + }, + { + "name": "fling_prompt_dialog_tip", + "value": "Inertial scrolling..." + }, + { + "name": "scroll_to_top_tip", + "value": "Scrolled to the top of the list!" + }, + { + "name": "scroll_to_bottom_tip", + "value": "Scrolled to the bottom of the list!" + }, + { + "name": "zoom_start_trigger_tip", + "value": "Zoom gesture started!" + }, + { + "name": "zoom_stop_trigger_tip", + "value": "Zoom gesture ended!" } ] } \ No newline at end of file diff --git a/entry/src/main/resources/base/media/daily_video.png b/entry/src/main/resources/base/media/daily_video.png new file mode 100644 index 0000000000000000000000000000000000000000..50d1bc6164ec948f602520920684b00ea21e01c8 Binary files /dev/null and b/entry/src/main/resources/base/media/daily_video.png differ diff --git a/entry/src/main/resources/base/profile/router_map.json b/entry/src/main/resources/base/profile/router_map.json new file mode 100644 index 0000000000000000000000000000000000000000..8c58a71f8fb73e1d52ddc2a358050427f9d8871d --- /dev/null +++ b/entry/src/main/resources/base/profile/router_map.json @@ -0,0 +1,44 @@ +{ + "routerMap": [ + { + "name": "ActionBarScene", + "pageSourceFile": "src/main/ets/pages/ActionBarScene.ets", + "buildFunction": "ActionBarSceneBuilder", + "data": { + "description": "this is ActionBarScene page." + } + }, + { + "name": "ButtonDisplayScene", + "pageSourceFile": "src/main/ets/pages/ButtonDisplayScene.ets", + "buildFunction": "ButtonDisplaySceneBuilder", + "data": { + "description": "this is ButtonDisplayScene page." + } + }, + { + "name": "ScrollScene", + "pageSourceFile": "src/main/ets/pages/ScrollScene.ets", + "buildFunction": "ScrollSceneBuilder", + "data": { + "description": "this is ScrollScene page." + } + }, + { + "name": "MarqueeDisplayScene", + "pageSourceFile": "src/main/ets/pages/MarqueeDisplayScene.ets", + "buildFunction": "MarqueeDisplaySceneBuilder", + "data": { + "description": "this is MarqueeDisplayScene page." + } + }, + { + "name": "AudioVolumeScene", + "pageSourceFile": "src/main/ets/pages/AudioVolumeScene.ets", + "buildFunction": "AudioVolumeSceneBuilder", + "data": { + "description": "this is AudioVolumeScene page." + } + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/en_US/element/string.json b/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..a8d5e7d0e12fe8a2941d4c6311570d8342def594 --- /dev/null +++ b/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,72 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "APILevelAdapt" + }, + { + "name": "action_bar_scene", + "value": "Action Bar Scene" + }, + { + "name": "button_display_scene", + "value": "Button Display Scene" + }, + { + "name": "scroll_scene", + "value": "Scroll Scene" + }, + { + "name": "marquee_display_scene", + "value": "Marquee Display Scene" + }, + { + "name": "audio_volume_scene", + "value": "Audio Volume Scene" + }, + { + "name": "index_label", + "value": "Multi-API Version Compatibility Sample" + }, + { + "name": "audio_volume_button_label", + "value": "Check volume manager support" + }, + { + "name": "support_label", + "value": "Supported" + }, + { + "name": "unsupport_label", + "value": "Not Supported" + }, + { + "name": "fling_prompt_dialog_tip", + "value": "Inertial scrolling..." + }, + { + "name": "scroll_to_top_tip", + "value": "Scrolled to the top of the list!" + }, + { + "name": "scroll_to_bottom_tip", + "value": "Scrolled to the bottom of the list!" + }, + { + "name": "zoom_start_trigger_tip", + "value": "Zoom gesture started!" + }, + { + "name": "zoom_stop_trigger_tip", + "value": "Zoom gesture ended!" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..01703d42e0c3a20143de818e85ce2e2cd42aac21 --- /dev/null +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,72 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "APILevelAdapt" + }, + { + "name": "action_bar_scene", + "value": "动作条场景" + }, + { + "name": "button_display_scene", + "value": "按钮展示场景" + }, + { + "name": "scroll_scene", + "value": "滚动场景" + }, + { + "name": "marquee_display_scene", + "value": "走马灯场景" + }, + { + "name": "audio_volume_scene", + "value": "音频音量场景" + }, + { + "name": "index_label", + "value": "多API版本兼容示例" + }, + { + "name": "audio_volume_button_label", + "value": "检测是否支持音量管理器相关功能" + }, + { + "name": "support_label", + "value": "支持" + }, + { + "name": "unsupport_label", + "value": "不支持" + }, + { + "name": "fling_prompt_dialog_tip", + "value": "惯性滚动中..." + }, + { + "name": "scroll_to_top_tip", + "value": "滚动到了顶部!" + }, + { + "name": "scroll_to_bottom_tip", + "value": "滚动到了底部!" + }, + { + "name": "zoom_start_trigger_tip", + "value": "手势缩放开始!" + }, + { + "name": "zoom_stop_trigger_tip", + "value": "手势缩放停止!" + } + ] +} \ No newline at end of file diff --git a/screenshots/pic1.png b/screenshots/pic1.png new file mode 100644 index 0000000000000000000000000000000000000000..d2d3ff9dd1859c9e647e9301caa954edb64935c1 Binary files /dev/null and b/screenshots/pic1.png differ diff --git a/screenshots/pic2.png b/screenshots/pic2.png new file mode 100644 index 0000000000000000000000000000000000000000..fa64dd0d4a337de3284591f638863f4d131760bc Binary files /dev/null and b/screenshots/pic2.png differ diff --git a/screenshots/pic3.png b/screenshots/pic3.png new file mode 100644 index 0000000000000000000000000000000000000000..d56c7c979a2e267bd3ef395dbcfdc7ef51a10b09 Binary files /dev/null and b/screenshots/pic3.png differ