├── .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 | 
18 |
19 | [点击下载PatternLockerDemo.apk](http://d.6short.com/qvhx) 或者扫描下方二维码下载安装
20 |
21 | 
22 |
23 | ## 基本用法
24 |
25 | [](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 | 
--------------------------------------------------------------------------------
/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 |
17 |
18 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
14 |
18 |
19 |
24 |
25 |
30 |
31 |
36 |
37 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_simple_pattern_checking.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
23 |
24 |
30 |
31 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_simple_pattern_setting.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
23 |
24 |
35 |
36 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_simple_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
17 |
18 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_whole_pattern_checking.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
23 |
24 |
35 |
36 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_whole_pattern_setting.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
23 |
24 |
35 |
36 |
51 |
52 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_whole_style.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
17 |
18 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 | #FFFFFF
7 |
8 | #F44336
9 | #e91e63
10 | #9c27b0
11 | #2196f3
12 | #4caf50
13 | #212121
14 | #668a989e
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 手势解锁demo
3 | 默认样式
4 | 简单自定义样式
5 | 完全自定义样式
6 |
7 | 设置手势密码
8 | 验证手势密码
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 |
3 | /**
4 | * Use `apply false` in the top-level build.gradle file to add a Gradle
5 | * plugin as a build dependency but not apply it to the current (root)
6 | * project. Don't use `apply false` in sub-projects. For more information,
7 | * see Applying external plugins with same version to subprojects.
8 | */
9 | alias(libs.plugins.android.application) apply false
10 | alias(libs.plugins.android.library) apply false
11 | alias(libs.plugins.kotlin.android) apply false
12 | alias(libs.plugins.maven.publish) apply false
13 | }
14 |
--------------------------------------------------------------------------------
/captures/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/1.png
--------------------------------------------------------------------------------
/captures/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/2.png
--------------------------------------------------------------------------------
/captures/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/3.png
--------------------------------------------------------------------------------
/captures/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/4.png
--------------------------------------------------------------------------------
/captures/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/5.png
--------------------------------------------------------------------------------
/captures/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/6.png
--------------------------------------------------------------------------------
/captures/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/7.png
--------------------------------------------------------------------------------
/captures/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/8.png
--------------------------------------------------------------------------------
/captures/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/9.png
--------------------------------------------------------------------------------
/captures/captures.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/captures.jpg
--------------------------------------------------------------------------------
/captures/captures.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/captures.png
--------------------------------------------------------------------------------
/captures/download_xcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/download_xcode.png
--------------------------------------------------------------------------------
/captures/hsg_Wechat_QR.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/captures/hsg_Wechat_QR.jpeg
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 | android.useAndroidX=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | minSdk = "24"
3 | compileSdk = "34"
4 | targetSdk = "34"
5 | versionCode = "21"
6 | versionName = "2.5.7"
7 |
8 | agp = "8.5.2"
9 | kotlin = "1.9.23"
10 | coreKtx = "1.13.0"
11 | appcompat = "1.7.0"
12 | junit = "4.13.2"
13 | junitVersion = "1.1.5"
14 | espressoCore = "3.5.1"
15 | lifecycleRuntimeKtx = "2.6.2"
16 | activityCompose = "1.8.0"
17 | composeBom = "2024.04.01"
18 | mavenPublish = "0.28.0"
19 |
20 | [libraries]
21 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
22 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
23 | junit = { group = "junit", name = "junit", version.ref = "junit" }
24 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
25 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
26 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
27 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
28 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
29 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
30 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
31 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
32 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
33 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
34 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
35 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
36 |
37 | [plugins]
38 | android-application = { id = "com.android.application", version.ref = "agp" }
39 | android-library = { id = "com.android.library", version.ref = "agp" }
40 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
41 | maven-publish = {id="com.vanniktech.maven.publish", version.ref = "mavenPublish"}
42 |
43 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ihsg/PatternLocker/af35beabac262968548332d9752ec0547c14b258/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Dec 29 15:08:36 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.library)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.maven.publish)
5 | }
6 |
7 | group = "com.github.ihsg"
8 |
9 | afterEvaluate {
10 | publishing {
11 | publications {
12 | // Creates a Maven publication called "release".
13 | create("release") {
14 | // Applies the component for the release build variant.\
15 | // from(components["release"])
16 | // You can then customize attributes of the publication as shown below.
17 | groupId = (group.toString())
18 | artifactId = "PatternLocker"
19 | version = libs.versions.versionName.get()
20 | }
21 | }
22 | }
23 | }
24 |
25 | android {
26 | namespace = "com.github.ihsg.patternlocker"
27 | compileSdk = libs.versions.compileSdk.get().toInt()
28 |
29 | defaultConfig {
30 | minSdk = libs.versions.minSdk.get().toInt()
31 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
32 | }
33 |
34 | buildTypes {
35 | release {
36 | isMinifyEnabled = false
37 | proguardFiles(
38 | getDefaultProguardFile("proguard-android-optimize.txt"),
39 | "proguard-rules.pro"
40 | )
41 | }
42 | }
43 | compileOptions {
44 | sourceCompatibility = JavaVersion.VERSION_17
45 | targetCompatibility = JavaVersion.VERSION_17
46 | }
47 | kotlinOptions {
48 | jvmTarget = "17"
49 | }
50 | composeOptions {
51 | kotlinCompilerExtensionVersion = "1.5.1"
52 | }
53 | packaging {
54 | resources {
55 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
56 | }
57 | }
58 | }
59 |
60 | dependencies {
61 | implementation(libs.androidx.core.ktx)
62 | implementation(libs.androidx.appcompat)
63 | testImplementation(libs.junit)
64 | androidTestImplementation(libs.androidx.junit)
65 | }
--------------------------------------------------------------------------------
/library/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 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/CellBean.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import kotlin.math.sqrt
4 |
5 | /**
6 | * Created by hsg on 20/09/2017.
7 | *
8 | * @param id 表示该cell的编号,9个cell的编号如下:
9 | *
10 | * 0 1 2
11 | * 3 4 5
12 | * 6 7 8
13 | *
14 | * @param x 表示该cell的x坐标(点坐标)
15 | * @param y 表示该cell的y坐标(点坐标)
16 | * x,y 点坐标编号如下:
17 | * (0,0) (1,0) (2,0)
18 | * (0,1) (1,1) (2,1)
19 | * (0,2) (1,2) (2,2)
20 | *
21 | * @param centerX 表示该cell的圆心x坐标(相对坐标)
22 | * @param centerY 表示该cell的圆心y坐标(相对坐标)
23 | * centerX, centerY 圆心坐标如下:
24 | * (radius, radius) (4radius, radius) (7radius, radius)
25 | * (radius, 4radius) (4radius, 4radius) (7radius, 4radius)
26 | * (radius, 7radius) (4radius, 7radius) (7radius, 7radius)
27 | *
28 | * @param radius 表示该cell的半径
29 | * @param isHit 表示该cell是否被设置的标记
30 | */
31 | data class CellBean(
32 | val id: Int,
33 | val x: Int,
34 | val y: Int,
35 | val centerX: Float,
36 | val centerY: Float,
37 | val radius: Float,
38 | var isHit: Boolean = false
39 | ) {
40 | /**
41 | * 是否触碰到该view
42 | *
43 | * @param x
44 | * @param y
45 | * @return
46 | */
47 | fun of(x: Float, y: Float): Boolean {
48 | val dx = this.centerX - x
49 | val dy = this.centerY - y
50 | return sqrt((dx * dx + dy * dy)).toDouble() <= this.radius.toDouble()
51 | }
52 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/CellFactory.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | /**
4 | * Created by hsg on 20/09/2017.
5 | */
6 |
7 | object CellFactory {
8 |
9 | fun buildCells(width: Int, height: Int): List {
10 | val result = ArrayList()
11 | val pWidth = width / 8f
12 | val pHeight = height / 8f
13 |
14 | for (i in 0..8) {
15 | result.add(CellBean(i,
16 | i % 3,
17 | i / 3,
18 | (i % 3 * 3 + 1) * pWidth,
19 | (i / 3 * 3 + 1) * pHeight,
20 | pWidth))
21 | }
22 |
23 | Logger.d("CellFactory", "result = $result")
24 |
25 | return result
26 | }
27 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultConfig.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.content.res.Resources
4 | import android.graphics.Color
5 | import android.graphics.Paint
6 | import android.util.TypedValue
7 | import androidx.annotation.ColorInt
8 |
9 | /**
10 | * Created by hsg on 23/09/2017.
11 | */
12 | internal object DefaultConfig {
13 | private const val DEFAULT_NORMAL_COLOR = "#2196F3"
14 | private const val DEFAULT_HIT_COLOR = "#3F51B5"
15 | private const val DEFAULT_ERROR_COLOR = "#F44336"
16 | private const val DEFAULT_FILL_COLOR = "#FFFFFF"
17 | private const val DEFAULT_LINE_WIDTH = 1
18 |
19 | const val defaultFreezeDuration = 1000//ms
20 | const val defaultEnableAutoClean = true
21 | const val defaultEnableHapticFeedback = false
22 | const val defaultEnableSkip = false
23 | const val defaultEnableLogger = false
24 |
25 | @ColorInt
26 | val defaultNormalColor: Int = Color.parseColor(DEFAULT_NORMAL_COLOR)
27 |
28 | @ColorInt
29 | val defaultHitColor: Int = Color.parseColor(DEFAULT_HIT_COLOR)
30 |
31 | @ColorInt
32 | val defaultErrorColor: Int = Color.parseColor(DEFAULT_ERROR_COLOR)
33 |
34 | @ColorInt
35 | val defaultFillColor: Int = Color.parseColor(DEFAULT_FILL_COLOR)
36 |
37 | fun getDefaultLineWidth(resources: Resources): Float {
38 | return convertDpToPx(DEFAULT_LINE_WIDTH.toFloat(), resources)
39 | }
40 |
41 | fun createPaint(): Paint {
42 | val paint = Paint()
43 | paint.isDither = true
44 | paint.isAntiAlias = true
45 | paint.strokeJoin = Paint.Join.ROUND
46 | paint.strokeCap = Paint.Cap.ROUND
47 | return paint
48 | }
49 |
50 | private fun convertDpToPx(dpValue: Float, resources: Resources): Float {
51 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, resources.displayMetrics)
52 | }
53 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultIndicatorHitCellView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import androidx.annotation.ColorInt
6 |
7 | /**
8 | * Created by hsg on 22/02/2018.
9 | */
10 |
11 | open class DefaultIndicatorHitCellView(val styleDecorator: DefaultStyleDecorator) : IHitCellView {
12 |
13 | private val paint: Paint by lazy {
14 | DefaultConfig.createPaint()
15 | }
16 |
17 | init {
18 | this.paint.style = Paint.Style.FILL
19 | }
20 |
21 | override fun draw(canvas: Canvas, cellBean: CellBean, isError: Boolean) {
22 | val saveCount = canvas.save()
23 |
24 | this.paint.color = this.getColor(isError)
25 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius, this.paint)
26 |
27 | canvas.restoreToCount(saveCount)
28 | }
29 |
30 | @ColorInt
31 | private fun getColor(isError: Boolean): Int {
32 | return if (isError) this.styleDecorator.errorColor else this.styleDecorator.hitColor
33 | }
34 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultIndicatorLinkedLineView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import android.graphics.Path
6 | import androidx.annotation.ColorInt
7 |
8 | /**
9 | * Created by hsg on 22/02/2018.
10 | */
11 |
12 | open class DefaultIndicatorLinkedLineView(val styleDecorator: DefaultStyleDecorator) : IIndicatorLinkedLineView {
13 |
14 | private val paint: Paint by lazy {
15 | DefaultConfig.createPaint()
16 | }
17 |
18 | init {
19 | this.paint.style = Paint.Style.STROKE
20 | }
21 |
22 | override fun draw(canvas: Canvas, hitIndexList: List, cellBeanList: List, isError: Boolean) {
23 | if (hitIndexList.isEmpty() || cellBeanList.isEmpty()) {
24 | return
25 | }
26 |
27 | val saveCount = canvas.save()
28 | val path = Path()
29 | var first = true
30 |
31 | hitIndexList.forEach {
32 | if (0 <= it && it < cellBeanList.size) {
33 | val c = cellBeanList[it]
34 | if (first) {
35 | path.moveTo(c.centerX, c.centerY)
36 | first = false
37 | } else {
38 | path.lineTo(c.centerX, c.centerY)
39 | }
40 | }
41 | }
42 |
43 | this.paint.color = this.getColor(isError)
44 | this.paint.strokeWidth = this.styleDecorator.lineWidth
45 | canvas.drawPath(path, this.paint)
46 | canvas.restoreToCount(saveCount)
47 | }
48 |
49 | @ColorInt
50 | private fun getColor(isError: Boolean): Int {
51 | return if (isError) this.styleDecorator.errorColor else this.styleDecorator.hitColor
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultIndicatorNormalCellView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 |
6 | /**
7 | * Created by hsg on 22/02/2018.
8 | */
9 |
10 | open class DefaultIndicatorNormalCellView(val styleDecorator: DefaultStyleDecorator) : INormalCellView {
11 |
12 | private val paint: Paint by lazy {
13 | DefaultConfig.createPaint()
14 | }
15 |
16 | init {
17 | this.paint.style = Paint.Style.FILL
18 | }
19 |
20 | override fun draw(canvas: Canvas, cellBean: CellBean) {
21 | val saveCount = canvas.save()
22 |
23 | //outer circle
24 | this.paint.color = this.styleDecorator.normalColor
25 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius, this.paint)
26 |
27 | //inner circle
28 | this.paint.color = this.styleDecorator.fillColor
29 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius - this.styleDecorator.lineWidth, this.paint)
30 |
31 | canvas.restoreToCount(saveCount)
32 | }
33 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultLockerHitCellView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import androidx.annotation.ColorInt
6 |
7 | /**
8 | * Created by hsg on 22/02/2018.
9 | */
10 |
11 | open class DefaultLockerHitCellView(val styleDecorator: DefaultStyleDecorator) : IHitCellView {
12 |
13 | private val paint: Paint by lazy {
14 | DefaultConfig.createPaint()
15 | }
16 |
17 | init {
18 | this.paint.style = Paint.Style.FILL
19 | }
20 |
21 | override fun draw(canvas: Canvas, cellBean: CellBean, isError: Boolean) {
22 | val saveCount = canvas.save()
23 |
24 | // draw outer circle
25 | this.paint.color = this.getColor(isError)
26 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius, this.paint)
27 |
28 | // draw fill circle
29 | this.paint.color = this.styleDecorator.fillColor
30 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius - this.styleDecorator.lineWidth, this.paint)
31 |
32 | // draw inner circle
33 | this.paint.color = this.getColor(isError)
34 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius / 5f, this.paint)
35 |
36 | canvas.restoreToCount(saveCount)
37 | }
38 |
39 | @ColorInt
40 | private fun getColor(isError: Boolean): Int {
41 | return if (isError) this.styleDecorator.errorColor else this.styleDecorator.hitColor
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultLockerLinkedLineView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import android.graphics.Path
6 | import androidx.annotation.ColorInt
7 |
8 | /**
9 | * Created by hsg on 22/02/2018.
10 | */
11 |
12 | open class DefaultLockerLinkedLineView(val styleDecorator: DefaultStyleDecorator) : ILockerLinkedLineView {
13 | companion object {
14 | private const val TAG = "DefaultLockerLinkedLineView"
15 | }
16 |
17 | private val paint: Paint by lazy {
18 | DefaultConfig.createPaint()
19 | }
20 |
21 | init {
22 | this.paint.style = Paint.Style.STROKE
23 | }
24 |
25 | override fun draw(canvas: Canvas, hitIndexList: List, cellBeanList: List, endX: Float, endY: Float, isError: Boolean) {
26 | Logger.d(TAG, "hitIndexList = $hitIndexList, cellBeanList = $cellBeanList")
27 | if (hitIndexList.isEmpty() || cellBeanList.isEmpty()) {
28 | return
29 | }
30 |
31 | val saveCount = canvas.save()
32 | val path = Path()
33 | var first = true
34 |
35 | hitIndexList.forEach {
36 | if (0 <= it && it < cellBeanList.size) {
37 | val c = cellBeanList[it]
38 | if (first) {
39 | path.moveTo(c.centerX, c.centerY)
40 | first = false
41 | } else {
42 | path.lineTo(c.centerX, c.centerY)
43 | }
44 | }
45 | }
46 |
47 | if ((endX != 0f || endY != 0f) && hitIndexList.size < 9) {
48 | path.lineTo(endX, endY)
49 | }
50 |
51 | this.paint.color = this.getColor(isError)
52 | this.paint.strokeWidth = this.styleDecorator.lineWidth
53 | canvas.drawPath(path, this.paint)
54 |
55 | canvas.restoreToCount(saveCount)
56 | }
57 |
58 | @ColorInt
59 | private fun getColor(isError: Boolean): Int {
60 | return if (isError) this.styleDecorator.errorColor else this.styleDecorator.hitColor
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultLockerNormalCellView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 |
6 | /**
7 | * Created by hsg on 22/02/2018.
8 | */
9 |
10 | open class DefaultLockerNormalCellView(val styleDecorator: DefaultStyleDecorator) : INormalCellView {
11 | private val paint: Paint by lazy {
12 | DefaultConfig.createPaint()
13 | }
14 |
15 | init {
16 | this.paint.style = Paint.Style.FILL
17 | }
18 |
19 | override fun draw(canvas: Canvas, cellBean: CellBean) {
20 | val saveCount = canvas.save()
21 |
22 | // draw outer circle
23 | this.paint.color = this.styleDecorator.normalColor
24 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius, this.paint)
25 |
26 | // draw fill circle
27 | this.paint.color = this.styleDecorator.fillColor
28 | canvas.drawCircle(cellBean.centerX, cellBean.centerY, cellBean.radius - this.styleDecorator.lineWidth, this.paint)
29 |
30 | canvas.restoreToCount(saveCount)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/DefaultStyleDecorator.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import androidx.annotation.ColorInt
4 |
5 |
6 | /**
7 | * Created by hsg on 28/04/2019.
8 | */
9 |
10 | data class DefaultStyleDecorator(
11 | @ColorInt var normalColor: Int,
12 | @ColorInt var fillColor: Int,
13 | @ColorInt var hitColor: Int,
14 | @ColorInt var errorColor: Int,
15 | var lineWidth: Float
16 | )
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/IHitCellView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 |
5 | /**
6 | * Created by hsg on 22/02/2018.
7 | */
8 |
9 | interface IHitCellView {
10 | /**
11 | * 绘制已设置的每个图案的样式
12 | *
13 | * @param canvas
14 | * @param cellBean
15 | * @param isError
16 | */
17 | fun draw(
18 | canvas: Canvas,
19 | cellBean: CellBean,
20 | isError: Boolean
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/IIndicatorLinkedLineView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 |
5 | /**
6 | * Created by hsg on 22/02/2018.
7 | */
8 |
9 | interface IIndicatorLinkedLineView {
10 | /**
11 | * 绘制指示器连接线
12 | *
13 | * @param canvas
14 | * @param hitIndexList
15 | * @param cellBeanList
16 | * @param isError
17 | */
18 | fun draw(
19 | canvas: Canvas,
20 | hitIndexList: List,
21 | cellBeanList: List,
22 | isError: Boolean
23 | )
24 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/ILockerLinkedLineView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 |
5 | /**
6 | * Created by hsg on 22/02/2018.
7 | */
8 |
9 | interface ILockerLinkedLineView {
10 | /**
11 | * 绘制图案密码连接线
12 | *
13 | * @param canvas
14 | * @param hitIndexList
15 | * @param cellBeanList
16 | * @param endX
17 | * @param endY
18 | * @param isError
19 | */
20 | fun draw(
21 | canvas: Canvas,
22 | hitIndexList: List,
23 | cellBeanList: List,
24 | endX: Float,
25 | endY: Float,
26 | isError: Boolean
27 | )
28 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/INormalCellView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.graphics.Canvas
4 |
5 | /**
6 | * Created by hsg on 22/02/2018.
7 | */
8 |
9 | interface INormalCellView {
10 | /**
11 | * 绘制正常情况下(即未设置的)每个图案的样式
12 | *
13 | * @param canvas
14 | * @param cellBean the target cell view
15 | */
16 | fun draw(
17 | canvas: Canvas,
18 | cellBean: CellBean
19 | )
20 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.util.Log
4 |
5 | internal class Logger {
6 | companion object {
7 | var enable = false
8 | fun d(tag: String, msg: String) {
9 | if (enable) {
10 | Log.d(tag, msg)
11 | }
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/OnPatternChangeListener.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | /**
4 | * Created by hsg on 14/10/2017.
5 | */
6 |
7 | interface OnPatternChangeListener {
8 | /**
9 | * 开始绘制图案时(即手指按下触碰到绘画区域时)会调用该方法
10 | *
11 | * @param view
12 | */
13 | fun onStart(view: PatternLockerView)
14 |
15 | /**
16 | * 图案绘制改变时(即手指在绘画区域移动时)会调用该方法,请注意只有 @param hitList改变了才会触发此方法
17 | *
18 | * @param view
19 | * @param hitIndexList
20 | */
21 | fun onChange(view: PatternLockerView, hitIndexList: List)
22 |
23 | /**
24 | * 图案绘制完成时(即手指抬起离开绘画区域时)会调用该方法
25 | *
26 | * @param view
27 | * @param hitIndexList
28 | */
29 | fun onComplete(view: PatternLockerView, hitIndexList: List)
30 |
31 | /**
32 | * 已绘制的图案被清除时会调用该方法
33 | *
34 | * @param view
35 | */
36 | fun onClear(view: PatternLockerView)
37 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/PatternIndicatorView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.util.AttributeSet
6 | import android.view.View
7 | import kotlin.math.min
8 |
9 | /**
10 | * Created by hsg on 20/09/2017.
11 | */
12 |
13 | open class PatternIndicatorView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
14 | companion object {
15 | private const val TAG = "PatternIndicatorView"
16 | }
17 |
18 | var linkedLineView: IIndicatorLinkedLineView? = null
19 | var normalCellView: INormalCellView? = null
20 | var hitCellView: IHitCellView? = null
21 |
22 | private var isError: Boolean = false
23 | private var hitIndexList = emptyList()
24 | private val cellBeanList: List by lazy {
25 | val w = this.width - this.paddingLeft - this.paddingRight
26 | val h = this.height - this.paddingTop - this.paddingBottom
27 | CellFactory.buildCells(w, h)
28 | }
29 |
30 | init {
31 | init(context, attrs, defStyleAttr)
32 | }
33 |
34 | fun updateState(hitIndexList: List?, isError: Boolean) {
35 | this.hitIndexList = hitIndexList ?: emptyList()
36 | this.isError = isError
37 | invalidate()
38 | }
39 |
40 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
41 | val a = min(widthMeasureSpec, heightMeasureSpec)
42 | super.onMeasure(a, a)
43 | }
44 |
45 | override fun onDraw(canvas: Canvas) {
46 | this.updateHitState()
47 | this.drawLinkedLine(canvas)
48 | this.drawCells(canvas)
49 | }
50 |
51 | private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
52 | this.initAttrs(context, attrs, defStyleAttr)
53 | }
54 |
55 | private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
56 | val ta = context.obtainStyledAttributes(attrs, R.styleable.PatternIndicatorView, defStyleAttr, 0)
57 |
58 | val normalColor = ta.getColor(R.styleable.PatternIndicatorView_piv_color, DefaultConfig.defaultNormalColor)
59 | val fillColor = ta.getColor(R.styleable.PatternIndicatorView_piv_fillColor, DefaultConfig.defaultFillColor)
60 | val hitColor = ta.getColor(R.styleable.PatternIndicatorView_piv_hitColor, DefaultConfig.defaultHitColor)
61 | val errorColor = ta.getColor(R.styleable.PatternIndicatorView_piv_errorColor, DefaultConfig.defaultErrorColor)
62 | val lineWidth = ta.getDimension(R.styleable.PatternIndicatorView_piv_lineWidth, DefaultConfig.getDefaultLineWidth(resources))
63 |
64 | ta.recycle()
65 |
66 | val decorator = DefaultStyleDecorator(normalColor, fillColor, hitColor, errorColor, lineWidth)
67 | this.normalCellView = DefaultIndicatorNormalCellView(decorator)
68 | this.hitCellView = DefaultIndicatorHitCellView(decorator)
69 | this.linkedLineView = DefaultIndicatorLinkedLineView(decorator)
70 | }
71 |
72 | private fun updateHitState() {
73 | //1. clear pre state
74 | this.cellBeanList.forEach {
75 | it.isHit = false
76 | }
77 |
78 | //2. update hit state
79 | this.hitIndexList.forEach {
80 | if (0 <= it && it < this.cellBeanList.size) {
81 | this.cellBeanList[it].isHit = true
82 | }
83 | }
84 | }
85 |
86 | private fun drawLinkedLine(canvas: Canvas) {
87 | if (this.hitIndexList.isNotEmpty()) {
88 | this.linkedLineView?.draw(canvas,
89 | this.hitIndexList,
90 | this.cellBeanList,
91 | this.isError)
92 | }
93 | }
94 |
95 | private fun drawCells(canvas: Canvas) {
96 | this.cellBeanList.forEach {
97 | if (it.isHit) {
98 | this.hitCellView?.draw(canvas, it, this.isError)
99 | } else {
100 | this.normalCellView?.draw(canvas, it)
101 | }
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/ihsg/patternlocker/PatternLockerView.kt:
--------------------------------------------------------------------------------
1 | package com.github.ihsg.patternlocker
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.util.AttributeSet
6 | import android.view.HapticFeedbackConstants
7 | import android.view.MotionEvent
8 | import android.view.View
9 | import kotlin.math.abs
10 | import kotlin.math.min
11 |
12 |
13 | /**
14 | * Created by hsg on 20/09/2017.
15 | */
16 |
17 | open class PatternLockerView @JvmOverloads constructor(
18 | context: Context,
19 | attrs: AttributeSet? = null,
20 | defStyleAttr: Int = 0
21 | ) : View(context, attrs, defStyleAttr) {
22 | companion object {
23 | private const val TAG = "PatternLockerView"
24 | }
25 |
26 | /**
27 | * 绘制完后是否自动清除标志位,如果开启了该标志位,延时@freezeDuration毫秒后自动清除已绘制图案
28 | */
29 | var enableAutoClean: Boolean = false
30 |
31 | /**
32 | * 能否跳过中间点标志位,如果开启了该标志,则可以不用连续
33 | */
34 | var enableSkip: Boolean = false
35 |
36 | /**
37 | * 是否开启触碰反馈,如果开启了该标志,则每连接一个cell则会震动
38 | */
39 | var enableHapticFeedback: Boolean = false
40 |
41 | /**
42 | * 绘制完成后多久可以清除(单位ms),只有在@enableAutoClean = true 时有效
43 | */
44 | var freezeDuration: Int = 0
45 |
46 | /**
47 | * 绘制连接线
48 | */
49 | var linkedLineView: ILockerLinkedLineView? = null
50 |
51 | /**
52 | * 绘制未操作时的cell样式
53 | */
54 | var normalCellView: INormalCellView? = null
55 |
56 | /**
57 | * 绘制操作时的cell样式
58 | */
59 | var hitCellView: IHitCellView? = null
60 |
61 | /**
62 | * 是否是错误的图案
63 | */
64 | private var isError: Boolean = false
65 |
66 | /**
67 | * 终点x坐标
68 | */
69 | private var endX: Float = 0f
70 |
71 | /**
72 | * 终点y坐标
73 | */
74 | private var endY: Float = 0f
75 |
76 | /**
77 | * 记录绘制多少个cell,用于判断是否调用OnPatternChangeListener
78 | */
79 | private var hitSize: Int = 0
80 |
81 | /**
82 | * 记录已绘制cell的id
83 | */
84 | private val hitIndexList: MutableList by lazy {
85 | mutableListOf()
86 | }
87 |
88 | /**
89 | * 监听器
90 | */
91 | private var listener: OnPatternChangeListener? = null
92 |
93 | /**
94 | * 真正的cell数组
95 | */
96 | private val cellBeanList: List by lazy {
97 | val w = this.width - this.paddingLeft - this.paddingRight
98 | val h = this.height - this.paddingTop - this.paddingBottom
99 | CellFactory.buildCells(w, h)
100 | }
101 |
102 | init {
103 | this.initAttrs(context, attrs, defStyleAttr)
104 | this.initData()
105 | }
106 |
107 | fun enableDebug() {
108 | Logger.enable = true
109 | }
110 |
111 | fun setOnPatternChangedListener(listener: OnPatternChangeListener?) {
112 | this.listener = listener
113 | }
114 |
115 | /**
116 | * 更改状态
117 | */
118 | fun updateStatus(isError: Boolean) {
119 | this.isError = isError
120 | invalidate()
121 | }
122 |
123 | /**
124 | * 清除已绘制图案
125 | */
126 | fun clearHitState() {
127 | this.clearHitData()
128 | this.isError = false
129 | this.listener?.onClear(this)
130 | invalidate()
131 | }
132 |
133 |
134 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
135 | val a = min(widthMeasureSpec, heightMeasureSpec)
136 | super.onMeasure(a, a)
137 | }
138 |
139 | override fun onDraw(canvas: Canvas) {
140 | this.drawLinkedLine(canvas)
141 | this.drawCells(canvas)
142 | }
143 |
144 | override fun onTouchEvent(event: MotionEvent): Boolean {
145 | if (!isEnabled) {
146 | return super.onTouchEvent(event)
147 | }
148 |
149 | var isHandle = false
150 | when (event.action) {
151 | MotionEvent.ACTION_DOWN -> {
152 | this.handleActionDown(event)
153 | isHandle = true
154 | }
155 |
156 | MotionEvent.ACTION_MOVE -> {
157 | this.handleActionMove(event)
158 | isHandle = true
159 | }
160 |
161 | MotionEvent.ACTION_UP -> {
162 | this.handleActionUp(event)
163 | isHandle = true
164 | }
165 |
166 | else -> {
167 | }
168 | }
169 | invalidate()
170 | return if (isHandle) true else super.onTouchEvent(event)
171 | }
172 |
173 | private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
174 | val ta =
175 | context.obtainStyledAttributes(attrs, R.styleable.PatternLockerView, defStyleAttr, 0)
176 |
177 | val normalColor =
178 | ta.getColor(R.styleable.PatternLockerView_plv_color, DefaultConfig.defaultNormalColor)
179 | val hitColor =
180 | ta.getColor(R.styleable.PatternLockerView_plv_hitColor, DefaultConfig.defaultHitColor)
181 | val errorColor = ta.getColor(
182 | R.styleable.PatternLockerView_plv_errorColor,
183 | DefaultConfig.defaultErrorColor
184 | )
185 | val fillColor =
186 | ta.getColor(R.styleable.PatternLockerView_plv_fillColor, DefaultConfig.defaultFillColor)
187 | val lineWidth = ta.getDimension(
188 | R.styleable.PatternLockerView_plv_lineWidth,
189 | DefaultConfig.getDefaultLineWidth(resources)
190 | )
191 |
192 | this.freezeDuration = ta.getInteger(
193 | R.styleable.PatternLockerView_plv_freezeDuration,
194 | DefaultConfig.defaultFreezeDuration
195 | )
196 | this.enableAutoClean = ta.getBoolean(
197 | R.styleable.PatternLockerView_plv_enableAutoClean,
198 | DefaultConfig.defaultEnableAutoClean
199 | )
200 | this.enableHapticFeedback = ta.getBoolean(
201 | R.styleable.PatternLockerView_plv_enableHapticFeedback,
202 | DefaultConfig.defaultEnableHapticFeedback
203 | )
204 | this.enableSkip = ta.getBoolean(
205 | R.styleable.PatternLockerView_plv_enableSkip,
206 | DefaultConfig.defaultEnableSkip
207 | )
208 |
209 | ta.recycle()
210 |
211 | // style
212 | val styleDecorator =
213 | DefaultStyleDecorator(normalColor, fillColor, hitColor, errorColor, lineWidth)
214 | this.normalCellView = DefaultLockerNormalCellView(styleDecorator)
215 | this.hitCellView = DefaultLockerHitCellView(styleDecorator)
216 | this.linkedLineView = DefaultLockerLinkedLineView(styleDecorator)
217 | }
218 |
219 | private fun initData() {
220 | Logger.enable = DefaultConfig.defaultEnableLogger
221 | this.hitIndexList.clear()
222 | }
223 |
224 | private fun drawLinkedLine(canvas: Canvas) {
225 | if (this.hitIndexList.isNotEmpty()) {
226 | this.linkedLineView?.draw(
227 | canvas,
228 | this.hitIndexList,
229 | this.cellBeanList,
230 | this.endX,
231 | this.endY,
232 | this.isError
233 | )
234 | }
235 | }
236 |
237 | private fun drawCells(canvas: Canvas) {
238 | this.cellBeanList.forEach {
239 | if (it.isHit && this.hitCellView != null) {
240 | this.hitCellView?.draw(canvas, it, this.isError)
241 | } else {
242 | this.normalCellView?.draw(canvas, it)
243 | }
244 | }
245 | }
246 |
247 | private fun handleActionDown(event: MotionEvent) {
248 | //1. reset to default state
249 | this.clearHitData()
250 |
251 | //2. update hit state
252 | this.updateHitState(event)
253 |
254 | //3. notify listener
255 | this.listener?.onStart(this)
256 | }
257 |
258 | private fun handleActionMove(event: MotionEvent) {
259 | printLogger()
260 |
261 | //1. update hit state
262 | this.updateHitState(event)
263 |
264 | //2. update end point
265 | this.endX = event.x
266 | this.endY = event.y
267 |
268 | //3. notify listener if needed
269 | val size = this.hitIndexList.size
270 | if (this.hitSize != size) {
271 | this.hitSize = size
272 | this.listener?.onChange(this, this.hitIndexList)
273 | }
274 | }
275 |
276 | private fun handleActionUp(event: MotionEvent) {
277 | this.printLogger()
278 |
279 | //1. update hit state
280 | this.updateHitState(event)
281 |
282 | //2. update end point
283 | this.endX = 0f
284 | this.endY = 0f
285 |
286 | //3. notify listener
287 | this.listener?.onComplete(this, this.hitIndexList)
288 |
289 |
290 | //4. startTimer if needed
291 | if (this.enableAutoClean && this.hitIndexList.size > 0) {
292 | this.startTimer()
293 | }
294 | }
295 |
296 | private fun updateHitState(event: MotionEvent) {
297 | this.cellBeanList.forEach {
298 | if (!it.isHit && it.of(event.x, event.y)) {
299 | if (!enableSkip && this.hitIndexList.isNotEmpty()) {
300 | val last = this.cellBeanList[this.hitIndexList.last()]
301 | val mayId = (last.id + it.id) / 2
302 | if (!this.hitIndexList.contains(mayId) && (abs(last.x - it.x) % 2 == 0) && (abs(
303 | last.y - it.y
304 | ) % 2 == 0)
305 | ) {
306 | this.cellBeanList[mayId].isHit = true
307 | this.hitIndexList.add(mayId)
308 | }
309 | }
310 | it.isHit = true
311 | this.hitIndexList.add(it.id)
312 | this.hapticFeedback()
313 | }
314 | }
315 | }
316 |
317 | private fun clearHitData() {
318 | if (this.hitIndexList.isNotEmpty()) {
319 | this.hitIndexList.clear()
320 | this.hitSize = 0
321 | this.cellBeanList.forEach { it.isHit = false }
322 | }
323 | }
324 |
325 | override fun onDetachedFromWindow() {
326 | this.setOnPatternChangedListener(null)
327 | this.removeCallbacks(this.action)
328 | super.onDetachedFromWindow()
329 | }
330 |
331 | private val action = Runnable {
332 | this.isEnabled = true
333 | this.clearHitState()
334 | }
335 |
336 | private fun startTimer() {
337 | this.isEnabled = false
338 | this.postDelayed(this.action, this.freezeDuration.toLong())
339 | }
340 |
341 | private fun hapticFeedback() {
342 | if (this.enableHapticFeedback) {
343 | this.performHapticFeedback(
344 | HapticFeedbackConstants.VIRTUAL_KEY,
345 | HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
346 | or HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
347 | )
348 | }
349 | }
350 |
351 | private fun printLogger() {
352 | if (Logger.enable) {
353 | Logger.d(
354 | TAG,
355 | "cellBeanList = ${this.cellBeanList}, hitIndexList = ${this.hitIndexList}"
356 | )
357 | }
358 | }
359 | }
--------------------------------------------------------------------------------
/library/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Library
3 |
4 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "PatternLocker"
23 | include(":app")
24 | include(":library")
--------------------------------------------------------------------------------