├── .github └── workflows │ └── copy_readme.yml ├── .gitignore ├── AppScope ├── app.json5 └── resources │ └── base │ ├── element │ └── string.json │ └── media │ └── app_icon.png ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build-profile.json5 ├── entry ├── .gitignore ├── build-profile.json5 ├── hvigorfile.ts ├── oh-package.json5 └── src │ ├── main │ ├── ets │ │ ├── entryability │ │ │ └── EntryAbility.ets │ │ ├── pages │ │ │ ├── Index.ets │ │ │ ├── PullToRefreshAppbar.ets │ │ │ └── PullToRefreshHeader.ets │ │ └── util │ │ │ └── ScreenUtils.ets │ ├── module.json5 │ └── resources │ │ ├── base │ │ ├── element │ │ │ ├── color.json │ │ │ └── string.json │ │ ├── media │ │ │ ├── 467141054.jpg │ │ │ ├── icon.png │ │ │ └── startIcon.png │ │ └── profile │ │ │ └── main_pages.json │ │ ├── en_US │ │ └── element │ │ │ └── string.json │ │ └── zh_CN │ │ └── element │ │ └── string.json │ └── ohosTest │ ├── ets │ ├── test │ │ ├── Ability.test.ets │ │ └── List.test.ets │ ├── testability │ │ ├── TestAbility.ets │ │ └── pages │ │ │ └── Index.ets │ └── testrunner │ │ └── OpenHarmonyTestRunner.ts │ ├── module.json5 │ └── resources │ └── base │ ├── element │ ├── color.json │ └── string.json │ ├── media │ └── icon.png │ └── profile │ └── test_pages.json ├── hvigor └── hvigor-config.json5 ├── hvigorfile.ts ├── oh-package.json5 └── pull_to_refresh ├── .DS_Store ├── .gitignore ├── .idea ├── .gitignore └── vcs.xml ├── .ohpmignore ├── BuildProfile.ets ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build-profile.json5 ├── example └── example.ets ├── hvigorfile.ts ├── index.ets ├── oh-package.json5 └── src └── main ├── ets ├── common │ ├── Controller.ets │ └── RefreshConstants.ets └── components │ └── PullToRefresh.ets ├── module.json5 └── resources ├── base └── element │ └── string.json ├── en_US └── element │ └── string.json └── zh_CN └── element └── string.json /.github/workflows/copy_readme.yml: -------------------------------------------------------------------------------- 1 | name: Copy Files on Changes 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | copy-on-changes: 10 | runs-on: ubuntu-latest 11 | env: 12 | DOCS_FOLDER: 'pull_to_refresh' 13 | 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Get Last Commit Timestamp 19 | id: last_commit_time 20 | run: | 21 | LAST_COMMIT_TS=$(git log -1 --format=%at) 22 | echo "::set-output name=last_commit_ts::$LAST_COMMIT_TS" 23 | 24 | - name: Check for Changes 25 | id: check_changes 26 | run: | 27 | git diff HEAD^ $DOCS_FOLDER/README.md $DOCS_FOLDER/CHANGELOG.md $DOCS_FOLDER/LICENSE || echo "Files have changed" 28 | 29 | - name: Copy Files to Root if Changed 30 | if: steps.check_changes.outputs.stdout == 'Files have changed' 31 | run: | 32 | cp $DOCS_FOLDER/README.md . 33 | cp $DOCS_FOLDER/CHANGELOG.md . 34 | cp $DOCS_FOLDER/LICENSE . 35 | 36 | - name: Calculate New Commit Time 37 | if: steps.check_changes.outputs.stdout == 'Files have changed' 38 | id: calculate_commit_time 39 | run: | 40 | LAST_COMMIT_TS=$(echo ${{ steps.last_commit_time.outputs.last_commit_ts }}) 41 | NEW_COMMIT_TS=$((LAST_COMMIT_TS + 60)) 42 | echo "::set-output name=new_commit_ts::$NEW_COMMIT_TS" 43 | 44 | - name: Commit and Push Changes 45 | if: steps.check_changes.outputs.stdout == 'Files have changed' 46 | run: | 47 | NEW_COMMIT_TS=$(echo ${{ steps.calculate_commit_time.outputs.new_commit_ts }}) 48 | git config --local user.email "zmtzawqlp@live.com" 49 | git config --local user.name "zmtzawqlp" 50 | git add README.md CHANGELOG.md LICENSE 51 | git commit --date="$NEW_COMMIT_TS" -m "Update README.md, CHANGELOG.md, and LICENSE in root directory" 52 | git push 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /local.properties 4 | /.idea 5 | **/build 6 | /.hvigor 7 | .cxx 8 | /.clangd 9 | /.clang-format 10 | /.clang-tidy 11 | **/.test 12 | oh-package-lock.json5 13 | -------------------------------------------------------------------------------- /AppScope/app.json5: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "bundleName": "com.example.pull_to_refresh", 4 | "vendor": "example", 5 | "versionCode": 1000000, 6 | "versionName": "1.0.0", 7 | "icon": "$media:app_icon", 8 | "label": "$string:app_name" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /AppScope/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "app_name", 5 | "value": "Example" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /AppScope/resources/base/media/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmonyCandies/pull_to_refresh/9f65d4c122697a1baa955fd19371f965cb2a0700/AppScope/resources/base/media/app_icon.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | * Initial Open Source release. 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2023 zmtzawqlp 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pull_to_refresh 2 | 3 | 快速自定义下拉刷新动画的组件 4 | 5 | - [pull\_to\_refresh](#pull_to_refresh) 6 | - [安装](#安装) 7 | - [参数](#参数) 8 | - [PullToRefreshIndicatorMode](#pulltorefreshindicatormode) 9 | - [配置参数](#配置参数) 10 | - [回调](#回调) 11 | - [onRefresh](#onrefresh) 12 | - [onReachEdge](#onreachedge) 13 | - [使用](#使用) 14 | - [导入引用](#导入引用) 15 | - [定义配置](#定义配置) 16 | - [使用 PullToRefresh](#使用-pulltorefresh) 17 | - [自定义下拉刷新效果](#自定义下拉刷新效果) 18 | 19 | | ![PullToRefreshHeader.gif](https://github.com/HarmonyCandies/HarmonyCandies/blob/main/gif/pull_to_refresh/PullToRefreshHeader.gif) | ![PullToRefreshAppbar.gif](https://github.com/HarmonyCandies/HarmonyCandies/blob/main/gif/pull_to_refresh/PullToRefreshAppbar.gif) | 20 | | --- | --- | 21 | 22 | 23 | ## 安装 24 | 25 | `ohpm install @candies/pull_to_refresh` 26 | 27 | 28 | ## 参数 29 | 30 | ### PullToRefreshIndicatorMode 31 | 32 | ``` typescript 33 | export enum PullToRefreshIndicatorMode { 34 | initial, // 初始状态 35 | drag, // 手势向下拉的状态. 36 | armed, // 被拖动得足够远,以至于触发“onRefresh”回调函数的上滑事件 37 | snap, // 用户没有拖动到足够远的地方并且释放回到初始化状态的过程 38 | refresh, // 正在执行刷新回调. 39 | done, // 刷新回调完成. 40 | canceled, // 用户取消了下拉刷新手势. 41 | error, // 刷新失败 42 | } 43 | ``` 44 | 45 | ### 配置参数 46 | 47 | 48 | | 参数 | 类型 | 描述 | 49 | | --- | --- |--- | 50 | | maxDragOffset | number | 最大拖动距离(非必填) | 51 | | reachToRefreshOffset | number | 到达满足触发刷新的距离(非必填) | 52 | | refreshOffset | number | 触发刷新的时候,停留的刷新距离(非必填) | 53 | | pullBackOnRefresh | boolean | 在触发刷新回调的时候是否执行回退动画(默认 `false`) | 54 | | pullBackAnimatorOptions | AnimatorOptions | 回退动画的一些配置(duration,easing,delay,fill) | 55 | | pullBackOnError | boolean | 刷新失败的时候,是否执行回退动画(默认 `false`) | 56 | 57 | 58 | * `maxDragOffset` 和 `reachToRefreshOffset` 如果不定义的话,会根据当前容器的高度设置默认值。 59 | 60 | 61 | ``` 62 | /// Set the default value of [maxDragOffset,reachToRefreshOffset] 63 | onAreaChange(oldValue: Area, newValue: Area) { 64 | if (this.maxDragOffset == undefined) { 65 | this.maxDragOffset = (newValue.height as number) / 5; 66 | } 67 | if (this.reachToRefreshOffset == undefined) { 68 | this.reachToRefreshOffset = this.maxDragOffset * 3 / 4; 69 | } 70 | else { 71 | this.reachToRefreshOffset = Math.min(this.reachToRefreshOffset, this.maxDragOffset); 72 | } 73 | } 74 | ``` 75 | 76 | * `pullBackAnimatorOptions` 的默认值如下: 77 | 78 | ``` typescript 79 | /// The options of pull back animation 80 | pullBackAnimatorOptions: AnimatorOptions = { 81 | duration: 400, 82 | easing: "friction", 83 | delay: 0, 84 | fill: "forwards", 85 | direction: "normal", 86 | iterations: 1, 87 | begin: 1.0, 88 | end: 0.0 89 | }; 90 | ``` 91 | 92 | ### 回调 93 | 94 | #### onRefresh 95 | 96 | 触发的下拉刷新事件 97 | ``` typescript 98 | /// A function that's called when the user has dragged the refresh indicator 99 | /// far enough to demonstrate that they want the app to refresh. The returned 100 | /// [Future] must complete when the refresh operation is finished. 101 | 102 | onRefresh: RefreshCallback = async () => true; 103 | ``` 104 | #### onReachEdge 105 | 106 | 是否我们到达了下拉刷新的边界,比如说,下拉刷新的内容是一个列表,那么边界就是到达列表的顶部位置。 107 | ``` typescript 108 | /// Whether we reach the edge to pull refresh 109 | onReachEdge: () => boolean = () => true; 110 | ``` 111 | 112 | ## 使用 113 | 114 | ### 导入引用 115 | ``` typescript 116 | import { 117 | PullToRefresh, 118 | pull_to_refresh, 119 | PullToRefreshIndicatorMode, 120 | } from '@candies/pull_to_refresh' 121 | ``` 122 | 123 | ### 定义配置 124 | 125 | ``` typescript 126 | @State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller(); 127 | ``` 128 | 129 | ### 使用 PullToRefresh 130 | 131 | 将需要支持下拉刷新的部分,通过 `@BuilderParam` 修饰的 `builder` 回调传入,或者尾随闭包初始化组件。 132 | 133 | [@BuilderParam装饰器:引用@Builder函数-快速入门-入门-HarmonyOS应用开发](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-builderparam-0000001524416541-V3) 134 | 135 | ``` typescript 136 | 137 | PullToRefresh( 138 | { 139 | refreshOffset: 150, 140 | maxDragOffset: 300, 141 | reachToRefreshOffset: 200, 142 | controller: this.controller, 143 | onRefresh: async () => { 144 | return new Promise((resolve) => { 145 | setTimeout(() => { 146 | // 定义的刷新方法,当刷新成功之后,返回回调,模拟 2 秒之后刷新完毕 147 | this.onRefresh().then((value) => resolve(value)); 148 | }, 2000); 149 | }); 150 | }, 151 | onReachEdge: () => { 152 | let yOffset = this.scroller.currentOffset().yOffset; 153 | return Math.abs(yOffset) < 0.001; 154 | } 155 | }) { 156 | // 我们自定义的下拉刷新头部 157 | PullToRefreshContainer({ 158 | lastRefreshTime: this.lastRefreshTime, 159 | controller: this.controller, 160 | }) 161 | List({ scroller: this.scroller }) { 162 | ForEach(this.listData, (item, index) => { 163 | ListItem() { 164 | Text(`${item}`,).align(Alignment.Center) 165 | }.height(100).width('100%') 166 | }, (item, index) => { 167 | return `${item}`; 168 | }) 169 | } 170 | // 必须设置 edgeEffect 171 | .edgeEffect(EdgeEffect.None) 172 | // 为了使下拉刷新的手势的过程中,不触发列表的滚动 173 | .onScrollFrameBegin((offset, state) => { 174 | if (this.controller.dragOffset > 0) { 175 | offset = 0; 176 | } 177 | return { offsetRemain: offset, }; 178 | }) 179 | } 180 | } 181 | ``` 182 | 183 | ### 自定义下拉刷新效果 184 | 185 | 你可以通过对 `Controller` 中 `dragOffset` 和 `mode` 的判断,创建属于自己的下拉刷新效果。如果下拉刷新失败了,你可以通过调用 `Controller` 的 `refresh() `方法来重新执行刷新动画。 186 | 187 | ``` typescript 188 | /// The current drag offset 189 | dragOffset: number = 0; 190 | /// The current pull mode 191 | mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial; 192 | ``` 193 | 194 | 下面是一个自定义下拉刷新头部的例子 195 | 196 | ``` typescript 197 | @Component 198 | struct PullToRefreshContainer { 199 | @Prop lastRefreshTime: number = 0; 200 | @Link controller: pull_to_refresh.Controller; 201 | 202 | getShowText(): string { 203 | let text = ''; 204 | if (this.controller.mode == PullToRefreshIndicatorMode.armed) { 205 | text = 'Release to refresh'; 206 | } else if (this.controller.mode == PullToRefreshIndicatorMode.refresh || 207 | this.controller.mode == PullToRefreshIndicatorMode.snap) { 208 | text = 'Loading...'; 209 | } else if (this.controller.mode == PullToRefreshIndicatorMode.done) { 210 | text = 'Refresh completed.'; 211 | } else if (this.controller.mode == PullToRefreshIndicatorMode.drag) { 212 | text = 'Pull to refresh'; 213 | } else if (this.controller.mode == PullToRefreshIndicatorMode.canceled) { 214 | text = 'Cancel refresh'; 215 | } else if (this.controller.mode == PullToRefreshIndicatorMode.error) { 216 | text = 'Refresh failed'; 217 | } 218 | return text; 219 | } 220 | 221 | getDate(): String { 222 | return (new Date(this.lastRefreshTime)).toTimeString(); 223 | } 224 | 225 | build() { 226 | Row() { 227 | if (this.controller.dragOffset != 0) 228 | Text(`${this.getShowText()}---${this.getDate()}`) 229 | if (this.controller.dragOffset > 50 && this.controller.mode == PullToRefreshIndicatorMode.refresh) 230 | LoadingProgress().width(50).height(50) 231 | } 232 | .justifyContent(FlexAlign.Center) 233 | .height(this.controller.dragOffset) 234 | .width('100%') 235 | .onClick(() => { 236 | if (this.controller.mode == PullToRefreshIndicatorMode.error) { 237 | this.controller.refresh(); 238 | } 239 | }) 240 | .backgroundColor('#22808080') 241 | } 242 | } 243 | ``` 244 | 245 | -------------------------------------------------------------------------------- /build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "signingConfigs": [], 4 | "products": [ 5 | { 6 | "name": "default", 7 | "signingConfig": "default", 8 | "compatibleSdkVersion": "4.0.0(10)", 9 | "runtimeOS": "HarmonyOS" 10 | } 11 | ], 12 | "buildModeSet": [ 13 | { 14 | "name": "debug", 15 | }, 16 | { 17 | "name": "release" 18 | } 19 | ] 20 | }, 21 | "modules": [ 22 | { 23 | "name": "entry", 24 | "srcPath": "./entry", 25 | "targets": [ 26 | { 27 | "name": "default", 28 | "applyToProducts": [ 29 | "default" 30 | ] 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "pull_to_refresh", 36 | "srcPath": "./pull_to_refresh" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /entry/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /.preview 4 | /build 5 | /.cxx 6 | /.test -------------------------------------------------------------------------------- /entry/build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "apiType": "stageMode", 3 | "buildOption": { 4 | }, 5 | "targets": [ 6 | { 7 | "name": "default", 8 | "runtimeOS": "HarmonyOS" 9 | }, 10 | { 11 | "name": "ohosTest", 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /entry/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | import { hapTasks } from '@ohos/hvigor-ohos-plugin'; 2 | 3 | export default { 4 | system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 5 | plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ 6 | } 7 | -------------------------------------------------------------------------------- /entry/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "license": "", 3 | "devDependencies": {}, 4 | "author": "", 5 | "name": "entry", 6 | "description": "Please describe the basic information.", 7 | "main": "", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "@candies/pull_to_refresh": "file:../pull_to_refresh" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /entry/src/main/ets/entryability/EntryAbility.ets: -------------------------------------------------------------------------------- 1 | import AbilityConstant from '@ohos.app.ability.AbilityConstant'; 2 | import hilog from '@ohos.hilog'; 3 | import UIAbility from '@ohos.app.ability.UIAbility'; 4 | import Want from '@ohos.app.ability.Want'; 5 | import window from '@ohos.window'; 6 | import { ScreenUtils } from '../util/ScreenUtils'; 7 | 8 | export default class EntryAbility extends UIAbility { 9 | onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 10 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); 11 | } 12 | 13 | onDestroy(): void { 14 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); 15 | } 16 | 17 | 18 | async onWindowStageCreate(windowStage: window.WindowStage): Promise { 19 | // Main window is created, set main page for this ability 20 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); 21 | AppStorage.setOrCreate("statusBarHeight", 36); 22 | ScreenUtils.enterImmersion(windowStage); 23 | 24 | windowStage.loadContent('pages/Index', (err, data) => { 25 | if (err.code) { 26 | hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); 27 | return; 28 | } 29 | hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); 30 | }); 31 | } 32 | 33 | onWindowStageDestroy(): void { 34 | // Main window is destroyed, release UI related resources 35 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); 36 | } 37 | 38 | onForeground(): void { 39 | // Ability has brought to foreground 40 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); 41 | } 42 | 43 | onBackground(): void { 44 | // Ability has back to background 45 | hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/Index.ets: -------------------------------------------------------------------------------- 1 | import router from '@ohos.router' 2 | import { ScreenUtils } from '../util/ScreenUtils'; 3 | 4 | @Entry 5 | @Component 6 | struct Index { 7 | title: string = 'pull_to_refresh demo'; 8 | pages: Array = [ 9 | { 10 | description: 'Show how to use pull to refresh notification to build a pull refresh header,and hide it on refresh done', 11 | route: 'pages/PullToRefreshHeader', 12 | }, 13 | { 14 | description: 'Show how to use pull to refresh notification to build a pull refresh appbar', 15 | route: 'pages/PullToRefreshAppbar', 16 | }, 17 | ] 18 | 19 | build() { 20 | Column() { 21 | Text(`${this.title}`) 22 | List() { 23 | ForEach(this.pages, (item:PageInfo, index) => { 24 | ListItem() { 25 | Text(`${index + 1}. ${item.description}`).onClick((x) => { 26 | router.pushUrl({ url: item.route }); 27 | }).margin(10) 28 | }.width('100%') 29 | }) 30 | }.divider({ strokeWidth: 1, color: Color.Gray }).margin({ top: 10 }) 31 | } 32 | .width('100%').height('100%').margin({ top: ScreenUtils.getStatusBarHeight() }) 33 | } 34 | } 35 | 36 | interface PageInfo { 37 | description: string; 38 | route: string; 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/PullToRefreshAppbar.ets: -------------------------------------------------------------------------------- 1 | import { PullToRefresh, pull_to_refresh, PullToRefreshIndicatorMode, } from '@candies/pull_to_refresh' 2 | import { ScreenUtils } from '../util/ScreenUtils'; 3 | import router from '@ohos.router'; 4 | 5 | @Entry 6 | @Component 7 | struct PullToRefreshAppbar { 8 | @State listData: Array = []; 9 | scroller: Scroller = new Scroller(); 10 | @State lastRefreshTime: number = Date.now(); 11 | @State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller(); 12 | 13 | aboutToAppear() { 14 | for (let index = 20; index > 0; index--) { 15 | this.listData.push(index); 16 | } 17 | } 18 | 19 | onRefresh(): void { 20 | let length = this.listData.length; 21 | let list: Array = []; 22 | for (let index = length; index < length + 20; index++) { 23 | list.push(index); 24 | } 25 | this.listData = [length + 1, ...this.listData]; 26 | this.lastRefreshTime = Date.now(); 27 | // this.listData.splice(0,0,...list.reverse()); 28 | //this.listData.unshift(...list.reverse()); 29 | } 30 | 31 | @Builder 32 | NavigationMenus() { 33 | 34 | } 35 | 36 | build() { 37 | Column() { 38 | PullToRefresh( 39 | { 40 | pullBackOnRefresh: true, 41 | maxDragOffset: 120, 42 | reachToRefreshOffset: 80, 43 | pullBackAnimatorOptions: { 44 | duration: 2000, 45 | easing: "friction", 46 | delay: 0, 47 | fill: "forwards", 48 | direction: "normal", 49 | iterations: 1, 50 | begin: 1.0, 51 | end: 0.0 52 | }, 53 | controller: this.controller, 54 | onRefresh: async () => { 55 | return new Promise((resolve) => { 56 | setTimeout(() => { 57 | resolve(true); 58 | this.onRefresh(); 59 | }, 2000); 60 | }); 61 | }, 62 | onReachEdge: () => { 63 | let yOffset = this.scroller.currentOffset().yOffset; 64 | return Math.abs(yOffset) < 0.001; 65 | } 66 | }) { 67 | Stack({ alignContent: Alignment.Top }) { 68 | Column() { 69 | Image($r('app.media.467141054')) 70 | .width("100%") 71 | .height(this.controller.dragOffset + 200) 72 | .objectFit(ImageFit.Cover) 73 | List({ scroller: this.scroller }) { 74 | ForEach(this.listData, (item:number, index) => { 75 | ListItem() { 76 | Text(`${item}`,).align(Alignment.Center) 77 | }.height(100).width('100%') 78 | }, (item:number, index) => { 79 | return `${item}`; 80 | }) 81 | } 82 | // we must do this 83 | .edgeEffect(EdgeEffect.None) 84 | // if we are in pull to refresh gesture, the list should not be scroll 85 | .onScrollFrameBegin((offset, state) => { 86 | if (this.controller.dragOffset > 0) { 87 | offset = 0; 88 | } 89 | return { offsetRemain: offset, }; 90 | },) 91 | } 92 | 93 | Row() { 94 | Text('back').onClick(() => { 95 | router.back(); 96 | }).margin({ left: 15, top: 15 }).fontColor(Color.White) 97 | if (this.controller.mode == PullToRefreshIndicatorMode.refresh) 98 | LoadingProgress().width(35).height(35).margin({ right: 15 }).color(Color.White) 99 | else 100 | Text('...') 101 | .width(24) 102 | .height(24).fontColor(Color.White) 103 | } 104 | .alignItems(VerticalAlign.Top) 105 | .align(Alignment.Top) 106 | .justifyContent(FlexAlign.SpaceBetween) 107 | .margin({ top: ScreenUtils.getStatusBarHeight() }) 108 | .width('100%') 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /entry/src/main/ets/pages/PullToRefreshHeader.ets: -------------------------------------------------------------------------------- 1 | import { PullToRefresh, pull_to_refresh, PullToRefreshIndicatorMode, } from '@candies/pull_to_refresh' 2 | import { ScreenUtils } from '../util/ScreenUtils'; 3 | 4 | @Entry 5 | @Component 6 | struct PullToRefreshHeader { 7 | @State listData: Array = []; 8 | scroller: Scroller = new Scroller(); 9 | @State lastRefreshTime: number = Date.now(); 10 | @State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller(); 11 | firstTime: boolean = true; 12 | 13 | aboutToAppear() { 14 | for (let index = 20; index > 0; index--) { 15 | this.listData.push(index); 16 | } 17 | } 18 | 19 | async onRefresh(): Promise { 20 | if (this.firstTime) { 21 | this.firstTime = false; 22 | return false; 23 | } 24 | let length = this.listData.length; 25 | 26 | this.listData = [length + 3, length + 2, length + 1, ...this.listData]; 27 | this.lastRefreshTime = Date.now(); 28 | // this.listData.splice(0,0,...list.reverse()); 29 | //this.listData.unshift(...list.reverse()); 30 | 31 | return true; 32 | } 33 | 34 | build() { 35 | Navigation() { 36 | PullToRefresh( 37 | { 38 | refreshOffset: 150, 39 | maxDragOffset: 300, 40 | reachToRefreshOffset: 200, 41 | controller: this.controller, 42 | onRefresh: async () => { 43 | return new Promise((resolve) => { 44 | setTimeout(() => { 45 | this.onRefresh().then((value) => resolve(value)); 46 | }, 2000); 47 | }); 48 | }, 49 | onReachEdge: () => { 50 | let yOffset = this.scroller.currentOffset().yOffset; 51 | return Math.abs(yOffset) < 0.001; 52 | } 53 | }) { 54 | PullToRefreshContainer({ 55 | lastRefreshTime: this.lastRefreshTime, 56 | controller: this.controller, 57 | }) 58 | List({ scroller: this.scroller }) { 59 | ForEach(this.listData, (item:number, index) => { 60 | ListItem() { 61 | Text(`${item}`,).align(Alignment.Center) 62 | }.height(100).width('100%') 63 | }, (item:number, index) => { 64 | return `${item}`; 65 | }) 66 | } 67 | // we must do this 68 | .edgeEffect(EdgeEffect.None) 69 | // if we are in pull to refresh gesture, the list should not be scroll 70 | .onScrollFrameBegin((offset, state) => { 71 | if (this.controller.dragOffset > 0) { 72 | offset = 0; 73 | } 74 | return { offsetRemain: offset, }; 75 | }) 76 | } 77 | } 78 | .titleMode(NavigationTitleMode.Mini) 79 | .title('PullToRefreshHeader').margin({ top: ScreenUtils.getStatusBarHeight() }) 80 | 81 | } 82 | } 83 | 84 | @Component 85 | struct PullToRefreshContainer { 86 | @Prop lastRefreshTime: number = 0; 87 | @Link controller: pull_to_refresh.Controller; 88 | 89 | getShowText(): string { 90 | let text = ''; 91 | if (this.controller.mode == PullToRefreshIndicatorMode.armed) { 92 | text = 'Release to refresh'; 93 | } else if (this.controller.mode == PullToRefreshIndicatorMode.refresh || 94 | this.controller.mode == PullToRefreshIndicatorMode.snap) { 95 | text = 'Loading...'; 96 | } else if (this.controller.mode == PullToRefreshIndicatorMode.done) { 97 | text = 'Refresh completed.'; 98 | } else if (this.controller.mode == PullToRefreshIndicatorMode.drag) { 99 | text = 'Pull to refresh'; 100 | } else if (this.controller.mode == PullToRefreshIndicatorMode.canceled) { 101 | text = 'Cancel refresh'; 102 | } else if (this.controller.mode == PullToRefreshIndicatorMode.error) { 103 | text = 'Refresh failed'; 104 | } 105 | return text; 106 | } 107 | 108 | getDate(): String { 109 | return (new Date(this.lastRefreshTime)).toTimeString(); 110 | } 111 | 112 | build() { 113 | Row() { 114 | if (this.controller.dragOffset != 0) 115 | Text(`${this.getShowText()}---${this.getDate()}`) 116 | if (this.controller.dragOffset > 50 && this.controller.mode == PullToRefreshIndicatorMode.refresh) 117 | LoadingProgress().width(50).height(50) 118 | } 119 | .justifyContent(FlexAlign.Center) 120 | .height(this.controller.dragOffset) 121 | .width('100%') 122 | .onClick(() => { 123 | if (this.controller.mode == PullToRefreshIndicatorMode.error) { 124 | this.controller.refresh(); 125 | } 126 | }) 127 | .backgroundColor('#22808080') 128 | } 129 | } 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /entry/src/main/ets/util/ScreenUtils.ets: -------------------------------------------------------------------------------- 1 | import window from '@ohos.window'; 2 | import display from '@ohos.display' 3 | 4 | export class ScreenUtils { 5 | static getStatusBarHeight() { 6 | let statusBarHeight = AppStorage.get('statusBarHeight'); 7 | return statusBarHeight; 8 | } 9 | 10 | static getBottomHeight() { 11 | return AppStorage.get('bottomHeight'); 12 | } 13 | 14 | static async enterImmersion(windowStage: window.WindowStage): Promise { 15 | 16 | let windowClass: window.Window = await windowStage.getMainWindow() 17 | 18 | // 获取状态栏和导航栏的高度 19 | windowClass.on("avoidAreaChange", (options: window.AvoidAreaOptions) => { 20 | if (options.type == window.AvoidAreaType.TYPE_SYSTEM) { 21 | // 将状态栏和导航栏的高度保存在AppStorage中 22 | let defaultDisplay = display.getDefaultDisplaySync(); 23 | AppStorage.setOrCreate("statusBarHeight", options.area.topRect.height / defaultDisplay.densityPixels); 24 | AppStorage.setOrCreate("bottomHeight", options.area.bottomRect.height / defaultDisplay.densityPixels); 25 | } 26 | }) 27 | // 设置窗口布局为沉浸式布局 28 | await windowClass.setWindowLayoutFullScreen(true) 29 | await windowClass.setWindowSystemBarEnable(["status"]) 30 | // 设置状态栏和导航栏的背景为透明 31 | await windowClass.setWindowSystemBarProperties({ 32 | navigationBarColor: "#00000000", 33 | statusBarColor: "#00000000", 34 | navigationBarContentColor: "#FF0000", 35 | statusBarContentColor: "#FF0000" 36 | }) 37 | } 38 | } -------------------------------------------------------------------------------- /entry/src/main/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "entry", 4 | "type": "entry", 5 | "description": "$string:module_desc", 6 | "mainElement": "EntryAbility", 7 | "deviceTypes": [ 8 | "phone", 9 | "tablet" 10 | ], 11 | "deliveryWithInstall": true, 12 | "installationFree": false, 13 | "pages": "$profile:main_pages", 14 | "abilities": [ 15 | { 16 | "name": "EntryAbility", 17 | "srcEntry": "./ets/entryability/EntryAbility.ets", 18 | "description": "$string:EntryAbility_desc", 19 | "icon": "$media:icon", 20 | "label": "$string:EntryAbility_label", 21 | "startWindowIcon": "$media:startIcon", 22 | "startWindowBackground": "$color:start_window_background", 23 | "exported": true, 24 | "skills": [ 25 | { 26 | "entities": [ 27 | "entity.system.home" 28 | ], 29 | "actions": [ 30 | "action.system.home" 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#FFFFFF" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "module description" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "label" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/467141054.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmonyCandies/pull_to_refresh/9f65d4c122697a1baa955fd19371f965cb2a0700/entry/src/main/resources/base/media/467141054.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmonyCandies/pull_to_refresh/9f65d4c122697a1baa955fd19371f965cb2a0700/entry/src/main/resources/base/media/icon.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/startIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmonyCandies/pull_to_refresh/9f65d4c122697a1baa955fd19371f965cb2a0700/entry/src/main/resources/base/media/startIcon.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/profile/main_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": [ 3 | "pages/Index", 4 | "pages/PullToRefreshHeader", 5 | "pages/PullToRefreshAppbar" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /entry/src/main/resources/en_US/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "module description" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "label" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/main/resources/zh_CN/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "模块描述" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "description" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "label" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/test/Ability.test.ets: -------------------------------------------------------------------------------- 1 | import hilog from '@ohos.hilog'; 2 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; 3 | 4 | export default function abilityTest() { 5 | describe('ActsAbilityTest', () => { 6 | // Defines a test suite. Two parameters are supported: test suite name and test suite function. 7 | beforeAll(() => { 8 | // Presets an action, which is performed only once before all test cases of the test suite start. 9 | // This API supports only one parameter: preset action function. 10 | }) 11 | beforeEach(() => { 12 | // Presets an action, which is performed before each unit test case starts. 13 | // The number of execution times is the same as the number of test cases defined by **it**. 14 | // This API supports only one parameter: preset action function. 15 | }) 16 | afterEach(() => { 17 | // Presets a clear action, which is performed after each unit test case ends. 18 | // The number of execution times is the same as the number of test cases defined by **it**. 19 | // This API supports only one parameter: clear action function. 20 | }) 21 | afterAll(() => { 22 | // Presets a clear action, which is performed after all test cases of the test suite end. 23 | // This API supports only one parameter: clear action function. 24 | }) 25 | it('assertContain', 0, () => { 26 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. 27 | hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); 28 | let a = 'abc'; 29 | let b = 'b'; 30 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions. 31 | expect(a).assertContain(b); 32 | expect(a).assertEqual(a); 33 | }) 34 | }) 35 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/test/List.test.ets: -------------------------------------------------------------------------------- 1 | import abilityTest from './Ability.test'; 2 | 3 | export default function testsuite() { 4 | abilityTest(); 5 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/testability/TestAbility.ets: -------------------------------------------------------------------------------- 1 | import UIAbility from '@ohos.app.ability.UIAbility'; 2 | import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; 3 | import hilog from '@ohos.hilog'; 4 | import { Hypium } from '@ohos/hypium'; 5 | import testsuite from '../test/List.test'; 6 | import window from '@ohos.window'; 7 | import Want from '@ohos.app.ability.Want'; 8 | import AbilityConstant from '@ohos.app.ability.AbilityConstant'; 9 | 10 | export default class TestAbility extends UIAbility { 11 | onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { 12 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); 13 | hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); 14 | hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:' + JSON.stringify(launchParam) ?? ''); 15 | let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator; 16 | abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); 17 | let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs; 18 | abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments(); 19 | hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); 20 | Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite); 21 | } 22 | 23 | onDestroy() { 24 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); 25 | } 26 | 27 | onWindowStageCreate(windowStage: window.WindowStage) { 28 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); 29 | windowStage.loadContent('testability/pages/Index', (err, data) => { 30 | if (err.code) { 31 | hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); 32 | return; 33 | } 34 | hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', 35 | JSON.stringify(data) ?? ''); 36 | }); 37 | } 38 | 39 | onWindowStageDestroy() { 40 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); 41 | } 42 | 43 | onForeground() { 44 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); 45 | } 46 | 47 | onBackground() { 48 | hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); 49 | } 50 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/testability/pages/Index.ets: -------------------------------------------------------------------------------- 1 | @Entry 2 | @Component 3 | struct Index { 4 | @State message: string = 'Hello World'; 5 | 6 | build() { 7 | Row() { 8 | Column() { 9 | Text(this.message) 10 | .fontSize(50) 11 | .fontWeight(FontWeight.Bold) 12 | } 13 | .width('100%') 14 | } 15 | .height('100%') 16 | } 17 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts: -------------------------------------------------------------------------------- 1 | import hilog from '@ohos.hilog'; 2 | import TestRunner from '@ohos.application.testRunner'; 3 | import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; 4 | import Want from '@ohos.app.ability.Want'; 5 | 6 | let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator | undefined = undefined 7 | let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs | undefined = undefined 8 | 9 | async function onAbilityCreateCallback() { 10 | hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); 11 | } 12 | 13 | async function addAbilityMonitorCallback(err : Error) { 14 | hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); 15 | } 16 | 17 | export default class OpenHarmonyTestRunner implements TestRunner { 18 | constructor() { 19 | } 20 | 21 | onPrepare() { 22 | hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); 23 | } 24 | 25 | async onRun() { 26 | hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); 27 | abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() 28 | abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() 29 | const bundleName = abilityDelegatorArguments.bundleName; 30 | const testAbilityName = 'TestAbility'; 31 | let lMonitor: AbilityDelegatorRegistry.AbilityMonitor = { 32 | abilityName: testAbilityName, 33 | onAbilityCreate: onAbilityCreateCallback, 34 | }; 35 | abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) 36 | const want: Want = { 37 | bundleName: bundleName, 38 | abilityName: testAbilityName 39 | }; 40 | abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); 41 | abilityDelegator.startAbility(want, (err, data) => { 42 | hilog.info(0x0000, 'testTag', 'startAbility : err : %{public}s', JSON.stringify(err) ?? ''); 43 | hilog.info(0x0000, 'testTag', 'startAbility : data : %{public}s',JSON.stringify(data) ?? ''); 44 | }) 45 | hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); 46 | } 47 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "entry_test", 4 | "type": "feature", 5 | "description": "$string:module_test_desc", 6 | "mainElement": "TestAbility", 7 | "deviceTypes": [ 8 | "phone", 9 | "tablet" 10 | ], 11 | "deliveryWithInstall": true, 12 | "installationFree": false, 13 | "pages": "$profile:test_pages", 14 | "abilities": [ 15 | { 16 | "name": "TestAbility", 17 | "srcEntry": "./ets/testability/TestAbility.ets", 18 | "description": "$string:TestAbility_desc", 19 | "icon": "$media:icon", 20 | "label": "$string:TestAbility_label", 21 | "exported": true, 22 | "startWindowIcon": "$media:icon", 23 | "startWindowBackground": "$color:start_window_background", 24 | "skills": [ 25 | { 26 | "actions": [ 27 | "action.system.home" 28 | ], 29 | "entities": [ 30 | "entity.system.home" 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#FFFFFF" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_test_desc", 5 | "value": "test ability description" 6 | }, 7 | { 8 | "name": "TestAbility_desc", 9 | "value": "the test ability" 10 | }, 11 | { 12 | "name": "TestAbility_label", 13 | "value": "test label" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmonyCandies/pull_to_refresh/9f65d4c122697a1baa955fd19371f965cb2a0700/entry/src/ohosTest/resources/base/media/icon.png -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/profile/test_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": [ 3 | "testability/pages/Index" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /hvigor/hvigor-config.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.0", 3 | "dependencies": { 4 | }, 5 | "execution": { 6 | // "daemon": true, /* Enable daemon compilation. Default: true */ 7 | // "incremental": true, /* Enable incremental compilation. Default: true */ 8 | // "parallel": true, /* Enable parallel compilation. Default: true */ 9 | // "typeCheck": false, /* Enable typeCheck. Default: false */ 10 | }, 11 | "logging": { 12 | // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ 13 | }, 14 | "debugging": { 15 | // "stacktrace": false /* Disable stacktrace compilation. Default: false */ 16 | } 17 | } -------------------------------------------------------------------------------- /hvigorfile.ts: -------------------------------------------------------------------------------- 1 | import { appTasks } from '@ohos/hvigor-ohos-plugin'; 2 | 3 | export default { 4 | system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 5 | plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ 6 | } 7 | -------------------------------------------------------------------------------- /oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.0", 3 | "license": "", 4 | "devDependencies": { 5 | "@ohos/hypium": "1.0.11" 6 | }, 7 | "author": "", 8 | "name": "example", 9 | "description": "Please describe the basic information.", 10 | "main": "", 11 | "version": "1.0.0", 12 | "dependencies": {} 13 | } -------------------------------------------------------------------------------- /pull_to_refresh/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmonyCandies/pull_to_refresh/9f65d4c122697a1baa955fd19371f965cb2a0700/pull_to_refresh/.DS_Store -------------------------------------------------------------------------------- /pull_to_refresh/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /.preview 4 | /build 5 | /.cxx 6 | /.test -------------------------------------------------------------------------------- /pull_to_refresh/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /pull_to_refresh/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pull_to_refresh/.ohpmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /.preview 4 | /build 5 | /.cxx 6 | /.test -------------------------------------------------------------------------------- /pull_to_refresh/BuildProfile.ets: -------------------------------------------------------------------------------- 1 | /** 2 | * Use these variables when you tailor your ArkTS code. They must be of the const type. 3 | */ 4 | export const HAR_VERSION = '1.0.1'; 5 | export const BUILD_MODE_NAME = 'debug'; 6 | export const DEBUG = true; 7 | export const TARGET_NAME = 'default'; 8 | 9 | /** 10 | * BuildProfile Class is used only for compatibility purposes. 11 | */ 12 | export default class BuildProfile { 13 | static readonly HAR_VERSION = HAR_VERSION; 14 | static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; 15 | static readonly DEBUG = DEBUG; 16 | static readonly TARGET_NAME = TARGET_NAME; 17 | } -------------------------------------------------------------------------------- /pull_to_refresh/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.1 2 | 3 | * 优化语法写法,以适配最新的 api12 和 ide,hvigor 5.0.0. 4 | 5 | ## 1.0.0 6 | 7 | * Initial Open Source release. 8 | 9 | -------------------------------------------------------------------------------- /pull_to_refresh/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2023 zmtzawqlp 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /pull_to_refresh/README.md: -------------------------------------------------------------------------------- 1 | # pull_to_refresh 2 | 3 | 快速自定义下拉刷新动画的组件 4 | 5 | - [pull\_to\_refresh](#pull_to_refresh) 6 | - [安装](#安装) 7 | - [参数](#参数) 8 | - [PullToRefreshIndicatorMode](#pulltorefreshindicatormode) 9 | - [配置参数](#配置参数) 10 | - [回调](#回调) 11 | - [onRefresh](#onrefresh) 12 | - [onReachEdge](#onreachedge) 13 | - [使用](#使用) 14 | - [导入引用](#导入引用) 15 | - [定义配置](#定义配置) 16 | - [使用 PullToRefresh](#使用-pulltorefresh) 17 | - [自定义下拉刷新效果](#自定义下拉刷新效果) 18 | 19 | | ![PullToRefreshHeader.gif](https://github.com/HarmonyCandies/HarmonyCandies/blob/main/gif/pull_to_refresh/PullToRefreshHeader.gif) | ![PullToRefreshAppbar.gif](https://github.com/HarmonyCandies/HarmonyCandies/blob/main/gif/pull_to_refresh/PullToRefreshAppbar.gif) | 20 | | --- | --- | 21 | 22 | 23 | ## 安装 24 | 25 | `ohpm install @candies/pull_to_refresh` 26 | 27 | 28 | ## 参数 29 | 30 | ### PullToRefreshIndicatorMode 31 | 32 | ``` typescript 33 | export enum PullToRefreshIndicatorMode { 34 | initial, // 初始状态 35 | drag, // 手势向下拉的状态. 36 | armed, // 被拖动得足够远,以至于触发“onRefresh”回调函数的上滑事件 37 | snap, // 用户没有拖动到足够远的地方并且释放回到初始化状态的过程 38 | refresh, // 正在执行刷新回调. 39 | done, // 刷新回调完成. 40 | canceled, // 用户取消了下拉刷新手势. 41 | error, // 刷新失败 42 | } 43 | ``` 44 | 45 | ### 配置参数 46 | 47 | 48 | | 参数 | 类型 | 描述 | 49 | | --- | --- |--- | 50 | | maxDragOffset | number | 最大拖动距离(非必填) | 51 | | reachToRefreshOffset | number | 到达满足触发刷新的距离(非必填) | 52 | | refreshOffset | number | 触发刷新的时候,停留的刷新距离(非必填) | 53 | | pullBackOnRefresh | boolean | 在触发刷新回调的时候是否执行回退动画(默认 `false`) | 54 | | pullBackAnimatorOptions | AnimatorOptions | 回退动画的一些配置(duration,easing,delay,fill) | 55 | | pullBackOnError | boolean | 刷新失败的时候,是否执行回退动画(默认 `false`) | 56 | 57 | 58 | * `maxDragOffset` 和 `reachToRefreshOffset` 如果不定义的话,会根据当前容器的高度设置默认值。 59 | 60 | 61 | ``` 62 | /// Set the default value of [maxDragOffset,reachToRefreshOffset] 63 | onAreaChange(oldValue: Area, newValue: Area) { 64 | if (this.maxDragOffset == undefined) { 65 | this.maxDragOffset = (newValue.height as number) / 5; 66 | } 67 | if (this.reachToRefreshOffset == undefined) { 68 | this.reachToRefreshOffset = this.maxDragOffset * 3 / 4; 69 | } 70 | else { 71 | this.reachToRefreshOffset = Math.min(this.reachToRefreshOffset, this.maxDragOffset); 72 | } 73 | } 74 | ``` 75 | 76 | * `pullBackAnimatorOptions` 的默认值如下: 77 | 78 | ``` typescript 79 | /// The options of pull back animation 80 | pullBackAnimatorOptions: AnimatorOptions = { 81 | duration: 400, 82 | easing: "friction", 83 | delay: 0, 84 | fill: "forwards", 85 | direction: "normal", 86 | iterations: 1, 87 | begin: 1.0, 88 | end: 0.0 89 | }; 90 | ``` 91 | 92 | ### 回调 93 | 94 | #### onRefresh 95 | 96 | 触发的下拉刷新事件 97 | ``` typescript 98 | /// A function that's called when the user has dragged the refresh indicator 99 | /// far enough to demonstrate that they want the app to refresh. The returned 100 | /// [Future] must complete when the refresh operation is finished. 101 | 102 | onRefresh: RefreshCallback = async () => true; 103 | ``` 104 | #### onReachEdge 105 | 106 | 是否我们到达了下拉刷新的边界,比如说,下拉刷新的内容是一个列表,那么边界就是到达列表的顶部位置。 107 | ``` typescript 108 | /// Whether we reach the edge to pull refresh 109 | onReachEdge: () => boolean = () => true; 110 | ``` 111 | 112 | ## 使用 113 | 114 | ### 导入引用 115 | ``` typescript 116 | import { 117 | PullToRefresh, 118 | pull_to_refresh, 119 | PullToRefreshIndicatorMode, 120 | } from '@candies/pull_to_refresh' 121 | ``` 122 | 123 | ### 定义配置 124 | 125 | ``` typescript 126 | @State controller: pull_to_refresh.Controller = new pull_to_refresh.Controller(); 127 | ``` 128 | 129 | ### 使用 PullToRefresh 130 | 131 | 将需要支持下拉刷新的部分,通过 `@BuilderParam` 修饰的 `builder` 回调传入,或者尾随闭包初始化组件。 132 | 133 | [@BuilderParam装饰器:引用@Builder函数-快速入门-入门-HarmonyOS应用开发](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/arkts-builderparam-0000001524416541-V3) 134 | 135 | ``` typescript 136 | 137 | PullToRefresh( 138 | { 139 | refreshOffset: 150, 140 | maxDragOffset: 300, 141 | reachToRefreshOffset: 200, 142 | controller: this.controller, 143 | onRefresh: async () => { 144 | return new Promise((resolve) => { 145 | setTimeout(() => { 146 | // 定义的刷新方法,当刷新成功之后,返回回调,模拟 2 秒之后刷新完毕 147 | this.onRefresh().then((value) => resolve(value)); 148 | }, 2000); 149 | }); 150 | }, 151 | onReachEdge: () => { 152 | let yOffset = this.scroller.currentOffset().yOffset; 153 | return Math.abs(yOffset) < 0.001; 154 | } 155 | }) { 156 | // 我们自定义的下拉刷新头部 157 | PullToRefreshContainer({ 158 | lastRefreshTime: this.lastRefreshTime, 159 | controller: this.controller, 160 | }) 161 | List({ scroller: this.scroller }) { 162 | ForEach(this.listData, (item, index) => { 163 | ListItem() { 164 | Text(`${item}`,).align(Alignment.Center) 165 | }.height(100).width('100%') 166 | }, (item, index) => { 167 | return `${item}`; 168 | }) 169 | } 170 | // 必须设置 edgeEffect 171 | .edgeEffect(EdgeEffect.None) 172 | // 为了使下拉刷新的手势的过程中,不触发列表的滚动 173 | .onScrollFrameBegin((offset, state) => { 174 | if (this.controller.dragOffset > 0) { 175 | offset = 0; 176 | } 177 | return { offsetRemain: offset, }; 178 | }) 179 | } 180 | } 181 | ``` 182 | 183 | ### 自定义下拉刷新效果 184 | 185 | 你可以通过对 `Controller` 中 `dragOffset` 和 `mode` 的判断,创建属于自己的下拉刷新效果。如果下拉刷新失败了,你可以通过调用 `Controller` 的 `refresh() `方法来重新执行刷新动画。 186 | 187 | ``` typescript 188 | /// The current drag offset 189 | dragOffset: number = 0; 190 | /// The current pull mode 191 | mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial; 192 | ``` 193 | 194 | 下面是一个自定义下拉刷新头部的例子 195 | 196 | ``` typescript 197 | @Component 198 | struct PullToRefreshContainer { 199 | @Prop lastRefreshTime: number = 0; 200 | @Link controller: pull_to_refresh.Controller; 201 | 202 | getShowText(): string { 203 | let text = ''; 204 | if (this.controller.mode == PullToRefreshIndicatorMode.armed) { 205 | text = 'Release to refresh'; 206 | } else if (this.controller.mode == PullToRefreshIndicatorMode.refresh || 207 | this.controller.mode == PullToRefreshIndicatorMode.snap) { 208 | text = 'Loading...'; 209 | } else if (this.controller.mode == PullToRefreshIndicatorMode.done) { 210 | text = 'Refresh completed.'; 211 | } else if (this.controller.mode == PullToRefreshIndicatorMode.drag) { 212 | text = 'Pull to refresh'; 213 | } else if (this.controller.mode == PullToRefreshIndicatorMode.canceled) { 214 | text = 'Cancel refresh'; 215 | } else if (this.controller.mode == PullToRefreshIndicatorMode.error) { 216 | text = 'Refresh failed'; 217 | } 218 | return text; 219 | } 220 | 221 | getDate(): String { 222 | return (new Date(this.lastRefreshTime)).toTimeString(); 223 | } 224 | 225 | build() { 226 | Row() { 227 | if (this.controller.dragOffset != 0) 228 | Text(`${this.getShowText()}---${this.getDate()}`) 229 | if (this.controller.dragOffset > 50 && this.controller.mode == PullToRefreshIndicatorMode.refresh) 230 | LoadingProgress().width(50).height(50) 231 | } 232 | .justifyContent(FlexAlign.Center) 233 | .height(this.controller.dragOffset) 234 | .width('100%') 235 | .onClick(() => { 236 | if (this.controller.mode == PullToRefreshIndicatorMode.error) { 237 | this.controller.refresh(); 238 | } 239 | }) 240 | .backgroundColor('#22808080') 241 | } 242 | } 243 | ``` 244 | 245 | -------------------------------------------------------------------------------- /pull_to_refresh/build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "apiType": "stageMode", 3 | "buildOption": { 4 | }, 5 | "targets": [ 6 | { 7 | "name": "default", 8 | "runtimeOS": "HarmonyOS" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /pull_to_refresh/example/example.ets: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarmonyCandies/pull_to_refresh/9f65d4c122697a1baa955fd19371f965cb2a0700/pull_to_refresh/example/example.ets -------------------------------------------------------------------------------- /pull_to_refresh/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | import { harTasks } from '@ohos/hvigor-ohos-plugin'; 2 | 3 | export default { 4 | system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 5 | plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ 6 | } 7 | -------------------------------------------------------------------------------- /pull_to_refresh/index.ets: -------------------------------------------------------------------------------- 1 | 2 | export { PullToRefresh } from './src/main/ets/components/PullToRefresh' 3 | export { PullToRefreshIndicatorMode, RefreshCallback } from './src/main/ets/common/RefreshConstants' 4 | export { default as pull_to_refresh } from './src/main/ets/common/Controller' 5 | 6 | -------------------------------------------------------------------------------- /pull_to_refresh/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "license": "Apache-2.0", 3 | "devDependencies": {}, 4 | "keywords": [ 5 | "pull", 6 | "refresh", 7 | "pulltorefresh" 8 | ], 9 | "author": "zmtzawqlp", 10 | "name": "@candies/pull_to_refresh", 11 | "description": "Harmony plugin for building pull to refresh effects with PullToRefresh quickly.", 12 | "main": "index.ets", 13 | "repository": "https://github.com/HarmonyCandies/pull_to_refresh", 14 | "version": "1.0.1", 15 | "homepage": "https://github.com/HarmonyCandies/pull_to_refresh", 16 | "dependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /pull_to_refresh/src/main/ets/common/Controller.ets: -------------------------------------------------------------------------------- 1 | import { PullToRefreshIndicatorMode, RefreshCallback } from './RefreshConstants'; 2 | import animator, { AnimatorOptions, AnimatorResult } from '@ohos.animator'; 3 | 4 | 5 | namespace pull_to_refresh { 6 | export class Controller { 7 | /// The current drag offset 8 | dragOffset: number = 0; 9 | /// The current pull mode 10 | mode: PullToRefreshIndicatorMode = PullToRefreshIndicatorMode.initial; 11 | /// internal 12 | onRefresh?: () => void | undefined = undefined; 13 | 14 | refresh() { 15 | if (this.onRefresh != undefined) { 16 | this.onRefresh(); 17 | } 18 | } 19 | } 20 | } 21 | 22 | export default pull_to_refresh; -------------------------------------------------------------------------------- /pull_to_refresh/src/main/ets/common/RefreshConstants.ets: -------------------------------------------------------------------------------- 1 | 2 | 3 | // The state machine moves through these modes only when the scrollable 4 | // identified by scrollableKey has been scrolled to its min or max limit. 5 | export enum PullToRefreshIndicatorMode { 6 | initial, // initial state 7 | drag, // Pointer is down. 8 | armed, // Dragged far enough that an up event will run the onRefresh callback. 9 | snap, // Animating to the indicator's final "displacement". 10 | refresh, // Running the refresh callback. 11 | done, // Animating the indicator's fade-out after refreshing. 12 | canceled, // Animating the indicator's fade-out after not arming. 13 | error, //refresh failed 14 | } 15 | 16 | /// The signature for a function that's called when the user has dragged a 17 | /// [PullToRefresh] far enough to demonstrate that they want the app to 18 | /// refresh. The returned [Future] must complete when the refresh operation is 19 | /// finished. 20 | /// 21 | /// Used by [PullToRefresh.onRefresh]. 22 | export type RefreshCallback = ()=> Promise; 23 | -------------------------------------------------------------------------------- /pull_to_refresh/src/main/ets/components/PullToRefresh.ets: -------------------------------------------------------------------------------- 1 | import { PullToRefreshIndicatorMode, RefreshCallback } from '../common/RefreshConstants'; 2 | 3 | import pull_to_refresh from '../common/Controller'; 4 | import animator, { AnimatorOptions, AnimatorResult, } from '@ohos.animator'; 5 | 6 | @Component 7 | export struct PullToRefresh { 8 | /// The children 9 | @BuilderParam 10 | builder: () => void; 11 | /// A function that's called when the user has dragged the refresh indicator 12 | /// far enough to demonstrate that they want the app to refresh. The returned 13 | /// [Future] must complete when the refresh operation is finished. 14 | onRefresh: RefreshCallback = async () => true; 15 | /// Whether we reach the edge to pull refresh 16 | onReachEdge: () => boolean = () => true; 17 | @Link controller: pull_to_refresh.Controller; 18 | /// The max drag offset 19 | @Prop maxDragOffset: number | undefined = undefined; 20 | /// The offset to be dragged far enough that an up event will run the onRefresh callback. 21 | @Prop reachToRefreshOffset: number | undefined = undefined; 22 | /// The offset to keep when it is refreshing 23 | @Prop refreshOffset: number | undefined = undefined; 24 | /// Whether start pull back animation when refresh. 25 | @Prop pullBackOnRefresh: boolean = false; 26 | /// The options of pull back animation 27 | @Prop pullBackAnimatorOptions: AnimatorOptions = { 28 | duration: 400, 29 | easing: "friction", 30 | delay: 0, 31 | fill: "forwards", 32 | direction: "normal", 33 | iterations: 1, 34 | begin: 1.0, 35 | end: 0.0 36 | }; 37 | /// Whether start pull back animation when refresh failed. 38 | @Prop pullBackOnError: boolean = false; 39 | /// The animatorResult 40 | /// 41 | animatorResult: AnimatorResult | undefined = undefined; 42 | 43 | aboutToAppear() { 44 | this.controller.onRefresh = () => { 45 | this.refresh(); 46 | }; 47 | } 48 | 49 | aboutToDisappear() { 50 | if (this.animatorResult != undefined) { 51 | this.animatorResult.finish(); 52 | this.animatorResult = undefined; 53 | } 54 | } 55 | 56 | build() { 57 | Column() { 58 | this.builder() 59 | }.onAreaChange((oldValue: Area, newValue: Area) => { 60 | if (this.maxDragOffset == undefined) { 61 | this.maxDragOffset = (newValue.height as number) / 5; 62 | } 63 | if (this.reachToRefreshOffset == undefined) { 64 | this.reachToRefreshOffset = this.maxDragOffset * 3 / 4; 65 | } else { 66 | this.reachToRefreshOffset = Math.min(this.reachToRefreshOffset, this.maxDragOffset); 67 | } 68 | }) 69 | .parallelGesture(PanGesture({ 70 | direction: PanDirection.Up | PanDirection.Down 71 | }) 72 | .onActionStart((event?: GestureEvent) => { 73 | if (this.controller.mode == PullToRefreshIndicatorMode.initial) { 74 | this.onInnerNoticed(PullToRefreshIndicatorMode.drag, 0); 75 | } 76 | }) 77 | .onActionUpdate(async (event?: GestureEvent) => { 78 | if (!event || this.animatorResult != undefined || this.controller.mode == PullToRefreshIndicatorMode.error) { 79 | return; 80 | } 81 | let offsetY = event.offsetY; 82 | if (offsetY > 0 && this.onReachEdge()) { 83 | offsetY = Math.min(this.maxDragOffset, offsetY); 84 | if (this.controller.dragOffset == offsetY) { 85 | return; 86 | } 87 | this.onInnerNoticed(offsetY >= this.reachToRefreshOffset! ? PullToRefreshIndicatorMode.armed : 88 | PullToRefreshIndicatorMode.drag, offsetY); 89 | } 90 | }).onActionEnd(async (event?: GestureEvent) => { 91 | if (!event || this.controller.mode == PullToRefreshIndicatorMode.error) { 92 | return; 93 | } 94 | let offsetY = this.controller.dragOffset; 95 | let isReadyToRefresh = offsetY > this.reachToRefreshOffset!; 96 | if (!isReadyToRefresh) { 97 | this.pullBack(offsetY, 0, this.pullBackAnimatorOptions.duration, () => PullToRefreshIndicatorMode.drag); 98 | return; 99 | } 100 | 101 | if (this.refreshOffset != undefined && isReadyToRefresh) { 102 | let refreshOffset = Math.min(this.refreshOffset, this.reachToRefreshOffset); 103 | await this.pullBack(offsetY, refreshOffset, 104 | this.pullBackAnimatorOptions.duration * (offsetY - refreshOffset) / offsetY, 105 | () => PullToRefreshIndicatorMode.snap); 106 | } 107 | this.refresh(); 108 | }).onActionCancel((event?: GestureEvent) => { 109 | if (!event) { 110 | return; 111 | } 112 | 113 | this.onInnerNoticed(PullToRefreshIndicatorMode.initial, 0); 114 | if (this.animatorResult != undefined) { 115 | this.animatorResult.finish(); 116 | this.animatorResult = undefined; 117 | } 118 | })) 119 | } 120 | 121 | private onInnerNoticed(mode: PullToRefreshIndicatorMode, dragOffset: number) { 122 | this.controller.mode = mode; 123 | this.controller.dragOffset = dragOffset; 124 | } 125 | 126 | refresh() { 127 | this.onInnerNoticed(PullToRefreshIndicatorMode.refresh, this.controller.dragOffset); 128 | if (this.pullBackOnRefresh) { 129 | let mode = PullToRefreshIndicatorMode.refresh; 130 | this.pullBack(this.controller.dragOffset, 0, this.pullBackAnimatorOptions.duration, () => mode); 131 | this.onRefresh().then((value) => { 132 | if (value) { 133 | mode = PullToRefreshIndicatorMode.done; 134 | } else { 135 | mode = PullToRefreshIndicatorMode.error; 136 | } 137 | if (this.controller.mode != mode) { 138 | this.onInnerNoticed(mode, this.controller.dragOffset); 139 | } 140 | }); 141 | } else { 142 | this.onRefresh().then((value) => { 143 | if (value) { 144 | this.pullBack(this.controller.dragOffset, 0, this.pullBackAnimatorOptions.duration, 145 | () => PullToRefreshIndicatorMode.done); 146 | } else { 147 | if (this.pullBackOnError) { 148 | this.pullBack(this.controller.dragOffset, 0, this.pullBackAnimatorOptions.duration, 149 | () => PullToRefreshIndicatorMode.error); 150 | } else { 151 | this.onInnerNoticed(PullToRefreshIndicatorMode.error, this.controller.dragOffset); 152 | } 153 | 154 | } 155 | }); 156 | } 157 | } 158 | 159 | private pullBack(begin: number, end: number, duration: number, 160 | getCurrentMode: () => PullToRefreshIndicatorMode): Promise { 161 | let animatorOptions: AnimatorOptions = { 162 | duration: duration, 163 | easing: this.pullBackAnimatorOptions.easing, 164 | delay: this.pullBackAnimatorOptions.delay, 165 | fill: this.pullBackAnimatorOptions.fill, 166 | direction: "normal", 167 | iterations: 1, 168 | begin: begin, 169 | end: end 170 | }; 171 | 172 | this.animatorResult = animator.create(animatorOptions); 173 | this.animatorResult.onframe = (progress 174 | ) => { 175 | let dragOffset = progress; 176 | this.onInnerNoticed(getCurrentMode(), dragOffset); 177 | } 178 | this.animatorResult.play(); 179 | 180 | return new Promise((resolve) => { 181 | if (this.animatorResult != null) { 182 | this.animatorResult!.onfinish = () => { 183 | if (end == 0) { 184 | this.onInnerNoticed(PullToRefreshIndicatorMode.initial, 0); 185 | } 186 | this.animatorResult = undefined; 187 | resolve(); 188 | }; 189 | } 190 | }); 191 | } 192 | } 193 | 194 | -------------------------------------------------------------------------------- /pull_to_refresh/src/main/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "pull_to_refresh", 4 | "type": "har", 5 | "deviceTypes": [ 6 | "default", 7 | "tablet" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pull_to_refresh/src/main/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "page_show", 5 | "value": "page from package" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /pull_to_refresh/src/main/resources/en_US/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "page_show", 5 | "value": "page from package" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /pull_to_refresh/src/main/resources/zh_CN/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "page_show", 5 | "value": "page from package" 6 | } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------