开发工具: 华为云码道

DevEco Studio: 26.0.0(26.0.0.851)

原始仓库: abdulmuizdev/ios_color_picker_with_title

本文配套仓库: oh-flutter/color_picker_with_title

ios_color_picker_with_title 是一个带标题的 iOS 风格取色器,复刻了系统 UI 的颜色面板:顶部一排 Grid / Spectrum / Sliders 切换,下面是取色区域和不透明度滑杆,底部是当前色块与历史颜色。它同时提供一条 iOS 原生取色器通道,但主体功能是纯 Dart 实现的。本文以 ios_color_picker_with_title 1.0.2 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

在这里插入图片描述

插件目前支持 android、ios、linux、macos、windows、web、ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以提交 1681b13 为参考。

在这里插入图片描述


一、插件简介与适配目标

取色器是主题设置、便签、画板这类应用的基础组件。ios_color_picker_with_title 的做法是用 Flutter 自己画出一个 iOS 风格的底部弹层,而不是调用系统取色器:弹层用 showModalBottomSheet 打开,三种面板分别用色板网格、HSL 色域和 RGB 分量滑杆实现,选中结果通过回调交给业务页面。

选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页

例如,笔记应用可以让用户挑一个背景色,绘图工具可以用它设置画笔颜色,设置页可以用它调整强调色。弹层底部会保留最近用过的颜色,长按其中一个可以删除。

取色有两条路径:一条是纯 Dart 的自定义弹层 showIOSCustomColorPicker,全平台可用;另一条是 showNativeIosColorPicker,通过平台通道调用 iOS 系统的 UIColorPickerViewController,仅 iOS 可用。本次适配的目标是让前者在鸿蒙上完整可用,并让插件本身能被正常注册。


二、环境准备

环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。

完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:

flutter --version
flutter doctor -v
hdc list targets

在这里插入图片描述
在这里插入图片描述

编辑用户

工程使用的工具链和 SDK 配置如下:

项目版本或配置用途
Flutter OHOS SDK3.47.7-ohos-1.0.0Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.47-devoh-flutter 对应开发分支
Dart SDK3.13.3Dart 语言与包管理环境
DevEco Studio26.0.0(26.0.0.851)工程 Sync、签名配置与 hvigor 构建
HarmonyOS 开发套件7.0.0(API 26)开发套件版本及对应的 API 级别
compileSdkVersion26.0.0编译时使用的 SDK API
targetSdkVersion26.0.0应用面向的行为版本
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
插件版本1.0.2pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物HAR被应用 entry 模块依赖

2.1 开发套件版本与工程中的 SDK 版本配置

7.0.0(API 26) 和 26.0.0 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:

  • 7.0.0(API 26) 表示 HarmonyOS 开发套件版本为 7.0.0,对应 API 26。
  • 26.0.0 是本文 HarmonyOS 应用工程中 compileSdkVersion 和 targetSdkVersion 的属性值。
  • 6.1.1(24) 是本文工程中 compatibleSdkVersion 的属性值,声明最低兼容 API 24。

对应的 product 配置为:

{
  "name": "default",
  "compatibleSdkVersion": "6.1.1(24)",
  "targetSdkVersion": "26.0.0",
  "runtimeOS": "HarmonyOS"
}

在这里插入图片描述

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 24。模板默认生成的是 compatibleSdkVersion: "26.0.0",装到 API 24 的测试机上会报 9568297 older sdk version,所以这里把它降到了与真机一致。

需要注意,降 compatibleSdkVersion 只影响"能不能装"和"按哪个行为版本运行",编译仍使用本机 DevEco 的 API 26 SDK。这是 hvigor 的默认行为:工程不写 compileSdkVersion 时按当前安装的 SDK 编译。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

color_picker_with_title 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

git clone https://atomgit.com/oh-flutter/color_picker_with_title.git
cd color_picker_with_title
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

git clone 会创建 color_picker_with_title/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。Git 仓库名是 color_picker_with_title,Dart 包名是 ios_color_picker_with_title,两者并不相同,写依赖和 --project-name 时不要混用。

适配后的仓库使用 master 分支,并在适配提交上打了 1.0.2-ohos-1.0.0 标签。需要与本文代码完全一致时,在干净仓库中执行:

git switch --detach 1.0.2-ohos-1.0.0

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

请添加图片描述

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。

3.3 在仓库根目录确认适配分支与版本标记

接着在 color_picker_with_title/ 根目录确认分支与版本标记。上游仓库只有一个 master 分支,适配改动直接落在这个分支上,并通过 tag 固定版本。分支名与 tag 的对应关系如下:

git branch --show-current
git tag --list
git log --oneline -3
git remote -v
git status --short --branch

分支策略与本系列其余库保持一致:分支名使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本,本例为 feat/ohos_ios_color_picker_with_title_1.0.2。适配完成后合并到 master,再在合并后的提交上打 tag:

git switch -c feat/ohos_ios_color_picker_with_title_1.0.2
git tag 1.0.2-ohos-1.0.0

如果该分支已存在,使用 git switch feat/ohos_ios_color_picker_with_title_1.0.2 切换即可。

