├── .gitignore ├── LICENSE ├── README.md ├── apk └── app-release.apk ├── app ├── .gitignore ├── build.gradle.kts ├── keystore.jks ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── ihsg │ │ └── demo │ │ ├── TestApplication.java │ │ ├── ui │ │ ├── MainActivity.kt │ │ ├── def │ │ │ ├── DefaultPatternCheckingActivity.kt │ │ │ ├── DefaultPatternSettingActivity.kt │ │ │ └── DefaultStyleActivity.kt │ │ ├── simple │ │ │ ├── SimplePatternCheckingActivity.kt │ │ │ ├── SimplePatternSettingActivity.kt │ │ │ └── SimpleStyleActivity.kt │ │ └── whole │ │ │ ├── RippleLockerHitCellView.kt │ │ │ ├── WholePatternCheckingActivity.kt │ │ │ ├── WholePatternSettingActivity.kt │ │ │ └── WholeStyleActivity.kt │ │ └── util │ │ ├── PatternHelper.kt │ │ ├── SecurityUtil.kt │ │ └── SharedPreferencesUtil.kt │ └── res │ ├── layout │ ├── activity_default_pattern_checking.xml │ ├── activity_default_pattern_setting.xml │ ├── activity_default_style.xml │ ├── activity_main.xml │ ├── activity_simple_pattern_checking.xml │ ├── activity_simple_pattern_setting.xml │ ├── activity_simple_style.xml │ ├── activity_whole_pattern_checking.xml │ ├── activity_whole_pattern_setting.xml │ └── activity_whole_style.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle.kts ├── captures ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── captures.jpg ├── captures.png ├── download_xcode.png └── hsg_Wechat_QR.jpeg ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── ihsg │ │ └── patternlocker │ │ ├── CellBean.kt │ │ ├── CellFactory.kt │ │ ├── DefaultConfig.kt │ │ ├── DefaultIndicatorHitCellView.kt │ │ ├── DefaultIndicatorLinkedLineView.kt │ │ ├── DefaultIndicatorNormalCellView.kt │ │ ├── DefaultLockerHitCellView.kt │ │ ├── DefaultLockerLinkedLineView.kt │ │ ├── DefaultLockerNormalCellView.kt │ │ ├── DefaultStyleDecorator.kt │ │ ├── IHitCellView.kt │ │ ├── IIndicatorLinkedLineView.kt │ │ ├── ILockerLinkedLineView.kt │ │ ├── INormalCellView.kt │ │ ├── Logger.kt │ │ ├── OnPatternChangeListener.kt │ │ ├── PatternIndicatorView.kt │ │ └── PatternLockerView.kt │ └── res │ └── values │ ├── attrs.xml │ └── strings.xml └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # refer to https://github.com/github/gitignore/blob/master/Android.gitignore 2 | 3 | # Built application files 4 | #*.apk 5 | #*.ap_ 6 | 7 | # Files for the Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | release/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | # captures/ 36 | 37 | # ignore intellij folder 38 | .idea/ 39 | *.iml 40 | *.ipr 41 | *.iws 42 | 43 | # Mac OS X 44 | .DS_Store 45 | *.swp 46 | 47 | # automatically generated by guild.gradle 48 | app/src/main/res/values-zh/strings.xml -------------------------------------------------------------------------------- /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 | ## Pattern Locker 2 | 3 | 此为Android App中常用控件之一的图案解锁(手势解锁、手势密码、九宫格密码、九宫格图形锁等)控件开源库,PatternLockerView为主控件,负责图案密码的设置和验证,PatternIndicatorView为指示器辅助控件,可根据设计需要选择使用。 4 | 5 | 由于本人水平有限,如果您在使用的过程中发现bug,或者发现有更好的实现方式和代码中写得不足的地方,请提issue或者PR,本人万分感激!!! 6 | 7 | ## 功能介绍 8 | - 支持自定义各状态下(未操作时、操作时以及操作出错时)线颜色、填充色和线宽; 9 | - 支持自定义各种状态下(未操作时、操作时以及操作出错时)每个CellView样式和连接线样式; 10 | - 支持设置图案绘制完成后延迟自动清除的时长(默认1秒); 11 | - 支持是否跳过中间点(默认不跳过); 12 | - 支持是否触碰震动反馈(默认不震动); 13 | - 支持指示器辅助控件可选择使用; 14 | - 业务逻辑(至少连点几个点、验证时最多可出错几次等)须自定义。 15 | 16 | ## 预览效果图 17 | ![setting](./captures/captures.png) 18 | 19 | [点击下载PatternLockerDemo.apk](http://d.6short.com/qvhx) 或者扫描下方二维码下载安装 20 | 21 | ![download](./captures/download_xcode.png) 22 | 23 | ## 基本用法 24 | 25 | [![](https://jitpack.io/v/ihsg/PatternLocker.svg)](https://jitpack.io/#ihsg/PatternLocker) 26 | 27 | 第一步: 首先打开项目根目录下的 build.gradle,添加jitpack仓库地址,代码如下: 28 | ````groovy 29 | allprojects { 30 | repositories { 31 | ... 32 | maven { url "https://jitpack.io" } 33 | } 34 | } 35 | ```` 36 | 37 | 第二步: 打开需要依赖此 library 的module,比如此demo中是 app 这个 module,添加: 38 | ````groovy 39 | dependencies { 40 | .... 41 | implementation 'com.github.ihsg:PatternLocker:2.5.7' 42 | } 43 | ```` 44 | 45 | 第三步: 在布局文件中添加PatternLockViewer和PatternIndicatorView(可根据设计需要选择使用)控件,示意如下: 46 | 47 | ````xml 48 | 49 | 55 | 56 | ...... 57 | 58 | 64 | 65 | 72 | 73 | 74 | ```` 75 | 第四步: 在java代码中为PatternLockerView添加OnPatternChangeListener并处理相应业务逻辑, 76 | 77 | ```kotlin 78 | patternLockerView.setOnPatternChangedListener(object : OnPatternChangeListener { 79 | override fun onStart(view: PatternLockerView) { 80 | //根据需要添加业务逻辑 81 | } 82 | 83 | override fun onChange(view: PatternLockerView, hitIndexList: List) { 84 | //根据需要添加业务逻辑 85 | } 86 | 87 | override fun onComplete(view: PatternLockerView, hitIndexList: List) { 88 | //根据需要添加业务逻辑 89 | } 90 | 91 | override fun onClear(view: PatternLockerView) { 92 | //根据需要添加业务逻辑 93 | } 94 | }) 95 | ``` 96 | 97 | 98 | 99 | OnPatternChangeListener接口说明如下: 100 | 101 | ````kotlin 102 | interface OnPatternChangeListener { 103 | /** 104 | * 开始绘制图案时(即手指按下触碰到绘画区域时)会调用该方法 105 | * 106 | * @param view 107 | */ 108 | fun onStart(view: PatternLockerView) 109 | 110 | /** 111 | * 图案绘制改变时(即手指在绘画区域移动时)会调用该方法,请注意只有 @param hitList改变了才会触发此方法 112 | * 113 | * @param view 114 | * @param hitIndexList 115 | */ 116 | fun onChange(view: PatternLockerView, hitIndexList: List) 117 | 118 | /** 119 | * 图案绘制完成时(即手指抬起离开绘画区域时)会调用该方法 120 | * 121 | * @param view 122 | * @param hitIndexList 123 | */ 124 | fun onComplete(view: PatternLockerView, hitIndexList: List) 125 | 126 | /** 127 | * 已绘制的图案被清除时会调用该方法 128 | * 129 | * @param view 130 | */ 131 | fun onClear(view: PatternLockerView) 132 | } 133 | ```` 134 | ## 常用方法说明 135 | 136 | ### 1. PatternLockerView 137 | 138 | ```kotlin 139 | // 监听器 140 | setOnPatternChangedListener(listener: OnPatternChangeListener?) 141 | 142 | // 设置当前绘制的是否是错误图案 143 | updateStatus(isError: Boolean) 144 | 145 | // 清除已绘制的图案 146 | clearHitState() 147 | ``` 148 | 149 | ### 2. PatternIndicatorView 150 | 151 | ```kotlin 152 | // 设置已绘制图案及状态 153 | updateState(hitIndexList: List?, isError: Boolean) 154 | ``` 155 | 156 | ## 已知问题解决方案 157 | 158 | #### 1. Java开发使用该库出现的错误: Didn't find class "kotlin.reflect.KProperty" 159 | 160 | 解决方案:请配置kotlin开发环境 [参考Kotlin官网](https://kotlinlang.org/docs/tutorials/kotlin-android.html) 161 | 162 | 步骤1: 在根目录下的buid.gradle 163 | 164 | ```groovy 165 | buildscript { 166 | ext { 167 | kotlinVersion = '1.3.40' 168 | } 169 | dependencies { 170 | ...... 171 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 172 | 173 | // NOTE: Do not place your application dependencies here; they belong 174 | // in the individual module build.gradle files 175 | } 176 | } 177 | ``` 178 | 179 | 步骤2: 在module目录下的buid.gradle 180 | 181 | ```groovy 182 | apply plugin: 'com.android.application' 183 | apply plugin: 'kotlin-android' 184 | 185 | dependencies { 186 | ...... 187 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 188 | } 189 | 190 | ``` 191 | 192 | ## 自由定制 193 | 194 | ### 1. 默认样式简单定制 195 | 可以通过xml和kotlin代码两种方式更改默认颜色、绘制时颜色、出错时颜色、填充色以及连接线粗细等 196 | 197 | > 推荐使用xml方式,更精简,更方便 198 | 199 | #### 1.1 xml方式 200 | - PatternLockerView可设置的属性 201 | 202 | | 属性名 | 说明 | 默认值 | 203 | | :------------- | :--------: | :-----: | 204 | | plv_color | 默认图案的颜色 | #2196F3 | 205 | | plv_hitColor | 绘制图案的颜色 | #3F51B5 | 206 | | plv_errorColor | 绘制图案出错时的颜色 | #F44336 | 207 | | plv_fillColor | 图案填充色 | #FFFFFF | 208 | | plv_lineWidth | 图案的线宽 | 1dp | 209 | | plv_enableAutoClean | 自动清除绘制图案 | true | 210 | | plv_enableSkip | 是否跳过中间点 | false | 211 | | plv_enableHapticFeedback | 是否使用触碰震动反馈 | false | 212 | | plv_freezeDuration | 操作完延迟自动清除时长(单位ms) | 1000 | 213 | 214 | 215 | 示例如下: 216 | ```xml 217 | 232 | ``` 233 | 234 | - PatternIndicatorView可设置的属性 235 | 236 | | 属性名 | 说明 | 默认值 | 237 | | :------------- | :------------- | :------ | 238 | | piv_color | 指示器默认图案的颜色 | #2196F3 | 239 | | piv_hitColor | 指示器中选中图案的颜色 | #3F51B5 | 240 | | piv_errorColor | 指示器中选中图案出错时的颜色 | #F44336 | 241 | | piv_fillColor | 图案填充色 | #FFFFFF | 242 | | piv_lineWidth | 图案的线宽 | 1dp | 243 | 244 | 示例如下: 245 | ```xml 246 | 261 | ``` 262 | #### 1.2 kotlin代码方式 263 | 264 | > 这里styleDecorator可以通过normalCellView、hitCellView和linkedLineView三者均可获取,下面以normalCellView为例。 265 | 266 | - PatternLockerView可设置的属性 267 | ````kotlin 268 | val plvStyle = (this.patternLockerView.normalCellView as DefaultLockerNormalCellView).styleDecorator 269 | 270 | plvStyle.normalColor = ContextCompat.getColor(this, R.color.colorWhite) 271 | plvStyle.fillColor = ContextCompat.getColor(this, R.color.color_blue) 272 | plvStyle.hitColor = ContextCompat.getColor(this, R.color.colorPrimaryDark) 273 | plvStyle.errorColor = ContextCompat.getColor(this, R.color.color_red) 274 | plvStyle.lineWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, 275 | resources.displayMetrics) 276 | ```` 277 | 278 | - PatternIndicatorView可设置的属性 279 | ````kotlin 280 | val pivStyle = (this.patternIndicatorView.normalCellView as DefaultLockerNormalCellView).styleDecorator 281 | 282 | pivStyle.normalColor = ContextCompat.getColor(this, R.color.colorWhite) 283 | pivStyle.fillColor = ContextCompat.getColor(this, R.color.color_blue) 284 | pivStyle.hitColor = ContextCompat.getColor(this, R.color.colorPrimaryDark) 285 | pivStyle.errorColor = ContextCompat.getColor(this, R.color.color_red) 286 | pivStyle.lineWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, 287 | resources.displayMetrics) 288 | ```` 289 | 290 | ### 2. 深度定制 291 | 292 | PatternLockerView和PatternIndicatorView均提供了设置连接线、各个小单元控件在不同状态下(正常、设置以及出错)的绘制方式的方法,只需要实现如下几个接口即可,从而可以根据设计需求高度自由定制。 293 | 294 | - 正常状态下各个小单元控件的样式(PatternLockerView和PatternIndicatorView通用) 295 | 296 | ```kotlin 297 | interface INormalCellView { 298 | /** 299 | * 绘制正常情况下(即未设置的)每个图案的样式 300 | * 301 | * @param canvas 302 | * @param cellBean the target cell view 303 | */ 304 | fun draw(canvas: Canvas, cellBean: CellBean) 305 | } 306 | ``` 307 | 308 | - 设置时各个小单元控件的样式(PatternLockerView和PatternIndicatorView通用) 309 | 310 | ```kotlin 311 | interface IHitCellView { 312 | /** 313 | * 绘制已设置的每个图案的样式 314 | * 315 | * @param canvas 316 | * @param cellBean 317 | * @param isError 318 | */ 319 | fun draw(canvas: Canvas, cellBean: CellBean, isError: Boolean) 320 | } 321 | ``` 322 | 323 | - PatternLockerView连接线的样式 324 | 325 | ```kotlin 326 | interface ILockerLinkedLineView { 327 | /** 328 | * 绘制图案密码连接线 329 | * 330 | * @param canvas 331 | * @param hitIndexList 332 | * @param cellBeanList 333 | * @param endX 334 | * @param endY 335 | * @param isError 336 | */ 337 | fun draw(canvas: Canvas, 338 | hitIndexList: List, 339 | cellBeanList: List, 340 | endX: Float, 341 | endY: Float, 342 | isError: Boolean) 343 | } 344 | ``` 345 | 346 | - PatternIndicatorView连接线的样式 347 | 348 | ```kotlin 349 | interface IIndicatorLinkedLineView { 350 | /** 351 | * 绘制指示器连接线 352 | * 353 | * @param canvas 354 | * @param hitIndexList 355 | * @param cellBeanList 356 | * @param isError 357 | */ 358 | fun draw(canvas: Canvas, 359 | hitIndexList: List, 360 | cellBeanList: List, 361 | isError: Boolean) 362 | } 363 | ``` 364 | 365 | 分别设置主控件和指示器的定制实现: 366 | 367 | ```kotlin 368 | // 主控件 369 | patternLockerView.normalCellView = ... //定制实现 370 | patternLockerView.hitCellView = ... //定制实现 371 | patternLockerView.linkedLineView = ... //定制实现 372 | 373 | // 指示器控件 374 | patternIndicatorView.normalCellView = ... //定制实现 375 | patternIndicatorView.hitCellView = ... //定制实现 376 | patternIndicatorView.linkedLineView = ... //定制实现 377 | ``` 378 | 379 | > 温馨提示:更详细的定制方式可参考Library中default开头的代码。 380 | 381 | ### 联系方式(微信) 382 | 383 | 当你遇到问题时,请先看demo了解用法,如果还是没有解决又比较着急,可以通过微信联系我哦! 384 | 385 | ![微信](./captures/hsg_Wechat_QR.jpeg) -------------------------------------------------------------------------------- /apk/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/apk/app-release.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "com.github.ihsg.demo" 8 | compileSdk = libs.versions.compileSdk.get().toInt() 9 | 10 | defaultConfig { 11 | applicationId = "com.github.ihsg.demo" 12 | minSdk = libs.versions.minSdk.get().toInt() 13 | targetSdk = libs.versions.targetSdk.get().toInt() 14 | versionCode = libs.versions.versionCode.get().toInt() 15 | versionName = libs.versions.versionName.get() 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary = true 20 | } 21 | } 22 | signingConfigs { 23 | create("myConfig") { 24 | storeFile = file("keystore.jks") 25 | storePassword = "654321" 26 | keyAlias = "Pattern" 27 | keyPassword = "654321" 28 | } 29 | } 30 | buildTypes { 31 | val signConfig = signingConfigs.getByName("myConfig") 32 | getByName("release") { 33 | isMinifyEnabled = false 34 | proguardFiles( 35 | getDefaultProguardFile("proguard-android-optimize.txt"), 36 | "proguard-rules.pro" 37 | ) 38 | signingConfig = signConfig 39 | } 40 | } 41 | compileOptions { 42 | sourceCompatibility = JavaVersion.VERSION_17 43 | targetCompatibility = JavaVersion.VERSION_17 44 | } 45 | kotlinOptions { 46 | jvmTarget = "17" 47 | } 48 | buildFeatures { 49 | viewBinding = true 50 | } 51 | composeOptions { 52 | kotlinCompilerExtensionVersion = "1.5.1" 53 | } 54 | packaging { 55 | resources { 56 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 57 | } 58 | } 59 | } 60 | 61 | dependencies { 62 | implementation(project(":library")) 63 | implementation(libs.androidx.core.ktx) 64 | implementation(libs.androidx.appcompat) 65 | implementation(libs.androidx.lifecycle.runtime.ktx) 66 | testImplementation(libs.junit) 67 | androidTestImplementation(libs.androidx.junit) 68 | } -------------------------------------------------------------------------------- /app/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/keystore.jks -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/hsg/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | /** 7 | * Created by hsg on 14/10/2017. 8 | */ 9 | 10 | public class TestApplication extends Application { 11 | private static Context context; 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | context = getApplicationContext(); 17 | } 18 | 19 | public static Context getContext() { 20 | return context; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.github.ihsg.demo.databinding.ActivityMainBinding 6 | import com.github.ihsg.demo.ui.def.DefaultStyleActivity 7 | import com.github.ihsg.demo.ui.simple.SimpleStyleActivity 8 | import com.github.ihsg.demo.ui.whole.WholeStyleActivity 9 | 10 | class MainActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | ActivityMainBinding.inflate(layoutInflater).apply { 16 | setContentView(root) 17 | 18 | btnDefault.setOnClickListener { 19 | DefaultStyleActivity.startAction(this@MainActivity) 20 | } 21 | 22 | btnSimple.setOnClickListener { 23 | SimpleStyleActivity.startAction(this@MainActivity) 24 | } 25 | 26 | btnWhole.setOnClickListener { 27 | WholeStyleActivity.startAction(this@MainActivity) 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/def/DefaultPatternCheckingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.def 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.content.ContextCompat 9 | import com.github.ihsg.demo.R 10 | import com.github.ihsg.demo.databinding.ActivityDefaultPatternCheckingBinding 11 | import com.github.ihsg.demo.util.PatternHelper 12 | import com.github.ihsg.patternlocker.OnPatternChangeListener 13 | import com.github.ihsg.patternlocker.PatternLockerView 14 | 15 | 16 | class DefaultPatternCheckingActivity : AppCompatActivity() { 17 | private var patternHelper: PatternHelper? = null 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | ActivityDefaultPatternCheckingBinding.inflate(layoutInflater).apply { 23 | setContentView(root) 24 | 25 | patternLockerView.linkedLineView = null 26 | patternLockerView.hitCellView = null 27 | 28 | patternLockerView.setOnPatternChangedListener(object : OnPatternChangeListener { 29 | override fun onStart(view: PatternLockerView) {} 30 | 31 | override fun onChange(view: PatternLockerView, hitIndexList: List) {} 32 | 33 | override fun onComplete(view: PatternLockerView, hitIndexList: List) { 34 | val isError = !isPatternOk(hitIndexList) 35 | view.updateStatus(isError) 36 | patternIndicatorView.updateState(hitIndexList, isError) 37 | updateMsg(textMsg) 38 | } 39 | 40 | override fun onClear(view: PatternLockerView) { 41 | finishIfNeeded() 42 | } 43 | }) 44 | 45 | textMsg.text = "绘制解锁图案" 46 | } 47 | 48 | patternHelper = PatternHelper() 49 | } 50 | 51 | private fun isPatternOk(hitIndexList: List): Boolean { 52 | patternHelper?.validateForChecking(hitIndexList) 53 | return patternHelper?.isOk == true 54 | } 55 | 56 | private fun updateMsg(textMsg: TextView) { 57 | textMsg.text = patternHelper?.message 58 | textMsg.setTextColor( 59 | if (patternHelper?.isOk == true) { 60 | ContextCompat.getColor(this, R.color.colorPrimary) 61 | } else { 62 | ContextCompat.getColor(this, R.color.colorAccent) 63 | } 64 | ) 65 | } 66 | 67 | private fun finishIfNeeded() { 68 | if (patternHelper?.isFinish == true) { 69 | finish() 70 | } 71 | } 72 | 73 | companion object { 74 | fun startAction(context: Context) { 75 | val intent = Intent(context, DefaultPatternCheckingActivity::class.java) 76 | context.startActivity(intent) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/def/DefaultPatternSettingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.def 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.content.ContextCompat 9 | import com.github.ihsg.demo.R 10 | import com.github.ihsg.demo.databinding.ActivityDefaultPatternSettingBinding 11 | import com.github.ihsg.demo.util.PatternHelper 12 | import com.github.ihsg.patternlocker.OnPatternChangeListener 13 | import com.github.ihsg.patternlocker.PatternLockerView 14 | 15 | class DefaultPatternSettingActivity : AppCompatActivity() { 16 | private var patternHelper: PatternHelper? = null 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | 21 | ActivityDefaultPatternSettingBinding.inflate(layoutInflater).apply { 22 | setContentView(root) 23 | 24 | patternIndicatorView.updateState(listOf(1, 2), false) 25 | patternLockerView.setOnPatternChangedListener(object : OnPatternChangeListener { 26 | override fun onStart(view: PatternLockerView) {} 27 | 28 | override fun onChange(view: PatternLockerView, hitIndexList: List) {} 29 | 30 | override fun onComplete(view: PatternLockerView, hitIndexList: List) { 31 | val isOk = isPatternOk(hitIndexList) 32 | view.updateStatus(!isOk) 33 | patternIndicatorView.updateState(hitIndexList, !isOk) 34 | updateMsg(textMsg) 35 | } 36 | 37 | override fun onClear(view: PatternLockerView) { 38 | finishIfNeeded() 39 | } 40 | }) 41 | textMsg.text = "设置解锁图案" 42 | } 43 | 44 | patternHelper = PatternHelper() 45 | } 46 | 47 | private fun isPatternOk(hitIndexList: List): Boolean { 48 | patternHelper?.validateForSetting(hitIndexList) 49 | return patternHelper?.isOk == true 50 | } 51 | 52 | private fun updateMsg(textMsg: TextView) { 53 | textMsg.text = patternHelper?.message 54 | textMsg.setTextColor( 55 | if (patternHelper?.isOk == true) { 56 | ContextCompat.getColor(this, R.color.colorPrimary) 57 | } else { 58 | ContextCompat.getColor(this, R.color.colorAccent) 59 | } 60 | ) 61 | } 62 | 63 | private fun finishIfNeeded() { 64 | if (patternHelper?.isFinish == true) { 65 | finish() 66 | } 67 | } 68 | 69 | companion object { 70 | fun startAction(context: Context) { 71 | val intent = Intent(context, DefaultPatternSettingActivity::class.java) 72 | context.startActivity(intent) 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/def/DefaultStyleActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.def 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.github.ihsg.demo.databinding.ActivityDefaultStyleBinding 8 | 9 | 10 | class DefaultStyleActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | ActivityDefaultStyleBinding.inflate(layoutInflater).apply { 16 | setContentView(root) 17 | 18 | btnSetting.setOnClickListener { 19 | DefaultPatternSettingActivity.startAction(this@DefaultStyleActivity) 20 | } 21 | btnChecking.setOnClickListener { 22 | DefaultPatternCheckingActivity.startAction(this@DefaultStyleActivity) 23 | } 24 | } 25 | } 26 | 27 | companion object { 28 | fun startAction(context: Context) { 29 | val intent = Intent(context, DefaultStyleActivity::class.java) 30 | context.startActivity(intent) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/simple/SimplePatternCheckingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.simple 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.util.TypedValue 7 | import android.widget.TextView 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.content.ContextCompat 10 | import com.github.ihsg.demo.R 11 | import com.github.ihsg.demo.databinding.ActivitySimplePatternCheckingBinding 12 | import com.github.ihsg.demo.util.PatternHelper 13 | import com.github.ihsg.patternlocker.DefaultIndicatorNormalCellView 14 | import com.github.ihsg.patternlocker.DefaultLockerNormalCellView 15 | import com.github.ihsg.patternlocker.OnPatternChangeListener 16 | import com.github.ihsg.patternlocker.PatternLockerView 17 | 18 | class SimplePatternCheckingActivity : AppCompatActivity() { 19 | private var patternHelper: PatternHelper? = null 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | ActivitySimplePatternCheckingBinding.inflate(layoutInflater).apply { 25 | setContentView(root) 26 | val context = this@SimplePatternCheckingActivity 27 | 28 | val pivStyle = (patternIndicatorView.normalCellView as DefaultIndicatorNormalCellView).styleDecorator 29 | pivStyle.normalColor = ContextCompat.getColor(context, R.color.colorWhite) 30 | pivStyle.fillColor = ContextCompat.getColor(context, R.color.color_blue) 31 | pivStyle.hitColor = ContextCompat.getColor(context, R.color.colorPrimaryDark) 32 | pivStyle.errorColor = ContextCompat.getColor(context, R.color.color_red) 33 | pivStyle.lineWidth = TypedValue.applyDimension( 34 | TypedValue.COMPLEX_UNIT_DIP, 2f, 35 | resources.displayMetrics 36 | ) 37 | 38 | val plvStyle = (patternLockerView.normalCellView as DefaultLockerNormalCellView).styleDecorator 39 | plvStyle.normalColor = ContextCompat.getColor(context, R.color.colorWhite) 40 | plvStyle.fillColor = ContextCompat.getColor(context, R.color.color_blue) 41 | plvStyle.hitColor = ContextCompat.getColor(context, R.color.colorPrimaryDark) 42 | plvStyle.errorColor = ContextCompat.getColor(context, R.color.color_red) 43 | plvStyle.lineWidth = TypedValue.applyDimension( 44 | TypedValue.COMPLEX_UNIT_DIP, 5f, 45 | resources.displayMetrics 46 | ) 47 | 48 | patternLockerView.setOnPatternChangedListener(object : OnPatternChangeListener { 49 | override fun onStart(view: PatternLockerView) {} 50 | 51 | override fun onChange(view: PatternLockerView, hitIndexList: List) {} 52 | 53 | override fun onComplete(view: PatternLockerView, hitIndexList: List) { 54 | val isError = !isPatternOk(hitIndexList) 55 | view.updateStatus(isError) 56 | patternIndicatorView.updateState(hitIndexList, isError) 57 | updateMsg(textMsg) 58 | } 59 | 60 | override fun onClear(view: PatternLockerView) { 61 | finishIfNeeded() 62 | } 63 | }) 64 | 65 | textMsg.text = "绘制解锁图案" 66 | } 67 | patternHelper = PatternHelper() 68 | } 69 | 70 | private fun isPatternOk(hitIndexList: List): Boolean { 71 | patternHelper?.validateForChecking(hitIndexList) 72 | return patternHelper?.isOk == true 73 | } 74 | 75 | private fun updateMsg(textMsg: TextView) { 76 | textMsg.text = patternHelper?.message 77 | textMsg.setTextColor( 78 | if (patternHelper?.isOk == true) { 79 | ContextCompat.getColor(this, R.color.colorPrimaryDark) 80 | } else { 81 | ContextCompat.getColor(this, R.color.color_red) 82 | } 83 | ) 84 | } 85 | 86 | private fun finishIfNeeded() { 87 | if (patternHelper?.isFinish == true) { 88 | finish() 89 | } 90 | } 91 | 92 | companion object { 93 | fun startAction(context: Context) { 94 | val intent = Intent(context, SimplePatternCheckingActivity::class.java) 95 | context.startActivity(intent) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/simple/SimplePatternSettingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.simple 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.content.ContextCompat 9 | import com.github.ihsg.demo.R 10 | import com.github.ihsg.demo.databinding.ActivitySimplePatternSettingBinding 11 | import com.github.ihsg.demo.util.PatternHelper 12 | import com.github.ihsg.patternlocker.OnPatternChangeListener 13 | import com.github.ihsg.patternlocker.PatternLockerView 14 | 15 | class SimplePatternSettingActivity : AppCompatActivity() { 16 | private var patternHelper: PatternHelper? = null 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | ActivitySimplePatternSettingBinding.inflate(layoutInflater).apply { 21 | setContentView(root) 22 | patternLockerView.setOnPatternChangedListener(object : OnPatternChangeListener { 23 | override fun onStart(view: PatternLockerView) {} 24 | 25 | override fun onChange(view: PatternLockerView, hitIndexList: List) {} 26 | 27 | override fun onComplete(view: PatternLockerView, hitIndexList: List) { 28 | val isOk = isPatternOk(hitIndexList) 29 | view.updateStatus(!isOk) 30 | patternIndicatorView.updateState(hitIndexList, !isOk) 31 | updateMsg(textMsg) 32 | } 33 | 34 | override fun onClear(view: PatternLockerView) { 35 | finishIfNeeded() 36 | } 37 | }) 38 | textMsg.text = "设置解锁图案" 39 | } 40 | patternHelper = PatternHelper() 41 | } 42 | 43 | private fun isPatternOk(hitIndexList: List): Boolean { 44 | patternHelper?.validateForSetting(hitIndexList) 45 | return patternHelper?.isOk == true 46 | } 47 | 48 | private fun updateMsg(textMsg: TextView) { 49 | textMsg.text = this.patternHelper?.message 50 | textMsg.setTextColor( 51 | if (patternHelper?.isOk == true) { 52 | ContextCompat.getColor(this, R.color.colorAccent) 53 | } else { 54 | ContextCompat.getColor(this, R.color.color_red) 55 | } 56 | ) 57 | } 58 | 59 | private fun finishIfNeeded() { 60 | if (this.patternHelper?.isFinish == true) { 61 | finish() 62 | } 63 | } 64 | 65 | companion object { 66 | fun startAction(context: Context) { 67 | val intent = Intent(context, SimplePatternSettingActivity::class.java) 68 | context.startActivity(intent) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/simple/SimpleStyleActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.simple 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.github.ihsg.demo.databinding.ActivitySimpleStyleBinding 8 | 9 | class SimpleStyleActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | ActivitySimpleStyleBinding.inflate(layoutInflater).apply { 15 | setContentView(root) 16 | btnSetting.setOnClickListener { 17 | SimplePatternSettingActivity.startAction(this@SimpleStyleActivity) 18 | } 19 | btnChecking.setOnClickListener { 20 | SimplePatternCheckingActivity.startAction(this@SimpleStyleActivity) 21 | } 22 | } 23 | } 24 | 25 | companion object { 26 | fun startAction(context: Context) { 27 | val intent = Intent(context, SimpleStyleActivity::class.java) 28 | context.startActivity(intent) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/whole/RippleLockerHitCellView.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.whole 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import androidx.annotation.ColorInt 6 | 7 | import com.github.ihsg.patternlocker.CellBean 8 | import com.github.ihsg.patternlocker.IHitCellView 9 | 10 | /** 11 | * Created by hsg on 24/02/2018. 12 | */ 13 | 14 | class RippleLockerHitCellView : IHitCellView { 15 | 16 | @ColorInt 17 | private var hitColor: Int = 0 18 | @ColorInt 19 | private var errorColor: Int = 0 20 | 21 | private val paint: Paint = Paint() 22 | 23 | init { 24 | paint.isDither = true 25 | paint.isAntiAlias = true 26 | paint.strokeJoin = Paint.Join.ROUND 27 | paint.strokeCap = Paint.Cap.ROUND 28 | this.paint.style = Paint.Style.FILL 29 | } 30 | 31 | @ColorInt 32 | fun getHitColor(): Int { 33 | return hitColor 34 | } 35 | 36 | fun setHitColor(@ColorInt hitColor: Int): RippleLockerHitCellView { 37 | this.hitColor = hitColor 38 | return this 39 | } 40 | 41 | @ColorInt 42 | fun getErrorColor(): Int { 43 | return errorColor 44 | } 45 | 46 | fun setErrorColor(@ColorInt errorColor: Int): RippleLockerHitCellView { 47 | this.errorColor = errorColor 48 | return this 49 | } 50 | 51 | override fun draw(canvas: Canvas, cellBean: CellBean, isError: Boolean) { 52 | val saveCount = canvas.save() 53 | 54 | this.paint.color = getColor(isError) and 0x14FFFFFF 55 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius, this.paint) 56 | 57 | this.paint.color = getColor(isError) and 0x43FFFFFF 58 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius * 2f / 3f, this.paint) 59 | 60 | this.paint.color = getColor(isError) 61 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius / 3f, this.paint) 62 | 63 | canvas.restoreToCount(saveCount) 64 | } 65 | 66 | @ColorInt 67 | private fun getColor(isError: Boolean): Int { 68 | return if (isError) this.getErrorColor() else this.getHitColor() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/whole/WholePatternCheckingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.whole 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.content.ContextCompat 9 | import com.github.ihsg.demo.R 10 | import com.github.ihsg.demo.databinding.ActivityWholePatternCheckingBinding 11 | import com.github.ihsg.demo.util.PatternHelper 12 | import com.github.ihsg.patternlocker.DefaultLockerNormalCellView 13 | import com.github.ihsg.patternlocker.OnPatternChangeListener 14 | import com.github.ihsg.patternlocker.PatternLockerView 15 | 16 | class WholePatternCheckingActivity : AppCompatActivity() { 17 | private var patternHelper: PatternHelper? = null 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | ActivityWholePatternCheckingBinding.inflate(layoutInflater).apply { 23 | setContentView(root) 24 | 25 | val decorator = (patternLockerView.normalCellView as DefaultLockerNormalCellView).styleDecorator 26 | 27 | patternLockerView.hitCellView = RippleLockerHitCellView() 28 | .setHitColor(decorator.hitColor) 29 | .setErrorColor(decorator.errorColor) 30 | 31 | patternLockerView.setOnPatternChangedListener(object : OnPatternChangeListener { 32 | override fun onStart(view: PatternLockerView) {} 33 | 34 | override fun onChange(view: PatternLockerView, hitIndexList: List) {} 35 | 36 | override fun onComplete(view: PatternLockerView, hitIndexList: List) { 37 | val isError = !isPatternOk(hitIndexList) 38 | view.updateStatus(isError) 39 | patternIndicatorView.updateState(hitIndexList, isError) 40 | updateMsg(textMsg) 41 | } 42 | 43 | override fun onClear(view: PatternLockerView) { 44 | finishIfNeeded() 45 | } 46 | }) 47 | textMsg.text = "绘制解锁图案" 48 | } 49 | 50 | patternHelper = PatternHelper() 51 | } 52 | 53 | private fun isPatternOk(hitIndexList: List): Boolean { 54 | patternHelper?.validateForChecking(hitIndexList) 55 | return patternHelper?.isOk == true 56 | } 57 | 58 | private fun updateMsg(textMsg: TextView) { 59 | textMsg.text = patternHelper?.message 60 | textMsg.setTextColor( 61 | if (this.patternHelper?.isOk == true) { 62 | ContextCompat.getColor(this, R.color.colorPrimaryDark) 63 | } else { 64 | ContextCompat.getColor(this, R.color.color_red) 65 | } 66 | ) 67 | } 68 | 69 | private fun finishIfNeeded() { 70 | if (patternHelper?.isFinish == true) { 71 | finish() 72 | } 73 | } 74 | 75 | companion object { 76 | fun startAction(context: Context) { 77 | val intent = Intent(context, WholePatternCheckingActivity::class.java) 78 | context.startActivity(intent) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/whole/WholePatternSettingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.whole 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.content.ContextCompat 9 | import com.github.ihsg.demo.R 10 | import com.github.ihsg.demo.databinding.ActivityWholePatternSettingBinding 11 | import com.github.ihsg.demo.util.PatternHelper 12 | import com.github.ihsg.patternlocker.DefaultLockerNormalCellView 13 | import com.github.ihsg.patternlocker.OnPatternChangeListener 14 | import com.github.ihsg.patternlocker.PatternLockerView 15 | 16 | class WholePatternSettingActivity : AppCompatActivity() { 17 | private var patternHelper: PatternHelper? = null 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | ActivityWholePatternSettingBinding.inflate(layoutInflater).apply { 23 | setContentView(root) 24 | 25 | val decorator = (patternLockerView.normalCellView as DefaultLockerNormalCellView).styleDecorator 26 | 27 | patternLockerView.hitCellView = RippleLockerHitCellView() 28 | .setHitColor(decorator.hitColor) 29 | .setErrorColor(decorator.errorColor) 30 | 31 | patternLockerView.setOnPatternChangedListener(object : OnPatternChangeListener { 32 | override fun onStart(view: PatternLockerView) {} 33 | 34 | override fun onChange(view: PatternLockerView, hitIndexList: List) {} 35 | 36 | override fun onComplete(view: PatternLockerView, hitIndexList: List) { 37 | val isOk = isPatternOk(hitIndexList) 38 | view.updateStatus(!isOk) 39 | patternIndicatorView.updateState(hitIndexList, !isOk) 40 | updateMsg(textMsg) 41 | } 42 | 43 | override fun onClear(view: PatternLockerView) { 44 | finishIfNeeded() 45 | } 46 | }) 47 | 48 | textMsg.text = "设置解锁图案" 49 | btnClean.setOnClickListener { patternLockerView.clearHitState() } 50 | } 51 | 52 | patternHelper = PatternHelper() 53 | 54 | } 55 | 56 | private fun isPatternOk(hitIndexList: List): Boolean { 57 | patternHelper?.validateForSetting(hitIndexList) 58 | return patternHelper?.isOk == true 59 | } 60 | 61 | private fun updateMsg(textMsg: TextView) { 62 | textMsg.text = patternHelper?.message 63 | textMsg.setTextColor( 64 | if (patternHelper?.isOk == true) { 65 | ContextCompat.getColor(this, R.color.colorPrimaryDark) 66 | } else { 67 | ContextCompat.getColor(this, R.color.color_red) 68 | } 69 | ) 70 | } 71 | 72 | private fun finishIfNeeded() { 73 | if (patternHelper?.isFinish == true) { 74 | finish() 75 | } 76 | } 77 | 78 | companion object { 79 | fun startAction(context: Context) { 80 | val intent = Intent(context, WholePatternSettingActivity::class.java) 81 | context.startActivity(intent) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/ui/whole/WholeStyleActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.ui.whole 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.github.ihsg.demo.databinding.ActivityWholeStyleBinding 8 | 9 | class WholeStyleActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | ActivityWholeStyleBinding.inflate(layoutInflater).apply { 15 | setContentView(root) 16 | 17 | btnSetting.setOnClickListener { 18 | WholePatternSettingActivity.startAction(this@WholeStyleActivity) 19 | } 20 | 21 | btnChecking.setOnClickListener { 22 | WholePatternCheckingActivity.startAction(this@WholeStyleActivity) 23 | } 24 | } 25 | } 26 | 27 | companion object { 28 | fun startAction(context: Context) { 29 | val intent = Intent(context, WholeStyleActivity::class.java) 30 | context.startActivity(intent) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/util/PatternHelper.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.util 2 | 3 | import android.text.TextUtils 4 | import com.github.ihsg.demo.util.SecurityUtil.encrypt 5 | import com.github.ihsg.demo.util.SharedPreferencesUtil.Companion.instance 6 | 7 | /** 8 | * Created by hsg on 14/10/2017. 9 | */ 10 | open class PatternHelper { 11 | var message: String? = null 12 | private set 13 | private var storagePwd: String? = null 14 | private var tmpPwd: String? = null 15 | private var times = 0 16 | var isFinish = false 17 | private set 18 | var isOk = false 19 | private set 20 | 21 | fun validateForSetting(hitIndexList: List?) { 22 | isFinish = false 23 | isOk = false 24 | if (hitIndexList == null || hitIndexList.size < MAX_SIZE) { 25 | tmpPwd = null 26 | message = sizeErrorMsg 27 | return 28 | } 29 | //1. draw first time 30 | if (TextUtils.isEmpty(tmpPwd)) { 31 | tmpPwd = convert2String(hitIndexList) 32 | message = reDrawMsg 33 | isOk = true 34 | return 35 | } 36 | //2. draw second times 37 | if (tmpPwd == convert2String(hitIndexList)) { 38 | message = settingSuccessMsg 39 | saveToStorage(tmpPwd) 40 | isOk = true 41 | isFinish = true 42 | } else { 43 | tmpPwd = null 44 | message = diffPreErrorMsg 45 | } 46 | } 47 | 48 | fun validateForChecking(hitIndexList: List?) { 49 | isOk = false 50 | if (hitIndexList == null || hitIndexList.size < MAX_SIZE) { 51 | times++ 52 | isFinish = times >= MAX_SIZE 53 | message = pwdErrorMsg 54 | return 55 | } 56 | storagePwd = fromStorage 57 | if (!TextUtils.isEmpty(storagePwd) && storagePwd == convert2String(hitIndexList)) { 58 | message = checkingSuccessMsg 59 | isOk = true 60 | isFinish = true 61 | } else { 62 | times++ 63 | isFinish = times >= MAX_SIZE 64 | message = pwdErrorMsg 65 | } 66 | } 67 | 68 | private val remainTimes: Int 69 | get() = if (times < 5) MAX_TIMES - times else 0 70 | private val reDrawMsg: String = "请再次绘制解锁图案" 71 | private val settingSuccessMsg: String = "手势解锁图案设置成功!" 72 | private val checkingSuccessMsg: String = "解锁成功!" 73 | private val sizeErrorMsg: String = String.format("至少连接个%d点,请重新绘制", MAX_SIZE) 74 | private val diffPreErrorMsg: String = "与上次绘制不一致,请重新绘制" 75 | private val pwdErrorMsg: String 76 | get() = String.format("密码错误,还剩%d次机会", remainTimes) 77 | 78 | private fun convert2String(hitIndexList: List): String { 79 | return hitIndexList.toString() 80 | } 81 | 82 | private fun saveToStorage(gesturePwd: String?) { 83 | val encryptPwd = encrypt(gesturePwd!!) 84 | instance!!.saveString(GESTURE_PWD_KEY, encryptPwd) 85 | } 86 | 87 | private val fromStorage: String? = instance?.getString(GESTURE_PWD_KEY)?.let { SecurityUtil.decrypt(it) } 88 | 89 | 90 | companion object { 91 | const val MAX_SIZE = 4 92 | const val MAX_TIMES = 5 93 | private const val GESTURE_PWD_KEY = "gesture_pwd_key" 94 | } 95 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/util/SecurityUtil.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.util 2 | 3 | import java.io.UnsupportedEncodingException 4 | import java.util.* 5 | import javax.crypto.Cipher 6 | import javax.crypto.spec.SecretKeySpec 7 | 8 | /** 9 | * AES encrypt/decrypt utility 10 | * Created by hsg on 14/10/2017. 11 | */ 12 | object SecurityUtil { 13 | private const val CIPHER_MODE = "AES/ECB/PKCS5Padding" 14 | private const val MASTER_PASSWORD = "Test123454321" 15 | 16 | private fun createKey(password: String?): SecretKeySpec { 17 | var data: ByteArray? = null 18 | val sb = StringBuffer(32) 19 | sb.append(password ?: "") 20 | while (sb.length < 32) { 21 | sb.append("0") 22 | } 23 | if (sb.length > 32) { 24 | sb.setLength(32) 25 | } 26 | try { 27 | data = sb.toString().toByteArray(charset("UTF-8")) 28 | } catch (e: UnsupportedEncodingException) { 29 | e.printStackTrace() 30 | } 31 | return SecretKeySpec(data, "AES") 32 | } 33 | 34 | private fun encrypt(content: ByteArray, password: String): ByteArray { 35 | val key = createKey(password) 36 | val cipher = Cipher.getInstance(CIPHER_MODE) 37 | cipher.init(Cipher.ENCRYPT_MODE, key) 38 | return cipher.doFinal(content) 39 | } 40 | 41 | fun encrypt(content: String, password: String = MASTER_PASSWORD): String { 42 | var data = content.toByteArray(charset("UTF-8")) 43 | data = encrypt(data, password) 44 | return byte2hex(data) 45 | } 46 | 47 | private fun decrypt(content: ByteArray, password: String): ByteArray { 48 | val key = createKey(password) 49 | val cipher = Cipher.getInstance(CIPHER_MODE) 50 | cipher.init(Cipher.DECRYPT_MODE, key) 51 | return cipher.doFinal(content) 52 | } 53 | 54 | fun decrypt(content: String, password: String = MASTER_PASSWORD): String? { 55 | var data = hex2byte(content) 56 | data = decrypt(data, password) 57 | return data.toString(charset("UTF-8")) 58 | } 59 | 60 | private fun byte2hex(b: ByteArray): String { // 一个字节的数, 61 | val sb = StringBuffer(b.size * 2) 62 | var tmp: String 63 | for (n in b.indices) { // 整数转成十六进制表示 64 | tmp = Integer.toHexString(b[n].toInt() and 0XFF) 65 | if (tmp.length == 1) { 66 | sb.append("0") 67 | } 68 | sb.append(tmp) 69 | } 70 | return sb.toString().uppercase(Locale.ROOT) // 转成大写 71 | } 72 | 73 | private fun hex2byte(inputString: String): ByteArray { 74 | if (inputString.length < 2) { 75 | return ByteArray(0) 76 | } 77 | val str = inputString.lowercase(Locale.ROOT) 78 | val l = inputString.length / 2 79 | val result = ByteArray(l) 80 | for (i in 0 until l) { 81 | val tmp = str.substring(2 * i, 2 * i + 2) 82 | result[i] = (tmp.toInt(16) and 0xFF).toByte() 83 | } 84 | return result 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/ihsg/demo/util/SharedPreferencesUtil.kt: -------------------------------------------------------------------------------- 1 | package com.github.ihsg.demo.util 2 | 3 | import android.content.SharedPreferences 4 | import com.github.ihsg.demo.TestApplication 5 | 6 | /** 7 | * Created by hsg on 14/10/2017. 8 | */ 9 | internal class SharedPreferencesUtil private constructor() { 10 | private val prefer: SharedPreferences = TestApplication.getContext() 11 | .getSharedPreferences("test", android.content.Context.MODE_PRIVATE) 12 | private val editor: SharedPreferences.Editor = prefer.edit() 13 | 14 | fun saveString(name: String?, data: String?) { 15 | editor.putString(name, data) 16 | editor.commit() 17 | } 18 | 19 | fun getString(name: String?): String? { 20 | return prefer.getString(name, null) 21 | } 22 | 23 | companion object { 24 | @JvmStatic 25 | var instance: SharedPreferencesUtil? = null 26 | get() { 27 | if (field == null) { 28 | synchronized(SharedPreferencesUtil::class.java) { 29 | if (field == null) { 30 | field = SharedPreferencesUtil() 31 | } 32 | } 33 | } 34 | return field 35 | } 36 | private set 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_default_pattern_checking.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 23 | 24 | 30 | 31 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_default_pattern_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 23 | 24 | 30 | 31 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_default_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 |