前言
五年前,有人告诉我,你可以错过其他技术,但千万不要错过 Flutter
。然而此刻,有人告诉我,如果你错过了 OpenHarmony,恐怕要错过下个时代了。
作为发展了 5
年的 FlutterCandies 社区,我们已拥有 70
的 Flutter
组件。我们当然也不会止步于 Flutter
。我们希望把我们的 Flutter
组件也能带到 OpenHarmony 生态当中,HarmonyCandies 便是为了这一刻。
以 Flutter
开发者的角度,尽可能提供相同 Api
的 OpenHarmony 组件。
本文默认您已经有一定的 OpenHarmony 开发经验,并且阅读过以下内容。
使用的 ide
版本为 DevEco Studio 4.0 Release
OpenHarmony v4.0 Release (2023-10-26),开发 sdk
为 api 9
,当然也适配了 api 10
。
下拉刷新
列表在一个 App
中最常见的呈现方式,而下拉刷新是其常见的一种效果。
在 Flutter
中你可以通过
pull_to_refresh_notification
来实现一个可以自定义任何效果的下拉刷新。
在.OpenHarmony 中你则可以使用 https://github.com/HarmonyCandies/pull_to_refresh
来实现。
安装
你可以通过下面的命令来下载安装
ohpm install @candies/pull_to_refresh
参数
PullToRefreshIndicatorMode
代码语言:ts复制export enum PullToRefreshIndicatorMode {
initial, // 初始状态
drag, // 手势向下拉的状态.
armed, // 被拖动得足够远,以至于触发“onRefresh”回调函数的上滑事件
snap, // 用户没有拖动到足够远的地方并且释放回到初始化状态的过程
refresh, // 正在执行刷新回调.
done, // 刷新回调完成.
canceled, // 用户取消了下拉刷新手势.
error, // 刷新失败
}
配置参数
参数 | 类型 | 描述 |
---|---|---|
maxDragOffset | number | 最大拖动距离(非必填) |
reachToRefreshOffset | number | 到达满足触发刷新的距离(非必填) |
refreshOffset | number | 触发刷新的时候,停留的刷新距离(非必填) |
pullBackOnRefresh | boolean | 在触发刷新回调的时候是否执行回退动画(默认 |
pullBackAnimatorOptions | AnimatorOptions | 回退动画的一些配置(duration,easing,delay,fill) |
pullBackOnError | boolean | 刷新失败的时候,是否执行回退动画(默认 |
maxDragOffset
和reachToRefreshOffset
如果不定义的话,会根据当前容器的高度设置默认值。
/// Set the default value of [maxDragOffset,reachToRefreshOffset]
onAreaChange(oldValue: Area, newValue: Area) {
if (this.maxDragOffset == undefined) {
this.maxDragOffset = (newValue.height as number) / 5;
}
if (this.reachToRefreshOffset == undefined) {
this.reachToRefreshOffset = this.maxDragOffset * 3 / 4;
}
else {
this.reachToRefreshOffset = Math.min(this.reachToRefreshOffset, this.maxDragOffset);
}
}
pullBackAnimatorOptions
的默认值如下:
/// The options of pull back animation
pullBackAnimatorOptions: AnimatorOptions = {
duration: 400,
easing: "friction",
delay: 0,
fill: "forwards",
direction: "normal",
iterations: 1,
begin: 1.0,
end: 0.0
};
回调
onRefresh
触发的下拉刷新事件
代码语言:ts复制/// A function that's called when the user has dragged the refresh indicator
/// far enough to demonstrate that they want the app to refresh. The returned
/// [Future] must complete when the refresh operation is finished.
onRefresh: RefreshCallback = async () => true;
onReachEdge
是否我们到达了下拉刷新的边界,比如说,下拉刷新的内容是一个列表,那么边界就是到达列表的顶部位置。
代码语言:ts复制/// Whether we reach the edge to pull refresh
onReachEdge: () => boolean = () => true;
使用
导入引用
代码语言:ts复制import {
PullToRefresh,
pull_to_refresh,
PullToRefreshIndicatorMode,
} from '@candies/pull_to_refresh'
定义配置
代码语言:ts复制@State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller();
使用 PullToRefresh
将需要支持下拉刷新的部分,通过 @BuilderParam
修饰的 builder
回调传入,或者尾随闭包初始化组件。
PullToRefresh(
{
refreshOffset: 150,
maxDragOffset: 300,
reachToRefreshOffset: 200,
controller: this.controller,
onRefresh: async () => {
return new Promise<boolean>((resolve) => {
setTimeout(() => {
// 定义的刷新方法,当刷新成功之后,返回回调,模拟 2 秒之后刷新完毕
this.onRefresh().then((value) => resolve(value));
}, 2000);
});
},
onReachEdge: () => {
let yOffset = this.scroller.currentOffset().yOffset;
return Math.abs(yOffset) < 0.001;
}
}) {
// 我们自定义的下拉刷新头部
PullToRefreshContainer({
lastRefreshTime: this.lastRefreshTime,
controller: this.controller,
})
List({ scroller: this.scroller }) {
ForEach(this.listData, (item, index) => {
ListItem() {
Text(`${item}`,).align(Alignment.Center)
}.height(100).width('100%')
}, (item, index) => {
return `${item}`;
})
}
// 必须设置 edgeEffect
.edgeEffect(EdgeEffect.None)
// 为了使下拉刷新的手势的过程中,不触发列表的滚动
.onScrollFrameBegin((offset, state) => {
if (this.controller.dragOffset > 0) {
offset = 0;
}
return { offsetRemain: offset, };
})
}
}
自定义下拉刷新效果
你可以通过对 Controller
中 dragOffset
和 mode
的判断,创建属于自己的下拉刷新效果。如果下拉刷新失败了,你可以通过调用 Controller
的 refresh()
方法来重新执行刷新动画。
/// The current drag offset
dragOffset: number = 0;
/// The current pull mode
mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial;
下面是一个自定义下拉刷新头部的例子
代码语言:ts复制@Component
struct PullToRefreshContainer {
@Prop lastRefreshTime: number = 0;
@Link controller: pull_to_refresh.Controller;
getShowText(): string {
let text = '';
if (this.controller.mode == PullToRefreshIndicatorMode.armed) {
text = 'Release to refresh';
} else if (this.controller.mode == PullToRefreshIndicatorMode.refresh ||
this.controller.mode == PullToRefreshIndicatorMode.snap) {
text = 'Loading...';
} else if (this.controller.mode == PullToRefreshIndicatorMode.done) {
text = 'Refresh completed.';
} else if (this.controller.mode == PullToRefreshIndicatorMode.drag) {
text = 'Pull to refresh';
} else if (this.controller.mode == PullToRefreshIndicatorMode.canceled) {
text = 'Cancel refresh';
} else if (this.controller.mode == PullToRefreshIndicatorMode.error) {
text = 'Refresh failed';
}
return text;
}
getDate(): String {
return (new Date(this.lastRefreshTime)).toTimeString();
}
build() {
Row() {
if (this.controller.dragOffset != 0)
Text(`${this.getShowText()}---${this.getDate()}`)
if (this.controller.dragOffset > 50 && this.controller.mode == PullToRefreshIndicatorMode.refresh)
LoadingProgress().width(50).height(50)
}
.justifyContent(FlexAlign.Center)
.height(this.controller.dragOffset)
.width('100%')
.onClick(() => {
if (this.controller.mode == PullToRefreshIndicatorMode.error) {
this.controller.refresh();
}
})
.backgroundColor('#22808080')
}
}
学废了
虽然练习时长只有一个月,但通过编写第一个 ArtUI
组件,还是学到了不少东西。
创建发布一个组件
创建组织
先到 OpenHarmony 三方库中心仓上面注册个账号,到 个人中心
=》组织管理
中,申请一个组织。这个组织名字以后要用到,因为普通三方作者,是不能使用 ohos
前缀的。
比如我注册的是组织名为 candies
,组件为 pull_to_refresh
。那么组件最终的名字就是 @candies/pull_to_refresh
最后用户可以通过 ohpm install @candies/pull_to_refresh
,来安装使用组件。
为啥这个要先做,因为审核很慢。
创建项目
写一个组件,必然也会给这个组件创建一个演示例子,在 Flutter
中发布一个组件,你可以使用下面的结构。
package
--example
而在 OpenHarmony
里面你只能使用下面的结构,这样才能方便你修改代码。
example
--package
2
种结构的区别是, package
下面肯定会需要加 README
,LICENSE
,但是 github
,gitee
默认只会显示根目录下面的 README
,第二种结构就要多复制一份到 example
目录下面。
但是 OpenHarmony 三方库中心仓 却要求,有点难顶啊。
ide
啥时候支持下第一种结构呀!
创建组件演示项目
创建一个项目。
创建组件项目
创建一个 Static Libray
(至于其他 Module
是什么意思,请自行查看文档)
创建好的目录长这样子
oh-package.json5
中是你的组件的信息。
这里你需要把名字改成 @candies/pull_to_refresh
即 (@你的组织名字/组件名字)
一个完整的 oh-package.json5
是这样的
{
"license": "Apache-2.0",
"devDependencies": {},
"keywords": [
"pull",
"refresh",
"pulltorefresh"
],
"author": "zmtzawqlp",
"name": "@candies/pull_to_refresh",
"description": "Harmony plugin for building pull to refresh effects with PullToRefresh quickly.",
"main": "index.ets",
"repository": "https://github.com/HarmonyCandies/pull_to_refresh",
"version": "1.0.0",
"homepage": "https://github.com/HarmonyCandies/pull_to_refresh",
"dependencies": {}
}
组件项目中 Index.ets
是入口,用于导出组件。跟 Flutter
中 lib
下面带 library 组件名;
标识的 dart
文件效果一样。
export { MainPage } from './src/main/ets/components/mainpage/MainPage'
引用组件项目
要想 Example
能引用到 pull_to_refresh
, 你还需要到
{
"license": "",
"devDependencies": {},
"author": "",
"name": "entry",
"description": "Please describe the basic information.",
"main": "",
"version": "1.0.0",
"dependencies": {
"@candies/pull_to_refresh": "file:../pull_to_refresh"
}
}
发布
在准备发布之前,请先阅读 贡献三方库 下面内容。
阅读操作完毕之后,你就可以打你的 har
包了。选中你的组件项目,在 Build
下面选择 Make Module 你的组件名字
。编译完成之后,你就可以在组件项目路径 builddefaultoutputsdefault
中找到你即将发布的包。
最后执行 ohpm publish xxx.har
(xxx.har
为上传包的本地路径)。上传成功之后,你就可以看到你的个人中心里面的消息和状态了,耐心等待审核。
我遇到的上架的问题主要是组织名称(当然,这是我自己猜的,后面会聊到这个),ohos
不是普通三方开发者使用的前缀, ohos
的库都在 OpenHarmony-TPC: OpenHarmony third party components (gitee.com)
下面。按道理你可以 pr
到这个下面,并且加入到 ohos
中,再发布。当然更欢迎大家能加入 candies
组织,大家一起生产有趣的小组件。
@Provide/@Consume
第一眼看到这个状态管理装饰器的时候,好亲切的感觉。这不是就是 Flutter
里面的 (provider | Flutter Package (flutter-io.cn)) 吗?
最开始设计 pull_to_refresh
的时候,想着跟 Flutter
中一样,父组件里面存放管理下拉刷新的状态,然后子组件里面监听状态,达到局部刷新的效果。
第一版的设计结构如下:
CustomWidget
中提供了@Provide('a')
CustomWidgetChild
中使用@Consume('a')
获取状态变化。
@Entry
@Component
struct HomePage {
@Builder
builder2($$: { a: string }) {
Text(`${$$.a}测试`)
}
build() {
Column() {
CustomWidget() {
CustomWidgetChild({ builder: this.builder2 })
}
}
}
}
@Component
struct CustomWidget {
@Provide('a') a: string = 'abc';
@BuilderParam
builder: () => void;
build() {
Column() {
Button('你好').onClick((x) => {
if (this.a == 'ddd') {
this.a = 'abc';
}
else {
this.a = 'ddd';
}
})
this.builder()
}
}
}
@Component
struct CustomWidgetChild {
@Consume('a') a: string;
@BuilderParam
builder: ($$: { a: string }) => void;
build() {
Column() {
this.builder({ a: this.a })
}
}
}
运行会报找不到 Provide
的错误。
通过分析由 ArkTS
生成的 js
文件(生成的 js
在 entrybuilddefaultcachedefaultdefault@CompileArkTSesmoduledebug
路径下面) ,我们可以分析得出:
CustomWidgetChild
其父组件实际上是 HomePage
,其内部 this
指向的也是 HomePage
,因此找不到 CustomWidget
的 @Provide
变量。
class HomePage extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1) {
super(parent, __localStorage, elmtId);
this.setInitiallyProvidedValue(params);
}
setInitiallyProvidedValue(params) {
}
updateStateVars(params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
}
aboutToBeDeleted() {
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
builder2($$, parent = null) {
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
Text.create(`${$$.a}测试`);
if (!isInitialRender) {
Text.pop();
}
ViewStackProcessor.StopGetAccessRecording();
});
Text.pop();
}
initialRender() {
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
Column.create();
if (!isInitialRender) {
Column.pop();
}
ViewStackProcessor.StopGetAccessRecording();
});
{
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
if (isInitialRender) {
ViewPU.create(new CustomWidget(this, {
builder: () => {
{
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
if (isInitialRender) {
ViewPU.create(new CustomWidgetChild(this, { builder: this.builder2 }, undefined, elmtId));
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {});
}
ViewStackProcessor.StopGetAccessRecording();
});
}
}
}, undefined, elmtId));
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {});
}
ViewStackProcessor.StopGetAccessRecording();
});
}
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}
class CustomWidget extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1) {
super(parent, __localStorage, elmtId);
this.__a = new ObservedPropertySimplePU('abc', this, "a");
this.addProvidedVar("a", this.__a);
this.builder = undefined;
this.setInitiallyProvidedValue(params);
}
setInitiallyProvidedValue(params) {
if (params.a !== undefined) {
this.a = params.a;
}
if (params.builder !== undefined) {
this.builder = params.builder;
}
}
updateStateVars(params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
}
aboutToBeDeleted() {
this.__a.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
get a() {
return this.__a.get();
}
set a(newValue) {
this.__a.set(newValue);
}
initialRender() {
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
Column.create();
if (!isInitialRender) {
Column.pop();
}
ViewStackProcessor.StopGetAccessRecording();
});
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
Button.createWithLabel('你好');
Button.onClick((x) => {
if (this.a == 'ddd') {
this.a = 'abc';
}
else {
this.a = 'ddd';
}
});
if (!isInitialRender) {
Button.pop();
}
ViewStackProcessor.StopGetAccessRecording();
});
Button.pop();
this.builder.bind(this)();
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}
class CustomWidgetChild extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1) {
super(parent, __localStorage, elmtId);
this.__a = this.initializeConsume("a", "a");
this.builder = undefined;
this.setInitiallyProvidedValue(params);
}
setInitiallyProvidedValue(params) {
if (params.builder !== undefined) {
this.builder = params.builder;
}
}
updateStateVars(params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
}
aboutToBeDeleted() {
this.__a.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
get a() {
return this.__a.get();
}
set a(newValue) {
this.__a.set(newValue);
}
initialRender() {
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
Column.create();
if (!isInitialRender) {
Column.pop();
}
ViewStackProcessor.StopGetAccessRecording();
});
this.builder.bind(this)(makeBuilderParameterProxy("builder", { a: () => (this["__a"] ? this["__a"] : this["a"]) }));
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}
ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent());
loadDocument(new HomePage(undefined, {}));
ViewStackProcessor.StopGetAccessRecording();
export {};
//# sourceMappingURL=Index.js.map
意思就是你只能写成下面的这种形式。虽然说 CustomWidgetChild
是看起来是通过 CustomWidget
的 builder
创建出来的,但是它们依然没有父子关系,这跟 Flutter
完全不是一套原理。
@Entry
@Component
struct HomePage {
@Provide('a') test: string = 'abc';
@Builder
builder2($$: { a: string }) {
Text(`${$$.a}测试`)
}
build() {
Column() {
CustomWidget() {
CustomWidgetChild({ builder: this.builder2 })
}
}
}
}
@Component
struct CustomWidget {
@Consume('a') a: string;
@BuilderParam
builder: () => void;
build() {
Column() {
Button('你好').onClick((x) => {
if (this.a == 'ddd') {
this.a = 'abc';
}
else {
this.a = 'ddd';
}
})
this.builder()
}
}
}
@Component
struct CustomWidgetChild {
@Consume('a') a: string;
@BuilderParam
builder: ($$: { a: string }) => void;
build() {
Column() {
this.builder({ a: this.a })
}
}
}
@Builder/@BuilderParam
在自定义组件中,如果你想传入其他的组件,你需要使用到 @Builder
和 @BuilderParam
, 代码如下:
@Component
struct Child {
@BuilderParam aBuilder0: () => void;
build() {
Column() {
this.aBuilder0()
}
}
}
@Entry
@Component
struct Parent {
@Builder componentBuilder() {
Text(`Parent builder `)
}
build() {
Column() {
Child({ aBuilder0: this.componentBuilder })
}
}
}
但是实际中写一个自定义组件的时候,会有这种需求。需要为 BuilderParam
修饰的内容的返回增加一些事件或者设置。比如下面例子,为 BuilderParamChild
的 builder
的返回增加 hitTestBehavior
设置。我这里将 builder
的返回修改为了 CommonMethod<any>
(组件都继承于该类,里面是一些公共的属性,事件),虽然这样可以让编辑器有提示,并且不报错,但是运行起来依然会提示 hitTestBehavior
找不到。
@Component
struct BuilderParamTestDemo {
build() {
Column(){
BuilderParamChild(){
Text('测试')
}
}
}
}
@Component
struct BuilderParamChild {
@BuilderParam
builder: () => CommonMethod<any>;
build() {
this.builder().hitTestBehavior(HitTestMode.None)
}
}
错误堆栈如下:
代码语言:ts复制E Error message: Cannot read property hitTestBehavior of undefined
E SourceCode:
E this.builder().hitTestBehavior.bind(this)(HitTestMode.None);
E ^
E Stacktrace:
E at initialRender (entry/src/main/ets/pages/Index.ets:20:5)
从生成的 js
中也能看到对应的代码。
"use strict";
class BuilderParamTestDemo extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1) {
super(parent, __localStorage, elmtId);
this.setInitiallyProvidedValue(params);
}
setInitiallyProvidedValue(params) {
}
updateStateVars(params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
}
aboutToBeDeleted() {
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
initialRender() {
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
Column.create();
if (!isInitialRender) {
Column.pop();
}
ViewStackProcessor.StopGetAccessRecording();
});
{
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
if (isInitialRender) {
ViewPU.create(new BuilderParamChild(this, {
builder: () => {
this.observeComponentCreation((elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
Text.create('测试');
if (!isInitialRender) {
Text.pop();
}
ViewStackProcessor.StopGetAccessRecording();
});
Text.pop();
}
}, undefined, elmtId));
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {});
}
ViewStackProcessor.StopGetAccessRecording();
});
}
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}
class BuilderParamChild extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1) {
super(parent, __localStorage, elmtId);
this.builder = undefined;
this.setInitiallyProvidedValue(params);
}
setInitiallyProvidedValue(params) {
if (params.builder !== undefined) {
this.builder = params.builder;
}
}
updateStateVars(params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
}
aboutToBeDeleted() {
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
initialRender() {
this.builder().hitTestBehavior.bind(this)(HitTestMode.None);
}
rerender() {
this.updateDirtyElements();
}
}
ViewStackProcessor.StartGetAccessRecordingFor(ViewStackProcessor.AllocateNewElmetIdForNextComponent());
loadDocument(new BuilderParamTestDemo(undefined, {}));
ViewStackProcessor.StopGetAccessRecording();
//# sourceMappingURL=Index.js.map
对应这个问题,官方的解释是
1.ArkUI
没有类似安卓基类组件。
2.目前 ArkUI
组件是没有具体的类型,也不支持组件继承。
3.如果需要给自定义构建方法添加属性,只能是套一层容器组件之后再给容器组件设置属性
4.从语法规范上来讲,BuilderParam
的方法类型就是 () => void
话虽然这样说,但我还是提出了疑问,那么有没有那种单纯的容器组件, 不管是用 Row
,还是 Column
或者其他功能容器,这里的含义都蛮奇怪的。
回答是,暂时没有。希望官方以后还是考虑一下这个,虽然我包个 Row/Column
是可以,但是感觉怪怪的。
@Component
struct BuilderParamChild {
@BuilderParam
builder: () => void;
build() {
Column() {
this.builder()
}.hitTestBehavior(HitTestMode.None)
}
}
状态装饰器
在给组件定义参数的时候,会遇到这个参数不必须设置,但后续需要根据情况给它一个默认值。
在 Flutter
中,我们可以通过定义参数为可空,然后在后续流程中判断这个参数是否为 null
,再给它默认值。
在 ArkTS
中我第一反应是这样写:
maxDragOffset: number | null = null;
但是,当这个参数如果用 @Prop
等状态装饰器修饰的时候,它是不允许简单类型和复杂类型的联合类型。这会引起很多奇怪的问题,在 api9
上面各种 carsh
,但是 api10
看起来是支持了(顺便说说,api9
和 api10
的相同代码,效果不一样的情况比比皆是)。而且 ide
的错误定位也很奇怪,比如我在做另一个组件 LikeButton
的时候,错误堆栈直接误导了我好久,最后排除法才搞好的。
Js-Engine: ark
page: pages/Index.js
Error message: ObservedPropertySimple value must not be an object
Stacktrace:
at ObservedPropertySimple (/mnt/disk/jenkins/ci/workspace/chipset_pipeline_release/china_compile/component_code/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:2179:2179)
at SynchedPropertySimpleOneWayPU (/mnt/disk/jenkins/ci/workspace/chipset_pipeline_release/china_compile/component_code/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/stateMgmt.js:3304:3304)
at CirclePainter (like_button/src/main/ets/painter/CirclePainter.ets:10:29)
at anonymous (like_button/src/main/ets/components/LikeButton.ets:461:35)
修复记录 fix on api9 · HarmonyCandies/like_button@eefe49d (github.com)
所以你可以这样写,通过是否为 undefined
,来判断用户是否设置过这个参数。
@Prop maxDragOffset: number = undefined;
写在最后
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
- 关注小编,同时可以期待后续文章ing