请添加图片描述

图 2:在 color_picker_with_title 仓库根目录查看分支、TAG 与远程地址。

3.4 自动补全 OHOS 适配结构

分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:

flutter create --template=plugin --platforms=ohos --project-name ios_color_picker_with_title .
git status --short
git diff -- pubspec.yaml lib example
  • --template=plugin 指定插件模板。
  • --platforms=ohos 指定需要补全的平台。
  • --project-name ios_color_picker_with_title 使用 Dart 包名,而不是仓库目录名。
  • 最后的 . 表示在当前插件目录补全工程,不是另建一层目录。

该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。

这个库在结构补全之外还有两处不得不改的地方。一是 super_tooltip:上游锁定的 2.0.9 在当前 Flutter 上无法编译,它自己的 TooltipPositionDelegate 与 package:flutter/widgets 新导出的同名类冲突;升到 2.1.1 后必须同步改写 lib/custom_picker/history_colors.dart 里的长按删除气泡样式(新版本把平铺参数拆成了配置对象),细节见 4.4。二是 shared_preferences:历史颜色和面板标签都靠它持久化,而 pub.dev 版本没有 OpenHarmony 实现,需要在 example 里显式引入 shared_preferences_ohos。

如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:

cd example
flutter create --platforms=ohos .
cd ..

配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos ios_color_picker_with_title;已有插件使用上面的 . 在当前目录补全。

请添加图片描述

图 3:在插件根目录输入 OHOS 结构补全命令,并检查依赖与平台声明的 diff。

3.5 适配后的项目目录

适配后的关键目录如下:

color_picker_with_title/
├── lib/
│   ├── show_ios_color_picker.dart
│   ├── custom_picker/
│   │   ├── ios_color_picker.dart
│   │   ├── pickers_selector_row.dart
│   │   ├── history_colors.dart
│   │   ├── color_observer.dart
│   │   ├── shared.dart
│   │   ├── helpers/cache_helper.dart
│   │   └── pickers/
│   └── native_picker/
│       ├── ios_color_picker.dart
│       ├── ios_color_picker_method_channel.dart
│       ├── ios_color_picker_platform_interface.dart
│       └── ios_color_picker_web.dart
├── ohos/
│   ├── index.ets
│   ├── oh-package.json5
│   └── src/main/
│       ├── ets/components/plugin/IosColorPickerWithTitlePlugin.ets
│       └── module.json5
├── example/
│   ├── lib/main.dart
│   └── ohos/
│       └── entry/
├── android/ ios/ linux/ macos/ windows/
└── pubspec.yaml

项目根目录如下,其中包含 ohos/、example/,以及 OpenHarmony 中英文说明和变更记录文件:

请添加图片描述

图 4:适配后的 color_picker_with_title 项目目录。

