├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nxg │ │ └── app │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nxg │ │ │ └── app │ │ │ ├── MainActivity.kt │ │ │ ├── main │ │ │ ├── AppApplication.kt │ │ │ ├── DoubleColorBallAdapter.kt │ │ │ ├── DoubleColorBallItemDecoration.kt │ │ │ ├── LoopScrollViewAdapter.kt │ │ │ ├── LoopScrollViewItemBean.kt │ │ │ ├── MainFragment.kt │ │ │ └── MainViewModel.kt │ │ │ └── utils │ │ │ └── AppUtils.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_app.jpeg │ │ ├── ic_launcher_background.xml │ │ ├── ic_lucky.png │ │ ├── loop_scroll_view_shape_item.xml │ │ ├── shape_double_color_ball_blue.xml │ │ ├── shape_double_color_ball_lottery.xml │ │ ├── shape_double_color_ball_lottery_normal.xml │ │ ├── shape_double_color_ball_lottery_pressed.xml │ │ └── shape_double_color_ball_red.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── double_color_ball_item.xml │ │ ├── loop_scroll_view_item_text.xml │ │ ├── loop_scroll_view_item_text_blue.xml │ │ ├── loop_scroll_view_item_text_red.xml │ │ └── main_fragment.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── nxg │ └── app │ └── ExampleUnitTest.kt ├── build.gradle ├── demo.gif ├── demo.jks ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── keystore.properties ├── recyclerviewloopscrollanimation ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nxg │ │ └── loopscrollanimation │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── nxg │ │ └── loopscrollanimation │ │ ├── RecyclerViewLoopScrollAnimation.kt │ │ └── utils │ │ ├── LogUtil.java │ │ └── ReflectUtils.java │ └── test │ └── java │ └── com │ └── nxg │ └── loopscrollanimation │ └── ExampleUnitTest.kt └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | 26 | - name: Build with Gradle 27 | run: | 28 | export storePassword=${{ secrets.STOREPASSWORD }} 29 | export keyAlias=${{ secrets.KEYALIAS }} 30 | export keyPassword=${{ secrets.KEYPASSWORD }} 31 | ./gradlew app:assembleRelease 32 | 33 | 34 | - name: Upload apk to pgyer.com 35 | run: find ${{ github.workspace }}/app/build/outputs/apk/release/ -name "*.apk*" -type f -exec curl -F "file=@{}" -F "uKey=${{ secrets.PGYER_UKEY }}" -F "_api_key=${{ secrets.PGYER_API_KEY }}" https://upload.pgyer.com/apiv1/app/upload \; 36 | 37 | - name: Upload apk to artifact 38 | uses: actions/upload-artifact@master 39 | if: always() 40 | with: 41 | name: lottery_app 42 | path: ${{ github.workspace }}/app/build/outputs/apk/release/*.apk 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | \.idea/ 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1.RecyclerViewLoopScrollAnimation项目介绍 2 | > 🍎 `RecyclerViewLoopScrollAnimation` 适用于Android RecyclerView的循环滚动动画的帮助类,可实现类似于老虎机抽奖,数字滚动等效果。 3 | 4 | [![](https://jitpack.io/v/xiangang/RecyclerViewLoopScrollAnimation.svg)](https://jitpack.io/#xiangang/RecyclerViewLoopScrollAnimation) [![Android CI](https://github.com/xiangang/RecyclerViewLoopScrollAnimation/actions/workflows/android.yml/badge.svg?branch=main)](https://github.com/xiangang/RecyclerViewLoopScrollAnimation/actions/workflows/android.yml) 5 | 6 | # 2. 效果展示 7 | 8 | git太大,压缩的厉害,导致帧率太低,建议下载工程运行查看实际效果。 9 | 10 | ![演示图](https://github.com/xiangang/RecyclerViewLoopScrollAnimation/blob/main/demo.gif) 11 | 12 | # 3. 使用方法: 13 | 14 | 第一步: 15 | 在你的根目录中的 build.gradle 文件中,repositories 标签下添加jitpack maven仓库: 16 | 17 | Add it in your root build.gradle at the end of repositories: 18 | 19 | ``` 20 | allprojects { 21 | repositories { 22 | ... 23 | maven { url 'https://jitpack.io' } 24 | } 25 | } 26 | 27 | ``` 28 | 若使用Gradle 7.0 ,则在setting.gradle中的dependencyResolutionManagement的repositories标签中添加: 29 | ``` 30 | dependencyResolutionManagement { 31 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 32 | repositories { 33 | ... 34 | maven { url 'https://jitpack.io' } 35 | ... 36 | } 37 | } 38 | ... 39 | 40 | ``` 41 | 42 | 第二步: 43 | 添加依赖 44 | Add the dependency 45 | 46 | ``` 47 | dependencies { 48 | implementation 'com.github.xiangang:RecyclerViewLoopScrollAnimation:v1.0.0-alpha02' 49 | } 50 | 51 | ``` 52 | 53 | # 4. 使用示例 54 | 使用实例如下: 55 | ```kotlin 56 | //创建一个RecyclerViewLoopScrollAnimation实例 57 | val recyclerViewLoopScrollAnimation = RecyclerViewLoopScrollAnimation() 58 | //创建一个Configuration 59 | val recyclerViewLoopScrollAnimationConfiguration = RecyclerViewLoopScrollAnimation.build { 60 | scrollAnimatorDuration = 4000L 61 | } 62 | //设置Configuration 63 | recyclerViewLoopScrollAnimation.setConfiguration(recyclerViewLoopScrollAnimationConfiguration) 64 | //将recyclerViewLoopScrollAnimation关联到RecyclerView 65 | recyclerViewLoopScrollAnimation.attachToRecyclerView(holder.recyclerView) 66 | //配合LooperLinearLayoutManager实现无限滚动 67 | val looperLinearLayoutManager = LooperLinearLayoutManager(context, RecyclerView.VERTICAL, false) 68 | recyclerView.layoutManager = looperLinearLayoutManager 69 | //设置滚动Action,用于滚动时返回当前显示View的Position 70 | recyclerViewLoopScrollAnimation.setRecyclerViewScrollAction(object:RecyclerViewScrollAction { 71 | override fun findFirstVisibleItemPosition(): Int { 72 | return looperLinearLayoutManager.findFirstVisibleItemPosition() 73 | } 74 | 75 | override fun findLastVisibleItemPosition(): Int { 76 | return looperLinearLayoutManager.findLastVisibleItemPosition() 77 | } 78 | }) 79 | //开始动画 80 | recyclerViewLoopScrollAnimation.start() 81 | ``` 82 | 83 | 更多用法可参考工程里的app。 84 | 85 | # 5. 其它 86 | 87 | RecyclerViewLoopScrollAnimation.Configuration配置类: 88 | 89 | ```kotlin 90 | /** 91 | * 配置 92 | */ 93 | class Configuration { 94 | /** 95 | * 是否禁止Item相应手指滑动 96 | */ 97 | var disableItemTouchListener = true 98 | 99 | /** 100 | * 第一阶段的滚动动画时长 101 | */ 102 | var scrollAnimatorDuration = 3000L 103 | 104 | /** 105 | * 第一阶段的滚动动画的插值器 106 | */ 107 | var scrollAnimatorInterpolator = AccelerateDecelerateInterpolator() 108 | 109 | /** 110 | * 是否开启弹簧动画 111 | */ 112 | var enableSpringAnimator = true 113 | 114 | /** 115 | * 第一阶段的滚动动画针对item的滚动步长(scrollBy方法的y参数) 116 | */ 117 | var scrollAnimatorScrollByYStep = 30 118 | 119 | /** 120 | * 第二阶段的弹簧动画的SpringForce 121 | */ 122 | var springAnimatorForce: SpringForce = SpringForce(0f) 123 | .setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY) 124 | .setStiffness(SpringForce.STIFFNESS_VERY_LOW) 125 | .setDampingRatio(0.2f) 126 | .setStiffness(350f) 127 | 128 | /** 129 | * 弹簧动画的起始位置 130 | */ 131 | var springAnimatorStartValue = -50f 132 | 133 | } 134 | ``` 135 | 136 | RecyclerViewLoopScrollAnimation.build的用法为[Function literals with receiver](https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver): 137 | 138 | ```kotlin 139 | /** 140 | * 配置 141 | */ 142 | //创建一个RecyclerViewLoopScrollAnimation实例 143 | val recyclerViewLoopScrollAnimation = RecyclerViewLoopScrollAnimation() 144 | //创建一个Configuration 145 | val recyclerViewLoopScrollAnimationConfiguration = RecyclerViewLoopScrollAnimation.build { 146 | scrollAnimatorDuration = 4000L 147 | } 148 | //设置Configuration 149 | recyclerViewLoopScrollAnimation.setConfiguration(recyclerViewLoopScrollAnimationConfiguration) 150 | 151 | ``` 152 | 153 | 扩展阅读:[你会用Kotlin实现构建者模式吗?](https://zhuanlan.zhihu.com/p/267145868) 154 | 155 | 156 | 157 | 其它接口: 158 | 159 | ``` 160 | /** 161 | * RecyclerView滚动过程中需要执行的Action 162 | */ 163 | fun setRecyclerViewScrollAction(recyclerViewScrollAction: RecyclerViewScrollAction) { 164 | this.recyclerViewScrollAction = recyclerViewScrollAction 165 | } 166 | 167 | /** 168 | * 设置滚动动画监听器 169 | */ 170 | fun setOnSpringAnimationEndListener(onScrollAnimatorListener: Animator.AnimatorListener) { 171 | this.onScrollAnimatorListener = onScrollAnimatorListener 172 | } 173 | 174 | /** 175 | * 设置弹簧动画结束监听器 176 | * 可用于动画结束后执行爆炸粒子效果 177 | */ 178 | fun setOnSpringAnimationEndListener(onAnimationEndListener: DynamicAnimation.OnAnimationEndListener) { 179 | this.onSpringAnimationEndListener = onAnimationEndListener 180 | } 181 | ``` 182 | 183 | 184 | 185 | # License 186 | 187 | ```text 188 | Copyright 2022 xiangang 189 | 190 | Licensed under the Apache License, Version 2.0 (the "License"); 191 | you may not use this file except in compliance with the License. 192 | You may obtain a copy of the License at 193 | 194 | http://www.apache.org/licenses/LICENSE-2.0 195 | 196 | Unless required by applicable law or agreed to in writing, software 197 | distributed under the License is distributed on an "AS IS" BASIS, 198 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 199 | See the License for the specific language governing permissions and 200 | limitations under the License. 201 | ``` 202 | 203 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | static def getAppReleaseTime() { 7 | return new Date().format("yyyyMMdd_HHmm", TimeZone.getTimeZone("Asia/Shanghai")) 8 | } 9 | 10 | // Remove private signing information from your project 11 | // 创建一个名为keystorePropertiesFile的变量,并将其初始化为rootProject文件夹中的keystore.properties文件。 12 | def keystorePropertiesFile = rootProject.file("keystore.properties") 13 | // 初始化一个名为keystoreProperties的新Properties()对象 14 | def keystoreProperties = new Properties() 15 | // 将keystore.properties文件加载到keystoreProperties对象中 16 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 17 | println "keystoreProperties->$keystoreProperties" 18 | 19 | def getStoreFile = { 20 | def storeFile = keystoreProperties['storeFile'] 21 | if (storeFile == null || storeFile.isEmpty()) { 22 | storeFile = '../demo.jks' 23 | } 24 | return storeFile 25 | } 26 | 27 | def getStorePassword = { 28 | def storePassword = keystoreProperties['storePassword'] 29 | if (storePassword == null || storePassword.isEmpty()) { 30 | storePassword = System.getenv("storePassword") 31 | } 32 | return storePassword 33 | } 34 | 35 | def getKeyAlias = { 36 | def keyAlias = keystoreProperties['keyAlias'] 37 | if (keyAlias == null || keyAlias.isEmpty()) { 38 | keyAlias = System.getenv("keyAlias") 39 | } 40 | return keyAlias 41 | } 42 | 43 | def getKeyPassword = { 44 | def keyPassword = keystoreProperties['keyPassword'] 45 | if (keyPassword == null || keyPassword.isEmpty()) { 46 | keyPassword = System.getenv("keyPassword") 47 | } 48 | return keyPassword 49 | } 50 | 51 | println "storePassword->${System.getenv("storePassword")}" 52 | println "keyAlias->${System.getenv("keyAlias")}" 53 | println "keyPassword->${System.getenv("keyPassword")}" 54 | 55 | println "getStoreFile->${getStoreFile()}" 56 | println "getStorePassword->${getStorePassword()}" 57 | println "getKeyAlias->${getKeyAlias()}" 58 | println "getKeyPassword->${getKeyPassword()}" 59 | 60 | android { 61 | compileSdk 31 62 | 63 | defaultConfig { 64 | applicationId "com.nxg.app" 65 | minSdk 21 66 | targetSdk 31 67 | versionCode 1 68 | versionName "1.0.0" 69 | 70 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 71 | } 72 | 73 | signingConfigs { 74 | 75 | release { 76 | storeFile file(getStoreFile()) 77 | storePassword getStorePassword() 78 | keyAlias getKeyAlias() 79 | keyPassword getKeyPassword() 80 | } 81 | } 82 | 83 | buildTypes { 84 | debug { 85 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 86 | manifestPlaceholders = [RELEASE_TIME: getAppReleaseTime()] 87 | multiDexEnabled = true 88 | } 89 | 90 | release { 91 | minifyEnabled true 92 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 93 | shrinkResources true //是否清理无用资源,依赖于minifyEnabled 94 | zipAlignEnabled true //是否启用zipAlign压缩 95 | signingConfig signingConfigs.release 96 | manifestPlaceholders = [RELEASE_TIME: getAppReleaseTime()] 97 | multiDexEnabled = true 98 | versionNameSuffix = '' 99 | // 自定义apk名称 100 | applicationVariants.all { variant -> 101 | variant.outputs.all { output -> 102 | def fileName = "lucky_cube_app_release_${variant.versionName}_${appReleaseTime}.apk" 103 | def outFile = output.outputFile 104 | if (outFile != null && outFile.name.endsWith('.apk')) { 105 | outputFileName = fileName 106 | } 107 | } 108 | } 109 | } 110 | 111 | } 112 | 113 | compileOptions { 114 | sourceCompatibility JavaVersion.VERSION_1_8 115 | targetCompatibility JavaVersion.VERSION_1_8 116 | } 117 | 118 | kotlinOptions { 119 | jvmTarget = '1.8' 120 | } 121 | 122 | buildFeatures { 123 | viewBinding true 124 | } 125 | } 126 | 127 | dependencies { 128 | 129 | implementation 'androidx.core:core-ktx:1.7.0' 130 | implementation 'androidx.appcompat:appcompat:1.4.0' 131 | implementation 'com.google.android.material:material:1.4.0' 132 | implementation 'androidx.constraintlayout:constraintlayout:2.1.2' 133 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 134 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' 135 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' 136 | implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' 137 | implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' 138 | implementation project(path: ':recyclerviewloopscrollanimation') 139 | testImplementation 'junit:junit:4.13.2' 140 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 141 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 142 | implementation 'io.coil-kt:coil:1.4.0' 143 | implementation 'com.github.xiangang:LooperLinearLayoutManager:v1.0.0-alpha01' 144 | 145 | } 146 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class com.nxg.recyclerview.widget.** {*;} 24 | -keep class androidx.recyclerview.widget.** {*;} -------------------------------------------------------------------------------- /app/src/androidTest/java/com/nxg/app/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.nxg.app", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import androidx.navigation.fragment.NavHostFragment 6 | import com.nxg.app.databinding.ActivityMainBinding 7 | 8 | class MainActivity : AppCompatActivity() { 9 | private lateinit var binding: ActivityMainBinding 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | binding = ActivityMainBinding.inflate(layoutInflater) 15 | setContentView(binding.root) 16 | /** 17 | * 这种方式适用于使用fragment控件 18 | */ 19 | //val navController = findNavController(R.id.nav_host_fragment_activity_main) 20 | /** 21 | * 使用 FragmentContainerView 创建 NavHostFragment,或通过 FragmentTransaction 手动将 NavHostFragment 添加到您的 activity 时, 22 | * 尝试通过 Navigation.findNavController(Activity, @IdRes int) 检索 activity 的 onCreate() 中的 NavController 将失败。 23 | * 您应改为直接从 NavHostFragment 检索 NavController。 24 | */ 25 | val navHostFragment = 26 | supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment 27 | val navController = navHostFragment.navController 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/main/AppApplication.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.main 2 | 3 | import android.app.Application 4 | import com.nxg.recyclerview.utils.LogUtil 5 | import kotlin.properties.ReadWriteProperty 6 | import kotlin.reflect.KProperty 7 | 8 | class AppApplication : Application() { 9 | 10 | companion object { 11 | const val TAG = "AppApplication" 12 | private var instance: AppApplication by NotNullSingleValueVar() 13 | fun instance() = instance 14 | } 15 | 16 | //定义一个属性管理类,进行非空和重复赋值的判断 17 | private class NotNullSingleValueVar : ReadWriteProperty { 18 | private var value: T? = null 19 | override fun getValue(thisRef: Any?, property: KProperty<*>): T { 20 | return value ?: throw IllegalStateException("application not initialized") 21 | } 22 | 23 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { 24 | this.value = if (this.value == null) value 25 | else throw IllegalStateException("application already initialized") 26 | } 27 | } 28 | 29 | 30 | override fun onCreate() { 31 | super.onCreate() 32 | instance = this 33 | LogUtil.i(TAG, "onCreate ") 34 | } 35 | 36 | override fun onLowMemory() { 37 | super.onLowMemory() 38 | LogUtil.i(TAG, "onLowMemory ") 39 | } 40 | 41 | override fun onTrimMemory(level: Int) { 42 | super.onTrimMemory(level) 43 | LogUtil.i(TAG, "onTrimMemory levent $level") 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/main/DoubleColorBallAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.main 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.LinearLayout 8 | import androidx.annotation.LayoutRes 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.nxg.app.R 11 | import com.nxg.loopscrollanimation.RecyclerViewLoopScrollAnimation 12 | import com.nxg.loopscrollanimation.RecyclerViewScrollAction 13 | import com.nxg.loopscrollanimation.utils.LogUtil 14 | import com.nxg.recyclerview.widget.LooperLinearLayoutManager 15 | 16 | /** 17 | * 双色球适配器 18 | */ 19 | class DoubleColorBallAdapter( 20 | private val context: Context, 21 | @LayoutRes var resource: Int, 22 | private val data: MutableList>, 23 | var fakeResult: MutableList = mutableListOf() 24 | ) : 25 | RecyclerView.Adapter() { 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DoubleColorBallViewHolder { 28 | return DoubleColorBallViewHolder( 29 | LayoutInflater.from(context).inflate(resource, parent, false) 30 | ) 31 | } 32 | 33 | override fun onBindViewHolder(holder: DoubleColorBallViewHolder, position: Int) { 34 | if (position < 6) { 35 | holder.doubleColorBallBg.setBackgroundResource(R.drawable.shape_double_color_ball_red) 36 | val loopScrollViewTextAdapter = LoopScrollViewTextAdapter( 37 | context, 38 | R.layout.loop_scroll_view_item_text_red, 39 | data[position] 40 | ) 41 | //LogUtil.i("DoubleColorBallAdapter", "i = $position, before ${data[position]}") 42 | holder.recyclerView.adapter = loopScrollViewTextAdapter 43 | } else { 44 | holder.doubleColorBallBg.setBackgroundResource(R.drawable.shape_double_color_ball_blue) 45 | val loopScrollViewTextAdapter = LoopScrollViewTextAdapter( 46 | context, 47 | R.layout.loop_scroll_view_item_text_blue, 48 | data[position] 49 | ) 50 | //LogUtil.i("DoubleColorBallAdapter", "i = $position, before ${data[position]}") 51 | holder.recyclerView.adapter = loopScrollViewTextAdapter 52 | 53 | } 54 | val looperLinearLayoutManager = 55 | LooperLinearLayoutManager(context, RecyclerView.VERTICAL, false) 56 | holder.recyclerView.layoutManager = looperLinearLayoutManager 57 | holder.recyclerViewLoopScrollAnimation.setConfiguration(holder.recyclerViewLoopScrollAnimationConfiguration) 58 | holder.recyclerViewLoopScrollAnimation.attachToRecyclerView(holder.recyclerView) 59 | holder.recyclerViewLoopScrollAnimation.setRecyclerViewScrollAction(object : 60 | RecyclerViewScrollAction { 61 | override fun findFirstVisibleItemPosition(): Int { 62 | return looperLinearLayoutManager.findFirstVisibleItemPosition() 63 | } 64 | 65 | override fun findLastVisibleItemPosition(): Int { 66 | val selectPosition = looperLinearLayoutManager.findLastVisibleItemPosition() 67 | //指定号码 68 | if (fakeResult.size > 0) { 69 | val adapter = holder.recyclerView.adapter as LoopScrollViewTextAdapter 70 | /*LogUtil.i( 71 | "DoubleColorBallAdapter", 72 | "adapter.dataList[selectPosition].text ${adapter.dataList[selectPosition].text}" 73 | ) 74 | LogUtil.i( 75 | "DoubleColorBallAdapter", 76 | "fakeResult[holder.adapterPosition].text ${fakeResult[holder.adapterPosition]}" 77 | )*/ 78 | adapter.dataList[selectPosition].text = fakeResult[holder.adapterPosition] 79 | adapter.notifyItemChanged(selectPosition) 80 | } 81 | return selectPosition 82 | } 83 | }) 84 | } 85 | 86 | override fun getItemCount(): Int { 87 | return data.size 88 | } 89 | 90 | class DoubleColorBallViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 91 | val recyclerView: RecyclerView = itemView.findViewById(R.id.double_color_ball_recycler_view) 92 | val doubleColorBallBg: LinearLayout = itemView.findViewById(R.id.double_color_ball_bg) 93 | val recyclerViewLoopScrollAnimation = RecyclerViewLoopScrollAnimation() 94 | val recyclerViewLoopScrollAnimationConfiguration = RecyclerViewLoopScrollAnimation.build { 95 | scrollAnimatorDuration = 4000L 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/main/DoubleColorBallItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.main 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | 5 | import android.view.View 6 | 7 | import android.graphics.Rect 8 | import androidx.recyclerview.widget.RecyclerView.ItemDecoration 9 | 10 | 11 | class DoubleColorBallItemDecoration(private val space: Int) : ItemDecoration() { 12 | override fun getItemOffsets( 13 | outRect: Rect, view: View, 14 | parent: RecyclerView, state: RecyclerView.State 15 | ) { 16 | if (parent.getChildAdapterPosition(view) != 0) outRect.left = space 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/main/LoopScrollViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.main 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.annotation.LayoutRes 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.nxg.app.R 11 | 12 | /** 13 | * 纯文字适配器 14 | */ 15 | class LoopScrollViewTextAdapter( 16 | private val context: Context, 17 | @LayoutRes var resource: Int, 18 | val dataList: List 19 | ) : 20 | RecyclerView.Adapter() { 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScrollViewTextViewHolder { 23 | return ScrollViewTextViewHolder( 24 | LayoutInflater.from(context).inflate(resource, parent, false) 25 | ) 26 | } 27 | 28 | override fun onBindViewHolder(holder: ScrollViewTextViewHolder, position: Int) { 29 | holder.textView.text = dataList[position].text 30 | } 31 | 32 | override fun getItemCount(): Int { 33 | return dataList.size 34 | } 35 | 36 | class ScrollViewTextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 37 | val textView: TextView = itemView.findViewById(R.id.text_number) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/main/LoopScrollViewItemBean.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.main 2 | 3 | data class TextBean(var text: String) 4 | 5 | data class ImageBean(var resId: Int) -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/main/MainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.main 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.lifecycle.ViewModelProvider 5 | import android.os.Bundle 6 | import androidx.fragment.app.Fragment 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.lifecycle.Observer 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.nxg.app.R 14 | import com.nxg.app.databinding.MainFragmentBinding 15 | import com.nxg.loopscrollanimation.RecyclerViewLoopScrollAnimation 16 | import com.nxg.loopscrollanimation.RecyclerViewScrollAction 17 | import com.nxg.loopscrollanimation.utils.LogUtil 18 | 19 | class MainFragment : Fragment() { 20 | 21 | companion object { 22 | fun newInstance() = MainFragment() 23 | } 24 | 25 | private lateinit var viewModel: MainViewModel 26 | 27 | private var _binding: MainFragmentBinding? = null 28 | 29 | // This property is only valid between onCreateView and 30 | // onDestroyView. 31 | private val binding get() = _binding!! 32 | 33 | private val doubleColorBallDataA = mutableListOf>() 34 | private val doubleColorBallDataB = mutableListOf>() 35 | private val doubleColorBallDataC = mutableListOf>() 36 | private val doubleColorBallDataD = mutableListOf>() 37 | private val doubleColorBallDataE = mutableListOf>() 38 | 39 | @SuppressLint("NotifyDataSetChanged") 40 | override fun onCreateView( 41 | inflater: LayoutInflater, container: ViewGroup?, 42 | savedInstanceState: Bundle? 43 | ): View? { 44 | viewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(AppApplication.instance()) 45 | .create(MainViewModel::class.java) 46 | _binding = MainFragmentBinding.inflate(inflater, container, false) 47 | val root: View = binding.root 48 | val doubleColorBallRecyclerViewA = binding.doubleColorBallRecyclerViewA 49 | val doubleColorBallRecyclerViewB = binding.doubleColorBallRecyclerViewB 50 | val doubleColorBallRecyclerViewC = binding.doubleColorBallRecyclerViewC 51 | val doubleColorBallRecyclerViewD = binding.doubleColorBallRecyclerViewD 52 | val doubleColorBallRecyclerViewE = binding.doubleColorBallRecyclerViewE 53 | val doubleColorBallAdapterA = 54 | DoubleColorBallAdapter( 55 | requireContext(), 56 | R.layout.double_color_ball_item, 57 | doubleColorBallDataA 58 | ) 59 | val doubleColorBallAdapterB = 60 | DoubleColorBallAdapter( 61 | requireContext(), 62 | R.layout.double_color_ball_item, 63 | doubleColorBallDataB 64 | ) 65 | val doubleColorBallAdapterC = 66 | DoubleColorBallAdapter( 67 | requireContext(), 68 | R.layout.double_color_ball_item, 69 | doubleColorBallDataC 70 | ) 71 | val doubleColorBallAdapterD = 72 | DoubleColorBallAdapter( 73 | requireContext(), 74 | R.layout.double_color_ball_item, 75 | doubleColorBallDataD 76 | ) 77 | val doubleColorBallAdapterE = 78 | DoubleColorBallAdapter( 79 | requireContext(), 80 | R.layout.double_color_ball_item, 81 | doubleColorBallDataE 82 | ) 83 | doubleColorBallRecyclerViewA.layoutManager = 84 | LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) 85 | doubleColorBallRecyclerViewA.adapter = doubleColorBallAdapterA 86 | doubleColorBallRecyclerViewA.addItemDecoration(DoubleColorBallItemDecoration(20)) 87 | 88 | doubleColorBallRecyclerViewB.layoutManager = 89 | LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) 90 | doubleColorBallRecyclerViewB.adapter = doubleColorBallAdapterB 91 | doubleColorBallRecyclerViewB.addItemDecoration(DoubleColorBallItemDecoration(20)) 92 | 93 | doubleColorBallRecyclerViewC.layoutManager = 94 | LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) 95 | doubleColorBallRecyclerViewC.adapter = doubleColorBallAdapterC 96 | doubleColorBallRecyclerViewC.addItemDecoration(DoubleColorBallItemDecoration(20)) 97 | 98 | doubleColorBallRecyclerViewD.layoutManager = 99 | LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) 100 | doubleColorBallRecyclerViewD.adapter = doubleColorBallAdapterD 101 | doubleColorBallRecyclerViewD.addItemDecoration(DoubleColorBallItemDecoration(20)) 102 | 103 | doubleColorBallRecyclerViewE.layoutManager = 104 | LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) 105 | doubleColorBallRecyclerViewE.adapter = doubleColorBallAdapterE 106 | doubleColorBallRecyclerViewE.addItemDecoration(DoubleColorBallItemDecoration(20)) 107 | viewModel.doubleColorBallNum.observe(viewLifecycleOwner, { 108 | doubleColorBallDataA.clear() 109 | doubleColorBallDataA.addAll(it) 110 | doubleColorBallDataA.shuffle() 111 | doubleColorBallDataB.clear() 112 | doubleColorBallDataB.addAll(it) 113 | doubleColorBallDataB.shuffle() 114 | doubleColorBallDataC.clear() 115 | doubleColorBallDataC.addAll(it) 116 | doubleColorBallDataC.shuffle() 117 | doubleColorBallDataD.clear() 118 | doubleColorBallDataD.addAll(it) 119 | doubleColorBallDataD.shuffle() 120 | doubleColorBallDataE.clear() 121 | doubleColorBallDataE.addAll(it) 122 | doubleColorBallDataE.shuffle() 123 | doubleColorBallAdapterA.notifyDataSetChanged() 124 | doubleColorBallAdapterB.notifyDataSetChanged() 125 | doubleColorBallAdapterC.notifyDataSetChanged() 126 | doubleColorBallAdapterD.notifyDataSetChanged() 127 | doubleColorBallAdapterE.notifyDataSetChanged() 128 | }) 129 | binding.doubleColorBallLottery.setOnClickListener { 130 | //开始摇号,遍历每一个RecyclerView调用RecyclerViewLoopScrollAnimation.start() 131 | val ballsA = viewModel.createSevenDoubleColorBall() 132 | LogUtil.i("MainFragment", "ballsA $ballsA") 133 | doubleColorBallAdapterA.fakeResult.clear() 134 | doubleColorBallAdapterA.fakeResult.addAll(ballsA) 135 | for ((index, _) in doubleColorBallDataA.withIndex()) { 136 | val viewHolder = 137 | doubleColorBallRecyclerViewA.findViewHolderForLayoutPosition(index) as DoubleColorBallAdapter.DoubleColorBallViewHolder 138 | viewHolder.recyclerViewLoopScrollAnimation.start(0L) 139 | } 140 | 141 | val ballsB = viewModel.createSevenDoubleColorBall() 142 | LogUtil.i("MainFragment", "ballsB $ballsB") 143 | doubleColorBallAdapterB.fakeResult.clear() 144 | doubleColorBallAdapterB.fakeResult.addAll(ballsB) 145 | for ((index, _) in doubleColorBallDataB.withIndex()) { 146 | val viewHolder = 147 | doubleColorBallRecyclerViewB.findViewHolderForLayoutPosition(index) as DoubleColorBallAdapter.DoubleColorBallViewHolder 148 | viewHolder.recyclerViewLoopScrollAnimation.start(100L) 149 | } 150 | 151 | val ballsC = viewModel.createSevenDoubleColorBall() 152 | LogUtil.i("MainFragment", "ballsC $ballsC") 153 | doubleColorBallAdapterC.fakeResult.clear() 154 | doubleColorBallAdapterC.fakeResult.addAll(ballsC) 155 | for ((index, _) in doubleColorBallDataC.withIndex()) { 156 | val viewHolder = 157 | doubleColorBallRecyclerViewC.findViewHolderForLayoutPosition(index) as DoubleColorBallAdapter.DoubleColorBallViewHolder 158 | viewHolder.recyclerViewLoopScrollAnimation.start(200L) 159 | } 160 | 161 | val ballsD = viewModel.createSevenDoubleColorBall() 162 | LogUtil.i("MainFragment", "ballsD $ballsD") 163 | doubleColorBallAdapterD.fakeResult.clear() 164 | doubleColorBallAdapterD.fakeResult.addAll(ballsD) 165 | for ((index, _) in doubleColorBallDataD.withIndex()) { 166 | val viewHolder = 167 | doubleColorBallRecyclerViewD.findViewHolderForLayoutPosition(index) as DoubleColorBallAdapter.DoubleColorBallViewHolder 168 | viewHolder.recyclerViewLoopScrollAnimation.start(300L) 169 | } 170 | 171 | val ballsE = viewModel.createSevenDoubleColorBall() 172 | LogUtil.i("MainFragment", "ballsE $ballsE") 173 | doubleColorBallAdapterE.fakeResult.clear() 174 | doubleColorBallAdapterE.fakeResult.addAll(ballsE) 175 | for ((index, _) in doubleColorBallDataE.withIndex()) { 176 | val viewHolder = 177 | doubleColorBallRecyclerViewE.findViewHolderForLayoutPosition(index) as DoubleColorBallAdapter.DoubleColorBallViewHolder 178 | viewHolder.recyclerViewLoopScrollAnimation.start(400L) 179 | } 180 | 181 | } 182 | viewModel.appInfo.observe(viewLifecycleOwner, { 183 | binding.appInfo.text = it 184 | }) 185 | viewModel.refreshDoubleColorBallNum() 186 | return root 187 | } 188 | 189 | 190 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.main 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import com.nxg.loopscrollanimation.utils.LogUtil 7 | import java.util.* 8 | import android.content.pm.PackageManager 9 | import androidx.lifecycle.AndroidViewModel 10 | import com.nxg.app.utils.AppUtils 11 | 12 | 13 | class MainViewModel(application: Application) : AndroidViewModel(application) { 14 | 15 | private val _appInfo = MutableLiveData() 16 | 17 | val appInfo: LiveData = _appInfo.apply { 18 | value = getAppInfo() 19 | } 20 | 21 | private fun getAppInfo(): String { 22 | val context = getApplication().applicationContext 23 | var appInfo = AppUtils.getAppName(context) + ":v" + AppUtils.getVersionName(context) 24 | if (getAppReleaseTime().isNotEmpty()) { 25 | appInfo += "_" + getAppReleaseTime() 26 | } 27 | return appInfo 28 | } 29 | 30 | private fun getAppReleaseTime(): String { 31 | val appReleaseTime = "" 32 | val context = getApplication().applicationContext 33 | context.packageManager.getApplicationInfo( 34 | "com.nxg.app", 35 | PackageManager.GET_META_DATA 36 | ).metaData.getString("RELEASE_TIME") 37 | return appReleaseTime 38 | } 39 | 40 | private val _doubleColorBallNum = MutableLiveData>>() 41 | val doubleColorBallNum: LiveData>> = _doubleColorBallNum 42 | private fun createDoubleColorBallNum(): MutableList> { 43 | val data = mutableListOf>() 44 | for (i in 0..6) { 45 | val numberList = mutableListOf() 46 | val bound = if (i < 6) { 47 | 33 48 | } else { 49 | 16 50 | } 51 | LogUtil.i("MainViewModel", "i = $i, bound $bound") 52 | for (j in 1..bound) { 53 | numberList.add( 54 | TextBean( 55 | if (j < 10) "0$j" else { 56 | j.toString() 57 | } 58 | ) 59 | ) 60 | } 61 | //LogUtil.i("MainViewModel", "i = $i, before $numberList") 62 | numberList.shuffle() 63 | //LogUtil.i("MainViewModel", "i = $i, after $numberList") 64 | data.add(i, numberList) 65 | } 66 | return data 67 | } 68 | 69 | fun refreshDoubleColorBallNum() { 70 | _doubleColorBallNum.value = createDoubleColorBallNum() 71 | } 72 | 73 | /** 74 | * 生成6个红色双色球号码 75 | */ 76 | private fun createSixRedDoubleColorBall(): List { 77 | val redBalls = mutableListOf() 78 | for (j in 1..33) { 79 | redBalls.add( 80 | if (j < 10) "0$j" else { 81 | j.toString() 82 | } 83 | ) 84 | } 85 | LogUtil.i("MainViewModel", "createSixRedDoubleColorBall before $redBalls") 86 | redBalls.shuffle() 87 | LogUtil.i("MainViewModel", "createSixRedDoubleColorBall shuffle after $redBalls") 88 | val balls = redBalls.subList(0, 6) 89 | LogUtil.i("MainViewModel", "createSixRedDoubleColorBall subList after $balls") 90 | balls.sort() 91 | LogUtil.i("MainViewModel", "createSixRedDoubleColorBall sort after $balls") 92 | return balls 93 | } 94 | 95 | /** 96 | * 生成1个蓝色双色球号码 97 | */ 98 | private fun createSingleBlueDoubleColorBall(): String { 99 | val blueBall = Random().nextInt(16) + 1 100 | LogUtil.i("MainViewModel", "createSingleBlueDoubleColorBall $blueBall") 101 | return if (blueBall < 10) "0$blueBall" else { 102 | blueBall.toString() 103 | } 104 | } 105 | 106 | /** 107 | * 随机生成6个红色双色球号码+1个蓝色双色球号码 108 | */ 109 | fun createSevenDoubleColorBall(): List { 110 | val balls = mutableListOf() 111 | val redBalls = createSixRedDoubleColorBall() 112 | balls.addAll(redBalls) 113 | val blueBall = createSingleBlueDoubleColorBall() 114 | balls.add(blueBall) 115 | LogUtil.i("MainViewModel", "createSevenDoubleColorBall $balls") 116 | return balls 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /app/src/main/java/com/nxg/app/utils/AppUtils.kt: -------------------------------------------------------------------------------- 1 | package com.nxg.app.utils 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageInfo 6 | import android.content.pm.PackageManager 7 | import android.graphics.Bitmap 8 | import android.graphics.drawable.BitmapDrawable 9 | 10 | object AppUtils { 11 | /** 12 | * 获取应用程序名称 13 | */ 14 | @Synchronized 15 | fun getAppName(context: Context): String? { 16 | try { 17 | val packageManager: PackageManager = context.packageManager 18 | val packageInfo: PackageInfo = packageManager.getPackageInfo( 19 | context.packageName, 0 20 | ) 21 | val labelRes = packageInfo.applicationInfo.labelRes 22 | return context.resources.getString(labelRes) 23 | } catch (e: Exception) { 24 | e.printStackTrace() 25 | } 26 | return null 27 | } 28 | 29 | /** 30 | * 获取应用程序版本名称信息 31 | * @param context 32 | * @return 当前应用的版本名称 33 | */ 34 | @Synchronized 35 | fun getVersionName(context: Context): String? { 36 | try { 37 | val packageManager: PackageManager = context.packageManager 38 | val packageInfo: PackageInfo = packageManager.getPackageInfo( 39 | context.packageName, 0 40 | ) 41 | return packageInfo.versionName 42 | } catch (e: Exception) { 43 | e.printStackTrace() 44 | } 45 | return null 46 | } 47 | 48 | /** 49 | * 获取应用程序版本名称信息 50 | * @param context 51 | * @return 当前应用的版本名称 52 | */ 53 | @Synchronized 54 | fun getVersionCode(context: Context): Int { 55 | try { 56 | val packageManager: PackageManager = context.packageManager 57 | val packageInfo: PackageInfo = packageManager.getPackageInfo( 58 | context.packageName, 0 59 | ) 60 | return packageInfo.versionCode 61 | } catch (e: Exception) { 62 | e.printStackTrace() 63 | } 64 | return 0 65 | } 66 | 67 | /** 68 | * 获取应用程序版本名称信息 69 | * @param context 70 | * @return 当前应用的版本名称 71 | */ 72 | @Synchronized 73 | fun getPackageName(context: Context): String? { 74 | try { 75 | val packageManager: PackageManager = context.packageManager 76 | val packageInfo: PackageInfo = packageManager.getPackageInfo( 77 | context.packageName, 0 78 | ) 79 | return packageInfo.packageName 80 | } catch (e: Exception) { 81 | e.printStackTrace() 82 | } 83 | return null 84 | } 85 | 86 | /** 87 | * 获取图标 bitmap 88 | * @param context 89 | */ 90 | @Synchronized 91 | fun getBitmap(context: Context): Bitmap { 92 | var packageManager: PackageManager? = null 93 | var applicationInfo: ApplicationInfo? = null 94 | try { 95 | packageManager = context.applicationContext 96 | .packageManager 97 | applicationInfo = packageManager.getApplicationInfo( 98 | context.packageName, 0 99 | ) 100 | } catch (e: PackageManager.NameNotFoundException) { 101 | applicationInfo = null 102 | } 103 | val d = 104 | packageManager!!.getApplicationIcon(applicationInfo!!) //xxx根据自己的情况获取drawable 105 | val bd = d as BitmapDrawable 106 | return bd.bitmap 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiangang/RecyclerViewLoopScrollAnimation/d35c782bfd73db21194873fc5eb440653b80c7e3/app/src/main/res/drawable/ic_app.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lucky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiangang/RecyclerViewLoopScrollAnimation/d35c782bfd73db21194873fc5eb440653b80c7e3/app/src/main/res/drawable/ic_lucky.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/loop_scroll_view_shape_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_double_color_ball_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_double_color_ball_lottery.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_double_color_ball_lottery_normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | /> 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_double_color_ball_lottery_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_double_color_ball_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/double_color_ball_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loop_scroll_view_item_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loop_scroll_view_item_text_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loop_scroll_view_item_text_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 |