diff --git a/.vscode/.cache/clangd/wecode-cpp.db b/.vscode/.cache/clangd/wecode-cpp.db new file mode 100644 index 0000000000000000000000000000000000000000..c5962f5b6be8cbeede4068fca0818ad92d227dbe Binary files /dev/null and b/.vscode/.cache/clangd/wecode-cpp.db differ diff --git a/.vscode/tags-34.wecode-db b/.vscode/tags-34.wecode-db new file mode 100644 index 0000000000000000000000000000000000000000..d33143e48b02381cb6188af91f2149c44a70ade6 Binary files /dev/null and b/.vscode/tags-34.wecode-db differ diff --git a/.vscode/tags-34.wecode-lock b/.vscode/tags-34.wecode-lock new file mode 100644 index 0000000000000000000000000000000000000000..fd1e976b3908e713eff26061ab401cf0d0f2de82 Binary files /dev/null and b/.vscode/tags-34.wecode-lock differ diff --git a/ArkUIKit/FloatingBall/entry/src/main/ets/pages/Index.ets b/ArkUIKit/FloatingBall/entry/src/main/ets/pages/Index.ets index 0f49e19a196bf0dae0d59985926a911e96d8a49f..fd172dcfe2801fd7d9617c14fa275e67da3ab55e 100644 --- a/ArkUIKit/FloatingBall/entry/src/main/ets/pages/Index.ets +++ b/ArkUIKit/FloatingBall/entry/src/main/ets/pages/Index.ets @@ -14,7 +14,7 @@ */ // [Start floating_ball_index] -// Index.ts +// Index.ets // 该页面利用按钮点击事件展示闪控球基本操作 import hilog from '@ohos.hilog'; import image from '@ohos.multimedia.image'; diff --git a/ArkUIWindowPipSamples/WindowPip/.gitignore b/ArkUIWindowPipSamples/WindowPip/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d2ff20141ceed86d87c0ea5d99481973005bab2b --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/AppScope/app.json5 b/ArkUIWindowPipSamples/WindowPip/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..6bceeef43b4f189418dd4f3d1ed85f5d8f9ae8d7 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/AppScope/app.json5 @@ -0,0 +1,25 @@ +/* +* 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. +*/ + +{ + "app": { + "bundleName": "com.sample.windowpip", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/element/string.json b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..75a8bcbbda2194c1a88411f69ce7c6339a3f22bb --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "WindowPip" + } + ] +} diff --git a/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/background.png b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/background.png differ diff --git a/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/foreground.png b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9427585b36d14b12477435b6419d1f07b3e0bb Binary files /dev/null and b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/foreground.png differ diff --git a/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/layered_image.json b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/README.md b/ArkUIWindowPipSamples/WindowPip/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e8cdff1398c6b4439ae9cb7f758ad7aa30799d95 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/README.md @@ -0,0 +1,106 @@ +# 实现画中画效果 + +## 介绍 + +本示例基于媒体服务和ArkUI的基本能力,通过XComponent、typeNode和NDK几种不同的方式来实现视频播放、手动和自动拉起画中画、画中画窗口控制视频播放和暂停等功能。 + +## 效果预览 + +![](screenshots/XComponentImplement2.gif) + +## 使用说明 + +1. 在主界面,点击不同的画中画实现方式查看对应效果; +2. 进入详情页面点击**startPip**,应用**拉起画中画**,点击**stopPip**,**关闭画中画**,点击**updateSize**,**改变画中画样式大小**; +3. 如果进入NDK接口实现的详情页面,先点击**更换模板**,选择画中画的样式,然后点击**创建画中画**,最后点击**开启画中画**即可查看具体样式效果。 + +## 工程目录 + +``` +├──entry/src/main +│ ├──cpp +│ │ ├──types/libentry +│ │ │ ├──index.d.ts // 提供JS侧的接口方法 +│ │ │ └──oh-package.json5 // 将index.d.ts与cpp文件关联 +│ │ ├──CMakeLists.txt // 配置CMake打包参数 +│ │ └──napi_ini.cpp // 实现Native侧的接口 +│ ├──ets +│ │ ├──ability +│ │ │ ├──PipManager.ets // 使用单页面Ability实现时的画中画控制器 +│ │ │ └──XCNodeController.ets // 使用单页面Ability实现时的自定义节点 +│ │ ├──entryability +│ │ │ └──EntryAbility.ets // 程序入口类 +│ │ ├──entryabackupbility +│ │ │ └──EntryBackupAbility.ets // 数据备份、恢复入口类 +│ │ ├──model +│ │ │ ├──AVPlayer.ets // 公共简易播放器 +│ │ │ └──NDKAVPlayer.ets // NDK实现的简易播放器 +│ │ ├──navigation +│ │ │ ├──Page1.ets // 使用Navigation导航时通过typeNode实现的画中画页面 +│ │ │ ├──PipManager.ets // 使用Navigation导航时通过typeNode实现的画中画控制器 +│ │ │ └──XCNodeController.ets // 使用Navigation导航时通过typeNode实现的自定义节点 +│ │ ├──nodeFree +│ │ │ └──PipManager.ets // 使用typeNode自由节点实现时的画中画控制器 +│ │ ├──pages +│ │ │ ├──AbilityImplementPage.ets // 使用单页面Ability实现时的主页面 +│ │ │ ├──Index.ets // 应用首页 +│ │ │ ├──NavigationImplementPage.ets // 使用Navigation导航时通过typeNode实现的主页面 +│ │ │ ├──NDKImplementPage.ets // 主页面,使用NDK接口实现 +│ │ │ ├──RouterImplementPage.ets // 使用Router导航时通过typeNode实现的主页面 +│ │ │ ├──TypeNodeFreePage.ets // 使用typeNode自由节点实现时的主页面 +│ │ │ └──XComponentImplementPage.ets // 使用XComponent实现时的主页面 +│ │ ├──route +│ │ │ ├──Page1.ets // 使用Router导航时通过typeNode实现的画中画页面 +│ │ │ ├──PipManager.ets // 使用Router导航时通过typeNode实现的画中画控制器 +│ │ │ └──XCNodeController.ets // 使用Router导航时通过typeNode实现的自定义节点 +│ │ ├──util +│ │ │ └──LogUtil.ets // 日志工具类 +│ │ ├──xcomponent +│ │ │ ├──AVPlayer.ets // 使用XComponent实现时的简易播放器 +│ │ │ └──Page1.ets // 使用XComponent实现时的画中画页面 +│ └────resources // 应用资源目录 +``` +## 具体实现 +1. 创建画中画控制器,注册生命周期事件以及控制事件回调。 + * 通过create(config: PiPConfiguration)接口创建画中画控制器实例。 + * 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画。 + * 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调。 + * 通过画中画控制器实例的on('controlPanelActionEvent')接口注册控制事件回调。 + +2. 启动画中画。 + + 创建画中画控制器实例后,通过startPiP接口启动画中画。 + +3. 更新媒体源尺寸信息。 + + 画中画媒体源更新后(如切换视频),通过画中画控制器实例的updateContentSize接口更新媒体源尺寸信息,以调整画中画窗口比例。 + +4. 关闭画中画。 + + 当不再需要显示画中画时,可根据业务需要,通过画中画控制器实例的stopPiP接口关闭画中画。 + +## 相关权限 + +不涉及 + +## 依赖 + +不涉及 + +## 约束与限制 + +1. 在API version 20之前,支持在Phone、Tablet设备使用XComponent实现画中画功能开发;从API version 20开始,支持在Phone、PC/2in1、Tablet设备使用XComponent实现画中画功能开发; + +2. 在API version 20之前,支持在Phone、Tablet设备使用typeNode实现画中画功能开发;从API version 20开始,支持在Phone、PC/2in1、Tablet设备使用typeNode实现画中画功能开发; + +3. 从API version 20开始,支持在Phone、PC/2in1、Tablet设备使用NDK接口实现画中画功能开发。 + +## 下载 +如需单独下载本工程,执行如下命令: +``` +git init +git config core.sparsecheckout true +echo code/DocsSample/ArkUIWindowPipSamples/WindowPip > .git/info/sparse-checkout +git remote add origin https://gitcode.com/openharmony/applications_app_samples.git +git pull origin master +``` \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/build-profile.json5 b/ArkUIWindowPipSamples/WindowPip/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..a9f20efb63aec1923fadbdee853db4c7e1ad98f7 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/build-profile.json5 @@ -0,0 +1,57 @@ +/* +* 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. +*/ + +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "6.0.0(20)", + "compatibleSdkVersion": "6.0.0(20)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/code-linter.json5 b/ArkUIWindowPipSamples/WindowPip/code-linter.json5 new file mode 100644 index 0000000000000000000000000000000000000000..e933bb7e85b32c303c78b409b44abe8e2f198645 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/code-linter.json5 @@ -0,0 +1,47 @@ +/* +* 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. +*/ + +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@security/no-unsafe-aes": "error", + "@security/no-unsafe-hash": "error", + "@security/no-unsafe-mac": "warn", + "@security/no-unsafe-dh": "error", + "@security/no-unsafe-dsa": "error", + "@security/no-unsafe-ecdsa": "error", + "@security/no-unsafe-rsa-encrypt": "error", + "@security/no-unsafe-rsa-sign": "error", + "@security/no-unsafe-rsa-key": "error", + "@security/no-unsafe-dsa-key": "error", + "@security/no-unsafe-dh-key": "error", + "@security/no-unsafe-3des": "error" + } +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/.gitignore b/ArkUIWindowPipSamples/WindowPip/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/build-profile.json5 b/ArkUIWindowPipSamples/WindowPip/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..c8185284d48889a6082ec715e530862d5bb327bf --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/build-profile.json5 @@ -0,0 +1,59 @@ +/* +* 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. +*/ + +{ + "apiType": "stageMode", + "buildOption": { + "resOptions": { + "copyCodeResource": { + "enable": false + } + }, + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "", + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + }, + "nativeLib": { + "debugSymbol": { + "strip": true, + "exclude": [] + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/hvigorfile.ts b/ArkUIWindowPipSamples/WindowPip/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..3460f55472ee2645d5367dbc3c409ca14235669c --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/hvigorfile.ts @@ -0,0 +1,21 @@ +/* +* 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 { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/obfuscation-rules.txt b/ArkUIWindowPipSamples/WindowPip/entry/obfuscation-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..272efb6ca3f240859091bbbfc7c5802d52793b0b --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/oh-package.json5 b/ArkUIWindowPipSamples/WindowPip/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..200a09a7febcfa5e38466db7eb66078681252e69 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/oh-package.json5 @@ -0,0 +1,27 @@ +/* +* 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. +*/ + +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "libentry.so": "file:./src/main/cpp/types/libentry" + } +} + diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/CMakeLists.txt b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..67bc360c0c72089483cf5ccea2539d12871c9ca4 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,16 @@ +# [Start ndk_implement_cmake_file] +# CMakeLists.txt +# the minimum version of CMake. +cmake_minimum_required(VERSION 3.5.0) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +project(MyApplication) +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) +target_link_libraries(entry PUBLIC libace_napi.z.so libace_ndk.z.so libnative_window_manager.so libhilog_ndk.z.so) +# [End ndk_implement_cmake_file] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/napi_init.cpp b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/napi_init.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e92107a9e5f4528314adc68678010ce414d9e511 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/napi_init.cpp @@ -0,0 +1,350 @@ +/* +* 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. +*/ + +// [Start ndk_implement_napi_init] +// napi_init.cpp +#include "napi/native_api.h" +#include +#include +#include +#include +#include "window_manager/oh_window_pip.h" +#include "hilog/log.h" +#include "js_native_api.h" +#include "napi/native_api.h" +#include +#include "rawfile/raw_file_manager.h" + +#define LOG_MSG_TAG "PiPMain" +#define LOG(format, ...) ((void)OH_LOG_Print(LOG_APP, LOG_INFO, 0xFF00, LOG_MSG_TAG, format, ##__VA_ARGS__)) +napi_ref jsCallback; +napi_env env_; + +napi_ref jsLifecycleCallback; +napi_env lifeEnv_; +int32_t g_minValue = 0; +int32_t g_maxValue = 255; + +inline bool ConvertFromJsNumber(napi_env env, napi_value jsValue, int32_t& value) +{ + return napi_get_value_int32(env, jsValue, &value) == napi_ok; +} + +inline bool ConvertFromJsNumber(napi_env env, napi_value jsValue, uint32_t& value) +{ + return napi_get_value_uint32(env, jsValue, &value) == napi_ok; +} + +inline bool ConvertFromJsNumber(napi_env env, napi_value jsValue, int64_t& value) +{ + return napi_get_value_int64(env, jsValue, &value) == napi_ok; +} + +inline bool ConvertFromJsNumber(napi_env env, napi_value jsValue, uint64_t& value) +{ + int64_t num; + auto res = napi_get_value_int64(env, jsValue, &num); + if (res == napi_ok) { + value = static_cast(num); + } + return res == napi_ok; +} + +inline bool ConvertFromJsNumber(napi_env env, napi_value jsValue, double& value) +{ + return napi_get_value_double(env, jsValue, &value) == napi_ok; +} + +inline bool ConvertFromJsNumber(napi_env env, napi_value jsValue, bool& value) +{ + return napi_get_value_bool(env, jsValue, &value) == napi_ok; +} + +inline bool ConvertFromJsNumber(napi_env env, napi_value jsValue, unsigned char& value) +{ + int32_t num; + if (napi_get_value_int32(env, jsValue, &num) != napi_ok) { + return false; + } + if (num < g_minValue || num > g_maxValue) { + return false; // 越界无效 + } + value = static_cast(num); + return true; +} + +template +bool ConvertFromJsValue(napi_env env, napi_value jsValue, T& value) +{ + if (jsValue == nullptr) { + return false; + } + using ValueType = std::remove_cv_t>; + if constexpr (std::is_same_v) { + return napi_get_value_bool(env, jsValue, &value) == napi_ok; + } else if constexpr (std::is_arithmetic_v) { + return ConvertFromJsNumber(env, jsValue, value); + } else if constexpr (std::is_same_v) { + size_t len = 0; + if (napi_get_value_string_utf8(env, jsValue, nullptr, 0, &len) != napi_ok) { + return false; + } + auto buffer = std::make_unique(len + 1); + size_t strLength = 0; + if (napi_get_value_string_utf8(env, jsValue, buffer.get(), len + 1, &strLength) == napi_ok) { + value = buffer.get(); + return true; + } + return false; + } else if constexpr (std::is_enum_v) { + std::make_signed_t numberValue = 0; + if (!ConvertFromJsNumber(env, jsValue, numberValue)) { + return false; + } + value = static_cast(numberValue); + return true; + } + return false; +} + +void PipStartPipCallback(uint32_t controllerId, uint8_t requestId, uint64_t surfaceId) +{ + if (jsCallback) { + napi_value global = nullptr; + napi_get_global(env_, &global); + size_t argc = 1; + std::string tStr = std::to_string(surfaceId); + const char* cStr = tStr.c_str(); + size_t length = strlen(cStr); + napi_value str; + napi_status status = napi_create_string_utf8(env_, cStr, length, &str); + napi_value argv[1] = {str}; + napi_value jsCallbackValue; + + napi_value result = nullptr; + if (!jsCallback) { + LOG("js callback is invalid"); + } + napi_get_reference_value(env_, jsCallback, &jsCallbackValue); + napi_call_function(env_, global, jsCallbackValue, argc, argv, &result); + } +} + +void LifecycleCallback(uint32_t controllerId, PictureInPicture_PipState state, int32_t errcode) +{ + if (jsLifecycleCallback) { + napi_value global = nullptr; + napi_get_global(lifeEnv_, &global); + size_t argc = 1; + napi_value pipState = nullptr; + napi_create_int32(lifeEnv_, static_cast (state), &pipState); + napi_value argv[1] = {pipState}; + napi_value jsCallbackValue; + + napi_value result = nullptr; + if (!jsCallback) { + LOG("js callback is invalid"); + } + napi_get_reference_value(lifeEnv_, jsLifecycleCallback, &jsCallbackValue); + napi_call_function(lifeEnv_, global, jsCallbackValue, argc, argv, &result); + } +} + +class PiPManager { +public: + static napi_value CreatePip(napi_env env, napi_callback_info info); + static napi_value StartPip(napi_env env, napi_callback_info info); + static napi_value RegisterStartPip(napi_env env, napi_callback_info info); + static napi_value DeletePip(napi_env env, napi_callback_info info); + static napi_value StopPip(napi_env env, napi_callback_info info); + static napi_value RegisterLifecycleListener(napi_env env, napi_callback_info info); + static void getElement(napi_env &env, uint32_t size, napi_value &controlGroupValue, + PictureInPicture_PipControlGroup controlGroup[]); +}; + +napi_value PiPManager::CreatePip(napi_env env, napi_callback_info info) +{ + size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + napi_value config = argv[0]; + + napi_value mainWindowIdValue = nullptr; + napi_value pipTemplateTypeValue = nullptr; + napi_value widthValue = nullptr; + napi_value heightValue = nullptr; + napi_value controlGroupValue = nullptr; + napi_value pipControllerIdValue = nullptr; + + uint32_t controllerId = -1; + uint32_t mainWindowId = -1; + PictureInPicture_PipTemplateType pipTemplateType = PictureInPicture_PipTemplateType::VIDEO_PLAY; + uint32_t width = -1; + uint32_t height = -1; + + napi_get_named_property(env, config, "mainWindowId", &mainWindowIdValue); + napi_get_named_property(env, config, "pipTemplateType", &pipTemplateTypeValue); + napi_get_named_property(env, config, "width", &widthValue); + napi_get_named_property(env, config, "height", &heightValue); + napi_get_named_property(env, config, "controlGroup", &controlGroupValue); + napi_get_named_property(env, config, "pipControllerId", &pipControllerIdValue); + + ConvertFromJsValue(env, mainWindowIdValue, mainWindowId); + ConvertFromJsValue(env, pipTemplateTypeValue, pipTemplateType); + ConvertFromJsValue(env, widthValue, width); + ConvertFromJsValue(env, heightValue, height); + ConvertFromJsValue(env, pipControllerIdValue, controllerId); + + uint32_t size = 0; + napi_get_array_length(env, controlGroupValue, &size); + PictureInPicture_PipControlGroup controlGroup[size]; + + PiPManager::getElement(env, size, controlGroupValue, controlGroup); + + napi_value result = nullptr; + PictureInPicture_PipConfig pipConfig; + OH_PictureInPicture_CreatePipConfig(&pipConfig); + OH_PictureInPicture_SetPipMainWindowId(pipConfig, mainWindowId); + OH_PictureInPicture_SetPipTemplateType(pipConfig, pipTemplateType); + OH_PictureInPicture_SetPipRect(pipConfig, width, height); + OH_PictureInPicture_SetPipControlGroup(pipConfig, controlGroup, size); + OH_PictureInPicture_SetPipNapiEnv(pipConfig, env); + int32_t res = OH_PictureInPicture_CreatePip(pipConfig, &controllerId); + OH_PictureInPicture_DestroyPipConfig(&pipConfig); + napi_create_uint32(env, controllerId, &result); + return result; +} + +void PiPManager::getElement(napi_env &env, uint32_t size, napi_value &controlGroupValue, + PictureInPicture_PipControlGroup controlGroup[]) +{ + for (uint32_t i = 0; i < size; i++) { + napi_value getElementValue = nullptr; + napi_get_element(env, controlGroupValue, i, &getElementValue); + PictureInPicture_PipControlGroup controlType; + if (ConvertFromJsValue(env, getElementValue, controlType)) { + controlGroup[i] = controlType; + } + LOG("controlType: %{public}d", controlType); + } +} + +napi_value PiPManager::StartPip(napi_env env, napi_callback_info info) +{ + size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + napi_value controlIdValue = argv[0]; + uint32_t controlId = -1; + ConvertFromJsValue(env, controlIdValue, controlId); + napi_value resultValue = nullptr; + int32_t result = OH_PictureInPicture_StartPip(controlId); + napi_create_uint32(env, result, &resultValue); + return resultValue; +} + +napi_value PiPManager::RegisterStartPip(napi_env env, napi_callback_info info) +{ + size_t argc = 2; + napi_value argv[2] = {nullptr}; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + napi_value controllerIdValue = argv[0]; + uint32_t controlId = -1; + napi_status status = napi_create_reference(env, argv[1], 1, &jsCallback); + env_ = env; + ConvertFromJsValue(env, controllerIdValue, controlId); + napi_value resultValue = nullptr; + int32_t result = OH_PictureInPicture_RegisterStartPipCallback(controlId, PipStartPipCallback); + napi_create_uint32(env, result, &resultValue); + return resultValue; +} + +napi_value PiPManager::DeletePip(napi_env env, napi_callback_info info) +{ + size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + uint32_t controlId = -1; + ConvertFromJsValue(env, argv[0], controlId); + napi_value resultValue = nullptr; + int32_t result = OH_PictureInPicture_DeletePip(controlId); + napi_create_uint32(env, result, &resultValue); + return resultValue; +} + +napi_value PiPManager::StopPip(napi_env env, napi_callback_info info) +{ + size_t argc = 1; + napi_value argv[1] = {nullptr}; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + uint32_t controlId = -1; + napi_value resultValue = nullptr; + + ConvertFromJsValue(env, argv[0], controlId); + uint32_t result = OH_PictureInPicture_StopPip(controlId); + napi_create_uint32(env, result, &resultValue); + return resultValue; +} + +napi_value PiPManager::RegisterLifecycleListener(napi_env env, napi_callback_info info) +{ + size_t argc = 2; + napi_value argv[2] = {nullptr}; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + uint32_t controlId = -1; + napi_status status = napi_create_reference(env, argv[1], 1, &jsLifecycleCallback); + lifeEnv_ = env; + if (status != napi_ok) { + LOG("register failed %{public}d", status); + } + ConvertFromJsValue(env, argv[0], controlId); + + napi_value resultValue = nullptr; + int32_t result = OH_PictureInPicture_RegisterLifecycleListener(controlId, LifecycleCallback); + napi_create_uint32(env, result, &resultValue); + return resultValue; +} + +EXTERN_C_START +static napi_value Init(napi_env env, napi_value exports) +{ + napi_property_descriptor desc[] = { + {"createPip", nullptr, PiPManager::CreatePip, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"startPip", nullptr, PiPManager::StartPip, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"registerStartPip", nullptr, PiPManager::RegisterStartPip, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"deletePip", nullptr, PiPManager::DeletePip, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"stopPip", nullptr, PiPManager::StopPip, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"registerLifecycleListener", nullptr, PiPManager::RegisterLifecycleListener, + nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); + return exports; +} +EXTERN_C_END + +static napi_module demoModule = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = nullptr, + .nm_register_func = Init, + .nm_modname = "entry", + .nm_priv = ((void*)0), + .reserved = { 0 }, +}; +extern "C" __attribute__((constructor)) void RegisterEntryModule(void) +{ + napi_module_register(&demoModule); +} +// [End ndk_implement_napi_init] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/types/libentry/Index.d.ts b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/types/libentry/Index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..a74ab09c97c7417f5cbddf3503d21ed52d2f32e9 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/types/libentry/Index.d.ts @@ -0,0 +1,45 @@ +/* +* 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. +*/ + +// [Start ndk_implement_node_api] +// Index.d.ts +export enum PiPControlGroup { + VIDEO_PLAY_VIDEO_PREVIOUS_NEXT = 101, + VIDEO_PLAY_FAST_FORWARD_BACKWARD = 102, + VIDEO_CALL_MICROPHONE_SWITCH = 201, + VIDEO_CALL_HANG_UP_BUTTON = 202, + VIDEO_CALL_CAMERA_SWITCH = 203, + VIDEO_CALL_MUTE_SWITCH = 204, + VIDEO_MEETING_HANG_UP_BUTTON = 301, + VIDEO_MEETING_CAMERA_SWITCH = 302, + VIDEO_MEETING_MUTE_SWITCH = 303, + VIDEO_MEETING_MICROPHONE_SWITCH = 304, + VIDEO_LIVE_VIDEO_PLAY_PAUSE = 401, + VIDEO_LIVE_MUTE_SWITCH = 402, +} +export interface PiPConfig { + mainWindowId: number; + pipTemplateType: number; + width: number; + height: number; + controlGroup: Array; +} +export declare const createPip: (config: PiPConfig) => number; +export declare const startPip: (controllerId: number) => number; +export declare const registerStartPip: (controllerId: number, jsCallback: Function) => number; +export declare const deletePip: (controllerId: number) => number; +export declare const stopPip: (controllerId: number) => number; +export declare const registerLifecycleListener: (controllerId: number, jsCallback: Function) => number; +// [End ndk_implement_node_api] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/types/libentry/oh-package.json5 b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/types/libentry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..0527b66e2e9381241ae6807ee8dce20ca706159b --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/cpp/types/libentry/oh-package.json5 @@ -0,0 +1,21 @@ +/* +* 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. +*/ + +{ + "name": "libentry.so", + "types": "./Index.d.ts", + "version": "1.0.0", + "description": "Please describe the basic information." +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/ability/PipManager.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/ability/PipManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..2cc5dd300d3eb8a0bec3fc41c267aaa5bb3799bf --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/ability/PipManager.ets @@ -0,0 +1,224 @@ +/* +* 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. +*/ + +// [Start ability_implement_pip_manager] +// ability/PipManager.ets +import { PiPWindow, typeNode } from '@kit.ArkUI'; // 引入PiPWindow模块 +import { BusinessError } from '@kit.BasicServicesKit'; +import { XCNodeController } from './XCNodeController'; +import { AVPlayer } from '../model/AVPlayer'; +import { Logger } from '../util/LogUtil'; + +// 自定义XComponentController +export class CustomXComponentController extends XComponentController { + onSurfaceCreated(surfaceId: string): void { + Logger.info(TAG, `onSurfaceCreated surfaceId: ${surfaceId}`); + if (PipManager.getInstance().player.surfaceID === surfaceId) { + return; + } + PipManager.getInstance().player.surfaceID = surfaceId; + PipManager.getInstance().player.avPlayerFdSrc(); + } + + onSurfaceDestroyed(surfaceId: string): void { + Logger.info(TAG, `onSurfaceDestroyed surfaceId: ${surfaceId}`); + } +} + +const TAG = 'PipManager'; + +export class PipManager { + private static instance: PipManager = new PipManager(); + private pipController?: PiPWindow.PiPController = undefined; + private xcNodeController: XCNodeController; + private mXComponentController: XComponentController; + private lifeCycleCallback: Set = new Set(); + public player: AVPlayer; + + public static getInstance(): PipManager { + return PipManager.instance; + } + + constructor() { + this.xcNodeController = new XCNodeController(); + this.player = new AVPlayer(); + this.mXComponentController = new CustomXComponentController(); + } + + public registerLifecycleCallback(callBack: Function) { + this.lifeCycleCallback.add(callBack); + } + + public unRegisterLifecycleCallback(callBack: Function): void { + this.lifeCycleCallback.delete(callBack); + } + + getNode(): typeNode.XComponent | null { + return this.xcNodeController.getNode(); + } + + onActionEvent(control: PiPWindow.ControlEventParam) { + switch (control.controlType) { + case PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE: + if (control.status === PiPWindow.PiPControlStatus.PAUSE) { + //停止视频 + } else if (control.status === PiPWindow.PiPControlStatus.PLAY) { + //播放视频 + } + break; + case PiPWindow.PiPControlType.VIDEO_NEXT: + // 切换到下一个视频 + break; + case PiPWindow.PiPControlType.VIDEO_PREVIOUS: + // 切换到上一个视频 + break; + case PiPWindow.PiPControlType.FAST_FORWARD: + // 视频进度快进 + break; + case PiPWindow.PiPControlType.FAST_BACKWARD: + // 视频进度后退 + break; + default: + break; + } + Logger.info('onActionEvent, controlType:' + control.controlType + ', status' + control.status); + } + + onStateChange(state: PiPWindow.PiPState, reason: string) { + let curState: string = ''; + this.xcNodeController.setCanAddNode( + state === PiPWindow.PiPState.ABOUT_TO_STOP || state === PiPWindow.PiPState.STOPPED); + if (this.lifeCycleCallback !== null) { + this.lifeCycleCallback.forEach((fun) => { + fun(state); + }); + } + switch (state) { + case PiPWindow.PiPState.ABOUT_TO_START: + curState = 'ABOUT_TO_START'; + // 将typeNode节点从布局移除 + this.xcNodeController.removeNode(); + break; + case PiPWindow.PiPState.STARTED: + curState = 'STARTED'; + break; + case PiPWindow.PiPState.ABOUT_TO_STOP: + curState = 'ABOUT_TO_STOP'; + break; + case PiPWindow.PiPState.STOPPED: + curState = 'STOPPED'; + break; + case PiPWindow.PiPState.ABOUT_TO_RESTORE: + curState = 'ABOUT_TO_RESTORE'; + break; + case PiPWindow.PiPState.ERROR: + curState = 'ERROR'; + break; + default: + break; + } + Logger.info(`[${TAG}] onStateChange: ${curState}, reason: ${reason}`); + } + + unregisterPipStateChangeListener() { + Logger.info(`${TAG} aboutToDisappear`); + this.pipController?.off('stateChange'); + this.pipController?.off('controlEvent'); + } + + getXComponentController(): CustomXComponentController { + return this.mXComponentController; + } + + // 步骤1:创建画中画控制器,注册生命周期事件以及控制事件回调 + init(ctx: Context) { + if (this.pipController !== null && this.pipController != undefined) { + return; + } + Logger.info(`${TAG} onPageShow`) + if (!PiPWindow.isPiPEnabled()) { + Logger.error(TAG, `picture in picture disabled for current OS`); + return; + } + let config: PiPWindow.PiPConfiguration = { + context: ctx, + componentController: this.getXComponentController(), + templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY, + contentWidth: 1920, // 使用typeNode启动画中画时,contentWidth需设置为大于0的值,否则创建画中画失败 + contentHeight: 1080, // 使用typeNode启动画中画时,contentHeight需设置为大于0的值,否则创建画中画失败 + }; + // 通过create接口创建画中画控制器实例 + + PiPWindow.create(config, this.xcNodeController.getNode()).then((controller: PiPWindow.PiPController) => { + this.pipController = controller; + // 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画 + this.pipController?.setAutoStartEnabled(true); + // 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调 + this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => { + this.onStateChange(state, reason); + }); + // 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调 + this.pipController.on('controlEvent', (control: PiPWindow.ControlEventParam) => { + this.onActionEvent(control); + }); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to create pip controller. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤2:启动画中画 + startPip() { + this.pipController?.startPiP().then(() => { + Logger.info(TAG, `Succeeded in starting pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to start pip. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤3:更新媒体源尺寸信息 + updateContentSize(width: number, height: number) { + if (this.pipController) { + this.pipController.updateContentSize(width, height); + } + } + + // 步骤4:关闭画中画 + stopPip() { + if (this.pipController === null || this.pipController === undefined) { + return; + } + let promise: Promise = this.pipController.stopPiP(); + promise.then(() => { + Logger.info(TAG, `Succeeded in stopping pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to stop pip. Cause:${err.code}, message:${err.message}`); + }); + } + + getNodeController(): XCNodeController { + Logger.info(TAG, `getNodeController.`); + return this.xcNodeController; + } + + setAutoStart(autoStart: boolean): void { + this.pipController?.setAutoStartEnabled(autoStart); + } + + // 将typeNode节点添加到原父节点 + addNode(): void { + this.xcNodeController.addNode(); + } +} +// [End ability_implement_pip_manager] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/ability/XCNodeController.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/ability/XCNodeController.ets new file mode 100644 index 0000000000000000000000000000000000000000..5c57763a1bfa1c53c97c3c7ade527a5d9934edd8 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/ability/XCNodeController.ets @@ -0,0 +1,93 @@ +/* +* 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. +*/ + +// [Start ability_implement_node_controller] +// ability/XCNodeController.ets +import { FrameNode, NodeController, typeNode } from '@kit.ArkUI'; +import { PipManager } from './PipManager'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'XCNodeController'; + +// 创建自定义NodeController +export class XCNodeController extends NodeController { + public xComponent: typeNode.XComponent | null = null; + private node: FrameNode | null = null; + private canAddNode: boolean = true; + + // 设置是否可以添加节点 + setCanAddNode(canAddNode: boolean) { + this.canAddNode = canAddNode; + } + + // 实现makeNode方法,当自定义NodeController被添加到布局时,该方法会被调用 + makeNode(context: UIContext): FrameNode | null { + this.node = new FrameNode(context); + this.node.commonAttribute + if (this.xComponent === null || this.xComponent === undefined) { + // 创建XComponent类型的typeNode + this.xComponent = typeNode.createNode(context, 'XComponent', { + type: XComponentType.SURFACE, // 类型设置为SURFACE + controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController + }); + } + if (this.canAddNode) { + + try { + this.xComponent.getParent()?.removeChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to removeChild'); + } + try { + this.node.appendChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to appendChild'); + } + } + return this.node; + } + + // 重新添加typeNode节点 + addNode() { + if (this.node !== null && this.node !== undefined) { + Logger.info(TAG, 'addNode'); + + try { + this.node.appendChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to appendChild'); + } + } + } + + // 移除typeNode节点 + removeNode() { + if (this.node !== null && this.node !== undefined) { + Logger.info(TAG, 'removeNode'); + + try { + this.node.removeChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to removeChild'); + } + } + } + + getNode(): typeNode.XComponent | null { + Logger.info(TAG, 'getNode is null: '+ (this.xComponent === null || this.xComponent === undefined)); + return this.xComponent; + } +} +// [End ability_implement_node_controller] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/entryability/EntryAbility.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..78bb3f3cedcd461bb0b5e31350cd460502db0837 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,96 @@ +/* +* 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. +*/ + +// [Start typenode_free_implement_part] +// [Start ndk_implement_entry_ability] +// [Start typenode_other_three_implement_part] +import { BusinessError } from '@kit.BasicServicesKit'; +import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { window } from '@kit.ArkUI'; +import { PipManager } from '../nodefree/PipManager'; +import { Logger } from '../util/LogUtil'; + +export default class EntryAbility extends UIAbility { +// [StartExclude typenode_free_implement_part] +// [StartExclude typenode_other_three_implement_part] + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + AppStorage.setOrCreate('UIAbilityContext', this.context); + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + } + + onDestroy(): void { + Logger.info('testTag', '%{public}s', 'Ability onDestroy'); + } + + // [EndExclude typenode_free_implement_part] + // [EndExclude typenode_other_three_implement_part] + onWindowStageCreate(windowStage: window.WindowStage): void { + // [StartExclude typenode_other_three_implement_part] + // Main window is created, set main page for this ability + Logger.info('testTag', '%{public}s', 'Ability onWindowStageCreate'); + let windowClass: window.Window | undefined = undefined; + let windowClassId: number = -1; + + windowStage.getMainWindow().then((window) => { + if (window == null) { + Logger.error('Failed to obtaining the window. Cause: The data is empty'); + return; + } + windowClass = window; + windowClass.setUIContent('pages/Index'); + windowClassId = windowClass.getWindowProperties().id; + AppStorage.setOrCreate('windowId', windowClassId); + Logger.info('Succeeded in obtaining the window') + + let ctx = window.getUIContext(); + AppStorage.setOrCreate('UIContext', ctx); + // 通过主窗口UIContext创建typeNode节点 + PipManager.getInstance().makeTypeNode(ctx); + }).catch((err: BusinessError) => { + Logger.error(`Failed to obtaining the window. Cause code: ${err.code}, message: ${err.message}`); + }); + // [EndExclude typenode_other_three_implement_part] + windowStage.loadContent('pages/Index', (err) => { + // [StartExclude typenode_other_three_implement_part] + if (err.code) { + Logger.error('testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); + return; + } + Logger.info('testTag', 'Succeeded in loading the content.'); + // [EndExclude typenode_other_three_implement_part] + }); + } + // [StartExclude typenode_free_implement_part] + // [StartExclude typenode_other_three_implement_part] + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + Logger.info('testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + Logger.info('testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + Logger.info('testTag', '%{public}s', 'Ability onBackground'); + } +// [EndExclude typenode_free_implement_part] +// [EndExclude typenode_other_three_implement_part] +} +// [End typenode_other_three_implement_part] +// [End typenode_free_implement_part] +// [End ndk_implement_entry_ability] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..62f7c396ba5cbd1734ae26d44c528d0f3a4bd5be --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,29 @@ +/* +* 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 { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; +import { Logger } from '../util/LogUtil'; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + Logger.info('testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + Logger.info('testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/model/AVPlayer.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/model/AVPlayer.ets new file mode 100644 index 0000000000000000000000000000000000000000..720ac444612f5de78089e5096df11d5b119b4673 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/model/AVPlayer.ets @@ -0,0 +1,77 @@ +/* +* 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. +*/ + +// [Start pip_common_avplayer] +// model/AVPlayer.ets +// 简易播放器实现 +import { BusinessError } from '@kit.BasicServicesKit'; +import { common } from '@kit.AbilityKit'; +import { media } from '@kit.MediaKit'; +import { Logger } from '../util/LogUtil'; + +export class AVPlayer { + private avPlayer?: media.AVPlayer; + public surfaceID: string = ''; + + setAVPlayerCallback() { + this.avPlayer?.on('seekDone', (seekDoneTime: number) => { + Logger.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); + }) + this.avPlayer?.on('stateChange', async (state, reason) => { + if (!this.avPlayer) { + return; + } + switch (state) { + case 'idle': + this.avPlayer.release(); + break; + case 'initialized': + this.avPlayer.surfaceId = this.surfaceID; + this.avPlayer.prepare().then(() => { + Logger.info('AVPlayer prepare succeeded.'); + }, (err: BusinessError) => { + Logger.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); + }); + break; + case 'prepared': + this.avPlayer.play(); + break; + case 'stopped': + this.avPlayer.reset(); + break; + default: + break; + } + }) + } + + async avPlayerFdSrc() { + + try { + this.avPlayer = await media.createAVPlayer(); + } catch(err) { + Logger.error(`create AVPlayer failed`); + }; + this.setAVPlayerCallback(); + let uiContext = AppStorage.get('UIContext') as UIContext; + let context = uiContext.getHostContext() as common.UIAbilityContext; + let fileDescriptor = await context.resourceManager.getRawFd('xxx.mp4'); + + if (this.avPlayer) { + this.avPlayer.fdSrc = fileDescriptor; + } + } +} +// [End pip_common_avplayer] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/model/NDKAVPlayer.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/model/NDKAVPlayer.ets new file mode 100644 index 0000000000000000000000000000000000000000..8fb8fd8ca189ac936a5a8aaa1981ebd3800c3f82 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/model/NDKAVPlayer.ets @@ -0,0 +1,173 @@ +/* +* 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. +*/ + +// [Start ndk_implement_avplayer] +// model/AVPlayer.ets +// 视频播放器简单实现 +import media from '@ohos.multimedia.media'; +import common from '@ohos.app.ability.common'; +import { BusinessError } from '@ohos.base'; +import resourceManager from '@ohos.resourceManager'; +import { Logger } from '../util/LogUtil'; + +export class AVPlayer { + public avPlayer?: media.AVPlayer; + private count: number = 0; + private surfaceID: string; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法 + public jumpNext: boolean = false; + public type: number = 0; // 用于区分主界面的player还是pip界面的player + public state_: string = '' + public playStatus: boolean = true; + + constructor(surfaceID: string, type: number) { + this.surfaceID = surfaceID; + this.type = type + } + + setSurfaceId(id: string) { + if (this.avPlayer) { + this.surfaceID = id; + this.avPlayer.surfaceId = id; + } + } + + updatePlayStatus(status: boolean) { + this.playStatus = status; + } + // 注册avplayer回调函数 + setAVPlayerCallback() { + // seek操作结果回调函数 + this.avPlayer?.on('seekDone', (seekDoneTime: number) => { + Logger.info(`PipMain AVPlayer seek succeeded, seek time is ${seekDoneTime}`); + }) + // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程 + this.avPlayer?.on('error', (err: BusinessError) => { + Logger.error(`PipMain Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); + this.avPlayer?.reset(); // 调用reset重置资源,触发idle状态 + }) + // 状态机变化回调函数 + this.avPlayer?.on('stateChange', async (state, reason) => { + if (!this.avPlayer) { + return; + } + this.state_ = state; + switch (state) { + case 'idle': // 成功调用reset接口后触发该状态机上报 + Logger.info('AVPlayer state idle called.'); + if (!this.jumpNext) { + this.avPlayer.release(); // 调用release接口销毁实例对象 + } else { + let uiContext: UIContext = AppStorage.get('UIAbilityContext') as UIContext; + let context = uiContext.getHostContext() as common.UIAbilityContext; + let fileDescriptor: resourceManager.RawFileDescriptor; + fileDescriptor = await context.resourceManager.getRawFd('640x360.mp4'); + // 为fdSrc赋值触发initialized状态机上报 + this.avPlayer.fdSrc = fileDescriptor; + } + break; + case 'initialized': // avplayer 设置播放源后触发该状态上报 + Logger.info('initialized called.'); + this.avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置 + this.avPlayer.prepare().then(() => { + Logger.info('AVPlayer prepare succeeded.'); + }, (err: BusinessError) => { + Logger.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); + }); + break; + case 'prepared': // prepare调用成功后上报该状态机 + Logger.info('AVPlayer state prepared called.'); + this.avPlayer.play(); // 调用播放接口开始播放 + break; + case 'playing': // play成功调用后触发该状态机上报 + Logger.info('AVPlayer state playing called.'); + this.jumpNext = false; + this.count++; + break; + case 'paused': // pause成功调用后触发该状态机上报 + Logger.info('AVPlayer state paused called.'); + // this.avPlayer.play(); // 再次播放接口开始播放 + break; + case 'completed': // 播放结束后触发该状态机上报 + Logger.info('AVPlayer state completed called.'); + this.playNext(); + ; //调用播放结束接口 + break; + case 'stopped': // stop接口成功调用后触发该状态机上报 + Logger.info('AVPlayer state stopped called.'); + this.avPlayer.reset(); // 调用reset接口初始化avplayer状态 + break; + case 'released': + Logger.info('AVPlayer state released called.'); + break; + default: + Logger.info('AVPlayer state unknown called.'); + break; + } + }) + this.avPlayer?.on('videoSizeChange', (width: number, height: number) => { + Logger.info('videoSizeChange width:' + width + ' height:' + height); + let context = AppStorage.get('UIAbilityContext') as common.UIAbilityContext; + }) + } + + // 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例 + async avPlayerFdSrc() { + // 创建avPlayer实例对象 + Logger.info('avPlayerFdSrc'); + this.avPlayer = await media.createAVPlayer(); + + // 创建状态机变化回调函数 + this.setAVPlayerCallback(); + // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址 + // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度 + + let context = AppStorage.get('UIAbilityContext') as common.UIAbilityContext; + let fileDescriptor = await context.resourceManager.getRawFd('640x360.mp4'); + Logger.info('getRawFd'); + // 为fdSrc赋值触发initialized状态机上报 + this.avPlayer.fdSrc = fileDescriptor; + } + + async playNext() { + if (this.avPlayer === null) { + return; + } + this.jumpNext = true; + this.avPlayer?.stop(); + } + + play() { + if (this.state_ === 'paused') { + this.avPlayer?.play(); + } + } + + pause() { + if (this.state_ === 'playing') { + this.avPlayer?.pause(); + } + } + + stopAvPlayer() { + Logger.info('stopAvPlayer>>>') + if (!this.avPlayer) { + return; + } + this.avPlayer.stop(); + Logger.info('stopping>>>'); + this.avPlayer.reset(); + } +} +// [End ndk_implement_avplayer] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/Page1.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/Page1.ets new file mode 100644 index 0000000000000000000000000000000000000000..a7f62a9c34fc8ada41067e528da7a32d81302804 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/Page1.ets @@ -0,0 +1,75 @@ +/* +* 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. +*/ + +// [Start navigation_implement_page1] +// navigation/Page1.ets +import { PipManager } from './PipManager'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'Page1'; + +@Entry +@Component +export struct Page1 { + build() { + NavDestination() { + Column() { + Text('This is Page1') + .fontSize(30) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 20 }) + + // 将typeNode添加到页面布局中 + NodeContainer(PipManager.getInstance().getNodeController()) + .size({ width: '100%', height: '800px' }) + + Row({ space: 20 }) { + Button('startPip') // 启动画中画 + .onClick(() => { + PipManager.getInstance().startPip(); + }) + Button('stopPip') // 停止画中画 + .onClick(() => { + PipManager.getInstance().stopPip(); + }) + Button('updateSize') // 更新视频尺寸 + .onClick(() => { + // 此处设置的宽高应为媒体内容宽高,需要通过媒体相关接口或回调获取 + // 例如使用AVPlayer播放视频时,可通过videoSizeChange回调获取媒体源更新后的尺寸 + PipManager.getInstance().updateContentSize(900, 1600); + }) + } + .backgroundColor('#4da99797') + .size({ width: '100%', height: 60 }) + .justifyContent(FlexAlign.SpaceAround) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .height('100%') + } + .title('page1') + .onShown(() => { + Logger.info(TAG, 'onShown') + PipManager.getInstance().init(this.getUIContext().getHostContext() as Context); + PipManager.getInstance().setAutoStart(true); + }) + .onHidden(() => { + Logger.info(TAG, 'onHidden') + PipManager.getInstance().setAutoStart(false); + PipManager.getInstance().removeNode(); + }) + } +} +// [End navigation_implement_page1] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/PipManager.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/PipManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..3cfa05a6d6a1ee04d49f07e1bd6580ed814f76b9 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/PipManager.ets @@ -0,0 +1,229 @@ +/* +* 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. +*/ + +// [Start navigation_implement_pip_manager] +// navigation/PipManager.ets +import { PiPWindow, typeNode } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { XCNodeController } from './XCNodeController'; +import { AVPlayer } from '../model/AVPlayer'; +import { Logger } from '../util/LogUtil'; + +export class CustomXComponentController extends XComponentController { + onSurfaceCreated(surfaceId: string): void { + Logger.info(TAG, `onSurfaceCreated surfaceId: ${surfaceId}`); + if (PipManager.getInstance().player.surfaceID === surfaceId) { + return; + } + // 将surfaceId设置给媒体源 + PipManager.getInstance().player.surfaceID = surfaceId; + PipManager.getInstance().player.avPlayerFdSrc(); + } + + onSurfaceDestroyed(surfaceId: string): void { + Logger.info(TAG, `onSurfaceDestroyed surfaceId: ${surfaceId}`); + } +} + +const TAG = 'PipManager'; + +export class PipManager { + private static instance: PipManager = new PipManager(); + private pipController?: PiPWindow.PiPController = undefined; + private xcNodeController: XCNodeController; + private mXComponentController: XComponentController; + private lifeCycleCallback: Set = new Set(); + public player: AVPlayer; + + public static getInstance(): PipManager { + return PipManager.instance; + } + + constructor() { + this.xcNodeController = new XCNodeController(); + this.player = new AVPlayer(); + this.mXComponentController = new CustomXComponentController(); + } + + public registerLifecycleCallback(callBack: Function) { + this.lifeCycleCallback.add(callBack); + } + + public unRegisterLifecycleCallback(callBack: Function): void { + this.lifeCycleCallback.delete(callBack); + } + + getNode(): typeNode.XComponent | null { + return this.xcNodeController.getNode(); + } + + onActionEvent(control: PiPWindow.ControlEventParam) { + switch (control.controlType) { + case PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE: + if (control.status === PiPWindow.PiPControlStatus.PAUSE) { + //停止视频 + } else if (control.status === PiPWindow.PiPControlStatus.PLAY) { + //播放视频 + } + break; + case PiPWindow.PiPControlType.VIDEO_NEXT: + // 切换到下一个视频 + break; + case PiPWindow.PiPControlType.VIDEO_PREVIOUS: + // 切换到上一个视频 + break; + case PiPWindow.PiPControlType.FAST_FORWARD: + // 视频进度快进 + break; + case PiPWindow.PiPControlType.FAST_BACKWARD: + // 视频进度后退 + break; + default: + break; + } + Logger.info('onActionEvent, controlType:' + control.controlType + ', status' + control.status); + } + + onStateChange(state: PiPWindow.PiPState, reason: string) { + let curState: string = ''; + this.xcNodeController.setCanAddNode( + state === PiPWindow.PiPState.ABOUT_TO_STOP || state === PiPWindow.PiPState.STOPPED) + if (this.lifeCycleCallback !== null) { + this.lifeCycleCallback.forEach((fun) => { + fun(state); + }); + } + switch (state) { + case PiPWindow.PiPState.ABOUT_TO_START: + curState = 'ABOUT_TO_START'; + // 将typeNode节点从布局移除 + this.xcNodeController.removeNode(); + break; + case PiPWindow.PiPState.STARTED: + curState = 'STARTED'; + break; + case PiPWindow.PiPState.ABOUT_TO_STOP: + curState = 'ABOUT_TO_STOP'; + break; + case PiPWindow.PiPState.STOPPED: + curState = 'STOPPED'; + break; + case PiPWindow.PiPState.ABOUT_TO_RESTORE: + curState = 'ABOUT_TO_RESTORE'; + break; + case PiPWindow.PiPState.ERROR: + curState = 'ERROR'; + break; + default: + break; + } + Logger.info(`[${TAG}] onStateChange: ${curState}, reason: ${reason}`); + } + + unregisterPipStateChangeListener() { + Logger.info(`${TAG} aboutToDisappear`); + this.pipController?.off('stateChange'); + this.pipController?.off('controlEvent'); + this.pipController = undefined; + } + + getXComponentController(): CustomXComponentController { + return this.mXComponentController; + } + + // 步骤1:创建画中画控制器,注册生命周期事件以及控制事件回调 + init(ctx: Context) { + if (this.pipController !== null && this.pipController != undefined) { + return; + } + Logger.info(`${TAG} onPageShow`) + if (!PiPWindow.isPiPEnabled()) { + Logger.error(TAG, `picture in picture disabled for current OS`); + return; + } + + let config: PiPWindow.PiPConfiguration = { + context: ctx, + componentController: this.getXComponentController(), + templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY, + contentWidth: 1920, // 使用typeNode启动画中画时,contentWidth需设置为大于0的值,否则创建画中画失败 + contentHeight: 1080, // 使用typeNode启动画中画时,contentHeight需设置为大于0的值,否则创建画中画失败 + }; + // 通过create接口创建画中画控制器实例 + + PiPWindow.create(config, this.xcNodeController.getNode()).then((controller: PiPWindow.PiPController) => { + this.pipController = controller; + // 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画 + this.pipController?.setAutoStartEnabled(true); + // 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调 + this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => { + this.onStateChange(state, reason); + }); + // 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调 + this.pipController.on('controlEvent', (control: PiPWindow.ControlEventParam) => { + this.onActionEvent(control); + }); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to create pip controller. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤2:启动画中画 + startPip() { + this.pipController?.startPiP().then(() => { + Logger.info(TAG, `Succeeded in starting pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to start pip. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤3:更新媒体源尺寸信息 + updateContentSize(width: number, height: number) { + if (this.pipController) { + this.pipController.updateContentSize(width, height); + } + } + + // 步骤4:关闭画中画 + stopPip() { + if (this.pipController === null || this.pipController === undefined) { + return; + } + let promise: Promise = this.pipController.stopPiP(); + promise.then(() => { + Logger.info(TAG, `Succeeded in stopping pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to stop pip. Cause:${err.code}, message:${err.message}`); + }); + } + + getNodeController(): XCNodeController { + Logger.info(TAG, `getNodeController.`); + return this.xcNodeController; + } + + setAutoStart(autoStart: boolean): void { + this.pipController?.setAutoStartEnabled(autoStart); + } + + removeNode() { + this.xcNodeController.removeNode(); + } + + addNode(): void { + this.xcNodeController.addNode(); + } +} +// [End navigation_implement_pip_manager] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/XCNodeController.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/XCNodeController.ets new file mode 100644 index 0000000000000000000000000000000000000000..bbb8af7022ad8cc9eed6c563335dd157feeced0b --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/navigation/XCNodeController.ets @@ -0,0 +1,92 @@ +/* +* 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. +*/ + +// [Start navigation_implement_node_controller] +// navigation/XCNodeController.ets +import { FrameNode, NodeController, typeNode } from '@kit.ArkUI'; +import { PipManager } from './PipManager'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'XCNodeController'; + +// 创建自定义NodeController +export class XCNodeController extends NodeController { + public xComponent: typeNode.XComponent| null = null; + private node: FrameNode | null = null; + private canAddNode: boolean = true; + + // 设置是否可以添加节点 + setCanAddNode(canAddNode: boolean) { + this.canAddNode = canAddNode; + } + + // 实现makeNode方法,当自定义NodeController被添加到布局时,该方法会被调用 + makeNode(context: UIContext): FrameNode | null { + Logger.info(TAG, 'makeNode'); + this.node = new FrameNode(context); + if (this.xComponent === null || this.xComponent === undefined) { + // 创建XComponent类型的typeNode + this.xComponent = typeNode.createNode(context, 'XComponent', { + type: XComponentType.SURFACE, // 类型设置为SURFACE + controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController + }); + } + if (this.canAddNode) { + + try { + this.xComponent.getParent()?.removeChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to removeChild'); + } + try { + this.node.appendChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to appendChild'); + } + } + return this.node; + } + + // 重新添加typeNode节点 + addNode() { + if (this.node !== null && this.node !== undefined) { + Logger.info(TAG, 'addNode id:'+(this.node?.getUniqueId())+' '+this.xComponent?.getUniqueId()); + try { + this.node.appendChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to appendChild'); + } + } + } + + // 移除typeNode节点 + removeNode() { + if (this.node !== null && this.node !== undefined) { + Logger.info(TAG, 'removeNode'); + + try { + this.node.removeChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to removeChild'); + } + } + } + + getNode(): typeNode.XComponent | null { + Logger.info(TAG, 'getNode is null:'+ (this.xComponent === null || this.xComponent === undefined)) + return this.xComponent; + } +} +// [End navigation_implement_node_controller] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/nodefree/PipManager.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/nodefree/PipManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..bd54822981ae8db657a47406ffc30f1587663811 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/nodefree/PipManager.ets @@ -0,0 +1,208 @@ +/* +* 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. +*/ + +// [Start typenode_free_implement_pip_manager] +// nodeFree/PipManager.ets +// 画中画控制器单例 +import { PiPWindow, typeNode } from '@kit.ArkUI'; // 引入PiPWindow模块 +import { BusinessError } from '@kit.BasicServicesKit'; +import { AVPlayer} from '../model/AVPlayer'; +import { Logger } from '../util/LogUtil'; + +// 自定义XComponentController +class CustomXComponentController extends XComponentController { + // 监听onSurfaceCreated,并将surfaceId设置给播放器 + onSurfaceCreated(surfaceId: string): void { + Logger.info(TAG, `onSurfaceCreated surfaceId: ${surfaceId}`); + if (PipManager.getInstance().player.surfaceID === surfaceId) { + return; + } + PipManager.getInstance().player.surfaceID = surfaceId; + PipManager.getInstance().player.avPlayerFdSrc(); + } + + onSurfaceDestroyed(surfaceId: string): void { + Logger.info(TAG, `onSurfaceDestroyed surfaceId: ${surfaceId}`); + } +} + +const TAG = 'PipManager'; + +export class PipManager { + public player: AVPlayer; + private static instance: PipManager = new PipManager(); + private pipController?: PiPWindow.PiPController = undefined; + private mXComponentController: XComponentController; + private xComponent: typeNode.XComponent| null = null; // typeNode节点 + + public static getInstance(): PipManager { + return PipManager.instance; + } + + constructor() { + this.player = new AVPlayer(); + this.mXComponentController = new CustomXComponentController(); + } + + onActionEvent(control: PiPWindow.ControlEventParam) { + switch (control.controlType) { + case PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE: + if (control.status === PiPWindow.PiPControlStatus.PAUSE) { + //停止视频 + } else if (control.status === PiPWindow.PiPControlStatus.PLAY) { + //播放视频 + } + break; + case PiPWindow.PiPControlType.VIDEO_NEXT: + // 切换到下一个视频 + break; + case PiPWindow.PiPControlType.VIDEO_PREVIOUS: + // 切换到上一个视频 + break; + case PiPWindow.PiPControlType.FAST_FORWARD: + // 视频进度快进 + break; + case PiPWindow.PiPControlType.FAST_BACKWARD: + // 视频进度后退 + break; + default: + break; + } + Logger.info('onActionEvent, controlType:' + control.controlType + ', status' + control.status); + } + + // 监听画中画生命周期 + onStateChange(state: PiPWindow.PiPState, reason: string) { + let curState: string = ''; + switch (state) { + case PiPWindow.PiPState.ABOUT_TO_START: + curState = 'ABOUT_TO_START'; + break; + case PiPWindow.PiPState.STARTED: + curState = 'STARTED'; + break; + case PiPWindow.PiPState.ABOUT_TO_STOP: + curState = 'ABOUT_TO_STOP'; + break; + case PiPWindow.PiPState.STOPPED: + curState = 'STOPPED'; + break; + case PiPWindow.PiPState.ABOUT_TO_RESTORE: + curState = 'ABOUT_TO_RESTORE'; + break; + case PiPWindow.PiPState.ERROR: + curState = 'ERROR'; + break; + default: + break; + } + Logger.info(`[${TAG}] onStateChange: ${curState}, reason: ${reason}`); + } + + // 解注册监听 + unregisterPipStateChangeListener() { + Logger.info(TAG, 'aboutToDisappear'); + this.pipController?.off('stateChange'); + this.pipController?.off('controlEvent'); + } + + getXComponentController(): CustomXComponentController { + return this.mXComponentController; + } + + // 步骤1:创建画中画控制器,注册生命周期事件以及控制事件回调 + init(ctx: Context) { + if (this.pipController !== null && this.pipController != undefined) { + return; + } + Logger.info(TAG, 'onPageShow'); + if (!PiPWindow.isPiPEnabled()) { + Logger.error(TAG, `picture in picture disabled for current OS`); + return; + } + + let config: PiPWindow.PiPConfiguration = { + context: ctx, + componentController: this.getXComponentController(), + templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY, + contentWidth: 1920, // 使用typeNode启动画中画时,contentWidth需设置为大于0的值,否则将设置为16:9默认比例 + contentHeight: 1080, // 使用typeNode启动画中画时,contentHeight需设置为大于0的值,否则将设置为16:9默认比例 + }; + // 通过create接口创建画中画控制器实例 + + PiPWindow.create(config, this.xComponent).then((controller: PiPWindow.PiPController) => { + this.pipController = controller; + // 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画 + this.pipController.setAutoStartEnabled(true); + // 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调 + this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => { + this.onStateChange(state, reason); + }); + // 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调 + this.pipController.on('controlEvent', (control: PiPWindow.ControlEventParam) => { + this.onActionEvent(control); + }); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to create pip controller. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤2:创建画中画控制器实例后,通过startPiP接口启动画中画 + startPip() { + this.pipController?.startPiP().then(() => { + Logger.info(TAG, `Succeeded in starting pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to start pip. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤3:更新媒体源尺寸信息 + updateContentSize(width: number, height: number) { + if (this.pipController) { + this.pipController.updateContentSize(width, height); + } + } + + // 步骤4:关闭画中画 + stopPip() { + if (this.pipController === null || this.pipController === undefined) { + return; + } + let promise: Promise = this.pipController.stopPiP(); + promise.then(() => { + Logger.info(TAG, `Succeeded in stopping pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to stop pip. Cause:${err.code}, message:${err.message}`); + }); + } + + setAutoStart(autoStart: boolean): void { + this.pipController?.setAutoStartEnabled(autoStart); + } + + // 创建typeNode节点 + makeTypeNode(ctx: UIContext) { + if (this.xComponent === null || this.xComponent === undefined) { + // 创建XComponent类型的typeNode + this.xComponent = typeNode.createNode(ctx, 'XComponent', { + // 类型设置为SURFACE + type: XComponentType.SURFACE, + // 设置XComponentController + controller: PipManager.getInstance().getXComponentController(), + }); + } + } +} +// [End typenode_free_implement_pip_manager] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/AbilityImplementPage.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/AbilityImplementPage.ets new file mode 100644 index 0000000000000000000000000000000000000000..7a339c5c2ddb674d270b8ce56e1fd9fa9cd8111d --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/AbilityImplementPage.ets @@ -0,0 +1,91 @@ +/* +* 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. +*/ + +// [Start ability_implement_index_page] +// pages/AbilityImplementPage.ets +import { PipManager } from '../ability/PipManager'; +import { PiPWindow } from '@kit.ArkUI'; // 引入PiPWindow模块 +import { Logger } from '../util/LogUtil'; + +const TAG = 'AbilityImplementPage' +@Entry +@Component +struct AbilityImplementPage { + private callback: Function = (state: PiPWindow.PiPState) => { + if (state === PiPWindow.PiPState.ABOUT_TO_STOP) { + // 画中画关闭或还原时触发ABOUT_TO_STOP生命周期,此时需要重新添加节点 + PipManager.getInstance().addNode(); + } + }; + + build() { + Column() { + Text('This is MainPage') + .fontSize(30) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 20 }) + + // 将typeNode添加到页面布局中 + NodeContainer(PipManager.getInstance().getNodeController()) + .size({ width: '100%', height: '800px' }) + + Row({ space: 20 }) { + Button('startPip') // 启动画中画 + .onClick(() => { + PipManager.getInstance().startPip(); + }) + + Button('stopPip') // 停止画中画 + .onClick(() => { + PipManager.getInstance().stopPip(); + }) + + Button('updateSize') // 更新视频尺寸 + .onClick(() => { + // 此处设置的宽高应为媒体内容宽高,需要通过媒体相关接口或回调获取 + // 例如使用AVPlayer播放视频时,可通过videoSizeChange回调获取媒体源更新后的尺寸 + PipManager.getInstance().updateContentSize(900, 1600); + }) + } + .backgroundColor('#4da99797') + .size({ width: '100%', height: 60 }) + .justifyContent(FlexAlign.SpaceAround) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .height('100%') + } + + aboutToAppear(): void { + PipManager.getInstance().registerLifecycleCallback(this.callback); + } + + aboutToDisappear(): void { + PipManager.getInstance().unregisterPipStateChangeListener(); + PipManager.getInstance().unRegisterLifecycleCallback(this.callback); + } + + onPageShow(): void { + Logger.info(TAG, 'onPageShow') + PipManager.getInstance().init(this.getUIContext().getHostContext() as Context); + PipManager.getInstance().setAutoStart(true); + } + + onPageHide(): void { + Logger.info(TAG, 'onPageHide') + PipManager.getInstance().setAutoStart(false); + } +} +// [End ability_implement_index_page] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/Index.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..1d10046d005337309d29f20d6b9cec8c00cce208 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/Index.ets @@ -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. +*/ + +// [Start Navigation] +// [Start TypeNode] +// [Start NDK] +// Index.ets +// 应用首页 +import { router } from '@kit.ArkUI'; + +@Entry +@Component +struct Index { + pathStack: NavPathStack = new NavPathStack(); + + build() { + Navigation(this.pathStack) { + Scroll() { + Flex({ direction: FlexDirection.Column }) { + // [StartExclude TypeNode] + // [StartExclude NDK] + this.featureButton('使用Navigation导航时通过TypeNode实现画中画', this.navImplement); + // [StartExclude Navigation] + this.featureButton('使用XComponent实现画中画', this.xComponent); + // [EndExclude TypeNode] + this.featureButton('使用TypeNode自由节点实现画中画', this.typeNodeFree); + // [StartExclude TypeNode] + this.featureButton('使用Router导航时通过TypeNode实现画中画', this.routerImplement); + this.featureButton('使用单页面Ability时通过TypeNode实现画中画', this.abilityImplement); + // [EndExclude NDK] + this.featureButton('使用NDK接口实现画中画(C++)', this.ndkImplement); + // [EndExclude Navigation] + // [EndExclude TypeNode] + } + } + } + .hideBackButton(true) + .titleMode(NavigationTitleMode.Mini) + .backgroundColor('#FFF1F3F5') + .mode(NavigationMode.Stack) + .title('画中画SampleCode') + } + + @Builder + featureButton(buttonText: string, callbackOnClick: () => void) { + Button({ type: ButtonType.Normal }) { + Row() { + Column() { + Text(buttonText) + .fontSize(24) + .fontWeight(FontWeight.Bold) + .fontColor('#000000') + Rect() + .radius(1) + .fill('#0A59F7') + .height(2) + .width(30) + } + .width('100%') + .alignItems(HorizontalAlign.Start) + } + .width('100%') + } + .width('90%') + .padding('5%') + .margin({ top: '3%', bottom: '2%', right: '3%' }) + .backgroundColor('#FFFFFF') + .borderRadius(20) + .onClick(callbackOnClick) + } + + // [StartExclude TypeNode] + // [StartExclude NDK] + private navImplement = () => { + this.getUIContext().getRouter().pushUrl({ url: 'pages/NavigationImplementPage' }, router.RouterMode.Standard) + } + // [StartExclude Navigation] + private xComponent = () => { + this.getUIContext().getRouter().pushUrl({ url: 'pages/XComponentImplementPage' }, router.RouterMode.Standard) + } + // [EndExclude TypeNode] + private typeNodeFree = () => { + this.getUIContext().getRouter().pushUrl({ url: 'pages/TypeNodeFreePage' }, router.RouterMode.Standard) + } + // [StartExclude TypeNode] + private routerImplement = () => { + this.getUIContext().getRouter().pushUrl({ url: 'pages/RouterImplementPage' }, router.RouterMode.Standard) + } + private abilityImplement = () => { + this.getUIContext().getRouter().pushUrl({ url: 'pages/AbilityImplementPage' }, router.RouterMode.Standard) + } + // [EndExclude NDK] + private ndkImplement = () => { + this.getUIContext().getRouter().pushUrl({ url: 'pages/NDKImplementPage' }, router.RouterMode.Standard) + } + // [EndExclude Navigation] + // [EndExclude TypeNode] +} +// [End Navigation] +// [End TypeNode] +// [End NDK] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/NDKImplementPage.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/NDKImplementPage.ets new file mode 100644 index 0000000000000000000000000000000000000000..e4a1db754a8b95baecb0966b57b22e53e81fbfcc --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/NDKImplementPage.ets @@ -0,0 +1,207 @@ +/* +* 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. +*/ + +// [Start ndk_implement_index_page] +// NDKImplementIndexPage.ets +// 画中画功能演示界面 +import testNapi, {PiPConfig} from 'libentry.so'; +import { PiPWindow } from '@kit.ArkUI'; +import { AVPlayer } from '../model/NDKAVPlayer'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'PipMain'; +export enum PiPControlGroup { + VIDEO_PLAY_VIDEO_PREVIOUS_NEXT = 101, + VIDEO_PLAY_FAST_FORWARD_BACKWARD = 102, + VIDEO_CALL_MICROPHONE_SWITCH = 201, + VIDEO_CALL_HANG_UP_BUTTON = 202, + VIDEO_CALL_CAMERA_SWITCH = 203, + VIDEO_CALL_MUTE_SWITCH = 204, + VIDEO_MEETING_HANG_UP_BUTTON = 301, + VIDEO_MEETING_CAMERA_SWITCH = 302, + VIDEO_MEETING_MUTE_SWITCH = 303, + VIDEO_MEETING_MICROPHONE_SWITCH = 304, + VIDEO_LIVE_VIDEO_PLAY_PAUSE = 401, + VIDEO_LIVE_MUTE_SWITCH = 402, +} + +@Entry +@Component +struct NDKImplementIndexPage { + @State message: string = 'Hello World'; + mXComponentController: XComponentController | null = new XComponentController(); + private controllerId: number = -1; + private contentWidth: number = 1920; + private contentHeight: number = 1080; + private pipType: PiPWindow.PiPTemplateType = PiPWindow.PiPTemplateType.VIDEO_PLAY; + private pipControlGroups: Array = []; + player?: AVPlayer; + surfaceId = ''; + + changeSurface = (surfaceId: string) => { + if(this.player) { + this.player.setSurfaceId(surfaceId); + return; + } + Logger.info(`[${TAG}] change surface failed`); + } + + private onStateChange = (state: PiPWindow.PiPState) => { + switch(state) { + case PiPWindow.PiPState.ABOUT_TO_START: + Logger.info(`[${TAG}] ABOUT_TO_START`); + break; + case PiPWindow.PiPState.STARTED: + Logger.info(`[${TAG}] STARTED`); + break; + case PiPWindow.PiPState.ABOUT_TO_STOP: + Logger.info(`[${TAG}] ABOUT_TO_STOP`); + break; + case PiPWindow.PiPState.STOPPED: + if (this.mXComponentController) { + this.changeSurface(this.mXComponentController?.getXComponentSurfaceId()); + } + Logger.info(`[${TAG}] STOPPED`); + break; + case PiPWindow.PiPState.ABOUT_TO_RESTORE: + this.changeSurface(this.surfaceId); + Logger.info(`[${TAG}] ABOUT_TO_RESTORE`); + break; + case PiPWindow.PiPState.ERROR: + Logger.info(`[${TAG}] ERROR`); + break; + default: + break; + } + } + + + build() { + RelativeContainer() { + Row({ space: 20 }) { + + Button('更换模板') + .bindMenu([ + { + value: '视频', + action: () => { + this.pipType = PiPWindow.PiPTemplateType.VIDEO_PLAY; + this.pipControlGroups = [PiPControlGroup.VIDEO_PLAY_VIDEO_PREVIOUS_NEXT]; + } + }, + { + value: '通话', + action: () => { + this.pipType = PiPWindow.PiPTemplateType.VIDEO_CALL; + this.pipControlGroups = [PiPControlGroup.VIDEO_CALL_HANG_UP_BUTTON, + PiPControlGroup.VIDEO_CALL_CAMERA_SWITCH, PiPControlGroup.VIDEO_CALL_MICROPHONE_SWITCH]; + } + }, + { + value: '会议', + action: () => { + this.pipType = PiPWindow.PiPTemplateType.VIDEO_MEETING; + this.pipControlGroups = [PiPControlGroup.VIDEO_MEETING_MICROPHONE_SWITCH, + PiPControlGroup.VIDEO_MEETING_HANG_UP_BUTTON, + PiPControlGroup.VIDEO_MEETING_CAMERA_SWITCH]; + } + }, + { + value: '直播', + action: () => { + this.pipType = PiPWindow.PiPTemplateType.VIDEO_LIVE; + this.pipControlGroups = [PiPControlGroup.VIDEO_LIVE_VIDEO_PLAY_PAUSE, + PiPControlGroup.VIDEO_LIVE_MUTE_SWITCH]; + } + } + ]) + } + .size({ width: '100%', height: 60 }) + .backgroundColor('#DDDDDD') + .justifyContent(FlexAlign.SpaceAround) + .alignRules({ + top: { anchor: '__container__', align: VerticalAlign.Top }, + middle: { anchor: '__container__', align: HorizontalAlign.Center } + }) + .id('pip_type_control') + XComponent({ + type: XComponentType.SURFACE, + controller: this.mXComponentController + }) + .onLoad(() => { + if (this.mXComponentController) { + this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); + } + this.player = new AVPlayer(this.surfaceId, 1); + this.player.avPlayerFdSrc(); + }) + .onDestroy(() => { + Logger.info(`[${TAG}] XComponent onDestroy`); + }) + .size({ width: '100%', height: '800px' }) + .margin({ top: 10 }) + .backgroundColor('#888888') + .alignRules({ + bottom: { anchor: '__container__', align: VerticalAlign.Center }, + middle: { anchor: '__container__', align: HorizontalAlign.Center } + }) + .id('x_component') + .size({ width: '100%', height: '800px' }) + Row({ space: 0 }) { + Button('创建画中画') + .onClick(() => { + let windowId: number | undefined = AppStorage.get('windowId'); + let config: PiPConfig = { + mainWindowId: windowId as number, + pipTemplateType: this.pipType, + width: this.contentWidth, + height: this.contentHeight, + controlGroup: this.pipControlGroups + } + this.controllerId = testNapi.createPip(config); + testNapi.registerStartPip(this.controllerId, this.changeSurface); + testNapi.registerLifecycleListener(this.controllerId, this.onStateChange); + }) + Button('开启画中画') + .onClick(() => { + testNapi.startPip(this.controllerId); + }) + } + .size({ width: '100%', height: 60 }) + .alignRules({ + top: { anchor: 'x_component', align: VerticalAlign.Bottom }, + left: { anchor: '__container__', align: HorizontalAlign.Start } + }) + .id('pip_control') + Row({ space: 0 }) { + Button('关闭画中画') + .onClick(() => { + testNapi.stopPip(this.controllerId); + }) + Button('删除控制器') + .onClick(() => { + testNapi.deletePip(this.controllerId); + }) + } + .size({ width: '100%', height: 60 }) + .alignRules({ + top: { anchor: 'pip_control', align: VerticalAlign.Bottom }, + left: { anchor: '__container__', align: HorizontalAlign.Start } + }) + } + .size({ width: '100%', height: '100%' }) + } +} +// [End ndk_implement_index_page] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/NavigationImplementPage.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/NavigationImplementPage.ets new file mode 100644 index 0000000000000000000000000000000000000000..49f389af5c1ffcef602874ecee10e9151be6de3c --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/NavigationImplementPage.ets @@ -0,0 +1,93 @@ +/* +* 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. +*/ + +// [Start navigation_implement_index_page] +// pages/NavigationImplementPage.ets +import { PipManager } from '../navigation/PipManager'; +import { Page1 } from '../navigation/Page1'; +import { PiPWindow } from '@kit.ArkUI'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'NavigationImplementPage'; + +@Entry +@Component +struct NavigationImplementPage { + @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack(); + // 画中画生命周期事件监听,用于页面及节点操作 + private callback: Function = (state: PiPWindow.PiPState) => { + Logger.info(TAG, `pipStateChange: state ${state}`); + if (state === PiPWindow.PiPState.ABOUT_TO_START) { + // 返回到上级页面(可选) + this.pageInfos.pop(); + } else if (state === PiPWindow.PiPState.ABOUT_TO_STOP) { + // 重新将typeNode节点添加到布局中,例如还原场景 + PipManager.getInstance().addNode(); + } else if (state === PiPWindow.PiPState.ABOUT_TO_RESTORE) { + // 如果在ABOUT_TO_START时返回了上级界面,需要还原时push到原界面 + this.jumpNext(); + } + }; + + jumpNext() { + if (this.pageInfos.getAllPathName()[0] === 'Page1') { + Logger.info(TAG, 'Page1 already at top'); + return; + } + this.pageInfos.pushPath({ name: 'Page1' }); + } + + aboutToAppear(): void { + PipManager.getInstance().registerLifecycleCallback(this.callback); + } + + aboutToDisappear(): void { + PipManager.getInstance().unregisterPipStateChangeListener(); + PipManager.getInstance().unRegisterLifecycleCallback(this.callback); + } + + @Builder + PageMap(name: string) { + if (name === 'Page1') { + Page1(); + } + } + + build() { + Navigation(this.pageInfos) { + Column() { + Text('This is Main Page') + Column() + .height('200px') + Row({ space: 12 }) { + Button('Jump Page1') + .width('80%') + .height(40) + .margin(20) + .onClick(() => { + this.jumpNext(); + }) + } + } + .height('100%') + .width('100%') + .justifyContent(FlexAlign.Center) + .backgroundColor('#DCDCDC') + } + .title('MainTitle') + .navDestination(this.PageMap) + } +} +// [End navigation_implement_index_page] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/RouterImplementPage.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/RouterImplementPage.ets new file mode 100644 index 0000000000000000000000000000000000000000..29ef4cfa507e0b3c5d5939999b9f94729c9a8ad2 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/RouterImplementPage.ets @@ -0,0 +1,89 @@ +/* +* 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. +*/ + +// [Start router_implement_index_page] +// pages/RouterImplementPage.ets +import { PipManager } from '../route/PipManager'; +import { PiPWindow, router, Router } from '@kit.ArkUI'; // 引入PiPWindow模块 +import { Logger } from '../util/LogUtil'; + +const TAG = 'RouterImplementPage' +@Entry +@Component +struct RouterImplementPage { + private page1: string = 'route/Page1'; + private pageRouter: Router | null = null; + + // 画中画生命周期事件监听,用于页面及节点操作 + private callback: Function = (state: PiPWindow.PiPState) => { + Logger.info(TAG, `pipStateChange: state ${state}`); + if (state === PiPWindow.PiPState.ABOUT_TO_START) { + // 返回到上级页面(可选) + this.pageRouter?.back(); + } else if (state === PiPWindow.PiPState.ABOUT_TO_STOP) { + // 重新将typeNode节点添加到布局中,例如还原场景 + PipManager.getInstance().addNode(); + } else if (state === PiPWindow.PiPState.ABOUT_TO_RESTORE) { + // 如果在ABOUT_TO_START时返回了上级界面,需要还原时push到原界面 + this.jumpNext(); + } + }; + + aboutToAppear(): void { + this.pageRouter = this.getUIContext().getRouter(); + PipManager.getInstance().registerLifecycleCallback(this.callback); + } + + aboutToDisappear(): void { + PipManager.getInstance().unregisterPipStateChangeListener(); + PipManager.getInstance().unRegisterLifecycleCallback(this.callback); + } + + jumpNext(): void { + let topPage = this.pageRouter?.getState(); + if (topPage !== undefined && (this.page1.toString() === topPage.path + topPage.name)) { + Logger.info(TAG, `page1 aready at top`) + return; + } + this.pageRouter?.pushUrl({ + url: this.page1 // 目标url + }, router.RouterMode.Standard, (err) => { + if (err) { + Logger.error(TAG, `Invoke pushUrl failed, code is ${err.code}: ${err.message}`); + return; + } + Logger.info(TAG, 'Invoke pushUrl succeeded.'); + }); + } + + build() { + Row() { + Column() { + Text('Main Page') + .fontSize(50) + .fontWeight(FontWeight.Bold) + + Button('Jump Next') + .onClick(() => { + this.jumpNext(); + }) + .margin({ top: 16, bottom: 16 }) + } + .width('100%') + } + .height('100%') + } +} +// [End router_implement_index_page] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/TypeNodeFreePage.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/TypeNodeFreePage.ets new file mode 100644 index 0000000000000000000000000000000000000000..f9f23b9bb33d86ec376f175870f0fc5c652d8714 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/TypeNodeFreePage.ets @@ -0,0 +1,79 @@ +/* +* 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. +*/ + +// [Start typenode_free_implement_index_page] +// 该页面用于展示应用布局文件,创建的typeNode节点不会添加到该布局中 +import { PipManager } from '../nodefree/PipManager'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'TypeNodeFreePage' +@Entry +@Component +struct TypeNodeFreePage { + build() { + Column() { + Text('This is MainPage') + .fontSize(30) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 20 }) + + Text('This is not typeNode') + .size({ width: '100%', height: '800px' }) + .fontSize(30) + .textAlign(TextAlign.Center) + .fontWeight(FontWeight.Bold) + .backgroundColor('#4d5b5858') + + Row({ space: 20 }) { + Button('startPip') // 启动画中画 + .onClick(() => { + PipManager.getInstance().startPip(); + }) + + Button('stopPip') // 停止画中画 + .onClick(() => { + PipManager.getInstance().stopPip(); + }) + + Button('updateSize') // 更新视频尺寸 + .onClick(() => { + PipManager.getInstance().updateContentSize(900, 1600); + }) + } + .backgroundColor('#4da99797') + .size({ width: '100%', height: 60 }) + .justifyContent(FlexAlign.SpaceAround) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .height('100%') + } + + aboutToDisappear(): void { + PipManager.getInstance().unregisterPipStateChangeListener(); // 解注册画中画生命周期及状态回调 + } + + onPageShow(): void { + Logger.info(TAG, 'onPageShow') + PipManager.getInstance().init(this.getUIContext().getHostContext() as Context); // 创建画中画控制器 + PipManager.getInstance().setAutoStart(true); // 设置应用退后台时自动启动画中画 + } + + onPageHide(): void { + Logger.info(TAG, 'onPageHide') + PipManager.getInstance().setAutoStart(false); + } +} +// [End typenode_free_implement_index_page] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/XComponentImplementPage.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/XComponentImplementPage.ets new file mode 100644 index 0000000000000000000000000000000000000000..65cfdaccd3b9b0403b449ebec679c4c915f8e7d0 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/pages/XComponentImplementPage.ets @@ -0,0 +1,59 @@ +/* +* 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. +*/ + +// [Start x_component_implement_index_page] +// XComponentImplementPage.ets +// 该页面用于展示Navigation在画中画场景的使用。如果UIAbility是单页面,则无需使用Navigation +import { Page1 } from '../xcomponent/Page1' + +@Entry +@Component +struct XComponentImplementPage { + @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack(); + private navId: string = 'navId'; + + @Builder + PageMap(name: string) { + if (name === 'pageOne') { + Page1({ navId: this.navId }); + } + } + + build() { + Navigation(this.pageInfos) { + Column() { + Button('pushPath', { stateEffect: true, type: ButtonType.Capsule }) + .width('80%') + .height(40) + .margin(20) + .onClick(() => { + this.pageInfos.pushPath({ name: 'pageOne' }) // 将name指定的NavDestination页面信息入栈 + }) + .stateStyles({ + pressed: { + .backgroundColor(Color.Red); + }, + normal: { + .backgroundColor(Color.Blue); + } + }) + } + } + .title('NavIndex') + .navDestination(this.PageMap) + .id(this.navId) // 设置Navigation组件的id属性 + } +} +// [End x_component_implement_index_page] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/Page1.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/Page1.ets new file mode 100644 index 0000000000000000000000000000000000000000..b49ba544e7a66b828ff2c88402b171fa29e1013f --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/Page1.ets @@ -0,0 +1,76 @@ +/* +* 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. +*/ + +// [Start router_implement_page1] +// route/Page1.ets +import { PipManager } from '../route/PipManager'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'Page1'; + +@Entry +@Component +export struct Page1 { + build() { + Column() { + Text('This is Page1') + .fontSize(30) + .fontWeight(FontWeight.Bold) + .margin({bottom: 20}) + + // 将typeNode添加到页面布局中 + NodeContainer(PipManager.getInstance().getNodeController()) + .size({ width: '100%', height: '800px' }) + + Row({ space: 20 }) { + Button('startPip')// 启动画中画 + .onClick(() => { + PipManager.getInstance().startPip(); + }) + + Button('stopPip')// 停止画中画 + .onClick(() => { + PipManager.getInstance().stopPip(); + }) + + Button('updateSize')// 更新视频尺寸 + .onClick(() => { + // 此处设置的宽高应为媒体内容宽高,需要通过媒体相关接口或回调获取 + // 例如使用AVPlayer播放视频时,可通过videoSizeChange回调获取媒体源更新后的尺寸 + PipManager.getInstance().updateContentSize(900, 1600); + }) + } + .backgroundColor('#4da99797') + .size({ width: '100%', height: 60 }) + .justifyContent(FlexAlign.SpaceAround) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .height('100%') + } + + onPageShow(): void { + Logger.info(TAG, 'onPageShow') + PipManager.getInstance().initPipController(this.getUIContext().getHostContext() as Context); + PipManager.getInstance().setAutoStart(true); + } + + onPageHide(): void { + Logger.info(TAG, 'onPageHide') + PipManager.getInstance().setAutoStart(false); + PipManager.getInstance().removeNode(); + } +} +// [End router_implement_page1] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/PipManager.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/PipManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..a56a82a130c4bc17d840975c222ce3eeda9104f5 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/PipManager.ets @@ -0,0 +1,227 @@ +/* +* 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. +*/ + +// [Start router_implement_pip_manager] +// route/PipManager.ets +import { PiPWindow, typeNode } from '@kit.ArkUI'; // 引入PiPWindow模块 +import { BusinessError } from '@kit.BasicServicesKit'; +import { XCNodeController } from './XCNodeController'; +import { AVPlayer } from '../model/AVPlayer'; +import { Logger } from '../util/LogUtil'; + +export class CustomXComponentController extends XComponentController { + onSurfaceCreated(surfaceId: string): void { + Logger.info(TAG, `onSurfaceCreated surfaceId: ${surfaceId}`); + if (PipManager.getInstance().player.surfaceID === surfaceId) { + return; + } + // 将surfaceId设置给媒体源 + PipManager.getInstance().player.surfaceID = surfaceId; + PipManager.getInstance().player.avPlayerFdSrc(); + } + + onSurfaceDestroyed(surfaceId: string): void { + Logger.info(TAG, `onSurfaceDestroyed surfaceId: ${surfaceId}`); + } +} + +const TAG = 'PipManager'; + +export class PipManager { + private static instance: PipManager = new PipManager(); + private pipController?: PiPWindow.PiPController = undefined; + private xcNodeController: XCNodeController; + private mXComponentController: XComponentController; + private lifeCycleCallback: Set = new Set(); + public player: AVPlayer; + + public static getInstance(): PipManager { + return PipManager.instance; + } + + constructor() { + this.xcNodeController = new XCNodeController(); + this.player = new AVPlayer(); + this.mXComponentController = new CustomXComponentController(); + } + + public registerLifecycleCallback(callBack: Function) { + this.lifeCycleCallback.add(callBack); + } + + public unRegisterLifecycleCallback(callBack: Function): void { + this.lifeCycleCallback.delete(callBack); + } + + getNode(): typeNode.XComponent | null { + return this.xcNodeController.getNode(); + } + + onActionEvent(control: PiPWindow.ControlEventParam) { + switch (control.controlType) { + case PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE: + if (control.status === PiPWindow.PiPControlStatus.PAUSE) { + //停止视频 + } else if (control.status === PiPWindow.PiPControlStatus.PLAY) { + //播放视频 + } + break; + case PiPWindow.PiPControlType.VIDEO_NEXT: + // 切换到下一个视频 + break; + case PiPWindow.PiPControlType.VIDEO_PREVIOUS: + // 切换到上一个视频 + break; + case PiPWindow.PiPControlType.FAST_FORWARD: + // 视频进度快进 + break; + case PiPWindow.PiPControlType.FAST_BACKWARD: + // 视频进度后退 + break; + default: + break; + } + Logger.info('onActionEvent, controlType:' + control.controlType + ', status' + control.status); + } + + onStateChange(state: PiPWindow.PiPState, reason: string) { + let curState: string = ''; + this.xcNodeController.setCanAddNode( + state === PiPWindow.PiPState.ABOUT_TO_STOP || state === PiPWindow.PiPState.STOPPED) + if (this.lifeCycleCallback !== null) { + this.lifeCycleCallback.forEach((fun) => { + fun(state) + }); + } + switch (state) { + case PiPWindow.PiPState.ABOUT_TO_START: + curState = 'ABOUT_TO_START'; + // 将typeNode节点从布局移除 + this.xcNodeController.removeNode(); + break; + case PiPWindow.PiPState.STARTED: + curState = 'STARTED'; + break; + case PiPWindow.PiPState.ABOUT_TO_STOP: + curState = 'ABOUT_TO_STOP'; + break; + case PiPWindow.PiPState.STOPPED: + curState = 'STOPPED'; + break; + case PiPWindow.PiPState.ABOUT_TO_RESTORE: + curState = 'ABOUT_TO_RESTORE'; + break; + case PiPWindow.PiPState.ERROR: + curState = 'ERROR'; + break; + default: + break; + } + Logger.info(`[${TAG}] onStateChange: ${curState}, reason: ${reason}`); + } + + unregisterPipStateChangeListener() { + Logger.info(`${TAG} aboutToDisappear`) + this.pipController?.off('stateChange'); + this.pipController?.off('controlEvent'); + this.pipController = undefined; + } + + getXComponentController(): CustomXComponentController { + return this.mXComponentController; + } + + // 步骤1:创建画中画控制器,注册生命周期事件以及控制事件回调 + initPipController(ctx: Context) { + if (this.pipController !== null && this.pipController != undefined) { + return; + } + Logger.info(`${TAG} onPageShow`) + if (!PiPWindow.isPiPEnabled()) { + Logger.error(TAG, `picture in picture disabled for current OS`); + return; + } + let config: PiPWindow.PiPConfiguration = { + context: ctx, + componentController: this.getXComponentController(), + templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY, + contentWidth: 1920, // 使用typeNode启动画中画时,contentWidth需设置为大于0的值,否则创建画中画失败 + contentHeight: 1080, // 使用typeNode启动画中画时,contentHeight需设置为大于0的值,否则创建画中画失败 + }; + // 通过create接口创建画中画控制器实例 + + PiPWindow.create(config, this.getNode()).then((controller: PiPWindow.PiPController) => { + this.pipController = controller; + // 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画 + this.pipController.setAutoStartEnabled(true) + // 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调 + this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => { + this.onStateChange(state, reason); + }); + // 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调 + this.pipController.on('controlEvent', (control: PiPWindow.ControlEventParam) => { + this.onActionEvent(control); + }); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to create pip controller. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤2:启动画中画 + startPip() { + this.pipController?.startPiP().then(() => { + Logger.info(TAG, `Succeeded in starting pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to start pip. Cause:${err.code}, message:${err.message}`); + }); + } + + // 步骤3:更新媒体源尺寸信息 + updateContentSize(width: number, height: number) { + if (this.pipController) { + this.pipController.updateContentSize(width, height); + } + } + + // 步骤4:关闭画中画 + stopPip() { + if (this.pipController) { + let promise: Promise = this.pipController.stopPiP(); + promise.then(() => { + Logger.info(TAG, `Succeeded in stopping pip.`); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to stop pip. Cause:${err.code}, message:${err.message}`); + }); + } + } + + getNodeController(): XCNodeController { + Logger.info(TAG, `getNodeController.`); + return this.xcNodeController; + } + + setAutoStart(autoStart: boolean): void { + this.pipController?.setAutoStartEnabled(autoStart); + } + + removeNode(): void { + this.xcNodeController.removeNode(); + } + + addNode(): void { + this.xcNodeController.addNode(); + } +} +// [End router_implement_pip_manager] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/XCNodeController.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/XCNodeController.ets new file mode 100644 index 0000000000000000000000000000000000000000..b6f02c41b889e6dbb7867d2741cb4bba28948d20 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/route/XCNodeController.ets @@ -0,0 +1,94 @@ +/* +* 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. +*/ + +// [Start router_implement_node_controller] +// route/XCNodeController.ets +import { FrameNode, NodeController, typeNode } from '@kit.ArkUI'; +import { PipManager } from './PipManager'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'XCNodeController'; +// 创建自定义NodeController +export class XCNodeController extends NodeController { + public xComponent: typeNode.XComponent | null = null; + private node: FrameNode | null = null; + private canAddNode: boolean = true; + + // 设置是否可以添加节点 + setCanAddNode(canAddNode: boolean) { + this.canAddNode = canAddNode; + } + + // 实现makeNode方法,当自定义NodeController被添加到布局时,该方法会被调用 + makeNode(context: UIContext): FrameNode | null { + this.node = new FrameNode(context); + this.node.commonAttribute + if (this.xComponent === null || this.xComponent === undefined) { + // 创建XComponent类型的typeNode + this.xComponent = typeNode.createNode(context, 'XComponent', { + // 类型设置为SURFACE + type: XComponentType.SURFACE, + // 设置XComponentController + controller: PipManager.getInstance().getXComponentController(), + }); + } + if (this.canAddNode) { + + try { + this.xComponent.getParent()?.removeChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to removeChild'); + } + try { + this.node.appendChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to appendChild'); + } + } + return this.node; + } + + // 重新添加typeNode节点 + addNode() { + if (this.node !== null && this.node !== undefined) { + Logger.info(TAG, 'addNode'); + + try { + this.node.appendChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to appendChild'); + } + } + } + + // 移除typeNode节点 + removeNode() { + if (this.node !== null && this.node !== undefined) { + Logger.info(TAG, 'removeNode'); + + try { + this.node.removeChild(this.xComponent); + } catch (error) { + Logger.error(TAG, 'Failed to removeChild'); + } + } + } + + getNode(): typeNode.XComponent | null { + Logger.info(TAG, 'getNode is null: '+ (this.xComponent === null || this.xComponent === undefined)); + return this.xComponent; + } +} +// [End router_implement_node_controller] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/util/LogUtil.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/util/LogUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..306d06ea16da769088c79d53e155f63d98817437 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/util/LogUtil.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 { hilog } from '@kit.PerformanceAnalysisKit'; + +export class Logger { + private static LOG_DOMAIN = 0x0000; + private static LOG_TAG = '[Sample_WindowPip]'; + + public static info(format: string, ...para: (string | number | boolean | object)[]): void { + hilog.info(Logger.LOG_DOMAIN, Logger.LOG_TAG, format, ...para); + } + + public static debug(format: string, ...para: (string | number | boolean | object)[]): void { + hilog.debug(Logger.LOG_DOMAIN, Logger.LOG_TAG, format, ...para); + } + + public static warn(format: string, ...para: (string | number | boolean | object)[]): void { + hilog.warn(Logger.LOG_DOMAIN, Logger.LOG_TAG, format, ...para); + } + + public static error(format: string, ...para: (string | number | boolean | object)[]): void { + hilog.error(Logger.LOG_DOMAIN, Logger.LOG_TAG, format, ...para); + } +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/xcomponent/Page1.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/xcomponent/Page1.ets new file mode 100644 index 0000000000000000000000000000000000000000..43f94cca98cb66ffa0eb4435ad5e863f7dd2af76 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/ets/xcomponent/Page1.ets @@ -0,0 +1,275 @@ +/* +* 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. +*/ + +// [Start x_component_implement_page1] +// xcomponent/Page1.ets +// 该页面用于展示画中画功能的基本使用 +import { AVPlayer } from '../model/AVPlayer'; +import { BuilderNode, FrameNode, NodeController, UIContext, PiPWindow } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { Logger } from '../util/LogUtil'; + +const TAG = 'Page1'; + +class Params { + public text: string = ''; + + constructor(text: string) { + this.text = text; + } +} + +// 开发者可以通过@Builder装饰器实现布局构建 +@Builder +function buildText(params: Params) { + Column() { + Text(params.text) + .fontSize(20) + .fontColor(Color.Red) + } + .width('100%') // 宽度方向充满画中画窗口 + .height('100%') // 高度方向充满画中画窗口 +} + +// 开发者可通过继承NodeController实现自定义UI控制器 +class TextNodeController extends NodeController { + private message: string; + private textNode: BuilderNode<[Params]> | null = null; + + constructor(message: string) { + super(); + this.message = message; + } + + // 通过BuilderNode加载自定义布局 + makeNode(context: UIContext): FrameNode | null { + this.textNode = new BuilderNode(context); + this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)); + return this.textNode.getFrameNode(); + } + + // 开发者可自定义该方法实现布局更新 + update(message: string) { + Logger.info(`update message: ${message}`); + if (this.textNode !== null) { + this.textNode.update(new Params(message)); + } + } +} + +@Entry +@Component +export struct Page1 { + @Consume('pageInfos') pageInfos: NavPathStack; + private surfaceId: string = ''; // surfaceId,用于关联XComponent与视频播放器 + private mXComponentController: XComponentController = new XComponentController(); + private player?: AVPlayer = undefined; + private pipController?: PiPWindow.PiPController = undefined; + private nodeController: TextNodeController = new TextNodeController('this is custom UI'); + navId: string = ''; + private options: XComponentOptions = { + type: XComponentType.SURFACE, + controller: this.mXComponentController + } + + build() { + NavDestination() { + Column() { + // XComponent控件,用于播放视频流 + XComponent(this.options) + .onLoad(() => { + this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); + // 需要设置AVPlayer的surfaceId为XComponentController的surfaceId + this.player = new AVPlayer(); + this.player.surfaceID = this.surfaceId; + this.player.avPlayerFdSrc(); + }) + .onDestroy(() => { + Logger.info(`[${TAG}] XComponent onDestroy`); + }) + .size({ width: '100%', height: '800px' }) + Row({ space: 20 }) { + Button('startPip') // 启动画中画 + .onClick(() => { + this.startPip(); + }) + .stateStyles({ + pressed: { + .backgroundColor(Color.Red); + }, + normal: { + .backgroundColor(Color.Blue); + } + }) + Button('stopPip') // 停止画中画 + .onClick(() => { + this.stopPip(); + }) + .stateStyles({ + pressed: { + .backgroundColor(Color.Red); + }, + normal: { + .backgroundColor(Color.Blue); + } + }) + Button('updateSize') // 更新视频尺寸 + .onClick(() => { + // 此处设置的宽高应为媒体内容宽高,需要通过媒体相关接口或回调获取 + // 例如使用AVPlayer播放视频时,可通过videoSizeChange回调获取媒体源更新后的尺寸 + this.updateContentSize(900, 1600); + }) + .stateStyles({ + pressed: { + .backgroundColor(Color.Red); + }, + normal: { + .backgroundColor(Color.Blue); + } + }) + } + .size({ width: '100%', height: 60 }) + .justifyContent(FlexAlign.SpaceAround) + } + .justifyContent(FlexAlign.Center) + .height('100%') + .width('100%') + } + } + + startPip() { + if (!PiPWindow.isPiPEnabled()) { + Logger.error(`picture in picture disabled for current OS`); + return; + } + let config: PiPWindow.PiPConfiguration = { + context: this.getUIContext().getHostContext() as Context, + componentController: this.mXComponentController, + // 当前page导航id + // 1、UIAbility使用Navigation管理页面,需要设置Navigation控件的id属性,并将该id设置给画中画控制器,确保还原场景下能够从画中画窗口恢复到原页面 + // 2、UIAbility使用Router管理页面时(画中画场景不推荐该导航方式),无需设置navigationId。注意:该场景下启动画中画后,不要进行页面切换,否则还原场景可能出现异常 + // 3、UIAbility只有单页面时,无需设置navigationId,还原场景下也能够从画中画窗口恢复到原页面 + navigationId: this.navId, + // 对于视频通话、视频会议等场景,需要设置相应的模板类型 + templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY, + // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例 + contentWidth: 1920, + // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例 + contentHeight: 1080, + // 可选,对于视频通话、视频会议和视频直播场景,可通过该属性选择对应模板类型下需显示的的控件组 + controlGroups: [PiPWindow.VideoPlayControlGroup.VIDEO_PREVIOUS_NEXT], + // 可选,如果需要在画中画显示内容上方展示自定义UI,可设置该参数。 + customUIController: this.nodeController, + }; + // 步骤1:创建画中画控制器,通过create接口创建画中画控制器实例 + PiPWindow.create(config).then((controller: PiPWindow.PiPController) => { + this.pipController = controller; + // 步骤1:初始化画中画控制器 + this.initPipController(); + // 步骤2:通过startPiP接口启动画中画 + this.pipController.startPiP().then(() => { + Logger.info(`Succeeded in starting pip.`); + }).catch((err: BusinessError) => { + Logger.error(`Failed to start pip. Cause:${err.code}, message:${err.message}`); + }); + }).catch((err: BusinessError) => { + Logger.error(`Failed to create pip controller. Cause:${err.code}, message:${err.message}`); + }); + } + + initPipController() { + if (!this.pipController) { + return; + } + // 步骤1:通过setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画,注册stateChange和controlPanelActionEvent回调 + this.pipController.setAutoStartEnabled(false /*or true if necessary*/); // 默认为false + this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => { + this.onStateChange(state, reason); + }); + this.pipController.on('controlPanelActionEvent', (event: PiPWindow.PiPActionEventType, status?: number) => { + this.onActionEvent(event, status); + }); + } + + onStateChange(state: PiPWindow.PiPState, reason: string) { + let curState: string = ''; + switch (state) { + case PiPWindow.PiPState.ABOUT_TO_START: + curState = 'ABOUT_TO_START'; + break; + case PiPWindow.PiPState.STARTED: + curState = 'STARTED'; + break; + case PiPWindow.PiPState.ABOUT_TO_STOP: + curState = 'ABOUT_TO_STOP'; + break; + case PiPWindow.PiPState.STOPPED: + curState = 'STOPPED'; + break; + case PiPWindow.PiPState.ABOUT_TO_RESTORE: + curState = 'ABOUT_TO_RESTORE'; + break; + case PiPWindow.PiPState.ERROR: + curState = 'ERROR'; + break; + default: + break; + } + Logger.info(`[${TAG}] onStateChange: ${curState}, reason: ${reason}`); + } + + onActionEvent(event: PiPWindow.PiPActionEventType, status?: number) { + switch (event) { + case 'playbackStateChanged': + // 开始或停止视频 + if (status === 0) { + // 停止视频 + } else if (status === 1) { + // 播放视频 + } + break; + case 'nextVideo': + // 播放下一个视频 + break; + case 'previousVideo': + // 播放上一个视频 + break; + default: + break; + } + } + + // 步骤3:视频内容变化时,向画中画控制器更新视频尺寸信息,用于调整画中画窗口比例 + updateContentSize(width: number, height: number) { + if (this.pipController) { + this.pipController.updateContentSize(width, height); + } + } + + // 步骤4:当不再需要显示画中画时,通过stopPiP接口关闭画中画 + stopPip() { + if (this.pipController) { + let promise: Promise = this.pipController.stopPiP(); + promise.then(() => { + Logger.info(`Succeeded in stopping pip.`); + this.pipController?.off('stateChange'); // 如果已注册stateChange回调,停止画中画时取消注册该回调 + this.pipController?.off('controlPanelActionEvent'); // 如果已注册controlPanelActionEvent回调,停止画中画时取消注册该回调 + }).catch((err: BusinessError) => { + Logger.error(`Failed to stop pip. Cause:${err.code}, message:${err.message}`); + }); + } + } +} +// [End x_component_implement_page1] \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/module.json5 b/ArkUIWindowPipSamples/WindowPip/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..7af284b4f10d65feea1bf74131eb1e9f4c3ecd93 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/module.json5 @@ -0,0 +1,67 @@ +/* +* 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. +*/ + +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "ohos.want.action.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ] + } +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/color.json b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/float.json b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000000000000000000000000000000000..33ea22304f9b1485b5f22d811023701b5d4e35b6 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/string.json b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..f94595515a99e0c828807e243494f57f09251930 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/background.png b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/background.png differ diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/foreground.png b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/foreground.png differ diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/layered_image.json b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/startIcon.png b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b Binary files /dev/null and b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/media/startIcon.png differ diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/profile/backup_config.json b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000000000000000000000000000000000..78f40ae7c494d71e2482278f359ec790ca73471a --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/profile/main_pages.json b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..f48fe4e9b30992588adca4668ec6764dde2653ce --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,12 @@ +{ + "src": [ + "pages/Index", + "pages/RouterImplementPage", + "route/Page1", + "pages/TypeNodeFreePage", + "pages/AbilityImplementPage", + "pages/XComponentImplementPage", + "pages/NDKImplementPage", + "pages/NavigationImplementPage" + ] +} diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/dark/element/color.json b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..79b11c2747aec33e710fd3a7b2b3c94dd9965499 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/mock/mock-config.json5 b/ArkUIWindowPipSamples/WindowPip/entry/src/mock/mock-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..2417dae33e86618dcf41b9c0c0f2835b30e47b34 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/mock/mock-config.json5 @@ -0,0 +1,17 @@ +/* +* 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. +*/ + +{ +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/Ability.test.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..85c78f67579d6e31b5f5aeea463e216b9b141048 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/List.test.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..27e578d9bfb3778a2a45b0deaf8e82f1bcbe47ee --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,20 @@ +/* +* 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 WindowPipTest from './WindowPip.test'; + +export default function testsuite() { + WindowPipTest(); +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/WindowPip.test.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/WindowPip.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..9b84e09d79a76ad028181bc6052425aa61295be1 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/ets/test/WindowPip.test.ets @@ -0,0 +1,482 @@ +/* + * 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 AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import { describe, it, expect } from '@ohos/hypium'; +import { Driver, ON } from '@ohos.UiTest'; +import hilog from '@ohos.hilog'; + +const TAG = '[Sample_WindowPip]'; +const DOMAIN = 0xF811 +const BUNDLE = 'WindowPip_' +const BUNDLE_NAME = AbilityDelegatorRegistry.getArguments().bundleName; + + +export default function WindowPipTest() { + + describe('ActsAbilityTest', () => { + /** + * 打开应用 + */ + it(BUNDLE + 'StartAbility_001', 0, async (done: Function) => { + hilog.info(DOMAIN, TAG, BUNDLE + "StartAbility_001, begin") + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `StartAbility_001 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('使用XComponent实现画中画')); + done(); + hilog.info(DOMAIN, TAG, BUNDLE + 'StartAbility_001 end'); + }) + + /** + * 点击XComponent实现按钮,调用画中画接口 + */ + it(BUNDLE + 'XComponent_001', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + "XComponent_001, begin") + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `XComponent_001 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('使用XComponent实现画中画')); + let clickBtn = await driver.findComponent(ON.text('使用XComponent实现画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('pushPath')); + clickBtn = await driver.findComponent(ON.text('pushPath')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('startPip')); + clickBtn = await driver.findComponent(ON.text('startPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('updateSize')); + clickBtn = await driver.findComponent(ON.text('updateSize')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('stopPip')); + clickBtn = await driver.findComponent(ON.text('stopPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.pressBack(); + await driver.delayMs(1000); + + await driver.pressBack(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'XComponent_001 end'); + }) + + /** + * 点击TypeNode自由节点实现按钮,调用画中画接口 + */ + it(BUNDLE + 'TypeNodeFree_001', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'TypeNodeFree_001 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `TypeNodeFree_001 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('使用TypeNode自由节点实现画中画')); + let clickBtn = await driver.findComponent(ON.text('使用TypeNode自由节点实现画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('startPip')); + clickBtn = await driver.findComponent(ON.text('startPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('updateSize')); + clickBtn = await driver.findComponent(ON.text('updateSize')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('stopPip')); + clickBtn = await driver.findComponent(ON.text('stopPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.pressBack(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'TypeNodeFree_001 end'); + }) + + /** + * 点击Router导航实现按钮,调用画中画接口 + */ + it(BUNDLE + 'RouterTypeNode_001', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'RouterTypeNode_001 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `RouterTypeNode_001 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('使用Router导航时通过TypeNode实现画中画')); + let clickBtn = await driver.findComponent(ON.text('使用Router导航时通过TypeNode实现画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('Jump Next')); + clickBtn = await driver.findComponent(ON.text('Jump Next')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('startPip')); + clickBtn = await driver.findComponent(ON.text('startPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('Jump Next')); + clickBtn = await driver.findComponent(ON.text('Jump Next')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('updateSize')); + clickBtn = await driver.findComponent(ON.text('updateSize')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('stopPip')); + clickBtn = await driver.findComponent(ON.text('stopPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.pressBack(); + await driver.delayMs(1000); + + await driver.pressBack(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'RouterTypeNode_001 end'); + }) + + /** + * 点击Navigation导航实现按钮,调用画中画接口 + */ + it(BUNDLE + 'NavigationTypeNode_001', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'NavigationTypeNode_001 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `NavigationTypeNode_001 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('使用Navigation导航时通过TypeNode实现画中画')); + let clickBtn = await driver.findComponent(ON.text('使用Navigation导航时通过TypeNode实现画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('Jump Page1')); + clickBtn = await driver.findComponent(ON.text('Jump Page1')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('startPip')); + clickBtn = await driver.findComponent(ON.text('startPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('Jump Page1')); + clickBtn = await driver.findComponent(ON.text('Jump Page1')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('updateSize')); + clickBtn = await driver.findComponent(ON.text('updateSize')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('stopPip')); + clickBtn = await driver.findComponent(ON.text('stopPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.pressBack(); + await driver.delayMs(1000); + + await driver.pressBack(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'NavigationTypeNode_001 end'); + }) + + /** + * 点击单页面Ability实现按钮,调用画中画接口 + */ + it(BUNDLE + 'AbilityTypeNode_001', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'AbilityTypeNode_001 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `AbilityTypeNode_001 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('使用单页面Ability时通过TypeNode实现画中画')); + let clickBtn = await driver.findComponent(ON.text('使用单页面Ability时通过TypeNode实现画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('startPip')); + clickBtn = await driver.findComponent(ON.text('startPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('updateSize')); + clickBtn = await driver.findComponent(ON.text('updateSize')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('stopPip')); + clickBtn = await driver.findComponent(ON.text('stopPip')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.pressBack(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'AbilityTypeNode_001 end'); + }) + + /** + * 点击NDK接口实现按钮,调用画中画接口--视频画中画样式 + */ + it(BUNDLE + 'NDKImplement_001', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_001 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `NDKImplement_001 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.swipe(500, 500, 500, 400); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('使用NDK接口实现画中画(C++)')); + let clickBtn = await driver.findComponent(ON.text('使用NDK接口实现画中画(C++)')); + await clickBtn.click(); + await driver.delayMs(1000); + + await driver.assertComponentExist(ON.text('更换模板')); + clickBtn = await driver.findComponent(ON.text('更换模板')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('视频')); + clickBtn = await driver.findComponent(ON.text('视频')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('创建画中画')); + clickBtn = await driver.findComponent(ON.text('创建画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('开启画中画')); + clickBtn = await driver.findComponent(ON.text('开启画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('关闭画中画')); + clickBtn = await driver.findComponent(ON.text('关闭画中画')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('删除控制器')); + clickBtn = await driver.findComponent(ON.text('删除控制器')); + await clickBtn.click(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_001 end'); + }) + + /** + * 点击NDK接口实现按钮,调用画中画接口--通话画中画样式 + */ + it(BUNDLE + 'NDKImplement_002', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_002 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `NDKImplement_002 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('更换模板')); + let clickBtn = await driver.findComponent(ON.text('更换模板')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('通话')); + clickBtn = await driver.findComponent(ON.text('通话')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('创建画中画')); + clickBtn = await driver.findComponent(ON.text('创建画中画')) + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('开启画中画')); + let startBtn = await driver.findComponent(ON.text('开启画中画')); + await startBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('关闭画中画')); + let closeBtn = await driver.findComponent(ON.text('关闭画中画')); + await closeBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('删除控制器')); + clickBtn = await driver.findComponent(ON.text('删除控制器')); + await clickBtn.click(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_002 end'); + }) + + /** + * 点击NDK接口实现按钮,调用画中画接口--会议画中画样式 + */ + it(BUNDLE + 'NDKImplement_003', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_003 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `NDKImplement_003 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('更换模板')); + let clickBtn = await driver.findComponent(ON.text('更换模板')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('会议')); + clickBtn = await driver.findComponent(ON.text('会议')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('创建画中画')); + clickBtn = await driver.findComponent(ON.text('创建画中画')) + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('开启画中画')); + let startBtn = await driver.findComponent(ON.text('开启画中画')); + await startBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('关闭画中画')); + let closeBtn = await driver.findComponent(ON.text('关闭画中画')); + await closeBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('删除控制器')); + clickBtn = await driver.findComponent(ON.text('删除控制器')); + await clickBtn.click(); + + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_003 end'); + }) + + /** + * 点击NDK接口实现按钮,调用画中画接口--直播画中画样式 + */ + it(BUNDLE + 'NDKImplement_004', 2, async () => { + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_004 begin') + let driver = Driver.create(); + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + try { + await abilityDelegator.startAbility({ + bundleName: BUNDLE_NAME, + abilityName: 'EntryAbility' + }); + } catch (exception) { + hilog.info(DOMAIN, TAG, BUNDLE + `NDKImplement_004 exception = ${JSON.stringify(exception)}`) + expect().assertFail(); + } + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('更换模板')); + let clickBtn = await driver.findComponent(ON.text('更换模板')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('直播')); + clickBtn = await driver.findComponent(ON.text('直播')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('创建画中画')); + clickBtn = await driver.findComponent(ON.text('创建画中画')) + await clickBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('开启画中画')); + let startBtn = await driver.findComponent(ON.text('开启画中画')); + await startBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('关闭画中画')); + let closeBtn = await driver.findComponent(ON.text('关闭画中画')); + await closeBtn.click(); + await driver.delayMs(1000); + await driver.assertComponentExist(ON.text('删除控制器')); + clickBtn = await driver.findComponent(ON.text('删除控制器')); + await clickBtn.click(); + await driver.delayMs(1000); + await driver.pressBack(); + await driver.delayMs(1500); + + hilog.info(DOMAIN, TAG, BUNDLE + 'NDKImplement_004 end'); + }) + }) +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/module.json5 b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..f225c56cc82a2ae54222da83b34f849825e0bd1b --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/ohosTest/module.json5 @@ -0,0 +1,28 @@ +/* +* 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. +*/ + +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/test/List.test.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..079278eb8075eab0cbcb75e27f31eabf614633fa --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/test/List.test.ets @@ -0,0 +1,20 @@ +/* +* 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 localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/entry/src/test/LocalUnit.test.ets b/ArkUIWindowPipSamples/WindowPip/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..eef44124a7870590c14a76063af1716a2a3ceaed --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,48 @@ +/* +* 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 { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/hvigor/hvigor-config.json5 b/ArkUIWindowPipSamples/WindowPip/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..e85175fe6503c67eccceb5a45dc989c6c4ed6083 --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/hvigor/hvigor-config.json5 @@ -0,0 +1,38 @@ +/* +* 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. +*/ + +{ + "modelVersion": "6.0.0", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + // "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/ArkUIWindowPipSamples/WindowPip/hvigorfile.ts b/ArkUIWindowPipSamples/WindowPip/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e8a731f141a2f8ab2caa8394c10484169d46e9d --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/hvigorfile.ts @@ -0,0 +1,21 @@ +/* +* 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 { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/ArkUIWindowPipSamples/WindowPip/oh-package.json5 b/ArkUIWindowPipSamples/WindowPip/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..eb630e11666045fcc8f953a6b98bb8784211860b --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/oh-package.json5 @@ -0,0 +1,25 @@ +/* +* 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. +*/ + +{ + "modelVersion": "6.0.0", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.24", + "@ohos/hamock": "1.0.1-rc2" + } +} diff --git a/ArkUIWindowPipSamples/WindowPip/ohosTest.md b/ArkUIWindowPipSamples/WindowPip/ohosTest.md new file mode 100644 index 0000000000000000000000000000000000000000..36a7e1b9bf789989212dc095a8a2be705f63ccfc --- /dev/null +++ b/ArkUIWindowPipSamples/WindowPip/ohosTest.md @@ -0,0 +1,24 @@ +# 画中画 测试用例归档 + +## 用例表 + +| 测试功能 | 预置条件 | 输入 | 预期输出 | 是否自动 | 测试结果 | +|------------------------------------|-----------------------------------|----------------------|-----------|------| -------- | +| 拉起应用 | 设备正常运行 | | 成功拉起应用 | 是 | Pass | +| 用XComponent方式创建画中画 | 进入XComponent实现的详情页面 | 点击【StartPip】 | 创建画中画 | 是 | Pass | +| 改变用XComponent方式创建的画中画尺寸 | 用XComponent实现方式创建了画中画 | 点击【updateSize】 | 改变画中画尺寸大小 | 是 | Pass | +| 删除用XComponent方式创建的画中画 | 用XComponent实现方式创建了画中画 | 点击【stopPip】 | 关闭画中画 | 是 | Pass | +| 用typeNode自由节点创建画中画 | 进入typeNode自由节点实现的详情页面 | 点击【StartPip】 | 创建画中画 | 是 | Pass | +| 改变typeNode自由节点创建的画中画尺寸 | 用typeNode自由节点实现方式创建了画中画 | 点击【updateSize】 | 改变画中画尺寸大小 | 是 | Pass | +| 删除typeNode自由节点创建的画中画 | 用typeNode自由节点实现方式创建了画中画 | 点击【stopPip】 | 关闭画中画 | 是 | Pass | +| 用router导航时通过typeNode创建画中画 | 进入用router导航时通过typeNode实现的详情页面 | 点击【StartPip】 | 创建画中画 | 是 | Pass | +| 改变用router导航时通过typeNode创建的画中画尺寸 | 用router导航时通过typeNode实现方式创建了画中画 | 点击【updateSize】 | 改变画中画尺寸大小 | 是 | Pass | +| 删除用router导航时通过typeNode创建的画中画 | 用router导航时通过typeNode实现方式创建了画中画 | 点击【stopPip】 | 关闭画中画 | 是 | Pass | +| 使用navigation导航时通过typeNode创建画中画 | 进入用navigation导航时通过typeNode实现的详情页面 | 点击【StartPip】 | 创建画中画 | 是 | Pass | +| 改变用navigation导航时通过typeNode创建的画中画尺寸 | 用navigation导航时通过typeNode实现方式创建了画中画 | 点击【updateSize】 | 改变画中画尺寸大小 | 是 | Pass | +| 删除用navigation导航时通过typeNode创建的画中画 | 用navigation导航时通过typeNode实现方式创建了画中画 | 点击【stopPip】 | 关闭画中画 | 是 | Pass | +| 使用单页面ability通过typeNode创建画中画 | 进入使用单页面ability通过typeNode实现的详情页面 | 点击【StartPip】 | 创建画中画 | 是 | Pass | +| 使用单页面ability调整通过typeNode创建的画中画尺寸 | 使用单页面ability通过typeNode实现方式创建了画中画 | 点击【updateSize】 | 改变画中画尺寸大小 | 是 | Pass | +| 使用单页面ability删除通过typeNode创建的画中画 | 使用单页面ability通过typeNode实现方式创建了画中画 | 点击【stopPip】 | 关闭画中画 | 是 | Pass | +| 使用NDK接口创建画中画 | 进入使用NDK接口实现的详情页面 | 点击【创建画中画】,再点击【开启画中画】 | 创建画中画 | 是 | Pass | +| 删除用NDK接口创建的画中画 | 通过NDK接口实现创建了画中画 | 点击【关闭画中画】 | 关闭画中画 | 是 | Pass | diff --git a/ArkUIWindowPipSamples/WindowPip/screenshots/XComponentImplement2.gif b/ArkUIWindowPipSamples/WindowPip/screenshots/XComponentImplement2.gif new file mode 100644 index 0000000000000000000000000000000000000000..5a4d5c2ecebfdc2f9dc51280799e1dcdfc5bb696 Binary files /dev/null and b/ArkUIWindowPipSamples/WindowPip/screenshots/XComponentImplement2.gif differ