文件主要职责
lib/show_ios_color_picker.dart对外入口,声明 IOSColorPickerController 与两条取色路径
lib/custom_picker/ios_color_picker.dart弹层主体:标题、面板切换、不透明度、当前色块与历史行
lib/custom_picker/pickers_selector_row.dartGrid / Spectrum / Sliders 三个面板的切换与渲染
lib/custom_picker/history_colors.dart历史颜色分页、长按删除气泡、shared_preferences 读写
lib/custom_picker/helpers/cache_helper.dartSharedPreferences 单例封装
lib/native_picker/*iOS 原生取色器的平台接口与 MethodChannel 实现
IosColorPickerWithTitlePlugin.ets注册 ios_color_picker 通道,提供 getPlatformVersion
插件 module.json5声明 HAR 模块及设备类型
示例 entry module.json5声明宿主应用 Ability、设备类型和权限场景
example/lib/main.dart展示预览卡片、两个取色按钮、描边设置和历史颜色

四、Dart 接口与通道分析

这个库的 Dart 层同时存在两种实现:纯 Dart 的弹层,和仅 iOS 可用的原生通道。阅读 lib/show_ios_color_picker.dart、lib/native_picker/ 和 lib/custom_picker/ 之后可以确认:业务真正使用的是 showIOSCustomColorPicker,它一路走到底都是 Flutter 自己的 widget,不经过任何原生代码;showNativeIosColorPicker 才会触发通道,并且入口处就有 assert(Platform.isIOS)。因此 OHOS 侧要做的是让插件壳能被注册、让原生通道对未实现的方法明确返回,而不是去桥接一个鸿蒙并不存在的系统取色器。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
showIOSCustomColorPicker(...)无无用 showModalBottomSheet 打开纯 Dart 弹层,选色结果走 onColorChanged
showNativeIosColorPicker(...)ios_color_picker / pickColorresult.notImplemented()仅 iOS 有效;OHOS 上不调用,Dart 侧已有 Platform.isIOS 断言
IosColorPickerPlatform.getPlatformColor(...)ios_color_picker / pickColor同上参数 darkMode、defaultColor、title 原样透传
原生取色回调ios_color_picker_stream 事件流不实现仅 iOS 使用,OHOS 上不订阅

原生端需要保持通道名和方法名与 Dart 一致。系统能力不可用时,通过 result.notImplemented() 或错误回调通知 Dart 层,不要静默返回空值。

4.1 跨端架构与调用时序

Flutter 侧和 HarmonyOS 侧之间实际只有一条会被用到的通道,另有一条 iOS 专用的事件流:

  1. MethodChannel('ios_color_picker'):承载 pickColor,只有 iOS 原生取色器会调用;
  2. EventChannel('ios_color_picker_stream'):iOS 原生取色器把用户拖动中的颜色持续回传;
  3. 纯 Dart 弹层:不经过任何通道,全部在 Flutter 渲染层内完成。

仅 iOS

Flutter 页面

IOSColorPickerController

showIOSCustomColorPicker

showModalBottomSheet

IosColorPicker

PickersSelectorRow

GridPicker

AreaColorPicker

SlidePicker

HistoryColors

CacheHelper / SharedPreferences

MethodChannel ios_color_picker

ArkTS IosColorPickerWithTitlePlugin

iOS UIColorPickerViewController

实线部分是鸿蒙上真正执行的路径:IOSColorPickerController 只负责创建 ColorController 并打开弹层,之后所有交互都在 IosColorPicker 内部完成。虚线部分是 iOS 原生取色器,鸿蒙上不会进入。

4.1.1 一次完整选色的时序
SharedPreferences PickersSelectorRow IosColorPicker showModalBottomSheet IOSColorPickerController Flutter App SharedPreferences PickersSelectorRow IosColorPicker showModalBottomSheet IOSColorPickerController Flutter App showIOSCustomColorPicker(context, onColorChanged) showModalBottomSheet(isScrollControlled) build IosColorPicker CacheHelper.init() PickersSelectorRow() getData(selector_index) 上次停留的面板 onColorChanged(color)(拖动或点击时持续触发) Navigator.pop(context)(点关闭或遮罩) setData(selector_index)

面板标签在 dispose 时写回,历史颜色在新增或删除时写回。两条写入都依赖 SharedPreferences,这也是本库在鸿蒙上唯一需要额外补依赖的地方。

4.2 颜色模型:lib/custom_picker/extensions.dart

库内部统一用 Flutter 的 Color 和 HSVColor 表示颜色,序列化交给三个扩展方法:

extension ColorExtensions on Color {
  /// Converts a [Color] to a `Map<String, double>`.
  Map<String, double> toMap() {
    return {
      "red": r,
      "green": g,
      "blue": b,
      "alpha": a,
    };
  }
}

extension MapToColorExtension on Map<Object?, Object?> {
  /// Converts a `Map<String, double>` to a [Color], clamping to valid range.
  Color toColor({bool clampValues = true}) { ... }
}

extension HexColor on Color {
  static Color fromHex(String hexString) { ... }

  String toHex({bool leadingHashSign = false}) =>
      '${leadingHashSign ? '#' : ''}'
      '${red.toRadixString(16).padLeft(2, '0')}'
      '${green.toRadixString(16).padLeft(2, '0')}'
      '${blue.toRadixString(16).padLeft(2, '0')}';
}
表示形式使用位置说明
Color业务回调、当前色块对外统一类型
HSVColorSlidePicker、AreaColorPicker色相/饱和度/明度换算与滑杆取值
Map<String, double>ios_color_picker 通道参数defaultColor 的原生参数格式,分量为 0–1 的 r/g/b/a
#AARRGGBB 字符串shared_preferences历史颜色逐条存为字符串

适配时已经把 toMap() 从旧的 red/green/blue/alpha 改成 r/g/b/a,因此它在新 Flutter 上不再报警告;toHex() 里仍是旧的 red/green/blue,flutter analyze 会给出 3 条 deprecated_member_use。连同 slider_helper.dart、slider_picker.dart、utils.dart 里同样的访问器,一共 12 条。这些属于上游实现,保留原样可避免在适配提交里混入与平台无关的重构,也不影响运行。

4.3 公开 API 与平台接口

IOSColorPickerController 是业务唯一需要持有的对象:

class IOSColorPickerController {
  Color selectedColor = Colors.green;

  Future<void> showNativeIosColorPicker({
    required ValueChanged<Color> onColorChanged,
    Color? startingColor,
    bool? darkMode,
    String? title,
  }) async { ... }

  void showIOSCustomColorPicker({
    required BuildContext context,
    required ValueChanged<Color> onColorChanged,
    Color? startingColor,
  }) async { ... }

  void cancelColorSubscription() { ... }

  void dispose() { ... }
}

各方法的行为如下:

  • showIOSCustomColorPicker:打开纯 Dart 弹层,返回 void,不等待用户选择;选色过程通过 onColorChanged 持续回调;
  • showNativeIosColorPicker:打开 iOS 原生取色器,入口处断言 Platform.isIOS,非 iOS 平台在 debug 模式下直接抛断言错误;
  • selectedColor:记录最近一次选中的颜色,也是下次打开弹层时的起始色;
  • cancelColorSubscription:取消 ios_color_picker_stream 的事件订阅,只在走过原生路径后才需要;
  • dispose:内部调用 cancelColorSubscription,页面销毁时调用一次即可。

IosColorPickerPlatform 定义平台接口,默认实例是 MethodChannelIosColorPicker,测试里可以替换成 mock 实现。

4.4 通道协议与渲染实现分析

4.4.1 通道名称必须与 Dart 完全一致
// lib/native_picker/ios_color_picker_method_channel.dart
final methodChannel = const MethodChannel('ios_color_picker');

// lib/show_ios_color_picker.dart
static const _eventChannel = EventChannel('ios_color_picker_stream');

通道名不能用包名推。模板默认会按插件名生成 ios_color_picker_with_title,而 Dart 实际用的是 ios_color_picker,两者不一致时 ArkTS 侧永远不会被调用。适配时按 Dart 的写法注册,这一点在 IosColorPickerWithTitlePlugin.ets 中已经固定。

4.4.2 pickColor 在 OpenHarmony 上返回 notImplemented
final color = await methodChannel.invokeMethod<Map>('pickColor', map);
if (color == null) return null;
return Map<String, double>.from(color).toColor();

Dart 侧只关心返回值是不是一个颜色 Map。OpenHarmony 标准 SDK 没有等价于 UIColorPickerViewController 的系统取色器组件,硬造一个原生面板既无法与 iOS 语义对齐,也没有必要——纯 Dart 弹层已经覆盖了同样的能力。因此 ArkTS 侧对这个方法统一回 notImplemented,并在代码里写明理由。

4.4.3 长按删除气泡:super_tooltip 2.1.x 的参数映射

history_colors.dart 用 SuperTooltip 做长按删除。2.0.9 无法在当前 Flutter 上编译,因为它自己的 TooltipPositionDelegate 与 package:flutter/widgets 新导出的同名类冲突;升到 2.1.1 后,原来平铺在 SuperTooltip 上的样式参数被拆成了配置对象,因此逐项映射:

barrierConfig: const BarrierConfiguration(show: false, sigmaX: 16, sigmaY: 16),
style: const TooltipStyle(hasShadow: false, bubbleDimensions: EdgeInsets.zero),
arrowConfig: const ArrowConfiguration(length: 8, tipDistance: 17),
positionConfig: const PositionConfiguration(preferredDirection: TooltipDirection.up),
旧参数(2.0.x)新参数(2.1.x)保持的效果
showBarrierBarrierConfiguration.show长按时不出现遮罩
sigmaX / sigmaYBarrierConfiguration.sigmaX/sigmaY遮罩模糊半径
hasShadowTooltipStyle.hasShadow气泡不带阴影
bubbleDimensionsTooltipStyle.bubbleDimensions气泡内边距由内容决定
arrowLength / arrowTipDistanceArrowConfiguration.length/tipDistance尖角长度与离目标的距离
popupDirectionPositionConfiguration.preferredDirection气泡朝向屏幕上方

映射后视觉效果与 2.0.x 一致,真机长按历史颜色可以看到相同的 “Delete” 气泡(见 8.6 的 fig5-delete.jpeg)。

4.4.4 历史颜色与面板标签的持久化
// lib/custom_picker/helpers/cache_helper.dart
Future<void> _ensureInitialized() async {
  if (!_isInitialized) {
    _sharedPreferences = await SharedPreferences.getInstance();
    _isInitialized = true;
  }
}
键类型写入时机读取时机
history_colorsList<String>新增、删除历史颜色弹层首次构建
selector_indexintPickersSelectorRow.dispose面板切换行 initState

两者都由 shared_preferences 承担。它的 pub.dev 发行版没有 OpenHarmony 实现,如果没有额外引入 shared_preferences_ohos,SharedPreferences.getInstance() 会抛 MissingPluginException:异常被异步调用吞掉后,界面表现就是历史颜色一行空白、下次打开弹层又回到 Grid 面板。适配时在 example 里补了这个依赖,真机上历史颜色和面板标签都能跨进程保留。


五、补全 OHOS 原生实现与工程配置

5.1 在 IosColorPickerWithTitlePlugin.ets 中补全插件壳

业务侧不直接接触 ArkTS。需要补全的是插件类本身:注册一个名为 ios_color_picker 的 MethodChannel,对 getPlatformVersion 返回系统版本,对其他方法(包括 pickColor)明确返回未实现。

IosColorPickerWithTitlePlugin 实现 FlutterPlugin 和 MethodCallHandler。下面是类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口。

原生插件位于:

ohos/src/main/ets/components/plugin/IosColorPickerWithTitlePlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import deviceInfo from '@ohos.deviceInfo';

其中:

  • FlutterPlugin 负责接入 Flutter Engine 生命周期;
  • MethodChannel 接收 Dart 发来的命令;
  • MethodCall / MethodResult 承载一次调用的参数与结果;
  • deviceInfo 提供 osFullName,用于拼出版本字符串。
5.1.2 连接 Flutter Engine
private channel: MethodChannel | null = null;

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.channel = new MethodChannel(binding.getBinaryMessenger(), "ios_color_picker");
  this.channel.setMethodCallHandler(this);
}

通道名必须是 ios_color_picker,与 4.4.1 中 Dart 侧的写法逐字一致。改成包名会让 Dart 调用落到"未实现方法"上。

5.1.3 通道方法
onMethodCall(call: MethodCall, result: MethodResult): void {
  try {
    if (call.method == "getPlatformVersion") {
      result.success(`OpenHarmony ${deviceInfo.osFullName}`);
    } else {
      // pickColor only has a native implementation on iOS; on OpenHarmony
      // users should use the pure-Dart custom picker (showIOSCustomColorPicker).
      result.notImplemented();
    }
  } catch (e) {
    result.error("IOS_COLOR_PICKER_ERROR", `method ${call.method} failed: ${e}`, null);
  }
}

getPlatformVersion 是模板保留的方法,Dart 层没有调用方,保留它是为了让插件在被直接调用通道时仍有可用返回值。pickColor 走 notImplemented,Dart 侧会收到 MissingPluginException;由于 showNativeIosColorPicker 在进入通道之前就有 Platform.isIOS 断言,正常使用时不会走到这一步。

try/catch 把异常统一转成 IOS_COLOR_PICKER_ERROR,避免原生侧的异常把调用方挂住。

5.1.4 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null);
  }
}

Flutter Engine 销毁时清掉通道 Handler,避免残留引用。

5.2 声明插件和宿主权限

本库不需要任何系统权限:纯 Dart 弹层只做界面绘制,历史颜色通过 shared_preferences 写入应用沙箱,原生侧唯一的通道方法 getPlatformVersion 读取的是系统版本属性。

5.2.1 插件 HAR 的权限

ohos/src/main/module.json5 保持模板内容即可,不需要 requestPermissions:

{
  "module": {
    "name": "ios_color_picker_with_title",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}
5.2.2 应用 entry 的权限

example/ohos/entry/src/main/module.json5 里有一条模板自带的 ohos.permission.INTERNET:

"requestPermissions": [
  { "name": "ohos.permission.INTERNET" }
]

这条声明与本库的取色、历史记录都无关,删掉不影响示例运行。保留时也需要注意:权限声明和运行时授权是两个步骤,module.json5 中的声明不会自动完成授权。

5.3 注册并导出插件

pubspec.yaml 通过以下配置声明 OHOS 插件类:

flutter:
  plugin:
    platforms:
      ohos:
        pluginClass: IosColorPickerWithTitlePlugin

插件的 ohos/index.ets 需要导出实现:

import IosColorPickerWithTitlePlugin from './src/main/ets/components/plugin/IosColorPickerWithTitlePlugin';
export default IosColorPickerWithTitlePlugin;

执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。本例生成的注册文件中包含三个插件:

flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
flutterEngine.getPlugins()?.add(new SharedPreferencesPlugin());
flutterEngine.getPlugins()?.add(new IosColorPickerWithTitlePlugin());

SharedPreferencesPlugin 来自 shared_preferences_ohos。它出现在这里,才说明历史颜色的持久化真的接通了;只看到 IosColorPickerWithTitlePlugin 时,取色功能正常但历史颜色会是空的。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。

注册异常的排查步骤见第九节 MissingPluginException。

5.4 检查 example 的 OHOS 应用结构

本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。

{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS",
        "targetSdkVersion": "26.0.0"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。

本例 AppScope/app.json5 里的 bundleName 沿用了同系列示例的 com.example.color_system_example。签名 Profile 是按包名签发的,改包名必须同时换一份匹配的 Profile,否则 SignHap 会失败;如果要把示例发布成独立应用,记得把这两处一起换掉。


六、补全交付文件并提交适配分支

6.1 除代码外还要补全哪些文件

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:

文件应写清楚的内容
README.OpenSource上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖
README.md原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息
README.OpenHarmony_CN.md简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题
README.OpenHarmony.md与中文说明对应的英文文档
CHANGELOG.OpenHarmony.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE / NOTICE保留上游许可证;NOTICE 按许可证和原项目要求保留或补充
example/README.md依赖方式、运行目录、签名、操作步骤与效果图;覆盖取色、历史颜色与删除
pubspec.yaml、ohos/oh-package.json5核对包名、版本、插件注册、仓库地址、许可证和依赖
.gitignore忽略构建缓存及本机签名材料,不漏提交必要源码和配置

README.OpenSource 记录库本身的来源与版本。本例的包名为 ios_color_picker_with_title,版本为 1.0.2,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。两份 README.OpenHarmony 里的依赖地址、TAG 名称和环境版本要与最终提交保持一致,CHANGELOG.OpenHarmony.md 需要同时记录 shared_preferences_ohos 这一项额外依赖。

6.2 提交前检查

提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:

git branch --show-current
git diff --check
git status --short
git diff --stat
git diff

检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。

6.3 提交并推送到 AtomGit

文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:

git add ohos pubspec.yaml example test .gitignore
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for ios_color_picker_with_title 1.0.2"
git remote -v
git branch --show-current
git push -u origin feat/ohos_ios_color_picker_with_title_1.0.2

本次适配实际拆成了三个提交:0895090 补 ohos 结构与插件壳、升级 super_tooltip 并重写示例页,5a5636e 把 compatibleSdkVersion 对齐到真机的 6.1.1(24),1681b13 给 example 加上 shared_preferences_ohos 并校正文档里的仓库地址与 TAG 名称。TAG 1.0.2-ohos-1.0.0 打在 1681b13 上。

DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 feat/ohos_ios_color_picker_with_title_1.0.2。

推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。


七、使用根目录 example 演示接入

仓库自带 example/,可以直接用来调试插件和体验取色效果。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

dependencies:
  flutter:
    sdk: flutter
  ios_color_picker_with_title:
    path: ../

  shared_preferences_ohos:
    git:
      url: https://atomgit.com/openharmony-sig/flutter_packages.git
      path: packages/shared_preferences/shared_preferences_ohos

../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。shared_preferences_ohos 是本库特有的补充项,它声明自己实现 shared_preferences,只在 ohos 平台生效,在其他平台上是被忽略的空实现。

从插件根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lock 中是否出现 shared_preferences_ohos,来源为 git,resolved-ref 指向 flutter_packages 的某个提交。

7.2 通过 AtomGit 引入插件

业务应用通过 AtomGit 引入时,将 ios_color_picker_with_title 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的 TAG:

dependencies:
  flutter:
    sdk: flutter
  ios_color_picker_with_title:
    git:
      url: https://atomgit.com/oh-flutter/color_picker_with_title.git
      ref: 1.0.2-ohos-1.0.0

注意 url 里的仓库名是 color_picker_with_title,而 dependencies 的键是 Dart 包名 ios_color_picker_with_title;两者不一致时以包名为准,仓库名只影响 URL。

使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_ios_color_picker_with_title_1.0.2。正式发布后可固定到 tag 或 commit。

使用取色器历史的项目还要记得按 7.1 的方式补上 shared_preferences_ohos,它属于宿主应用的依赖,不会随插件自动带入。

7.3 调用接口实现取色

下面的页面展示一个取色按钮和一个颜色预览块,可用于 example/lib/main.dart。仓库中的完整 Demo 还提供描边宽度、描边颜色、预览开关和历史颜色。

import 'package:flutter/material.dart';
import 'package:ios_color_picker_with_title/show_ios_color_picker.dart';

void main() {
  runApp(const MaterialApp(home: ColorPickerPage()));
}

class ColorPickerPage extends StatefulWidget {
  const ColorPickerPage({super.key});

  
  State<ColorPickerPage> createState() => _ColorPickerPageState();
}

class _ColorPickerPageState extends State<ColorPickerPage> {
  final IOSColorPickerController _controller = IOSColorPickerController();
  Color _selected = Colors.green;

  void _pickColor() {
    _controller.showIOSCustomColorPicker(
      context: context,
      startingColor: _selected,
      onColorChanged: (Color color) {
        if (!mounted) return;
        setState(() => _selected = color);
      },
    );
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('iOS 风格取色器')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              width: 160,
              height: 160,
              decoration: BoxDecoration(
                color: _selected,
                borderRadius: BorderRadius.circular(12),
              ),
            ),
            const SizedBox(height: 12),
            Text(_selected.toARGB32().toRadixString(16).toUpperCase()),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: _pickColor,
              child: const Text('打开取色器'),
            ),
          ],
        ),
      ),
    );
  }
}

startingColor 传当前颜色,用户在弹层里拖动时不会被重置;onColorChanged 会随拖动连续触发,因此 setState 要加 mounted 判断。

7.4 页面退出时的处理

IOSColorPickerController 内部持有 EventChannel 的订阅字段,原生路径用过之后必须取消。纯 Dart 路径不会创建订阅,但统一在 dispose 中调用 _controller.dispose() 是最省心的写法,两种情况都覆盖。

多个页面都需要同一份取色状态时,可以让应用级服务持有 selectedColor,页面只负责打开弹层并回传结果,避免每个页面各自维护一份控制器。


八、验证、构建与鸿蒙设备运行效果

8.1 分别验证插件与 example

从插件仓库根目录执行:

flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

本机实测结果:

  • 插件 flutter analyze:14 条提示,全部为 info/warning,没有 error。其中 13 条是 deprecated_member_use——lib/custom_picker/extensions.dart、utils.dart、pickers/slider_picker/slider_helper.dart、pickers/slider_picker/slider_picker.dart 各 3 条,用的是 color.red/green/blue/value 这批已废弃的访问器;另有 1 条在 example/lib/main.dart:97,同样是 backgroundColor.value。剩下 1 条是 test/ios_color_picker_method_channel_test.dart 里未使用的局部变量 platform。
  • example 单独执行 flutter analyze:1 条提示,即上面那处 value,建议改用 toARGB32()。
  • flutter test:本机工具链无法完成。flutter_tester 启动后直接挂住(此前其他库报 Invalid SDK hash),因此这两个用例本次未执行。仓库里的 test/ 目录保留原样:ios_color_picker_method_channel_test.dart 中的断言被上游注释掉了,ios_color_picker_test.dart 用 MockIosColorPickerPlatform 验证 getPlatformColor 的参数透传,属于接口层测试,与 OHOS 行为无关。

Dart 测试覆盖接口和页面逻辑,取色面板的真实交互还需要在鸿蒙设备上验证,这也是第八节其他小节的主要内容。

8.2 确认设备连接

hdc list targets
flutter devices

设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。

8.3 配置签名

真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs;
  4. 为 default product 选择或生成签名;
  5. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行 Flutter 构建或运行。

签名材料保存在本机,公开仓库中只保留构建所需的通用配置。

8.4 运行示例

以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:

flutter run -d <device-id>

也可以先构建 HAP:

flutter build hap --debug

典型产物位于:

example/ohos/entry/build/default/outputs/default/

目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。

本次适配时,flutter build hap --debug 在本机停在 hvigor 的 ohpm install 阶段,报 00306053 Specification Limit Violation 和 BATCH RECURSION exceeds STACK limits。这种情况下可以先手动装依赖,再直接调 hvigor:

cd example/ohos
"D:/Program Files/Huawei/DevEco Studio/tools/ohpm/bin/ohpm" install

然后在 PowerShell 中补上 JAVA_HOME 再执行(路径含空格,用 Bash 直接调 .bat 容易失败):

$env:JAVA_HOME="D:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="$env:JAVA_HOME\bin;" + $env:PATH
cd D:\flutter\lib3\ios_color_picker_with_title\example\ohos
& "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" assembleHap -p product=default -p buildMode=debug --no-daemon

完成后产物出现在同一路径下,用 hdc install 安装即可:

hdc -t <device-id> install -r example/ohos/entry/build/default/outputs/default/entry-default-signed.hap

这条绕行方式是为了绕开本机 ohpm 的环境问题,正式发布仍应走 flutter build hap。

8.5 在设备上测试取色

  1. 打开应用,确认首页显示预览卡片、当前色值与"历史颜色:暂无记录";
  2. 点击"Custom picker (all platforms)",确认弹出底部取色面板,标题为 Colors;
  3. 在 Grid 面板点一个色块,确认背后首页的背景色同步变化;
  4. 切到 Spectrum,在色域里拖动,确认颜色连续变化;
  5. 切到 Sliders,拖动 RED / GREEN / BLUE 三个滑杆,确认色值和数值同步;再把 OPACITY 拖到 40%,确认底部色块出现透明网格;
  6. 点历史行末尾的"+",确认当前颜色被加入历史;
  7. 长按历史里任意一个颜色,确认弹出 Delete 气泡;
  8. 点右上角关闭,确认首页使用最终颜色,历史行出现刚选过的颜色;
  9. 杀掉应用重新打开,再进入取色器,确认仍停留在上次的面板,历史颜色也还在。

本文已在真机完成第 1–9 步(见 8.6,设备 TLR-AL00 / API 24)。其中第 5 步会看到 Sliders 面板底部的黄黑溢出提示条,属布局计算问题,说明见 9.11。

8.6 鸿蒙设备运行效果

完成适配后,showIOSCustomColorPicker 在鸿蒙设备上可以正常打开、选色并回传给业务页面;三种面板、不透明度、历史颜色和长按删除都可用,历史颜色与面板标签在应用重启后仍然保留。

首页与三种面板:

示例首页Grid 面板Spectrum 面板
预览卡片、取色按钮、描边设置,历史为空12×10 色板网格,底部一行默认历史色HSL 色域,圆环标记当前颜色

Sliders 面板与历史颜色操作:

Sliders 面板加入历史颜色长按删除
三个分量滑杆,不透明度拖到 40%,底部为 24 像素溢出提示点"+"把当前色写入 history_colorssuper_tooltip 气泡显示 Delete

选色结果与持久化:

选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页 选色后的首页

选色后的首页重启后重新打开
背景色变为 FFB92D5D,历史行出现两条记录直接停在 Sliders 面板,历史颜色仍在

Sliders 面板底部的黄黑条纹是 Flutter 的溢出提示:弹层高度按 340 + componentsHeight(context) 写死,三个分量滑杆加不透明度行在该机 361vp 的逻辑宽度下超出约 24 像素。这是上游的布局计算问题,与平台无关,Release 包不显示提示条,功能也不受影响;需要彻底解决时按 9.11 调整高度计算。


九、FAQ:适配过程与使用问题

9.1 Missing SDK components

典型错误如下:

Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.

这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchains、ets、js、native、previewer;
  5. 执行 flutter config --ohos-sdk <正确路径>;
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 Sync 和 Compile 首先读取本机 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。

当前工程的 compatibleSdkVersion 是 6.1.1(24),因此 API 24 设备在安装版本门槛上是满足的;取色器不依赖任何设备专属能力,装上即可使用。

9.2 DevEco Studio 中看不到 entry 模块

插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。

请直接使用 DevEco Studio 打开:

color_picker_with_title/example/ohos

如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 无法手动签名

签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。

建议先确认:

  • 打开的是 example/ohos;
  • SDK 组件完整并且 Sync 成功;
  • entry 的模块类型为 entry;
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效。

9.4 点击取色按钮没有弹出面板

按以下顺序检查:

  1. 是否调用的是 showIOSCustomColorPicker,而不是 showNativeIosColorPicker——后者在鸿蒙上会命中 assert(Platform.isIOS);
  2. showIOSCustomColorPicker 的 context 是否来自当前可见页面的 build,用已经销毁的 context 打开 showModalBottomSheet 会抛异常;
  3. 页面是否被 Scaffold 或 MaterialApp 包裹,showModalBottomSheet 需要 Navigator 与 Overlay;
  4. IosColorPicker 使用了固定高度 340 + componentsHeight(context),如果外层再套一层高度受限的容器(例如固定高度的 Column),弹层内容会被裁剪,看起来像"打开了但空白";
  5. hilog 中是否出现 Channel not implemented,出现时说明走到了原生路径,见 9.5。

9.5 MissingPluginException

这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:

cd example
flutter clean
flutter pub get
flutter run -d <device-id>

如果仍然出现,检查自动生成的插件注册文件中是否包含 IosColorPickerWithTitlePlugin 与 SharedPreferencesPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。

需要说明的是:本库的取色功能不走通道,即使插件未注册,弹层也能正常打开和选色。真正受影响的是历史颜色和面板标签,表现为历史一行为空、重启后回到 Grid,处理方式见 9.6。

9.6 历史颜色为空,或者再次打开不记得上次的面板

原因是 shared_preferences 没有 OpenHarmony 实现。CacheHelper 里的 SharedPreferences.getInstance() 会抛 MissingPluginException,异常发生在异步初始化中,界面不会报错,只是历史颜色列表为空、selector_index 写不进去。

处理方式:在宿主应用(本例是 example/pubspec.yaml)中补充依赖,然后重新 flutter pub get 并完整重建:

dependencies:
  shared_preferences_ohos:
    git:
      url: https://atomgit.com/openharmony-sig/flutter_packages.git
      path: packages/shared_preferences/shared_preferences_ohos

验证办法有两个:查看 example/pubspec.lock 是否出现 shared_preferences_ohos;查看 GeneratedPluginRegistrant.ets 中是否出现 new SharedPreferencesPlugin()。两者都出现后,历史颜色和面板标签才能跨进程保留。

9.7 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • 包名与签名 Profile 不匹配;
  • 安装包的 compatibleSdkVersion 高于设备 API;
  • 手机上已经安装了使用不同证书签名的同包名应用。

示例工程的 bundleName 是 com.example.color_system_example,与同系列其他示例相同。如果设备上已经装过同包名、不同证书的应用,安装会失败,需要先卸载旧应用或换一个包名(换包名要同时换签名 Profile)。

9.8 flutter create 不认识 ohos,或包名不合法

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name ios_color_picker_with_title;仓库名 color_picker_with_title 不能直接作为 Dart 包名。生成后检查 diff,再补充 ArkTS 业务实现。

9.9 AtomGit 依赖提示找不到分支或无权限

先检查 URL 是否指向已同步的目标仓库,再确认 feat/ohos_ios_color_picker_with_title_1.0.2 已推送。分支尚未推送时,可以先使用第七节的 TAG 或提交号 1681b13。私有仓库还需在本机配置 Git 认证。

9.10 改了本地 ArkTS,Demo 为什么没变化

先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。

另外,修改 pubspec.yaml 的依赖(例如新增 shared_preferences_ohos)之后必须完整重建一次,GeneratedPluginRegistrant.ets 才会重新生成;只做热重载时新插件不会注册。

9.11 Sliders 面板底部溢出 24 像素

现象是切到 Sliders 面板后,不透明度行上方出现一条黄黑警示条,提示 BOTTOM OVERFLOWED BY 24 PIXELS;Release 包不显示警示条,但内容仍会被裁掉 24 像素。

原因在弹层高度:

Container(
  height: 340 + componentsHeight(context),
  ...
)

componentsHeight(context) 在 lib/custom_picker/shared.dart 中按宽度推算:

double componentsHeight(BuildContext context) {
  return (maxWidth(context) - 32) - (((maxWidth(context) - 32) / 12) * 2);
}

在 1084×2412、DPR 3 的机型上,逻辑宽度约 361vp,算出的高度只够放 Grid 与 Spectrum 的内容;Sliders 面板是三个"标签 + 滑杆 + 数值"行,比网格高出约 24 像素,于是溢出。

两种改法:

  1. 把 Sliders 面板的高度从固定值改成按内容自适应,例如把 SlidePicker 外面那层 Container 的 height 去掉,让内部的 Column 决定高度,弹层高度相应改为 null 并用 ConstrainedBox 约束上限;
  2. 保持固定高度,在切到 Sliders 时按比例减少 componentsHeight 的系数(例如把 / 12 改成 / 14),代价是网格面板会略矮。

这两种改法都会改变上游布局参数,本次适配没有动它,只在文档和本节记录现象,避免适配提交里混入界面调整。


相关链接

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