diff --git a/README.md b/README.md index dbcf667b37bc58a28f79a75adf606029c945b154..bcaae311ed1c73a7767f46a301fc65b8783a5558 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,11 @@ - 支持暂无更多数据; - 支持自定义Header和Footer; - 支持Web的刷新(目前体验不太好); -- 未来计划支持二楼模式下拉刷新; +- 支持二楼功能; + +## 功能演示 + +![功能演示](https://gitee.com/Duke_Bit/ElfRefresh/blob/master/images/two_level_show.gif) ## 下载安装 @@ -97,20 +101,22 @@ struct ListPage { ### ElfRefreshConfigurator -| 属性 | 功能描述描述 | -|:---------------------------:|:--------------------:| -| hasRefresh | 是否具有下拉刷新功能 | -| hasLoadMore | 是否具有上拉加载功能 | -| maxTranslate | 下拉刷新距离 | -| sensitivity | 下拉上拉灵敏度 | -| animDuration | 滑动结束后,回弹动画执行时间 | -| refreshHeight | 下拉组件高度 | -| loadHeight | 上拉组件高度 | -| refreshAnimDuration | 自动下拉动画执行一次的时间 | -| refreshCompleteTextHoldTime | 下拉刷新完毕后, 刷新成功文本停留的时间 | -| headerStyle | 头部样式 | -| footerStyle | 底部样式 | -| copyWith | 拷贝配置并更新配置 | +| 属性 | 功能描述描述 | +|:---------------------------:|:----------------------------:| +| hasRefresh | 是否具有下拉刷新功能 | +| hasLoadMore | 是否具有上拉加载功能 | +| maxTranslate | 下拉刷新距离 | +| closeTwoOffset | 关闭二楼高度,支持百分比和calc函数,以二楼高度为基准 | +| sensitivity | 下拉上拉灵敏度 | +| animDuration | 滑动结束后,回弹动画执行时间 | +| refreshHeight | 下拉组件高度 | +| loadHeight | 上拉组件高度 | +| twoLevelHeight | 二楼高度支持百分比和calc函数 | +| refreshAnimDuration | 自动下拉动画执行一次的时间 | +| refreshCompleteTextHoldTime | 下拉刷新完毕后, 刷新成功文本停留的时间 | +| headerStyle | 头部样式 | +| footerStyle | 底部样式 | +| copyWith | 拷贝配置并更新配置 | ## 自定义HeaderFooter @@ -143,6 +149,12 @@ struct ListPage { | **onStart** | `() => void` | 刷新动画开始时触发 | | **onFinish** | `(isSuccess: boolean) => void` | 刷新完成时触发(带成功状态) | +## 二楼使用 +查看下面Demo + +[页面](https://gitee.com/Duke_Bit/ElfRefresh/blob/master/entry/src/main/ets/pages/TwoPage.ets) +[二楼组件](https://gitee.com/Duke_Bit/ElfRefresh/blob/master/entry/src/main/ets/components/CustomHeader.ets) + ## 约束与限制 在下述版本验证通过: diff --git a/entry/src/main/ets/components/CustomHeader.ets b/entry/src/main/ets/components/CustomHeader.ets new file mode 100644 index 0000000000000000000000000000000000000000..1d0a56cfe51a5b7cf52427787e36625983e44507 --- /dev/null +++ b/entry/src/main/ets/components/CustomHeader.ets @@ -0,0 +1,80 @@ +import { ElfRefreshController, ElfRefreshHeader } from '@duke/elf-refresh' + +interface MiniApp { + name: string, + icon: Resource +} + +@Preview +@ComponentV2 +struct CustomTwoHeader { + @Param @Require header: ElfRefreshHeader + @Param @Require control: ElfRefreshController + localMiniApp: MiniApp[] = [] + @Local scaleValue: number = 0 + + aboutToAppear(): void { + for (let i = 1; i <= 8; i++) { + this.localMiniApp.push({ + name: `小程序${i}`, + icon: $r('app.media.startIcon') + }) + } + this.header.onMoving = () => { + this.scaleValue = this.header.percent + } + } + + build() { + Stack() { + Column() { + Search({ + placeholder: '搜索小程序', + }) + .fontColor('#666666') + .backgroundColor('#999999') + Text('最近使用过的小程序') + .fontSize(16) + .margin({ top: 16 }) + .fontColor('#666666') + .fontWeight(FontWeight.Medium) + List({ space: 16 }) { + ForEach(this.localMiniApp, (item: MiniApp) => { + ListItem() { + Column({ space: 16 }) { + Image(item.icon) + .width('100%') + .aspectRatio(1) + + Text(item.name) + .width('100%') + .textAlign(TextAlign.Center) + .fontColor(Color.White) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + }.width('100%') + } + }) + }.lanes(4, 16) + .width('100%') + .margin({ top: 16 }) + .layoutWeight(1) + .edgeEffect(EdgeEffect.Spring,{alwaysEnabled:true}) + }.padding(16) + .alignItems(HorizontalAlign.Start) + .scale({ + centerX: "50%", + centerY: 0, + x: this.scaleValue, + y: this.scaleValue + }).height('100%') + }.width('100%') + .height(this.header.height) + .backgroundColor('#ff0f1216') + } +} + +@Builder +export function CustomTwoHeaderBuilder(header: ElfRefreshHeader, control: ElfRefreshController) { + CustomTwoHeader({ header: header, control: control }) +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 71451108dc53f3c6de7f1ed0feed5ce25e299893..85411a7e19bf2718a031388ea60997ea7a945c9d 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -24,6 +24,11 @@ struct Index { .onClick(() => { router.pushUrl({ url: 'pages/NestScollPage2' }) }) + + Button("2楼效果展示") + .onClick(() => { + router.pushUrl({ url: 'pages/TwoPage' }) + }) } .height('100%') .width('100%') diff --git a/entry/src/main/ets/pages/TwoPage.ets b/entry/src/main/ets/pages/TwoPage.ets new file mode 100644 index 0000000000000000000000000000000000000000..d20f8ffdf867b757a24c5618bf7c48e26dbbda52 --- /dev/null +++ b/entry/src/main/ets/pages/TwoPage.ets @@ -0,0 +1,56 @@ +import { ElfGlobalConfig, ElfRefreshComponent, ElfRefreshController } from '@duke/elf-refresh'; +import { CustomTwoHeaderBuilder } from '../components/CustomHeader'; +import { TitleBar } from '../components/TitleBar'; + +@Entry +@ComponentV2 +struct TwoPage { + @Local data: string[] = [] + controller: ElfRefreshController = new ElfRefreshController() + + aboutToAppear(): void { + for (let i = 0; i < 100; i++) { + this.data.push('item' + i) + } + } + + @Builder + builderList() { + Column() { + TitleBar({ title: '二楼展示' }) + List() { + ForEach(this.data, (item: string) => { + ListItem() { + Text("测试数据" + item) + .width("95%") + .height(50) + .margin(10) + .textAlign(TextAlign.Center) + .border({ width: 1, color: Color.Pink }) + } + }) + } + .layoutWeight(1) + .width('100%') + //下面代码建议添加,添加了会让刷新更流畅 + .edgeEffect(EdgeEffect.None) + } + } + + build() { + Stack() { + ElfRefreshComponent({ + controller: this.controller, + refreshConfigurator: ElfGlobalConfig.getInstance().getDefaultConfigurator().copyWith({ hasTwoHeader: true ,twoLevelHeight:'calc(100% - 44)'}), + onRefresh: async () => true, + onLoadMore: async () => true, + twoLevel: { builderHeader: wrapBuilder(CustomTwoHeaderBuilder) }, + content:()=>{this.builderList()} + }) + .height('100%') + .width('100%') + } + .height('100%') + .width('100%') + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json index b0a018e99a7b966c5a32ff676c125a97121e3b54..06465e8f626321e192c35c42a4dc19ed009137c7 100644 --- a/entry/src/main/resources/base/profile/main_pages.json +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -4,6 +4,7 @@ "pages/ListPage", "pages/TestWebPage", "pages/NestScollPage2", - "pages/NestScollPage1" + "pages/NestScollPage1", + "pages/TwoPage" ] } \ No newline at end of file diff --git a/images/two_level_show.gif b/images/two_level_show.gif new file mode 100644 index 0000000000000000000000000000000000000000..8f3ee0ef7769b22a3adfa2c975534a453f385128 Binary files /dev/null and b/images/two_level_show.gif differ diff --git a/library/CHANGELOG.md b/library/CHANGELOG.md index 8d8ca0b8cd0817e4f49dd2d8bf89f2f359e5c974..ee529524a4672ef6dbdab402ad242bd530ddff9e 100644 --- a/library/CHANGELOG.md +++ b/library/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [v1.1.0] 2025.03.18 + +- 修复潜在的bug, +- 修改了部分默认配置 +- 修改了状态命名,使其更符合语义化 +- 新增二楼功能,实现看后面Demo + [页面](https://gitee.com/Duke_Bit/ElfRefresh/blob/dev/entry/src/main/ets/pages/TwoPage.ets) + [二楼组件](https://gitee.com/Duke_Bit/ElfRefresh/blob/dev/entry/src/main/ets/components/CustomHeader.ets) + + ## [v1.0.1] 2025.03.14 - 优化基础Header和Footer的样式 diff --git a/library/Index.ets b/library/Index.ets index 96ef542df037e8defcdcd3c7e109a0de97a32f6b..89024b601a96b384bfc28c1e32a21666d3a4350e 100644 --- a/library/Index.ets +++ b/library/Index.ets @@ -1,6 +1,6 @@ export { ElfRefreshComponent } from "./src/main/ets/components/ElfRefreshComponent"; -export { ElfCustomHeaderFooter } from "./src/main/ets/components/ElfCustomHeaderFooter"; +export { ElfCustomHeaderFooter } from "./src/main/ets/model/ElfCustomHeaderFooter"; export { ClassicsHeaderFooter } from "./src/main/ets/components/ClassicsHeaderFooter"; diff --git a/library/README.md b/library/README.md index dbcf667b37bc58a28f79a75adf606029c945b154..bcaae311ed1c73a7767f46a301fc65b8783a5558 100644 --- a/library/README.md +++ b/library/README.md @@ -11,7 +11,11 @@ - 支持暂无更多数据; - 支持自定义Header和Footer; - 支持Web的刷新(目前体验不太好); -- 未来计划支持二楼模式下拉刷新; +- 支持二楼功能; + +## 功能演示 + +![功能演示](https://gitee.com/Duke_Bit/ElfRefresh/blob/master/images/two_level_show.gif) ## 下载安装 @@ -97,20 +101,22 @@ struct ListPage { ### ElfRefreshConfigurator -| 属性 | 功能描述描述 | -|:---------------------------:|:--------------------:| -| hasRefresh | 是否具有下拉刷新功能 | -| hasLoadMore | 是否具有上拉加载功能 | -| maxTranslate | 下拉刷新距离 | -| sensitivity | 下拉上拉灵敏度 | -| animDuration | 滑动结束后,回弹动画执行时间 | -| refreshHeight | 下拉组件高度 | -| loadHeight | 上拉组件高度 | -| refreshAnimDuration | 自动下拉动画执行一次的时间 | -| refreshCompleteTextHoldTime | 下拉刷新完毕后, 刷新成功文本停留的时间 | -| headerStyle | 头部样式 | -| footerStyle | 底部样式 | -| copyWith | 拷贝配置并更新配置 | +| 属性 | 功能描述描述 | +|:---------------------------:|:----------------------------:| +| hasRefresh | 是否具有下拉刷新功能 | +| hasLoadMore | 是否具有上拉加载功能 | +| maxTranslate | 下拉刷新距离 | +| closeTwoOffset | 关闭二楼高度,支持百分比和calc函数,以二楼高度为基准 | +| sensitivity | 下拉上拉灵敏度 | +| animDuration | 滑动结束后,回弹动画执行时间 | +| refreshHeight | 下拉组件高度 | +| loadHeight | 上拉组件高度 | +| twoLevelHeight | 二楼高度支持百分比和calc函数 | +| refreshAnimDuration | 自动下拉动画执行一次的时间 | +| refreshCompleteTextHoldTime | 下拉刷新完毕后, 刷新成功文本停留的时间 | +| headerStyle | 头部样式 | +| footerStyle | 底部样式 | +| copyWith | 拷贝配置并更新配置 | ## 自定义HeaderFooter @@ -143,6 +149,12 @@ struct ListPage { | **onStart** | `() => void` | 刷新动画开始时触发 | | **onFinish** | `(isSuccess: boolean) => void` | 刷新完成时触发(带成功状态) | +## 二楼使用 +查看下面Demo + +[页面](https://gitee.com/Duke_Bit/ElfRefresh/blob/master/entry/src/main/ets/pages/TwoPage.ets) +[二楼组件](https://gitee.com/Duke_Bit/ElfRefresh/blob/master/entry/src/main/ets/components/CustomHeader.ets) + ## 约束与限制 在下述版本验证通过: diff --git a/library/oh-package.json5 b/library/oh-package.json5 index 13d442149590fc552d13252791d011be7861d0d1..bf6d2f56bf7dccc5da48d286dd7afe8f342f7d73 100644 --- a/library/oh-package.json5 +++ b/library/oh-package.json5 @@ -12,6 +12,8 @@ "上拉加载", "PullToRefresh", "加载", + "loadMore", + "load" ], "repository": "https://gitee.com/Duke_Bit/ElfRefresh", "homepage": "https://gitee.com/Duke_Bit", diff --git a/library/src/main/ets/ElfGlobalConfig.ets b/library/src/main/ets/ElfGlobalConfig.ets index bdb0588609710bfe3e82e67b159d2bbc381e79c9..89a419e7125ca91af44bc397c3b866a2beaa471a 100644 --- a/library/src/main/ets/ElfGlobalConfig.ets +++ b/library/src/main/ets/ElfGlobalConfig.ets @@ -1,5 +1,5 @@ import { ClassicsHeaderFooter } from "./components/ClassicsHeaderFooter" -import { ElfCustomHeaderFooter } from "./components/ElfCustomHeaderFooter" +import { ElfCustomHeaderFooter } from "./model/ElfCustomHeaderFooter" import { ElfRefreshConfigurator } from "./model/ElfRefreshConfigurator" // 步骤1:创建全局配置管理器 diff --git a/library/src/main/ets/ElfRefreshController.ets b/library/src/main/ets/ElfRefreshController.ets index ca38a39605f418d40303940029a9f3457b997802..c2ac99df508ce172073adcf796b69b58fec9ecfe 100644 --- a/library/src/main/ets/ElfRefreshController.ets +++ b/library/src/main/ets/ElfRefreshController.ets @@ -1,8 +1,25 @@ +import { RefreshState } from "./constant/RefreshState"; + +/** + * @author duke + * @description 刷新控制器 + */ +@ObservedV2 export class ElfRefreshController{ + @Trace + state:RefreshState = RefreshState.IS_FREE + @Trace + topOffset:number = 0 + @Trace + bottomOffset:number = 0 + /** + * 以下属性均为内部调用 + */ initAutoRefresh:boolean = false hasMore:boolean = true onAutoRefresh?:()=>void; onSetHasMore?:(hasMore:boolean)=>void + onCloseTwoHeader?:()=>void autoRefresh(){ if(!this.onAutoRefresh){ this.initAutoRefresh = true @@ -16,4 +33,7 @@ export class ElfRefreshController{ this.onSetHasMore && this.onSetHasMore(hasMore) } + closeTwoHeader(){ + this.onCloseTwoHeader && this.onCloseTwoHeader() + } } \ No newline at end of file diff --git a/library/src/main/ets/components/ClassicsHeaderFooter.ets b/library/src/main/ets/components/ClassicsHeaderFooter.ets index e6c0733ef68715ca6db76513f7af31b8ea101d42..1ffeb8c8f5fbd1035d90c81ee3df50bd341c9c07 100644 --- a/library/src/main/ets/components/ClassicsHeaderFooter.ets +++ b/library/src/main/ets/components/ClassicsHeaderFooter.ets @@ -1,10 +1,14 @@ -import { ElfCustomHeaderFooter } from "./ElfCustomHeaderFooter"; +import { ElfCustomHeaderFooter } from "../model/ElfCustomHeaderFooter"; import { ElfRefreshHeader } from "../model/ElfRefreshHeader"; import { formatDate } from "../uitls/TimeUtil"; import { RefreshState } from "../constant/RefreshState"; import { ElfLoadFooter } from "../model/ElfLoadFooter"; - +/** + * @author duke + * @description 默认的经典样式 + * 可以作为自定义的参考 + */ @ComponentV2 struct ElfClassicsHeaderCell { @Param @Require header: ElfRefreshHeader @@ -28,8 +32,10 @@ struct ElfClassicsHeaderCell { } this.header.onMoving = () => { - if (this.header.state == RefreshState.IS_PULL_DOWN_2) { + if (this.header.state == RefreshState.IS_PULL_DOWN_RELEASE_REFRESH) { this.title = $r('app.string.elf_header_release') + } else if(this.header.state == RefreshState.IS_PULL_DOWN_RELEASE_TWO_LEVEL){ + this.title = $r('app.string.elf_header_secondary') } else { this.title = $r('app.string.elf_header_pulling') } @@ -47,7 +53,7 @@ struct ElfClassicsHeaderCell { build() { Row({ space: 8 }) { Stack() { - if (this.header.state == RefreshState.IS_PULL_DOWN_1 || this.header.state == RefreshState.IS_PULL_DOWN_2) { + if (this.header.state == RefreshState.IS_PULL_DOWN_REFRESH || this.header.state == RefreshState.IS_PULL_DOWN_RELEASE_REFRESH) { Image($r('sys.media.ohos_ic_public_arrow_down')) .rotate({ x: 0, @@ -55,7 +61,7 @@ struct ElfClassicsHeaderCell { z: 1, centerX: '50%', centerY: '50%', - angle: this.header.state == RefreshState.IS_PULL_DOWN_2 ? 180 : 0 + angle: this.header.state == RefreshState.IS_PULL_DOWN_RELEASE_REFRESH ? 180 : 0 }) .animation({ duration: 300, @@ -94,6 +100,11 @@ struct ElfClassicsHeaderCell { } } +/** + * @author duke + * @description 默认的经典样式 + * 可以作为自定义的参考 + */ @ComponentV2 struct ElfClassicsFooterCell { @Param @Require footer: ElfLoadFooter @@ -112,7 +123,7 @@ struct ElfClassicsFooterCell { } this.footer.onMoving = () => { - if (this.footer.state == RefreshState.IS_PULL_UP_2) { + if (this.footer.state == RefreshState.IS_PULL_UP_RELEASE_LOAD) { this.title = $r('app.string.elf_footer_release') } else { this.title = $r('app.string.elf_footer_pulling') @@ -129,7 +140,7 @@ struct ElfClassicsFooterCell { if (this.footer.hasMore) { Stack() { - if(this.footer.state == RefreshState.IS_PULL_UP_1 || this.footer.state == RefreshState.IS_PULL_UP_2) { + if(this.footer.state == RefreshState.IS_PULL_UP_LOAD || this.footer.state == RefreshState.IS_PULL_UP_RELEASE_LOAD) { Image($r('sys.media.ohos_ic_public_arrow_down')) .rotate({ x: 0, @@ -137,7 +148,7 @@ struct ElfClassicsFooterCell { z: 1, centerX: '50%', centerY: '50%', - angle: this.footer.state ==RefreshState.IS_PULL_UP_1 ? 180 : 0 + angle: this.footer.state ==RefreshState.IS_PULL_UP_LOAD ? 180 : 0 }) .animation({ duration: 300, @@ -168,17 +179,30 @@ struct ElfClassicsFooterCell { } } - +/** + * @author duke + * @description 默认的经典样式包装器 + * 可以作为自定义的参考 + */ @Builder function ElfClassicsHeader(header: ElfRefreshHeader) { ElfClassicsHeaderCell({ header: header }) } +/** + * @author duke + * @description 默认的经典样式包装器 + * 可以作为自定义的参考 + */ @Builder function ElfClassicsFooter(footer: ElfLoadFooter) { ElfClassicsFooterCell({ footer: footer }) } +/** + * @author duke + * @description 经典样式 + */ export class ClassicsHeaderFooter implements ElfCustomHeaderFooter { builderHeader: WrappedBuilder<[ElfRefreshHeader]> = wrapBuilder(ElfClassicsHeader) builderFooter: WrappedBuilder<[ElfLoadFooter]> = wrapBuilder(ElfClassicsFooter) diff --git a/library/src/main/ets/components/ElfRefreshComponent.ets b/library/src/main/ets/components/ElfRefreshComponent.ets index aa73f48e0e0532471d2c04f19c8791b07d84dd94..f32d8d3deec1f2755cedc38ae115ef28b362e985 100644 --- a/library/src/main/ets/components/ElfRefreshComponent.ets +++ b/library/src/main/ets/components/ElfRefreshComponent.ets @@ -1,5 +1,5 @@ import { util } from "@kit.ArkTS" -import { ElfCustomHeaderFooter } from "./ElfCustomHeaderFooter"; +import { ElfCustomHeaderFooter } from "../model/ElfCustomHeaderFooter"; import { ElfRefreshConfigurator } from "../model/ElfRefreshConfigurator"; import { RefreshState } from "../constant/RefreshState"; import { ElfRefreshHeader } from "../model/ElfRefreshHeader"; @@ -8,22 +8,56 @@ import { AnimatorResult } from "@kit.ArkUI"; import { CustomStyle } from "../constant/CustomStyle"; import { ElfRefreshController } from "../ElfRefreshController"; import { ElfGlobalConfig } from "../../../../Index"; +import { ElfTwoLevelHeader } from "../model/ElfTwoLevelHeader"; +import { LengthUtil } from "../uitls/LengthUtil"; +/** + * @author duke + * @description 智能刷新组件 + */ @ComponentV2 export struct ElfRefreshComponent { - @BuilderParam content: () => void + @BuilderParam content?: () => void + /** + * 自定义Header Footer样式 + */ @Param @Once custom: ElfCustomHeaderFooter = ElfGlobalConfig.getInstance().getDefaultCustom() + /** + * 配置 + */ @Param @Once refreshConfigurator: ElfRefreshConfigurator = ElfGlobalConfig.getInstance().getDefaultConfigurator() + /** + * 控制器 + */ @Param @Once controller: ElfRefreshController = new ElfRefreshController() + /** + * 二楼UI + */ + @Param twoLevel: ElfTwoLevelHeader | undefined = undefined + /** + * 针对框架无法完全屏蔽掉一些特殊的子组件滑动的情况 + * 滚动偏移量大于0 则不触发刷新 + * 可以一定程度上当做开关使用 + */ @Param childOffsetInput: number = 0 + /** + * 对于存在嵌套滑动的情况,让框架能够准确的识别需要拦截的子组件id + * 其余的不处理 + */ @Param targetRefreshId: string | undefined = undefined @Local private state: RefreshState = RefreshState.IS_FREE; @Local private trYTop: number = 0 //移动的动画 @Local private trYBottom: number = 0 //移动的动画 - @Local childOffset: number = 0 + @Local private childOffset: number = 0 + /** + * 刷新回调 + */ @Event onRefresh: () => Promise = async () => { return true } + /** + * 加载回调 + */ @Event onLoadMore: () => Promise = async () => { return true } @@ -35,26 +69,34 @@ export struct ElfRefreshComponent { private currentRecognizer: GestureRecognizer = new GestureRecognizer() private header = new ElfRefreshHeader(this.state, this.refreshConfigurator.getMaxTranslate(), this.refreshConfigurator.getRefreshHeight(), 0, 0) + private twoHeader = new ElfRefreshHeader(this.state, this.refreshConfigurator.getMaxTranslate(), + 0, 0, 0) private footer = new ElfLoadFooter(this.state, this.refreshConfigurator.getMaxTranslate(), this.refreshConfigurator.getRefreshHeight(), 0, 0, true) private timerId: number = 0 private animList: Set = new Set() private lastOffset: number = 0 + private groupHeight: number = 0 @Monitor('state') changeState(iMonitor: IMonitor) { let oldState = iMonitor.value('state')!.before let newState = iMonitor.value('state')!.now this.header.state = newState + this.twoHeader.state = newState this.footer.state = newState + this.controller.state = newState if (oldState == RefreshState.IS_FREE) { - if (newState == RefreshState.IS_PULL_DOWN_1 || newState == RefreshState.IS_PULL_DOWN_2) { + if (newState == RefreshState.IS_PULL_DOWN_REFRESH || newState == RefreshState.IS_PULL_DOWN_RELEASE_REFRESH || + newState == RefreshState.IS_PULL_DOWN_RELEASE_TWO_LEVEL) { this.header.onStart && this.header.onStart() - } else if (newState == RefreshState.IS_PULL_UP_1 || newState == RefreshState.IS_PULL_UP_2) { + this.twoHeader.onStart && this.twoHeader.onStart() + } else if (newState == RefreshState.IS_PULL_UP_LOAD || newState == RefreshState.IS_PULL_UP_RELEASE_LOAD) { this.footer.onStart && this.footer.onStart() } - } else if (newState == RefreshState.IS_REFRESHING) { + } else if (newState == RefreshState.IS_REFRESHING || newState == RefreshState.IS_TWO_LEVEL) { this.header.onReleased && this.header.onReleased() + this.twoHeader.onReleased && this.twoHeader.onReleased() } else if (newState == RefreshState.IS_LOADING) { this.footer.onReleased && this.footer.onReleased() } @@ -68,16 +110,19 @@ export struct ElfRefreshComponent { } } - @Monitor('tryTop') + @Monitor('trYTop') changeTrYTop(iMonitor: IMonitor) { - let tryTop = iMonitor.value('tryTop')!.now + let tryTop = iMonitor.value('trYTop')!.now this.header.setOffset(tryTop) + this.twoHeader.setOffset(tryTop) + this.controller.topOffset = tryTop } @Monitor('trYBottom') changeTrYBottom(iMonitor: IMonitor) { let trYBottom = iMonitor.value('trYBottom')!.now this.footer.setOffset(trYBottom) + this.controller.bottomOffset = trYBottom } aboutToAppear(): void { @@ -91,6 +136,9 @@ export struct ElfRefreshComponent { this.controller.onSetHasMore = (hasMore) => { this.footer.hasMore = hasMore } + this.controller.onCloseTwoHeader = () => { + this.closeRefresh() + } } autoRefresh() { @@ -126,215 +174,251 @@ export struct ElfRefreshComponent { } build() { - Scroll() { - Stack({ alignContent: Alignment.Top }) { - Stack() { - this.content() - }.zIndex(2) - .translate({ - y: this.trYTop || this.trYBottom - }) - - if (this.refreshConfigurator.getHasRefresh() && (this.custom.builderHeader || ElfGlobalConfig.getInstance().getDefaultCustom().builderHeader)) { - Stack({ alignContent: Alignment.Bottom }) { - if(this.custom.builderHeader) { - this.custom.builderHeader.builder(this.header) - }else { - ElfGlobalConfig.getInstance().getDefaultCustom().builderHeader?.builder(this.header) + Stack() { + if (this.twoLevel && this.refreshConfigurator.getHasTwoHeader()) { + Stack() { //二楼空位 + this.twoLevel.builderHeader.builder(this.twoHeader, this.controller) + }.height(this.trYTop) + .position({ top: 0 }) + .clip(true) + } + Scroll() { + Stack({ alignContent: Alignment.Top }) { + Stack() { + if (this.content) { + this.content() } - }.zIndex(this.refreshConfigurator.getHeaderStyle() == CustomStyle.FixedBehind ? 1 : 3) - .height(this.refreshConfigurator.getHeaderStyle() == CustomStyle.Scale ? this.trYTop : - this.refreshConfigurator.getRefreshHeight()) - .position({ top: 0 }) + }.zIndex(2) .translate({ - y: this.refreshConfigurator.getHeaderStyle() == CustomStyle.Translate ? - this.trYTop - this.refreshConfigurator.getRefreshHeight() : 0 + y: this.trYTop || this.trYBottom }) - } - if (this.refreshConfigurator.getHasLoadMore() && (this.custom.builderFooter || ElfGlobalConfig.getInstance().getDefaultCustom().builderFooter)) { - Stack({ alignContent: Alignment.Top }) { - if(this.custom.builderFooter) { - this.custom.builderFooter.builder(this.footer) - }else { - ElfGlobalConfig.getInstance().getDefaultCustom().builderFooter?.builder(this.footer) + + if (this.refreshConfigurator.getHasRefresh() && + (this.custom.builderHeader || ElfGlobalConfig.getInstance().getDefaultCustom().builderHeader)) { + Stack({ alignContent: Alignment.Bottom }) { + if (this.custom.builderHeader) { + this.custom.builderHeader.builder(this.header) + } else { + ElfGlobalConfig.getInstance().getDefaultCustom().builderHeader?.builder(this.header) + } } - }.zIndex(this.refreshConfigurator.getFooterStyle() == CustomStyle.FixedBehind ? 1 : 3) - .height(this.refreshConfigurator.getFooterStyle() == CustomStyle.Scale ? this.trYBottom : - this.refreshConfigurator.getRefreshHeight()) - .position({ bottom: 0 }) - .translate({ - y: this.refreshConfigurator.getFooterStyle() == CustomStyle.Translate || - this.refreshConfigurator.getFooterStyle() == CustomStyle.Scale ? - this.trYBottom + this.refreshConfigurator.getRefreshHeight() : 0 - }) + .zIndex(this.refreshConfigurator.getHeaderStyle() == CustomStyle.FixedBehind ? 1 : 3) + .height(this.refreshConfigurator.getHeaderStyle() == CustomStyle.Scale ? this.trYTop : + this.refreshConfigurator.getRefreshHeight()) + .position({ top: 0 }) + .translate({ + y: this.refreshConfigurator.getHeaderStyle() == CustomStyle.Translate ? + this.trYTop - this.refreshConfigurator.getRefreshHeight() : 0 + }) + .transition(TransitionEffect.OPACITY) + .visibility(this.state != RefreshState.IS_TWO_LEVEL ? Visibility.Visible : Visibility.None) + } + if (this.refreshConfigurator.getHasLoadMore() && + (this.custom.builderFooter || ElfGlobalConfig.getInstance().getDefaultCustom().builderFooter)) { + Stack({ alignContent: Alignment.Top }) { + if (this.custom.builderFooter) { + this.custom.builderFooter.builder(this.footer) + } else { + ElfGlobalConfig.getInstance().getDefaultCustom().builderFooter?.builder(this.footer) + } + }.zIndex(this.refreshConfigurator.getFooterStyle() == CustomStyle.FixedBehind ? 1 : 3) + .height(this.refreshConfigurator.getFooterStyle() == CustomStyle.Scale ? this.trYBottom : + this.refreshConfigurator.getRefreshHeight()) + .position({ bottom: 0 }) + .translate({ + y: this.refreshConfigurator.getFooterStyle() == CustomStyle.Translate || + this.refreshConfigurator.getFooterStyle() == CustomStyle.Scale ? + this.trYBottom + this.refreshConfigurator.getRefreshHeight() : 0 + }) + } } + .clip(true) + .height('100%') } - .clip(true) - .height('100%') - } - .scrollBar(BarState.Off) - .id(this.currentId) - .parallelGesture( - GestureGroup(GestureMode.Parallel, - PanGesture(this.panOption) - .onActionUpdate((event: GestureEvent) => { - if (!this.childRecognizer || !this.currentRecognizer) { - return - } - if (this.childRecognizer!.getState() != GestureRecognizerState.SUCCESSFUL || - this.currentRecognizer.getState() != GestureRecognizerState.SUCCESSFUL) { // 如果识别器状态不是SUCCESSFUL,则不做控制 - return; - } - let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; - if (target instanceof ScrollableTargetInfo) { - if (target.isEnd()) { // 在移动过程中实时根据当前组件状态,控制识别器的开闭状态 - if ((event.offsetY - this.lastOffset) < 0) { + .scrollBar(BarState.Off) + .id(this.currentId) + .responseRegion({ + x: 0, + y: this.state == RefreshState.IS_TWO_LEVEL ? this.twoHeader.height : 0, + width: '100%', + height: '100%' + }) + .shouldBuiltInRecognizerParallelWith((_: GestureRecognizer, others: Array) => { + for (let i = 0; i < others.length; i++) { + let target = others[i].getEventTargetInfo(); + if (target) { + if (others[i].isBuiltIn() && + others[i].getType() == GestureControl.GestureType.PAN_GESTURE && + (!this.targetRefreshId || this.targetRefreshId == target.getId())) { // 找到将要组成并行手势的识别器 + // 保存当前组件的识别器 + this.childRecognizer = others[i]; // 保存将要组成并行手势的识别器 + let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; + if (target instanceof ScrollableTargetInfo) { + if (target.isEnd() || target.isBegin()) { this.childRecognizer.setEnabled(false) - if (this.trYBottom > 0) { - this.currentRecognizer.setEnabled(false) - } else { - this.currentRecognizer.setEnabled(true) - } - } else { - if (this.trYBottom >= 0) { - this.childRecognizer.setEnabled(true) - this.currentRecognizer.setEnabled(false) - } else { - this.childRecognizer.setEnabled(false) - this.currentRecognizer.setEnabled(true) - } - } - } else if (target.isBegin()) { - if ((event.offsetY - this.lastOffset) > 0) { - this.childRecognizer.setEnabled(false) - if (this.trYTop < 0) { - this.currentRecognizer.setEnabled(false) - } else { - this.currentRecognizer.setEnabled(true) - } - } else { - if (this.trYTop <= 0) { - this.childRecognizer.setEnabled(true) - this.currentRecognizer.setEnabled(false) - } else { - this.childRecognizer.setEnabled(false) - this.currentRecognizer.setEnabled(true) - } } + } + return others[i]; // 返回将要组成并行手势的识别器 + } + } + } + return undefined; + }) + .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, _: GestureRecognizer, + other: Array) => { + if (other && other.length > 0) { + for (let i = 0; i < other.length; i++) { + if (other[i].getTag() == "actionRefresh" && other[i].getEventTargetInfo().getId() == this.currentId) { + this.currentRecognizer = other[i] + break + } + } + } + if (this.currentRecognizer && this.childRecognizer) { + let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; + let panEvent = event as PanGestureEvent; + if (target instanceof ScrollableTargetInfo) { // 找到响应链上对应并行的识别器 + if (target.isEnd()) { // 根据当前组件状态以及移动方向动态控制识别器使能状态 + if (panEvent && panEvent.offsetY < 0) { + this.childRecognizer.setEnabled(false) + this.currentRecognizer.setEnabled(true) } else { this.childRecognizer.setEnabled(true) this.currentRecognizer.setEnabled(false) } - } else { - if (this.childOffset == 0) { - if ((event.offsetY - this.lastOffset) > 0) { - this.childRecognizer.setEnabled(false) - if (this.trYTop < 0) { - this.currentRecognizer.setEnabled(false) - } else { - this.currentRecognizer.setEnabled(true) - } - } else { - if (this.trYTop <= 0) { - this.childRecognizer.setEnabled(true) - this.currentRecognizer.setEnabled(false) - } else { - this.childRecognizer.setEnabled(false) - this.currentRecognizer.setEnabled(true) - } - } + } else if (target.isBegin()) { + if (panEvent.offsetY > 0 || this.state == RefreshState.IS_TWO_LEVEL) { + this.childRecognizer.setEnabled(false) + this.currentRecognizer.setEnabled(true) } else { this.childRecognizer.setEnabled(true) this.currentRecognizer.setEnabled(false) } - } - this.lastOffset = event.offsetY - }), - PanGesture(this.panOption) - .onActionStart((event) => { - this.touchYOld = event.offsetY - }) - .onActionUpdate((event: GestureEvent) => { - this.onActionUpdate(event.offsetY) - }) - .onActionEnd(() => { - this.onActionEnd() - }).tag("actionRefresh") - ) - - ) - .shouldBuiltInRecognizerParallelWith((_: GestureRecognizer, others: Array) => { - for (let i = 0; i < others.length; i++) { - let target = others[i].getEventTargetInfo(); - if (target) { - if (others[i].isBuiltIn() && - others[i].getType() == GestureControl.GestureType.PAN_GESTURE && - (!this.targetRefreshId || this.targetRefreshId == target.getId())) { // 找到将要组成并行手势的识别器 - // 保存当前组件的识别器 - this.childRecognizer = others[i]; // 保存将要组成并行手势的识别器 - let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; - if (target instanceof ScrollableTargetInfo) { - if (target.isEnd()||target.isBegin()) { - this.childRecognizer.setEnabled(false) - } - } - return others[i]; // 返回将要组成并行手势的识别器 - } - } - } - return undefined; - }) - .onGestureRecognizerJudgeBegin((event: BaseGestureEvent, _: GestureRecognizer, - other: Array) => { - if (other && other.length > 0) { - for (let i = 0; i < other.length; i++) { - if (other[i].getTag() == "actionRefresh" && other[i].getEventTargetInfo().getId() == this.currentId) { - this.currentRecognizer = other[i] - break - } - } - } - if (this.currentRecognizer && this.childRecognizer) { - let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; - let panEvent = event as PanGestureEvent; - if (target instanceof ScrollableTargetInfo) { // 找到响应链上对应并行的识别器 - if (target.isEnd()) { // 根据当前组件状态以及移动方向动态控制识别器使能状态 - if (panEvent && panEvent.offsetY < 0) { - this.childRecognizer.setEnabled(false) - this.currentRecognizer.setEnabled(true) - } else { - this.childRecognizer.setEnabled(true) - this.currentRecognizer.setEnabled(false) - } - } else if (target.isBegin()) { - if (panEvent.offsetY > 0) { - this.childRecognizer.setEnabled(false) - this.currentRecognizer.setEnabled(true) } else { this.childRecognizer.setEnabled(true) this.currentRecognizer.setEnabled(false) } } else { - this.childRecognizer.setEnabled(true) - this.currentRecognizer.setEnabled(false) - } - } else { - if (this.childOffset == 0) { - if (panEvent.offsetY > 0) { - this.childRecognizer.setEnabled(false) - this.currentRecognizer.setEnabled(true) + if (this.childOffset == 0) { + if (panEvent.offsetY > 0) { + this.childRecognizer.setEnabled(false) + this.currentRecognizer.setEnabled(true) + } else { + this.childRecognizer.setEnabled(true) + this.currentRecognizer.setEnabled(false) + } } else { this.childRecognizer.setEnabled(true) this.currentRecognizer.setEnabled(false) } - } else { - this.childRecognizer.setEnabled(true) - this.currentRecognizer.setEnabled(false) } } - } - return GestureJudgeResult.CONTINUE - }) + return GestureJudgeResult.CONTINUE + }) + .parallelGesture( + GestureGroup(GestureMode.Parallel, + PanGesture(this.panOption) + .onActionUpdate((event: GestureEvent) => { + if (!this.childRecognizer || !this.currentRecognizer) { + return + } + if (this.childRecognizer!.getState() != GestureRecognizerState.SUCCESSFUL || + this.currentRecognizer.getState() != GestureRecognizerState.SUCCESSFUL) { // 如果识别器状态不是SUCCESSFUL,则不做控制 + return; + } + let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo; + if (target instanceof ScrollableTargetInfo) { + if (target.isEnd()) { // 在移动过程中实时根据当前组件状态,控制识别器的开闭状态 + if ((event.offsetY - this.lastOffset) < 0) { + this.childRecognizer.setEnabled(false) + if (this.trYBottom > 0) { + this.currentRecognizer.setEnabled(false) + } else { + this.currentRecognizer.setEnabled(true) + } + } else { + if (this.trYBottom >= 0) { + this.childRecognizer.setEnabled(true) + this.currentRecognizer.setEnabled(false) + } else { + this.childRecognizer.setEnabled(false) + this.currentRecognizer.setEnabled(true) + } + } + } else if (target.isBegin()) { + if ((event.offsetY - this.lastOffset) > 0) { + this.childRecognizer.setEnabled(false) + if (this.trYTop < 0) { + this.currentRecognizer.setEnabled(false) + } else { + this.currentRecognizer.setEnabled(true) + } + } else { + if (this.trYTop <= 0) { + this.childRecognizer.setEnabled(true) + this.currentRecognizer.setEnabled(false) + } else { + this.childRecognizer.setEnabled(false) + this.currentRecognizer.setEnabled(true) + } + } + } else { + this.childRecognizer.setEnabled(true) + this.currentRecognizer.setEnabled(false) + } + } else { + if (this.childOffset == 0) { + if ((event.offsetY - this.lastOffset) > 0) { + this.childRecognizer.setEnabled(false) + if (this.trYTop < 0) { + this.currentRecognizer.setEnabled(false) + } else { + this.currentRecognizer.setEnabled(true) + } + } else { + if (this.trYTop <= 0) { + this.childRecognizer.setEnabled(true) + this.currentRecognizer.setEnabled(false) + } else { + this.childRecognizer.setEnabled(false) + this.currentRecognizer.setEnabled(true) + } + } + } else { + this.childRecognizer.setEnabled(true) + this.currentRecognizer.setEnabled(false) + } + } + this.lastOffset = event.offsetY + }), + PanGesture(this.panOption) + .onActionStart((event) => { + this.touchYOld = event.offsetY + }) + .onActionUpdate((event: GestureEvent) => { + this.onActionUpdate(event.offsetY) + }) + .onActionCancel(() => { + this.onActionEnd() + }) + .onActionEnd(() => { + this.onActionEnd() + }) + .tag("actionRefresh") + ) + ) + .onSizeChange((_, newValue) => { + if (newValue.height) { + this.groupHeight = newValue.height as number + if (this.twoLevel && this.refreshConfigurator.getHasTwoHeader()) { + let twoHeight = this.refreshConfigurator.getTwoLevelHeight() + let target: number = LengthUtil.parseToVP(twoHeight, this.groupHeight) + this.twoHeader.height = target + } + } + }) + } + .height('100%') } /*** @@ -342,10 +426,10 @@ export struct ElfRefreshComponent { * @param offsetY */ private onActionUpdate(offsetY: number): void { - let release = this.refreshConfigurator.getMaxTranslate() * 0.75 if (this.state == RefreshState.IS_FREE || - this.state == RefreshState.IS_PULL_DOWN_1 || this.state == RefreshState.IS_PULL_DOWN_2 || - this.state == RefreshState.IS_PULL_UP_1 || this.state == RefreshState.IS_PULL_UP_2) { + this.state == RefreshState.IS_PULL_DOWN_REFRESH || this.state == RefreshState.IS_PULL_DOWN_RELEASE_REFRESH || + this.state == RefreshState.IS_PULL_DOWN_RELEASE_TWO_LEVEL || this.state == RefreshState.IS_TWO_LEVEL || + this.state == RefreshState.IS_PULL_UP_LOAD || this.state == RefreshState.IS_PULL_UP_RELEASE_LOAD) { this.touchYNew = offsetY; @@ -356,32 +440,44 @@ export struct ElfRefreshComponent { let isUpAction = distanceY < 0; if ((this.state == RefreshState.IS_FREE && isPullAction) || // 处于自由状态且列表处于顶部位置 并且 当前手势是下拉手势 - this.state == RefreshState.IS_PULL_DOWN_1 || this.state == RefreshState.IS_PULL_DOWN_2) { // 处于下拉状态中 + this.state == RefreshState.IS_PULL_DOWN_REFRESH || this.state == RefreshState.IS_PULL_DOWN_RELEASE_REFRESH || + this.state == RefreshState.IS_PULL_DOWN_RELEASE_TWO_LEVEL || + this.state == RefreshState.IS_TWO_LEVEL) { // 处于下拉状态中 if (this.refreshConfigurator.getHasRefresh()) { // 获取最新位移距离 let trY = this.touchYNew - this.touchYOld; - - // 计算当前需要位移的总距离 - trY = this.getTranslateYOfRefresh(trY); - if (trY < release) { - this.state = RefreshState.IS_PULL_DOWN_1; + if (this.state != RefreshState.IS_TWO_LEVEL) { + // 计算当前需要位移的总距离 + trY = this.getTranslateYOfRefresh(trY); } else { - this.state = RefreshState.IS_PULL_DOWN_2; + // 下拉值计算 + trY = this.getTranslateYOfTwo(trY); + } + let release = this.refreshConfigurator.getRefreshHeight() * 0.75 + if (trY < release) { + this.state = RefreshState.IS_PULL_DOWN_REFRESH; + } else if (trY < this.refreshConfigurator.getRefreshHeight() || !this.refreshConfigurator.getHasTwoHeader() || + !this.twoLevel) { + this.state = RefreshState.IS_PULL_DOWN_RELEASE_REFRESH; + } else if (this.state != RefreshState.IS_TWO_LEVEL) { + this.state = RefreshState.IS_PULL_DOWN_RELEASE_TWO_LEVEL; } this.trYTop = trY this.header.onMoving && this.header.onMoving() + this.twoHeader.onMoving && this.twoHeader.onMoving() } } else if (this.refreshConfigurator.getHasLoadMore()) { if ((this.state == RefreshState.IS_FREE && isUpAction) || - this.state == RefreshState.IS_PULL_UP_1 || this.state == RefreshState.IS_PULL_UP_2) { + this.state == RefreshState.IS_PULL_UP_LOAD || this.state == RefreshState.IS_PULL_UP_RELEASE_LOAD) { // 获取最新的位移距离 let trY = this.touchYNew - this.touchYOld; // 计算当前需要位移的总距离 trY = this.getTranslateYOfLoadMore(trY); + let release = this.refreshConfigurator.getLoadHeight() * 0.75 if (trY > -release) { - this.state = RefreshState.IS_PULL_UP_1; + this.state = RefreshState.IS_PULL_UP_LOAD; } else { - this.state = RefreshState.IS_PULL_UP_2; + this.state = RefreshState.IS_PULL_UP_RELEASE_LOAD; } this.trYBottom = trY this.footer.onMoving && this.footer.onMoving() @@ -396,10 +492,10 @@ export struct ElfRefreshComponent { */ private onActionEnd(): void { if (this.trYTop > 0) { // 下拉结束 - if (this.state == RefreshState.IS_PULL_DOWN_1) { + if (this.state == RefreshState.IS_PULL_DOWN_REFRESH) { this.closeRefresh(); return - } else if (this.state == RefreshState.IS_PULL_DOWN_2) { + } else if (this.state == RefreshState.IS_PULL_DOWN_RELEASE_REFRESH) { let animator = this.getUIContext().createAnimator({ duration: this.refreshConfigurator.getAnimDuration(), easing: "ease", @@ -413,10 +509,10 @@ export struct ElfRefreshComponent { animator.onFrame = (progress) => { this.trYTop = progress this.header.onMoving && this.header.onMoving() + this.twoHeader.onMoving && this.twoHeader.onMoving() } animator.onFinish = () => { this.trYTop = this.refreshConfigurator.getRefreshHeight() - this.header.onMoving && this.header.onMoving() this.state = RefreshState.IS_REFRESHING this.onRefresh().then((result) => { this.onRefreshFinish(result) @@ -428,12 +524,23 @@ export struct ElfRefreshComponent { this.animList.add(animator) animator.play() return + } else if (this.state == RefreshState.IS_PULL_DOWN_RELEASE_TWO_LEVEL) { + this.state = RefreshState.IS_TWO_LEVEL + this.openTwo() + return + } else if (this.state == RefreshState.IS_TWO_LEVEL) { + if(this.trYTop <= LengthUtil.parseToVP(this.refreshConfigurator.getCloseTwoOffset(),this.twoHeader.height)){ + this.closeRefresh() + }else{ + this.openTwo() + } + return } } else if (this.trYBottom < 0) { // 上拉结束 if (!this.footer.hasMore) { return } - if (this.state == RefreshState.IS_PULL_UP_1) { + if (this.state == RefreshState.IS_PULL_UP_LOAD) { this.closeLoad(); } else { let animator = this.getUIContext().createAnimator({ @@ -452,7 +559,6 @@ export struct ElfRefreshComponent { } animator.onFinish = () => { this.trYBottom = -this.refreshConfigurator.getLoadHeight() - this.footer.onMoving && this.footer.onMoving() this.state = RefreshState.IS_LOADING this.onLoadMore().then((result) => { this.onLoadFinish(result) @@ -499,6 +605,30 @@ export struct ElfRefreshComponent { }, this.refreshConfigurator.getRefreshCompleteTextHoldTime()) } + private openTwo() { + let animator = this.getUIContext().createAnimator({ + duration: this.refreshConfigurator.getAnimDuration(), + easing: "ease", + delay: 0, + fill: "forwards", + direction: "normal", + iterations: 1, + begin: this.trYTop, + end: this.twoHeader.height + }) + animator.onFrame = (progress) => { + this.trYTop = progress + this.header.onMoving && this.header.onMoving() + this.twoHeader.onMoving && this.twoHeader.onMoving() + } + animator.onFinish = () => { + this.trYTop = this.twoHeader.height + this.animList.delete(animator) + } + this.animList.add(animator) + animator.play() + } + /** * 关闭下拉刷新 */ @@ -518,7 +648,6 @@ export struct ElfRefreshComponent { } animator.onFinish = () => { this.trYTop = 0 - this.header.onMoving && this.header.onMoving() this.state = RefreshState.IS_FREE this.animList.delete(animator) } @@ -545,7 +674,6 @@ export struct ElfRefreshComponent { } animator.onFinish = () => { this.trYBottom = 0 - this.footer.onMoving && this.footer.onMoving() this.state = RefreshState.IS_FREE this.animList.delete(animator) } @@ -588,6 +716,36 @@ export struct ElfRefreshComponent { return 0; } + private getTranslateYOfTwo(newTranslateY: number): number { + if (this.refreshConfigurator !== undefined) { + let minTranslateY = this.twoHeader.height / 2; + let sensitivity = this.refreshConfigurator.getSensitivity(); + if (sensitivity !== undefined && this.trYTop !== undefined) { + // 阻尼值计算 + if (this.trYTop / minTranslateY > 1.8) { + newTranslateY = newTranslateY * 1 * sensitivity; + } else if (this.trYTop / minTranslateY > 1.6) { + newTranslateY = newTranslateY * 0.8 * sensitivity; + } else if (this.trYTop / minTranslateY > 1.4) { + newTranslateY = newTranslateY * 0.6 * sensitivity; + } else if (this.trYTop / minTranslateY < 1.2) { + newTranslateY = newTranslateY * 0.4 * sensitivity; + } else { + newTranslateY = newTranslateY * 0.2 * sensitivity; + } + // 下拉值计算 + if (this.trYTop + newTranslateY < minTranslateY) { + return minTranslateY; + } else if (this.trYTop + newTranslateY > this.twoHeader.height) { + return this.twoHeader.height; + } else { + return this.trYTop + newTranslateY; + } + } + } + return 0 + } + /** * 获取当前上拉位移 * @param newTranslateY diff --git a/library/src/main/ets/constant/CustomStyle.ts b/library/src/main/ets/constant/CustomStyle.ts index 7a97d75cb1cbd67bf41bc6f28e6f7a8e23589f0f..6308d19d210357dddaf29207a4a8b5ca0603ab47 100644 --- a/library/src/main/ets/constant/CustomStyle.ts +++ b/library/src/main/ets/constant/CustomStyle.ts @@ -1,3 +1,7 @@ +/** + * @author duke + * 自定义样式 + */ export enum CustomStyle { Translate, //平行移动 特点: HeaderView高度不会改变, Scale, //拉伸形变 特点:在下拉和上弹(HeaderView高度改变)时候,会自动触发OnDraw事件 diff --git a/library/src/main/ets/constant/RefreshState.ets b/library/src/main/ets/constant/RefreshState.ets index da332087aa5eacce52abded11d3dd6dbcde671a7..1d1d58b7332a8804be52b14872a082820a450aa2 100644 --- a/library/src/main/ets/constant/RefreshState.ets +++ b/library/src/main/ets/constant/RefreshState.ets @@ -1,11 +1,17 @@ -export enum RefreshState{ - IS_FREE = 0, - IS_PULL_DOWN_1, - IS_PULL_DOWN_2, - IS_REFRESHING, - IS_REFRESHED, - IS_PULL_UP_1, - IS_PULL_UP_2, - IS_LOADING, - IS_LOADED +/** + * @author duke + * @description 刷新状态 + */ +export enum RefreshState { + IS_FREE = 0,// 自由 + IS_PULL_DOWN_REFRESH,// 下拉刷新 + IS_PULL_DOWN_RELEASE_REFRESH,// 下拉释放刷新 + IS_PULL_DOWN_RELEASE_TWO_LEVEL,// 下拉释放二级刷新 + IS_TWO_LEVEL,// 二级显示中 + IS_REFRESHING,// 正在刷新 + IS_REFRESHED,// 刷新完成 + IS_PULL_UP_LOAD,// 上拉加载 + IS_PULL_UP_RELEASE_LOAD,// 上拉释放加载 + IS_LOADING,// 正在加载 + IS_LOADED// 加载完成 } \ No newline at end of file diff --git a/library/src/main/ets/components/ElfCustomHeaderFooter.ets b/library/src/main/ets/model/ElfCustomHeaderFooter.ets similarity index 47% rename from library/src/main/ets/components/ElfCustomHeaderFooter.ets rename to library/src/main/ets/model/ElfCustomHeaderFooter.ets index ad230570ff1110a0be8312c5661b9fbb31a06d17..0cbc95ed7bd2406b2ab11a64a4ce284b1982a00e 100644 --- a/library/src/main/ets/components/ElfCustomHeaderFooter.ets +++ b/library/src/main/ets/model/ElfCustomHeaderFooter.ets @@ -1,6 +1,10 @@ -import { ElfLoadFooter } from "../model/ElfLoadFooter"; -import { ElfRefreshHeader } from "../model/ElfRefreshHeader"; +import { ElfLoadFooter } from "./ElfLoadFooter"; +import { ElfRefreshHeader } from "./ElfRefreshHeader"; +/** + * @author duke + * @description 自定义头尾 + */ export interface ElfCustomHeaderFooter{ builderHeader?:WrappedBuilder<[ElfRefreshHeader]> diff --git a/library/src/main/ets/model/ElfLoadFooter.ets b/library/src/main/ets/model/ElfLoadFooter.ets index 3f864fcd31c70b3e9459909f12d41e3cbfd0e0a4..c0f8f0a54be167473e9f7556ff25cd9928645e98 100644 --- a/library/src/main/ets/model/ElfLoadFooter.ets +++ b/library/src/main/ets/model/ElfLoadFooter.ets @@ -1,8 +1,15 @@ import { ElfRefreshHeader } from "./ElfRefreshHeader" import { RefreshState } from "../constant/RefreshState" +/** + * @author duke + * @description 加载更多控制器 + */ @ObservedV2 export class ElfLoadFooter extends ElfRefreshHeader { + /** + * 是否还有更多 + */ @Trace hasMore: boolean diff --git a/library/src/main/ets/model/ElfRefreshConfigurator.ts b/library/src/main/ets/model/ElfRefreshConfigurator.ts index cee03e898824905b809e6b3d89a6dfebc1bc6b5e..2d66e016cb6c11024a2f759bf6cf24c7434c455e 100644 --- a/library/src/main/ets/model/ElfRefreshConfigurator.ts +++ b/library/src/main/ets/model/ElfRefreshConfigurator.ts @@ -1,19 +1,53 @@ import { CustomStyle } from "../constant/CustomStyle"; +/** + * @author duke + * @description 配置类 + */ export class ElfRefreshConfigurator { - private hasRefresh: boolean = true; // 是否具有下拉刷新功能 - private hasLoadMore: boolean = true; // 是否具有上拉加载功能 - private maxTranslate: number = 100; // 可下拉上拉的最大距离 - private sensitivity: number = 0.7; // 下拉上拉灵敏度 - private animDuration: number = 150; // 滑动结束后,回弹动画执行时间 - private refreshHeight: number = 60; // 下拉动画高度 - private loadHeight: number = 60; // 上拉动画高度 - private refreshAnimDuration: number = 1000; // 自动下拉动画执行一次的时间 - private refreshCompleteTextHoldTime: number = 1000; //下拉刷新完毕后, 刷新成功文本停留的时间 - private headerStyle:CustomStyle = CustomStyle.Translate // 头部样式 - private footerStyle:CustomStyle = CustomStyle.Translate // 尾部样式 - - setHeaderStyle(headerStyle:CustomStyle) { + hasRefresh: boolean = true; // 是否具有下拉刷新功能 + hasLoadMore: boolean = true; // 是否具有上拉加载功能 + hasTwoHeader: boolean = false; // 是否具有两楼 + maxTranslate: number = 160; // 可下拉上拉的最大距离 + closeTwoOffset: number | string = '80%'; // 两楼关闭的偏移量 + sensitivity: number = 0.7; // 下拉上拉灵敏度 + animDuration: number = 150; // 滑动结束后,回弹动画执行时间 + refreshHeight: number = 80; // 下拉动画高度 + loadHeight: number = 80; // 上拉动画高度 + twoLevelHeight: string | number = '100%'; //二楼高度 + refreshAnimDuration: number = 1000; // 自动下拉动画执行一次的时间 + refreshCompleteTextHoldTime: number = 1000; //下拉刷新完毕后, 刷新成功文本停留的时间 + headerStyle: CustomStyle = CustomStyle.Translate // 头部样式 + footerStyle: CustomStyle = CustomStyle.Translate // 尾部样式 + + public setCloseTwoOffset(value: number | string) { + this.closeTwoOffset = value; + return this + } + + public getCloseTwoOffset(): number | string { + return this.closeTwoOffset; + } + + public setTwoLevelHeight(value: string | number) { + this.twoLevelHeight = value; + return this + } + + public getTwoLevelHeight(): string | number { + return this.twoLevelHeight; + } + + setHasTwoHeader(hasTwoHeader: boolean) { + this.hasTwoHeader = hasTwoHeader; + return this; + } + + getHasTwoHeader() { + return this.hasTwoHeader; + } + + setHeaderStyle(headerStyle: CustomStyle) { this.headerStyle = headerStyle; return this; } @@ -22,7 +56,7 @@ export class ElfRefreshConfigurator { return this.headerStyle; } - setFooterStyle(footerStyle:CustomStyle) { + setFooterStyle(footerStyle: CustomStyle) { this.footerStyle = footerStyle; return this; } diff --git a/library/src/main/ets/model/ElfRefreshHeader.ets b/library/src/main/ets/model/ElfRefreshHeader.ets index eeec48eebaf5b1d12c9c84a858d871d840831899..317a1f96514a4424fc928eaa107bea122fda7f6f 100644 --- a/library/src/main/ets/model/ElfRefreshHeader.ets +++ b/library/src/main/ets/model/ElfRefreshHeader.ets @@ -1,9 +1,23 @@ import { RefreshState } from "../constant/RefreshState" +/** + * @author duke + * @description 下拉刷新控制器 + */ @ObservedV2 export class ElfRefreshHeader { + /** + * 最大位移量 + */ maxTranslate: number + /** + * 容器高度 + */ + @Trace height: number + /** + * 当前状态 + */ @Trace state: RefreshState /** diff --git a/library/src/main/ets/model/ElfTwoLevelHeader.ets b/library/src/main/ets/model/ElfTwoLevelHeader.ets new file mode 100644 index 0000000000000000000000000000000000000000..7f0ef1c61adfee5b77af35228098522f08dadbe8 --- /dev/null +++ b/library/src/main/ets/model/ElfTwoLevelHeader.ets @@ -0,0 +1,10 @@ +import { ElfRefreshController } from "../ElfRefreshController"; +import { ElfRefreshHeader } from "./ElfRefreshHeader"; + +/** + * @author duke + * @description 自定义二级表头 + */ +export interface ElfTwoLevelHeader{ + builderHeader:WrappedBuilder<[ElfRefreshHeader,ElfRefreshController]> +} \ No newline at end of file diff --git a/library/src/main/ets/uitls/LengthUtil.ets b/library/src/main/ets/uitls/LengthUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..8b5103cb86e018f7bc2b93d620c8e7f1dec7383c --- /dev/null +++ b/library/src/main/ets/uitls/LengthUtil.ets @@ -0,0 +1,152 @@ +export class LengthUtil { + /** + * 核心转换方法(新增calc支持) + * @param input 输入字符串,支持格式:'calc( 50% - 20vp )' | '100vp' | '50%' + * @param parentSize 父容器尺寸(单位vp) + * @returns 转换后的vp数值 + */ + static parseToVP(input: string | number, parentSize: number): number { + if(typeof input === 'number'){ + return input; + } + // 统一处理输入空白 + const cleanInput = input.replace(/\s+/g, ''); + + // 解析calc表达式 + if (cleanInput.startsWith('calc(')) { + return LengthUtil._parseCalc(cleanInput, parentSize); + } + + // 处理百分比 + if (cleanInput.endsWith('%')) { + const percent = parseFloat(cleanInput) / 100; + return Math.round(parentSize * percent); + } + + // 处理vp单位 + if (cleanInput.endsWith('vp')) { + return parseFloat(cleanInput); + } + + // 处理lpx单位 + if (cleanInput.endsWith('lpx')) { + const lpxValue = parseFloat(cleanInput); + return px2vp(lpx2px(lpxValue)); + } + + // 处理px单位 + if (cleanInput.endsWith('px')) { + const pxValue = parseFloat(cleanInput); + return px2vp(pxValue); // 实际项目中可用px2vp()函数转换 + } + + // 无单位默认vp处理 + if (/^\d+$/.test(input)) { + return parseFloat(input); + } + + return 0 + } + + /** + * 解析calc表达式核心方法 + * @private + */ + private static _parseCalc(expr: string, parentSize: number): number { + // 提取表达式内容并校验格式 + const match = expr.match(/^calc\((.+)\)$/); + if (!match) { + throw new Error(`Invalid calc expression: ${expr}`); + } + + // 拆分运算元素 + const elements = LengthUtil._tokenizeCalc(match[1]); + + // 执行计算 + return LengthUtil._evaluateCalc(elements, parentSize); + } + + /** + * 分词器(支持嵌套计算) + * @private + */ + private static _tokenizeCalc(expr: string): Array { + const tokens: Array = []; + let current = ''; + const operators = new Set(['+', '-', '*', '/']); + + for (const char of expr) { + if (operators.has(char)) { + if (current) { + if (current.endsWith('%')) { + tokens.push(current); + } else { + tokens.push(LengthUtil._parseToken(current)); + } + + current = ''; + } + tokens.push(char); + } else { + current += char; + } + } + + if (current) { + tokens.push(LengthUtil._parseToken(current)); + } + + return tokens; + } + + /** + * 解析单个token + * @private + */ + private static _parseToken(token: string): number { + try { + return LengthUtil.parseToVP(token, 0); // 临时parentSize,实际计算时会覆盖 + } catch { + throw new Error(`Invalid token: ${token}`); + } + } + + /** + * 计算逻辑(支持运算符优先级) + * @private + */ + private static _evaluateCalc(tokens: Array, parentSize: number): number { + // 处理乘除 + for (let i = 0; i < tokens.length; i++) { + const op = tokens[i]; + if (op === '*' || op === '/') { + const left = tokens[i - 1] as number; + const right = LengthUtil._getOperandValue(tokens[i + 1], parentSize); + const result = op === '*' ? left * right : left / right; + tokens.splice(i - 1, 3, result); + i -= 2; + } + } + + // 处理加减 + let result = LengthUtil._getOperandValue(tokens[0], parentSize); + for (let i = 1; i < tokens.length; i += 2) { + const op = tokens[i] as string; + const right = LengthUtil._getOperandValue(tokens[i + 1], parentSize); + result = op === '+' ? result + right : result - right; + } + + return Math.round(result); + } + + /** + * 获取操作数实际值 + * @private + */ + private static _getOperandValue(token: string | number, parentSize: number): number { + if (typeof token === 'number') { + return token; + } + return LengthUtil.parseToVP(token, parentSize); + } +} \ No newline at end of file diff --git a/library/src/main/ets/uitls/TimeUtil.ts b/library/src/main/ets/uitls/TimeUtil.ts index 6de6c189180be9a60dd37be9c407f45d56a4aa28..23e56ef0ec2f2d2f0140d28bf3b23051371fb8ae 100644 --- a/library/src/main/ets/uitls/TimeUtil.ts +++ b/library/src/main/ets/uitls/TimeUtil.ts @@ -1,3 +1,9 @@ +/** + * 格式化日期 + * @param date 日期对象 + * @param format 格式化字符串,默认为yyyy-MM-dd HH:mm:ss + * @returns 格式化后的日期字符串 + */ export const formatDate = (date: Date, format = 'yyyy-MM-dd HH:mm:ss'): string => { const year = date.getFullYear() const month = date.getMonth() + 1 diff --git a/library/src/main/resources/base/element/float.json b/library/src/main/resources/base/element/float.json deleted file mode 100644 index 33ea22304f9b1485b5f22d811023701b5d4e35b6..0000000000000000000000000000000000000000 --- a/library/src/main/resources/base/element/float.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "float": [ - { - "name": "page_text_font_size", - "value": "50fp" - } - ] -} diff --git a/library/src/main/resources/base/element/string.json b/library/src/main/resources/base/element/string.json index cecf80cb6b577d497788c7da4c22f8c5573b03f7..5485d3c2fca7ad9e19cee387f59c78fb33f5084d 100644 --- a/library/src/main/resources/base/element/string.json +++ b/library/src/main/resources/base/element/string.json @@ -8,6 +8,10 @@ "name": "elf_header_release", "value": "Release To Refresh" }, + { + "name": "elf_header_secondary", + "value": "Release To Second Floor" + }, { "name": "elf_header_refreshing", "value": "Refreshing..." diff --git a/library/src/main/resources/zh/element/string.json b/library/src/main/resources/zh/element/string.json index 875ab421ec98bc4a8f7d554793e9f5f279f2504f..755d914de2c99e97368f4ff9972fe66c4649b799 100644 --- a/library/src/main/resources/zh/element/string.json +++ b/library/src/main/resources/zh/element/string.json @@ -8,6 +8,10 @@ "name": "elf_header_release", "value": "释放立即刷新" }, + { + "name": "elf_header_secondary", + "value": "释放进入二楼" + }, { "name": "elf_header_refreshing", "value": "正在刷新…"