Flutter 到 OpenHarmony,不是有手就行吗 (下拉刷新)_flutter openharmony(2)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新HarmonyOS鸿蒙全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img

img
img
htt

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注鸿蒙)
img

正文

列表在一个 App 中最常见的呈现方式,而下拉刷新是其常见的一种效果。

Flutter 中你可以通过
pull\_to\_refresh\_notification 来实现一个可以自定义任何效果的下拉刷新。

在.OpenHarmony 中你则可以使用 https://github.com/HarmonyCandies/pull\_to\_refresh来实现。

安装

你可以通过下面的命令来下载安装

ohpm install @candies/pull_to_refresh

参数

PullToRefreshIndicatorMode

export enum PullToRefreshIndicatorMode {
initial, // 初始状态
drag, // 手势向下拉的状态.
armed, // 被拖动得足够远,以至于触发“onRefresh”回调函数的上滑事件
snap, // 用户没有拖动到足够远的地方并且释放回到初始化状态的过程
refresh, // 正在执行刷新回调.
done, // 刷新回调完成.
canceled, // 用户取消了下拉刷新手势.
error, // 刷新失败
}

配置参数
参数类型描述
maxDragOffsetnumber最大拖动距离(非必填)
reachToRefreshOffsetnumber到达满足触发刷新的距离(非必填)
refreshOffsetnumber触发刷新的时候,停留的刷新距离(非必填)
pullBackOnRefreshboolean在触发刷新回调的时候是否执行回退动画(默认 false)
pullBackAnimatorOptionsAnimatorOptions回退动画的一些配置(duration,easing,delay,fill)
pullBackOnErrorboolean刷新失败的时候,是否执行回退动画(默认 false)
  • maxDragOffsetreachToRefreshOffset 如果不定义的话,会根据当前容器的高度设置默认值。

/// 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

触发的下拉刷新事件

/// 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

是否我们到达了下拉刷新的边界,比如说,下拉刷新的内容是一个列表,那么边界就是到达列表的顶部位置。

/// Whether we reach the edge to pull refresh
onReachEdge: () => boolean = () => true;

使用

导入引用

import {
PullToRefresh,
pull_to_refresh,
PullToRefreshIndicatorMode,
} from ‘@candies/pull_to_refresh’

定义配置

@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((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, };
})
}
}

自定义下拉刷新效果

你可以通过对 ControllerdragOffsetmode 的判断,创建属于自己的下拉刷新效果。如果下拉刷新失败了,你可以通过调用 Controllerrefresh() 方法来重新执行刷新动画。

/// The current drag offset
dragOffset: number = 0;
/// The current pull mode
mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial;

下面是一个自定义下拉刷新头部的例子

@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 下面肯定会需要加 READMELICENSE,但是 githubgitee 默认只会显示根目录下面的 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 是入口,用于导出组件。跟 Flutterlib 下面带 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 你的组件名字。编译完成之后,你就可以在组件项目路径 build\default\outputs\default\ 中找到你即将发布的包。

最后执行 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(KaTeX parse error: Can't use function '$' in math mode at position 26: …ing }) { 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 文件(生成的 jsentry\build\default\cache\default\default@CompileArkTS\esmodule\debug 路径下面) ,我们可以分析得出:
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(KaTeX parse error: Can't use function '$' in math mode at position 149: …; 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) {

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

}
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) {

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
[外链图片转存中…(img-2vCLiQ3j-1713124573331)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值