patternChanges(PatternLockView patternLockView) {
28 | checkNotNull(patternLockView, "view == null");
29 | return new PatternLockViewCompoundObservable(patternLockView, false);
30 | }
31 |
32 | /**
33 | * Create an observable for all events of this {@code view}.
34 | *
35 | * Warning: The created observable keeps a strong reference to {@code view}.
36 | * Unsubscribe to free this reference.
37 | *
38 | * Note: A value will be emitted immediately on subscribe.
39 | */
40 | public static Observable patternChanges(PatternLockView patternLockView,
41 | boolean emitInitialValue) {
42 | checkNotNull(patternLockView, "view == null");
43 | return new PatternLockViewCompoundObservable(patternLockView, emitInitialValue);
44 | }
45 |
46 | /**
47 | * Create an observable for only the pattern complete event of this {@code view}.
48 | *
49 | * Warning: The created observable keeps a strong reference to {@code view}.
50 | * Unsubscribe to free this reference.
51 | */
52 | public static Observable patternComplete(PatternLockView patternLockView) {
53 | checkNotNull(patternLockView, "view == null");
54 | return new PatternLockViewCompleteObservable(patternLockView, false);
55 | }
56 |
57 | /**
58 | * Create an observable for only the pattern complete event of this {@code view}.
59 | *
60 | * Warning: The created observable keeps a strong reference to {@code view}.
61 | * Unsubscribe to free this reference.
62 | *
63 | * Note: A value will be emitted immediately on subscribe.
64 | */
65 | public static Observable patternComplete(PatternLockView patternLockView,
66 | boolean emitInitialValues) {
67 | checkNotNull(patternLockView, "view == null");
68 | return new PatternLockViewCompleteObservable(patternLockView, emitInitialValues);
69 | }
70 |
71 | /**
72 | * Create an observable for only the pattern progress event of this {@code view}.
73 | *
74 | * Warning: The created observable keeps a strong reference to {@code view}.
75 | * Unsubscribe to free this reference.
76 | */
77 | public static Observable patternProgress(PatternLockView patternLockView) {
78 | checkNotNull(patternLockView, "view == null");
79 | return new PatternLockViewProgressObservable(patternLockView, false);
80 | }
81 |
82 | /**
83 | * Create an observable for only the pattern progress event of this {@code view}.
84 | *
85 | * Warning: The created observable keeps a strong reference to {@code view}.
86 | * Unsubscribe to free this reference.
87 | *
88 | * Note: A value will be emitted immediately on subscribe.
89 | */
90 | public static Observable patternProgress(PatternLockView patternLockView,
91 | boolean emitInitialValues) {
92 | checkNotNull(patternLockView, "view == null");
93 | return new PatternLockViewProgressObservable(patternLockView, emitInitialValues);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/events/BasePatternLockEvent.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.events;
2 |
3 | import android.support.annotation.Nullable;
4 |
5 | import com.andrognito.patternlockview.PatternLockView;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | /**
11 | * Created by aritraroy on 01/04/17.
12 | */
13 |
14 | public abstract class BasePatternLockEvent {
15 | protected List mPattern;
16 |
17 | protected BasePatternLockEvent(List pattern) {
18 | mPattern = pattern;
19 | }
20 |
21 | @Nullable
22 | public List getPattern() {
23 | if (mPattern == null) {
24 | return new ArrayList<>();
25 | }
26 | return new ArrayList<>(mPattern);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/events/PatternLockCompleteEvent.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.events;
2 |
3 | import com.andrognito.patternlockview.PatternLockView;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Created by aritraroy on 01/04/17.
9 | */
10 |
11 | public class PatternLockCompleteEvent extends BasePatternLockEvent {
12 |
13 | public PatternLockCompleteEvent(List pattern) {
14 | super(pattern);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/events/PatternLockCompoundEvent.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.events;
2 |
3 | import android.support.annotation.IntDef;
4 |
5 | import com.andrognito.patternlockview.PatternLockView;
6 |
7 | import java.lang.annotation.Retention;
8 | import java.lang.annotation.RetentionPolicy;
9 | import java.util.List;
10 |
11 | import static com.andrognito.rxpatternlockview.events.PatternLockCompoundEvent.EventType.PATTERN_CLEARED;
12 | import static com.andrognito.rxpatternlockview.events.PatternLockCompoundEvent.EventType.PATTERN_COMPLETE;
13 | import static com.andrognito.rxpatternlockview.events.PatternLockCompoundEvent.EventType.PATTERN_PROGRESS;
14 | import static com.andrognito.rxpatternlockview.events.PatternLockCompoundEvent.EventType.PATTERN_STARTED;
15 |
16 | /**
17 | * Created by aritraroy on 27/03/17.
18 | */
19 |
20 | public final class PatternLockCompoundEvent extends BasePatternLockEvent {
21 |
22 | @IntDef({PATTERN_STARTED, PATTERN_PROGRESS, PATTERN_COMPLETE, PATTERN_CLEARED})
23 | @Retention(RetentionPolicy.SOURCE)
24 | public @interface EventType {
25 | int PATTERN_STARTED = 0;
26 | int PATTERN_PROGRESS = 1;
27 | int PATTERN_COMPLETE = 2;
28 | int PATTERN_CLEARED = 3;
29 | }
30 |
31 | private final int mEventType;
32 |
33 | public PatternLockCompoundEvent(@EventType int eventType, List pattern) {
34 | super(pattern);
35 | mEventType = eventType;
36 | }
37 |
38 | @EventType
39 | public int getEventType() {
40 | return mEventType;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/events/PatternLockProgressEvent.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.events;
2 |
3 | import com.andrognito.patternlockview.PatternLockView;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * Created by aritraroy on 01/04/17.
9 | */
10 |
11 | public class PatternLockProgressEvent extends BasePatternLockEvent {
12 |
13 | public PatternLockProgressEvent(List pattern) {
14 | super(pattern);
15 | }
16 | }
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/observables/BasePatternLockViewObservable.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.observables;
2 |
3 | import com.andrognito.patternlockview.PatternLockView;
4 |
5 | import io.reactivex.Observable;
6 | import io.reactivex.Observer;
7 |
8 | /**
9 | * Created by aritraroy on 01/04/17.
10 | */
11 |
12 | public abstract class BasePatternLockViewObservable
13 | extends Observable {
14 | protected PatternLockView mPatternLockView;
15 | protected boolean mEmitInitialValue;
16 |
17 | protected BasePatternLockViewObservable(PatternLockView patternLockView, boolean emitInitialValue) {
18 | mPatternLockView = patternLockView;
19 | mEmitInitialValue = emitInitialValue;
20 | }
21 |
22 | protected abstract void subscribeListener(Observer super BasePatternLockEvent> observer);
23 | }
24 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/observables/PatternLockViewCompleteObservable.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.observables;
2 |
3 | import com.andrognito.patternlockview.PatternLockView;
4 | import com.andrognito.patternlockview.listener.PatternLockViewListener;
5 | import com.andrognito.rxpatternlockview.events.PatternLockCompleteEvent;
6 |
7 | import java.util.List;
8 |
9 | import io.reactivex.Observer;
10 | import io.reactivex.android.MainThreadDisposable;
11 |
12 | /**
13 | * Created by aritraroy on 01/04/17.
14 | */
15 |
16 | public class PatternLockViewCompleteObservable extends
17 | BasePatternLockViewObservable {
18 |
19 | public PatternLockViewCompleteObservable(PatternLockView patternLockView, boolean emitInitialValues) {
20 | super(patternLockView, emitInitialValues);
21 | }
22 |
23 | @Override
24 | protected void subscribeListener(Observer super PatternLockCompleteEvent> observer) {
25 | InternalListener internalListener = new InternalListener(mPatternLockView, observer);
26 | observer.onSubscribe(internalListener);
27 | mPatternLockView.addPatternLockListener(internalListener);
28 | }
29 |
30 | @Override
31 | protected void subscribeActual(Observer super PatternLockCompleteEvent> observer) {
32 | subscribeListener(observer);
33 | if (mEmitInitialValue) {
34 | observer.onNext(new PatternLockCompleteEvent(mPatternLockView.getPattern()));
35 | }
36 | }
37 |
38 | private static final class InternalListener extends MainThreadDisposable
39 | implements PatternLockViewListener {
40 | private final PatternLockView view;
41 | private final Observer super PatternLockCompleteEvent> observer;
42 |
43 | InternalListener(PatternLockView view, Observer super PatternLockCompleteEvent> observer) {
44 | this.view = view;
45 | this.observer = observer;
46 | }
47 |
48 | @Override
49 | public void onStarted() {
50 |
51 | }
52 |
53 | @Override
54 | public void onProgress(List progressPattern) {
55 |
56 | }
57 |
58 | @Override
59 | public void onComplete(List pattern) {
60 | if (!isDisposed()) {
61 | observer.onNext(new PatternLockCompleteEvent(pattern));
62 | }
63 | }
64 |
65 | @Override
66 | public void onCleared() {
67 |
68 | }
69 |
70 | @Override
71 | protected void onDispose() {
72 | view.removePatternLockListener(this);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/observables/PatternLockViewCompoundObservable.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.observables;
2 |
3 | import com.andrognito.patternlockview.PatternLockView;
4 | import com.andrognito.patternlockview.listener.PatternLockViewListener;
5 | import com.andrognito.rxpatternlockview.events.PatternLockCompoundEvent;
6 |
7 | import java.util.List;
8 |
9 | import io.reactivex.Observer;
10 | import io.reactivex.android.MainThreadDisposable;
11 |
12 |
13 | /**
14 | * Created by aritraroy on 27/03/17.
15 | */
16 |
17 | public class PatternLockViewCompoundObservable
18 | extends BasePatternLockViewObservable {
19 |
20 | public PatternLockViewCompoundObservable(PatternLockView patternLockView, boolean emitInitialValue) {
21 | super(patternLockView, emitInitialValue);
22 | }
23 |
24 | @Override
25 | protected void subscribeActual(Observer super PatternLockCompoundEvent> observer) {
26 | subscribeListener(observer);
27 | if (mEmitInitialValue) {
28 | observer.onNext(new PatternLockCompoundEvent(PatternLockCompoundEvent.EventType.PATTERN_STARTED,
29 | mPatternLockView.getPattern()));
30 | }
31 | }
32 |
33 | @Override
34 | protected void subscribeListener(Observer super PatternLockCompoundEvent> observer) {
35 | InternalListener internalListener = new InternalListener(mPatternLockView, observer);
36 | observer.onSubscribe(internalListener);
37 | mPatternLockView.addPatternLockListener(internalListener);
38 | }
39 |
40 | private static final class InternalListener extends MainThreadDisposable
41 | implements PatternLockViewListener {
42 | private final PatternLockView view;
43 | private final Observer super PatternLockCompoundEvent> observer;
44 |
45 | InternalListener(PatternLockView view, Observer super PatternLockCompoundEvent>
46 | observer) {
47 | this.view = view;
48 | this.observer = observer;
49 | }
50 |
51 | @Override
52 | public void onStarted() {
53 | if (!isDisposed()) {
54 | observer.onNext(new PatternLockCompoundEvent(PatternLockCompoundEvent
55 | .EventType.PATTERN_STARTED, null));
56 | }
57 | }
58 |
59 | @Override
60 | public void onProgress(List progressPattern) {
61 | if (!isDisposed()) {
62 | observer.onNext(new PatternLockCompoundEvent(PatternLockCompoundEvent
63 | .EventType.PATTERN_PROGRESS, progressPattern));
64 | }
65 | }
66 |
67 | @Override
68 | public void onComplete(List pattern) {
69 | if (!isDisposed()) {
70 | observer.onNext(new PatternLockCompoundEvent(PatternLockCompoundEvent
71 | .EventType.PATTERN_COMPLETE, pattern));
72 | }
73 | }
74 |
75 | @Override
76 | public void onCleared() {
77 | if (!isDisposed()) {
78 | observer.onNext(new PatternLockCompoundEvent(PatternLockCompoundEvent
79 | .EventType.PATTERN_CLEARED, null));
80 | }
81 | }
82 |
83 | @Override
84 | protected void onDispose() {
85 | view.removePatternLockListener(this);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/observables/PatternLockViewProgressObservable.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.observables;
2 |
3 | import com.andrognito.patternlockview.PatternLockView;
4 | import com.andrognito.patternlockview.listener.PatternLockViewListener;
5 | import com.andrognito.rxpatternlockview.events.PatternLockProgressEvent;
6 |
7 | import java.util.List;
8 |
9 | import io.reactivex.Observer;
10 | import io.reactivex.android.MainThreadDisposable;
11 |
12 | /**
13 | * Created by aritraroy on 01/04/17.
14 | */
15 |
16 | public class PatternLockViewProgressObservable extends
17 | BasePatternLockViewObservable {
18 |
19 | public PatternLockViewProgressObservable(PatternLockView patternLockView, boolean emitInitialValue) {
20 | super(patternLockView, emitInitialValue);
21 | }
22 |
23 | @Override
24 | protected void subscribeListener(Observer super PatternLockProgressEvent> observer) {
25 | InternalListener internalListener = new InternalListener(mPatternLockView, observer);
26 | observer.onSubscribe(internalListener);
27 | mPatternLockView.addPatternLockListener(internalListener);
28 | }
29 |
30 | @Override
31 | protected void subscribeActual(Observer super PatternLockProgressEvent> observer) {
32 | subscribeListener(observer);
33 | if (mEmitInitialValue) {
34 | observer.onNext(new PatternLockProgressEvent(mPatternLockView.getPattern()));
35 | }
36 | }
37 |
38 | private static final class InternalListener extends MainThreadDisposable
39 | implements PatternLockViewListener {
40 | private final PatternLockView view;
41 | private final Observer super PatternLockProgressEvent> observer;
42 |
43 | InternalListener(PatternLockView view, Observer super PatternLockProgressEvent> observer) {
44 | this.view = view;
45 | this.observer = observer;
46 | }
47 |
48 | @Override
49 | public void onStarted() {
50 |
51 | }
52 |
53 | @Override
54 | public void onProgress(List progressPattern) {
55 | if (!isDisposed()) {
56 | observer.onNext(new PatternLockProgressEvent(progressPattern));
57 | }
58 | }
59 |
60 | @Override
61 | public void onComplete(List pattern) {
62 |
63 | }
64 |
65 | @Override
66 | public void onCleared() {
67 |
68 | }
69 |
70 | @Override
71 | protected void onDispose() {
72 | view.removePatternLockListener(this);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/java/com/andrognito/rxpatternlockview/utils/Preconditions.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.rxpatternlockview.utils;
2 |
3 | import android.os.Looper;
4 |
5 | import io.reactivex.Observer;
6 |
7 | /**
8 | * Created by aritraroy on 02/04/17.
9 | */
10 |
11 | public final class Preconditions {
12 |
13 | private Preconditions() {
14 | throw new AssertionError("You can not instantiate this class. Use its static utility " +
15 | "methods instead");
16 | }
17 |
18 | public static void checkNotNull(Object value, String message) {
19 | if (value == null) {
20 | throw new NullPointerException(message);
21 | }
22 | }
23 |
24 | public static boolean checkMainThread(Observer> observer) {
25 | if (Looper.myLooper() != Looper.getMainLooper()) {
26 | observer.onError(new IllegalStateException(
27 | "Expected to be called on the main thread but was " + Thread.currentThread().getName()));
28 | return false;
29 | }
30 | return true;
31 | }
32 | }
--------------------------------------------------------------------------------
/patternlockview-reactive/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RxPatternLockView
3 |
4 |
--------------------------------------------------------------------------------
/patternlockview/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | ext {
4 | bintrayRepo = 'maven'
5 | bintrayName = 'patternlockview'
6 |
7 | publishedGroupId = 'com.andrognito.patternlockview'
8 | libraryName = 'patternlockview'
9 | artifact = 'patternlockview'
10 |
11 | libraryDescription = 'An easy-to-use, customizable, Material Design complaint Pattern Lock ' +
12 | 'view for Android'
13 |
14 | siteUrl = 'https://github.com/aritraroy/PatternLockView'
15 | gitUrl = 'https://github.com/aritraroy/PatternLockView.git'
16 |
17 | libraryVersion = '1.0.0'
18 |
19 | developerId = 'aritraroy'
20 | developerName = 'Aritra Roy'
21 | developerEmail = 'aritra.roy.in@gmail.com'
22 |
23 | licenseName = 'The Apache Software License, Version 2.0'
24 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
25 | allLicenses = ["Apache-2.0"]
26 | }
27 |
28 | android {
29 | compileSdkVersion rootProject.ext.compileSdkVersion
30 | buildToolsVersion rootProject.ext.buildToolsVersion
31 |
32 | defaultConfig {
33 | minSdkVersion rootProject.ext.minSdkVersion
34 | targetSdkVersion rootProject.ext.targetSdkVersion
35 | versionCode Integer.parseInt(project.VERSION_CODE)
36 | versionName project.VERSION_NAME
37 | }
38 | }
39 |
40 | dependencies {
41 | compile rootProject.ext.supportV7
42 | }
43 |
44 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle'
45 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle'
--------------------------------------------------------------------------------
/patternlockview/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/aritraroy/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 |
--------------------------------------------------------------------------------
/patternlockview/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/patternlockview/src/main/java/com/andrognito/patternlockview/PatternLockView.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.patternlockview;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ValueAnimator;
6 | import android.content.Context;
7 | import android.content.res.TypedArray;
8 | import android.graphics.Canvas;
9 | import android.graphics.Paint;
10 | import android.graphics.Path;
11 | import android.graphics.Rect;
12 | import android.os.Build;
13 | import android.os.Debug;
14 | import android.os.Parcel;
15 | import android.os.Parcelable;
16 | import android.os.SystemClock;
17 | import android.support.annotation.ColorInt;
18 | import android.support.annotation.Dimension;
19 | import android.support.annotation.IntDef;
20 | import android.util.AttributeSet;
21 | import android.view.HapticFeedbackConstants;
22 | import android.view.MotionEvent;
23 | import android.view.View;
24 | import android.view.accessibility.AccessibilityEvent;
25 | import android.view.accessibility.AccessibilityManager;
26 | import android.view.animation.AnimationUtils;
27 | import android.view.animation.Interpolator;
28 |
29 | import com.andrognito.patternlockview.listener.PatternLockViewListener;
30 | import com.andrognito.patternlockview.utils.PatternLockUtils;
31 | import com.andrognito.patternlockview.utils.ResourceUtils;
32 |
33 | import java.lang.annotation.Retention;
34 | import java.lang.annotation.RetentionPolicy;
35 | import java.util.ArrayList;
36 | import java.util.List;
37 |
38 | import static com.andrognito.patternlockview.PatternLockView.AspectRatio.ASPECT_RATIO_HEIGHT_BIAS;
39 | import static com.andrognito.patternlockview.PatternLockView.AspectRatio.ASPECT_RATIO_SQUARE;
40 | import static com.andrognito.patternlockview.PatternLockView.AspectRatio.ASPECT_RATIO_WIDTH_BIAS;
41 | import static com.andrognito.patternlockview.PatternLockView.PatternViewMode.AUTO_DRAW;
42 | import static com.andrognito.patternlockview.PatternLockView.PatternViewMode.CORRECT;
43 | import static com.andrognito.patternlockview.PatternLockView.PatternViewMode.WRONG;
44 |
45 | /**
46 | * Displays a powerful, customizable and Material Design complaint pattern lock in the screen which
47 | * can be used to lock any Activity or Fragment from the user
48 | */
49 | public class PatternLockView extends View {
50 |
51 | /**
52 | * Represents the aspect ratio for the View
53 | */
54 | @IntDef({ASPECT_RATIO_SQUARE, ASPECT_RATIO_WIDTH_BIAS, ASPECT_RATIO_HEIGHT_BIAS})
55 | @Retention(RetentionPolicy.SOURCE)
56 | public @interface AspectRatio {
57 | // Width and height will be same. Minimum of width and height
58 | int ASPECT_RATIO_SQUARE = 0;
59 | // Width will be fixed. The height will be the minimum of width and height
60 | int ASPECT_RATIO_WIDTH_BIAS = 1;
61 | // Height will be fixed. The width will be the minimum of width and height
62 | int ASPECT_RATIO_HEIGHT_BIAS = 2;
63 | }
64 |
65 | /**
66 | * Represents the different modes in which this view can be represented
67 | */
68 | @IntDef({CORRECT, AUTO_DRAW, WRONG})
69 | @Retention(RetentionPolicy.SOURCE)
70 | public @interface PatternViewMode {
71 | /**
72 | * This state represents a correctly drawn pattern by the user. The color of the path and
73 | * the dots both would be changed to this color.
74 | *
75 | * (NOTE - Consider showing this state in a friendly color)
76 | */
77 | int CORRECT = 0;
78 | /**
79 | * Automatically draw the pattern for demo or tutorial purposes.
80 | */
81 | int AUTO_DRAW = 1;
82 | /**
83 | * This state represents a wrongly drawn pattern by the user. The color of the path and
84 | * the dots both would be changed to this color.
85 | *
86 | * (NOTE - Consider showing this state in an attention-seeking color)
87 | */
88 | int WRONG = 2;
89 | }
90 |
91 | private static final int DEFAULT_PATTERN_DOT_COUNT = 3;
92 | private static final boolean PROFILE_DRAWING = false;
93 |
94 | /**
95 | * The time (in millis) spend in animating each circle of a lock pattern if
96 | * the animating mode is set. The entire animation should take this constant
97 | * the length of the pattern to complete.
98 | */
99 | private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;
100 |
101 | // Amount of time (in millis) spent to animate a dot
102 | private static final int DEFAULT_DOT_ANIMATION_DURATION = 190;
103 | // Amount of time (in millis) spent to animate a path ends
104 | private static final int DEFAULT_PATH_END_ANIMATION_DURATION = 100;
105 | // This can be used to avoid updating the display for very small motions or noisy panels
106 | private static final float DEFAULT_DRAG_THRESHOLD = 0.0f;
107 |
108 | private DotState[][] mDotStates;
109 | private int mPatternSize;
110 | private boolean mDrawingProfilingStarted = false;
111 | private long mAnimatingPeriodStart;
112 | private float mHitFactor = 0.6f;
113 |
114 | // Made static so that the static inner class can use it
115 | private static int sDotCount;
116 |
117 | private boolean mAspectRatioEnabled;
118 | private int mAspectRatio;
119 | private int mNormalStateColor;
120 | private int mWrongStateColor;
121 | private int mCorrectStateColor;
122 | private int mPathWidth;
123 | private int mDotNormalSize;
124 | private int mDotSelectedSize;
125 | private int mDotAnimationDuration;
126 | private int mPathEndAnimationDuration;
127 |
128 | private Paint mDotPaint;
129 | private Paint mPathPaint;
130 |
131 | private List mPatternListeners;
132 | // The pattern represented as a list of connected {@link Dot}
133 | private ArrayList mPattern;
134 |
135 | /**
136 | * Lookup table for the dots of the pattern we are currently drawing.
137 | * This will be the dots of the complete pattern unless we are animating,
138 | * in which case we use this to hold the dots we are drawing for the in
139 | * progress animation.
140 | */
141 | private boolean[][] mPatternDrawLookup;
142 |
143 | private float mInProgressX = -1;
144 | private float mInProgressY = -1;
145 |
146 | private int mPatternViewMode = CORRECT;
147 | private boolean mInputEnabled = true;
148 | private boolean mInStealthMode = false;
149 | private boolean mEnableHapticFeedback = true;
150 | private boolean mPatternInProgress = false;
151 |
152 | private float mViewWidth;
153 | private float mViewHeight;
154 |
155 | private final Path mCurrentPath = new Path();
156 | private final Rect mInvalidate = new Rect();
157 | private final Rect mTempInvalidateRect = new Rect();
158 |
159 | private Interpolator mFastOutSlowInInterpolator;
160 | private Interpolator mLinearOutSlowInInterpolator;
161 |
162 | public PatternLockView(Context context) {
163 | this(context, null);
164 | }
165 |
166 | public PatternLockView(Context context, AttributeSet attrs) {
167 | super(context, attrs);
168 |
169 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PatternLockView);
170 | try {
171 | sDotCount = typedArray.getInt(R.styleable.PatternLockView_dotCount,
172 | DEFAULT_PATTERN_DOT_COUNT);
173 | mAspectRatioEnabled = typedArray.getBoolean(R.styleable.PatternLockView_aspectRatioEnabled,
174 | false);
175 | mAspectRatio = typedArray.getInt(R.styleable.PatternLockView_aspectRatio,
176 | ASPECT_RATIO_SQUARE);
177 | mPathWidth = (int) typedArray.getDimension(R.styleable.PatternLockView_pathWidth,
178 | ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_path_width));
179 | mNormalStateColor = typedArray.getColor(R.styleable.PatternLockView_normalStateColor,
180 | ResourceUtils.getColor(getContext(), R.color.white));
181 | mCorrectStateColor = typedArray.getColor(R.styleable.PatternLockView_correctStateColor,
182 | ResourceUtils.getColor(getContext(), R.color.white));
183 | mWrongStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongStateColor,
184 | ResourceUtils.getColor(getContext(), R.color.pomegranate));
185 | mDotNormalSize = (int) typedArray.getDimension(R.styleable.PatternLockView_dotNormalSize,
186 | ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_dot_size));
187 | mDotSelectedSize = (int) typedArray.getDimension(R.styleable
188 | .PatternLockView_dotSelectedSize,
189 | ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_dot_selected_size));
190 | mDotAnimationDuration = typedArray.getInt(R.styleable.PatternLockView_dotAnimationDuration,
191 | DEFAULT_DOT_ANIMATION_DURATION);
192 | mPathEndAnimationDuration = typedArray.getInt(R.styleable.PatternLockView_pathEndAnimationDuration,
193 | DEFAULT_PATH_END_ANIMATION_DURATION);
194 | } finally {
195 | typedArray.recycle();
196 | }
197 |
198 | // The pattern will always be symmetrical
199 | mPatternSize = sDotCount * sDotCount;
200 | mPattern = new ArrayList<>(mPatternSize);
201 | mPatternDrawLookup = new boolean[sDotCount][sDotCount];
202 |
203 | mDotStates = new DotState[sDotCount][sDotCount];
204 | for (int i = 0; i < sDotCount; i++) {
205 | for (int j = 0; j < sDotCount; j++) {
206 | mDotStates[i][j] = new DotState();
207 | mDotStates[i][j].mSize = mDotNormalSize;
208 | }
209 | }
210 |
211 | mPatternListeners = new ArrayList<>();
212 |
213 | initView();
214 | }
215 |
216 | private void initView() {
217 | setClickable(true);
218 |
219 | mPathPaint = new Paint();
220 | mPathPaint.setAntiAlias(true);
221 | mPathPaint.setDither(true);
222 | mPathPaint.setColor(mNormalStateColor);
223 | mPathPaint.setStyle(Paint.Style.STROKE);
224 | mPathPaint.setStrokeJoin(Paint.Join.ROUND);
225 | mPathPaint.setStrokeCap(Paint.Cap.ROUND);
226 | mPathPaint.setStrokeWidth(mPathWidth);
227 |
228 | mDotPaint = new Paint();
229 | mDotPaint.setAntiAlias(true);
230 | mDotPaint.setDither(true);
231 |
232 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
233 | && !isInEditMode()) {
234 | mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
235 | getContext(), android.R.interpolator.fast_out_slow_in);
236 | mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
237 | getContext(), android.R.interpolator.linear_out_slow_in);
238 | }
239 | }
240 |
241 | @Override
242 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
243 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
244 |
245 | if (!mAspectRatioEnabled) {
246 | return;
247 | }
248 |
249 | int oldWidth = resolveMeasured(widthMeasureSpec, getSuggestedMinimumWidth());
250 | int oldHeight = resolveMeasured(heightMeasureSpec, getSuggestedMinimumHeight());
251 |
252 | int newWidth;
253 | int newHeight;
254 | switch (mAspectRatio) {
255 | case ASPECT_RATIO_SQUARE:
256 | newWidth = newHeight = Math.min(oldWidth, oldHeight);
257 | break;
258 | case ASPECT_RATIO_WIDTH_BIAS:
259 | newWidth = oldWidth;
260 | newHeight = Math.min(oldWidth, oldHeight);
261 | break;
262 |
263 | case ASPECT_RATIO_HEIGHT_BIAS:
264 | newWidth = Math.min(oldWidth, oldHeight);
265 | newHeight = oldHeight;
266 | break;
267 |
268 | default:
269 | throw new IllegalStateException("Unknown aspect ratio");
270 | }
271 | setMeasuredDimension(newWidth, newHeight);
272 | }
273 |
274 | @Override
275 | protected void onDraw(Canvas canvas) {
276 | ArrayList pattern = mPattern;
277 | int patternSize = pattern.size();
278 | boolean[][] drawLookupTable = mPatternDrawLookup;
279 |
280 | if (mPatternViewMode == AUTO_DRAW) {
281 | int oneCycle = (patternSize + 1) * MILLIS_PER_CIRCLE_ANIMATING;
282 | int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart)
283 | % oneCycle;
284 | int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
285 |
286 | clearPatternDrawLookup();
287 | for (int i = 0; i < numCircles; i++) {
288 | Dot dot = pattern.get(i);
289 | drawLookupTable[dot.mRow][dot.mColumn] = true;
290 | }
291 |
292 | boolean needToUpdateInProgressPoint = numCircles > 0
293 | && numCircles < patternSize;
294 |
295 | if (needToUpdateInProgressPoint) {
296 | float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING))
297 | / MILLIS_PER_CIRCLE_ANIMATING;
298 |
299 | Dot currentDot = pattern.get(numCircles - 1);
300 | float centerX = getCenterXForColumn(currentDot.mColumn);
301 | float centerY = getCenterYForRow(currentDot.mRow);
302 |
303 | Dot nextDot = pattern.get(numCircles);
304 | float dx = percentageOfNextCircle
305 | * (getCenterXForColumn(nextDot.mColumn) - centerX);
306 | float dy = percentageOfNextCircle
307 | * (getCenterYForRow(nextDot.mRow) - centerY);
308 | mInProgressX = centerX + dx;
309 | mInProgressY = centerY + dy;
310 | }
311 | invalidate();
312 | }
313 |
314 | Path currentPath = mCurrentPath;
315 | currentPath.rewind();
316 |
317 | // Draw the dots
318 | for (int i = 0; i < sDotCount; i++) {
319 | float centerY = getCenterYForRow(i);
320 | for (int j = 0; j < sDotCount; j++) {
321 | DotState dotState = mDotStates[i][j];
322 | float centerX = getCenterXForColumn(j);
323 | float size = dotState.mSize * dotState.mScale;
324 | float translationY = dotState.mTranslateY;
325 | drawCircle(canvas, (int) centerX, (int) centerY + translationY,
326 | size, drawLookupTable[i][j], dotState.mAlpha);
327 | }
328 | }
329 |
330 | // Draw the path of the pattern (unless we are in stealth mode)
331 | boolean drawPath = !mInStealthMode;
332 | if (drawPath) {
333 | mPathPaint.setColor(getCurrentColor(true));
334 |
335 | boolean anyCircles = false;
336 | float lastX = 0f;
337 | float lastY = 0f;
338 | for (int i = 0; i < patternSize; i++) {
339 | Dot dot = pattern.get(i);
340 |
341 | // Only draw the part of the pattern stored in
342 | // the lookup table (this is only different in case
343 | // of animation)
344 | if (!drawLookupTable[dot.mRow][dot.mColumn]) {
345 | break;
346 | }
347 | anyCircles = true;
348 |
349 | float centerX = getCenterXForColumn(dot.mColumn);
350 | float centerY = getCenterYForRow(dot.mRow);
351 | if (i != 0) {
352 | DotState state = mDotStates[dot.mRow][dot.mColumn];
353 | currentPath.rewind();
354 | currentPath.moveTo(lastX, lastY);
355 | if (state.mLineEndX != Float.MIN_VALUE
356 | && state.mLineEndY != Float.MIN_VALUE) {
357 | currentPath.lineTo(state.mLineEndX, state.mLineEndY);
358 | } else {
359 | currentPath.lineTo(centerX, centerY);
360 | }
361 | canvas.drawPath(currentPath, mPathPaint);
362 | }
363 | lastX = centerX;
364 | lastY = centerY;
365 | }
366 |
367 | // Draw last in progress section
368 | if ((mPatternInProgress || mPatternViewMode == AUTO_DRAW)
369 | && anyCircles) {
370 | currentPath.rewind();
371 | currentPath.moveTo(lastX, lastY);
372 | currentPath.lineTo(mInProgressX, mInProgressY);
373 |
374 | mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
375 | mInProgressX, mInProgressY, lastX, lastY) * 255f));
376 | canvas.drawPath(currentPath, mPathPaint);
377 | }
378 | }
379 | }
380 |
381 | @Override
382 | protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
383 | int adjustedWidth = width - getPaddingLeft() - getPaddingRight();
384 | mViewWidth = adjustedWidth / (float) sDotCount;
385 |
386 | int adjustedHeight = height - getPaddingTop() - getPaddingBottom();
387 | mViewHeight = adjustedHeight / (float) sDotCount;
388 | }
389 |
390 | @Override
391 | protected Parcelable onSaveInstanceState() {
392 | Parcelable superState = super.onSaveInstanceState();
393 | return new SavedState(superState,
394 | PatternLockUtils.patternToString(this, mPattern),
395 | mPatternViewMode, mInputEnabled, mInStealthMode,
396 | mEnableHapticFeedback);
397 | }
398 |
399 | @Override
400 | protected void onRestoreInstanceState(Parcelable state) {
401 | final SavedState savedState = (SavedState) state;
402 | super.onRestoreInstanceState(savedState.getSuperState());
403 | setPattern(CORRECT,
404 | PatternLockUtils.stringToPattern(this, savedState.getSerializedPattern()));
405 | mPatternViewMode = savedState.getDisplayMode();
406 | mInputEnabled = savedState.isInputEnabled();
407 | mInStealthMode = savedState.isInStealthMode();
408 | mEnableHapticFeedback = savedState.isTactileFeedbackEnabled();
409 | }
410 |
411 | @Override
412 | public boolean onHoverEvent(MotionEvent event) {
413 | if (((AccessibilityManager) getContext().getSystemService(
414 | Context.ACCESSIBILITY_SERVICE)).isTouchExplorationEnabled()) {
415 | final int action = event.getAction();
416 | switch (action) {
417 | case MotionEvent.ACTION_HOVER_ENTER:
418 | event.setAction(MotionEvent.ACTION_DOWN);
419 | break;
420 | case MotionEvent.ACTION_HOVER_MOVE:
421 | event.setAction(MotionEvent.ACTION_MOVE);
422 | break;
423 | case MotionEvent.ACTION_HOVER_EXIT:
424 | event.setAction(MotionEvent.ACTION_UP);
425 | break;
426 | }
427 | onTouchEvent(event);
428 | event.setAction(action);
429 | }
430 | return super.onHoverEvent(event);
431 | }
432 |
433 | @Override
434 | public boolean onTouchEvent(MotionEvent event) {
435 | if (!mInputEnabled || !isEnabled()) {
436 | return false;
437 | }
438 |
439 | switch (event.getAction()) {
440 | case MotionEvent.ACTION_DOWN:
441 | handleActionDown(event);
442 | return true;
443 | case MotionEvent.ACTION_UP:
444 | handleActionUp(event);
445 | return true;
446 | case MotionEvent.ACTION_MOVE:
447 | handleActionMove(event);
448 | return true;
449 | case MotionEvent.ACTION_CANCEL:
450 | mPatternInProgress = false;
451 | resetPattern();
452 | notifyPatternCleared();
453 |
454 | if (PROFILE_DRAWING) {
455 | if (mDrawingProfilingStarted) {
456 | Debug.stopMethodTracing();
457 | mDrawingProfilingStarted = false;
458 | }
459 | }
460 | return true;
461 | }
462 | return false;
463 | }
464 |
465 | /**
466 | * Returns the list of dots in the current selected pattern. This list is independent of the
467 | * internal pattern dot list
468 | */
469 | @SuppressWarnings("unchecked")
470 | public List getPattern() {
471 | return (List) mPattern.clone();
472 | }
473 |
474 | @PatternViewMode
475 | public int getPatternViewMode() {
476 | return mPatternViewMode;
477 | }
478 |
479 | public boolean isInStealthMode() {
480 | return mInStealthMode;
481 | }
482 |
483 | public boolean isTactileFeedbackEnabled() {
484 | return mEnableHapticFeedback;
485 | }
486 |
487 | public boolean isInputEnabled() {
488 | return mInputEnabled;
489 | }
490 |
491 | public int getDotCount() {
492 | return sDotCount;
493 | }
494 |
495 | public boolean isAspectRatioEnabled() {
496 | return mAspectRatioEnabled;
497 | }
498 |
499 | @AspectRatio
500 | public int getAspectRatio() {
501 | return mAspectRatio;
502 | }
503 |
504 | public int getNormalStateColor() {
505 | return mNormalStateColor;
506 | }
507 |
508 | public int getWrongStateColor() {
509 | return mWrongStateColor;
510 | }
511 |
512 | public int getCorrectStateColor() {
513 | return mCorrectStateColor;
514 | }
515 |
516 | public int getPathWidth() {
517 | return mPathWidth;
518 | }
519 |
520 | public int getDotNormalSize() {
521 | return mDotNormalSize;
522 | }
523 |
524 | public int getDotSelectedSize() {
525 | return mDotSelectedSize;
526 | }
527 |
528 | public int getPatternSize() {
529 | return mPatternSize;
530 | }
531 |
532 | public int getDotAnimationDuration() {
533 | return mDotAnimationDuration;
534 | }
535 |
536 | public int getPathEndAnimationDuration() {
537 | return mPathEndAnimationDuration;
538 | }
539 |
540 | /**
541 | * Set the pattern explicitly rather than waiting for the user to input a
542 | * pattern. You can use this for help or demo purposes
543 | *
544 | * @param patternViewMode The mode in which the pattern should be displayed
545 | * @param pattern The pattern
546 | */
547 | public void setPattern(@PatternViewMode int patternViewMode, List pattern) {
548 | mPattern.clear();
549 | mPattern.addAll(pattern);
550 | clearPatternDrawLookup();
551 | for (Dot dot : pattern) {
552 | mPatternDrawLookup[dot.mRow][dot.mColumn] = true;
553 | }
554 | setViewMode(patternViewMode);
555 | }
556 |
557 | /**
558 | * Set the display mode of the current pattern. This can be useful, for
559 | * instance, after detecting a pattern to tell this view whether change the
560 | * in progress result to correct or wrong.
561 | */
562 | public void setViewMode(@PatternViewMode int patternViewMode) {
563 | mPatternViewMode = patternViewMode;
564 | if (patternViewMode == AUTO_DRAW) {
565 | if (mPattern.size() == 0) {
566 | throw new IllegalStateException(
567 | "you must have a pattern to "
568 | + "animate if you want to set the display mode to animate");
569 | }
570 | mAnimatingPeriodStart = SystemClock.elapsedRealtime();
571 | final Dot first = mPattern.get(0);
572 | mInProgressX = getCenterXForColumn(first.mColumn);
573 | mInProgressY = getCenterYForRow(first.mRow);
574 | clearPatternDrawLookup();
575 | }
576 | invalidate();
577 | }
578 |
579 | public void setDotCount(int dotCount) {
580 | sDotCount = dotCount;
581 | mPatternSize = sDotCount * sDotCount;
582 | mPattern = new ArrayList<>(mPatternSize);
583 | mPatternDrawLookup = new boolean[sDotCount][sDotCount];
584 |
585 | mDotStates = new DotState[sDotCount][sDotCount];
586 | for (int i = 0; i < sDotCount; i++) {
587 | for (int j = 0; j < sDotCount; j++) {
588 | mDotStates[i][j] = new DotState();
589 | mDotStates[i][j].mSize = mDotNormalSize;
590 | }
591 | }
592 |
593 | requestLayout();
594 | invalidate();
595 | }
596 |
597 | public void setAspectRatioEnabled(boolean aspectRatioEnabled) {
598 | mAspectRatioEnabled = aspectRatioEnabled;
599 | requestLayout();
600 | }
601 |
602 | public void setAspectRatio(@AspectRatio int aspectRatio) {
603 | mAspectRatio = aspectRatio;
604 | requestLayout();
605 | }
606 |
607 | public void setNormalStateColor(@ColorInt int normalStateColor) {
608 | mNormalStateColor = normalStateColor;
609 | }
610 |
611 | public void setWrongStateColor(@ColorInt int wrongStateColor) {
612 | mWrongStateColor = wrongStateColor;
613 | }
614 |
615 | public void setCorrectStateColor(@ColorInt int correctStateColor) {
616 | mCorrectStateColor = correctStateColor;
617 | }
618 |
619 | public void setPathWidth(@Dimension int pathWidth) {
620 | mPathWidth = pathWidth;
621 |
622 | initView();
623 | invalidate();
624 | }
625 |
626 | public void setDotNormalSize(@Dimension int dotNormalSize) {
627 | mDotNormalSize = dotNormalSize;
628 |
629 | for (int i = 0; i < sDotCount; i++) {
630 | for (int j = 0; j < sDotCount; j++) {
631 | mDotStates[i][j] = new DotState();
632 | mDotStates[i][j].mSize = mDotNormalSize;
633 | }
634 | }
635 |
636 | invalidate();
637 | }
638 |
639 | public void setDotSelectedSize(@Dimension int dotSelectedSize) {
640 | mDotSelectedSize = dotSelectedSize;
641 | }
642 |
643 | public void setDotAnimationDuration(int dotAnimationDuration) {
644 | mDotAnimationDuration = dotAnimationDuration;
645 | invalidate();
646 | }
647 |
648 | public void setPathEndAnimationDuration(int pathEndAnimationDuration) {
649 | mPathEndAnimationDuration = pathEndAnimationDuration;
650 | }
651 |
652 | /**
653 | * Set whether the View is in stealth mode. If {@code true}, there will be
654 | * no visible feedback (path drawing, dot animating, etc) as the user enters the pattern
655 | */
656 | public void setInStealthMode(boolean inStealthMode) {
657 | mInStealthMode = inStealthMode;
658 | }
659 |
660 | public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
661 | mEnableHapticFeedback = tactileFeedbackEnabled;
662 | }
663 |
664 | /**
665 | * Enabled/disables any user input of the view. This can be useful to lock the view temporarily
666 | * while showing any message to the user so that the user cannot get the view in
667 | * an unwanted state
668 | */
669 | public void setInputEnabled(boolean inputEnabled) {
670 | mInputEnabled = inputEnabled;
671 | }
672 |
673 | public void setEnableHapticFeedback(boolean enableHapticFeedback) {
674 | mEnableHapticFeedback = enableHapticFeedback;
675 | }
676 |
677 | public void addPatternLockListener(PatternLockViewListener patternListener) {
678 | mPatternListeners.add(patternListener);
679 | }
680 |
681 | public void removePatternLockListener(PatternLockViewListener patternListener) {
682 | mPatternListeners.remove(patternListener);
683 | }
684 |
685 | public void clearPattern() {
686 | resetPattern();
687 | }
688 |
689 | private int resolveMeasured(int measureSpec, int desired) {
690 | int result;
691 | int specSize = MeasureSpec.getSize(measureSpec);
692 | switch (MeasureSpec.getMode(measureSpec)) {
693 | case MeasureSpec.UNSPECIFIED:
694 | result = desired;
695 | break;
696 | case MeasureSpec.AT_MOST:
697 | result = Math.max(specSize, desired);
698 | break;
699 | case MeasureSpec.EXACTLY:
700 | default:
701 | result = specSize;
702 | }
703 | return result;
704 | }
705 |
706 | private void notifyPatternProgress() {
707 | sendAccessEvent(R.string.message_pattern_dot_added);
708 | notifyListenersProgress(mPattern);
709 | }
710 |
711 | private void notifyPatternStarted() {
712 | sendAccessEvent(R.string.message_pattern_started);
713 | notifyListenersStarted();
714 | }
715 |
716 | private void notifyPatternDetected() {
717 | sendAccessEvent(R.string.message_pattern_detected);
718 | notifyListenersComplete(mPattern);
719 | }
720 |
721 | private void notifyPatternCleared() {
722 | sendAccessEvent(R.string.message_pattern_cleared);
723 | notifyListenersCleared();
724 | }
725 |
726 | private void resetPattern() {
727 | mPattern.clear();
728 | clearPatternDrawLookup();
729 | mPatternViewMode = CORRECT;
730 | invalidate();
731 | }
732 |
733 | private void notifyListenersStarted() {
734 | for (PatternLockViewListener patternListener : mPatternListeners) {
735 | if (patternListener != null) {
736 | patternListener.onStarted();
737 | }
738 | }
739 | }
740 |
741 | private void notifyListenersProgress(List pattern) {
742 | for (PatternLockViewListener patternListener : mPatternListeners) {
743 | if (patternListener != null) {
744 | patternListener.onProgress(pattern);
745 | }
746 | }
747 | }
748 |
749 | private void notifyListenersComplete(List pattern) {
750 | for (PatternLockViewListener patternListener : mPatternListeners) {
751 | if (patternListener != null) {
752 | patternListener.onComplete(pattern);
753 | }
754 | }
755 | }
756 |
757 | private void notifyListenersCleared() {
758 | for (PatternLockViewListener patternListener : mPatternListeners) {
759 | if (patternListener != null) {
760 | patternListener.onCleared();
761 | }
762 | }
763 | }
764 |
765 | private void clearPatternDrawLookup() {
766 | for (int i = 0; i < sDotCount; i++) {
767 | for (int j = 0; j < sDotCount; j++) {
768 | mPatternDrawLookup[i][j] = false;
769 | }
770 | }
771 | }
772 |
773 | /**
774 | * Determines whether the point x, y will add a new point to the current
775 | * pattern (in addition to finding the dot, also makes heuristic choices
776 | * such as filling in gaps based on current pattern).
777 | *
778 | * @param x The x coordinate
779 | * @param y The y coordinate
780 | */
781 | private Dot detectAndAddHit(float x, float y) {
782 | final Dot dot = checkForNewHit(x, y);
783 | if (dot != null) {
784 | // Check for gaps in existing pattern
785 | Dot fillInGapDot = null;
786 | final ArrayList pattern = mPattern;
787 | if (!pattern.isEmpty()) {
788 | Dot lastDot = pattern.get(pattern.size() - 1);
789 | int dRow = dot.mRow - lastDot.mRow;
790 | int dColumn = dot.mColumn - lastDot.mColumn;
791 |
792 | int fillInRow = lastDot.mRow;
793 | int fillInColumn = lastDot.mColumn;
794 |
795 | if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
796 | fillInRow = lastDot.mRow + ((dRow > 0) ? 1 : -1);
797 | }
798 |
799 | if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
800 | fillInColumn = lastDot.mColumn + ((dColumn > 0) ? 1 : -1);
801 | }
802 |
803 | fillInGapDot = Dot.of(fillInRow, fillInColumn);
804 | }
805 |
806 | if (fillInGapDot != null
807 | && !mPatternDrawLookup[fillInGapDot.mRow][fillInGapDot.mColumn]) {
808 | addCellToPattern(fillInGapDot);
809 | }
810 | addCellToPattern(dot);
811 | if (mEnableHapticFeedback) {
812 | performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
813 | HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
814 | | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
815 | }
816 | return dot;
817 | }
818 | return null;
819 | }
820 |
821 | private void addCellToPattern(Dot newDot) {
822 | mPatternDrawLookup[newDot.mRow][newDot.mColumn] = true;
823 | mPattern.add(newDot);
824 | if (!mInStealthMode) {
825 | startDotSelectedAnimation(newDot);
826 | }
827 | notifyPatternProgress();
828 | }
829 |
830 | private void startDotSelectedAnimation(Dot dot) {
831 | final DotState dotState = mDotStates[dot.mRow][dot.mColumn];
832 | startSizeAnimation(mDotNormalSize, mDotSelectedSize, mDotAnimationDuration,
833 | mLinearOutSlowInInterpolator, dotState, new Runnable() {
834 |
835 | @Override
836 | public void run() {
837 | startSizeAnimation(mDotSelectedSize, mDotNormalSize, mDotAnimationDuration,
838 | mFastOutSlowInInterpolator, dotState, null);
839 | }
840 | });
841 | startLineEndAnimation(dotState, mInProgressX, mInProgressY,
842 | getCenterXForColumn(dot.mColumn), getCenterYForRow(dot.mRow));
843 | }
844 |
845 | private void startLineEndAnimation(final DotState state,
846 | final float startX, final float startY, final float targetX,
847 | final float targetY) {
848 | ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
849 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
850 |
851 | @Override
852 | public void onAnimationUpdate(ValueAnimator animation) {
853 | float t = (Float) animation.getAnimatedValue();
854 | state.mLineEndX = (1 - t) * startX + t * targetX;
855 | state.mLineEndY = (1 - t) * startY + t * targetY;
856 | invalidate();
857 | }
858 |
859 | });
860 | valueAnimator.addListener(new AnimatorListenerAdapter() {
861 |
862 | @Override
863 | public void onAnimationEnd(Animator animation) {
864 | state.mLineAnimator = null;
865 | }
866 |
867 | });
868 | valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
869 | valueAnimator.setDuration(mPathEndAnimationDuration);
870 | valueAnimator.start();
871 | state.mLineAnimator = valueAnimator;
872 | }
873 |
874 | private void startSizeAnimation(float start, float end, long duration,
875 | Interpolator interpolator, final DotState state,
876 | final Runnable endRunnable) {
877 | ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
878 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
879 |
880 | @Override
881 | public void onAnimationUpdate(ValueAnimator animation) {
882 | state.mSize = (Float) animation.getAnimatedValue();
883 | invalidate();
884 | }
885 |
886 | });
887 | if (endRunnable != null) {
888 | valueAnimator.addListener(new AnimatorListenerAdapter() {
889 |
890 | @Override
891 | public void onAnimationEnd(Animator animation) {
892 | if (endRunnable != null) {
893 | endRunnable.run();
894 | }
895 | }
896 | });
897 | }
898 | valueAnimator.setInterpolator(interpolator);
899 | valueAnimator.setDuration(duration);
900 | valueAnimator.start();
901 | }
902 |
903 | /**
904 | * Helper method to map a given x, y to its corresponding cell
905 | *
906 | * @param x The x coordinate
907 | * @param y The y coordinate
908 | * @return
909 | */
910 | private Dot checkForNewHit(float x, float y) {
911 | final int rowHit = getRowHit(y);
912 | if (rowHit < 0) {
913 | return null;
914 | }
915 | final int columnHit = getColumnHit(x);
916 | if (columnHit < 0) {
917 | return null;
918 | }
919 |
920 | if (mPatternDrawLookup[rowHit][columnHit]) {
921 | return null;
922 | }
923 | return Dot.of(rowHit, columnHit);
924 | }
925 |
926 | /**
927 | * Helper method to find the row that y coordinate falls into
928 | *
929 | * @param y The y coordinate
930 | * @return The mRow that y falls in, or -1 if it falls in no mRow
931 | */
932 | private int getRowHit(float y) {
933 | final float squareHeight = mViewHeight;
934 | float hitSize = squareHeight * mHitFactor;
935 |
936 | float offset = getPaddingTop() + (squareHeight - hitSize) / 2f;
937 | for (int i = 0; i < sDotCount; i++) {
938 | float hitTop = offset + squareHeight * i;
939 | if (y >= hitTop && y <= hitTop + hitSize) {
940 | return i;
941 | }
942 | }
943 | return -1;
944 | }
945 |
946 | /**
947 | * Helper method to find the column x falls into
948 | *
949 | * @param x The x coordinate
950 | * @return The mColumn that x falls in, or -1 if it falls in no mColumn
951 | */
952 | private int getColumnHit(float x) {
953 | final float squareWidth = mViewWidth;
954 | float hitSize = squareWidth * mHitFactor;
955 |
956 | float offset = getPaddingLeft() + (squareWidth - hitSize) / 2f;
957 | for (int i = 0; i < sDotCount; i++) {
958 |
959 | final float hitLeft = offset + squareWidth * i;
960 | if (x >= hitLeft && x <= hitLeft + hitSize) {
961 | return i;
962 | }
963 | }
964 | return -1;
965 | }
966 |
967 | private void handleActionMove(MotionEvent event) {
968 | float radius = mPathWidth;
969 | int historySize = event.getHistorySize();
970 | mTempInvalidateRect.setEmpty();
971 | boolean invalidateNow = false;
972 | for (int i = 0; i < historySize + 1; i++) {
973 | float x = i < historySize ? event.getHistoricalX(i) : event
974 | .getX();
975 | float y = i < historySize ? event.getHistoricalY(i) : event
976 | .getY();
977 | Dot hitDot = detectAndAddHit(x, y);
978 | int patternSize = mPattern.size();
979 | if (hitDot != null && patternSize == 1) {
980 | mPatternInProgress = true;
981 | notifyPatternStarted();
982 | }
983 | // Note current x and y for rubber banding of in progress patterns
984 | float dx = Math.abs(x - mInProgressX);
985 | float dy = Math.abs(y - mInProgressY);
986 | if (dx > DEFAULT_DRAG_THRESHOLD || dy > DEFAULT_DRAG_THRESHOLD) {
987 | invalidateNow = true;
988 | }
989 |
990 | if (mPatternInProgress && patternSize > 0) {
991 | final ArrayList pattern = mPattern;
992 | final Dot lastDot = pattern.get(patternSize - 1);
993 | float lastCellCenterX = getCenterXForColumn(lastDot.mColumn);
994 | float lastCellCenterY = getCenterYForRow(lastDot.mRow);
995 |
996 | // Adjust for drawn segment from last cell to (x,y). Radius
997 | // accounts for line width.
998 | float left = Math.min(lastCellCenterX, x) - radius;
999 | float right = Math.max(lastCellCenterX, x) + radius;
1000 | float top = Math.min(lastCellCenterY, y) - radius;
1001 | float bottom = Math.max(lastCellCenterY, y) + radius;
1002 |
1003 | // Invalidate between the pattern's new cell and the pattern's
1004 | // previous cell
1005 | if (hitDot != null) {
1006 | float width = mViewWidth * 0.5f;
1007 | float height = mViewHeight * 0.5f;
1008 | float hitCellCenterX = getCenterXForColumn(hitDot.mColumn);
1009 | float hitCellCenterY = getCenterYForRow(hitDot.mRow);
1010 |
1011 | left = Math.min(hitCellCenterX - width, left);
1012 | right = Math.max(hitCellCenterX + width, right);
1013 | top = Math.min(hitCellCenterY - height, top);
1014 | bottom = Math.max(hitCellCenterY + height, bottom);
1015 | }
1016 |
1017 | // Invalidate between the pattern's last cell and the previous
1018 | // location
1019 | mTempInvalidateRect.union(Math.round(left), Math.round(top),
1020 | Math.round(right), Math.round(bottom));
1021 | }
1022 | }
1023 | mInProgressX = event.getX();
1024 | mInProgressY = event.getY();
1025 |
1026 | // To save updates, we only invalidate if the user moved beyond a
1027 | // certain amount.
1028 | if (invalidateNow) {
1029 | mInvalidate.union(mTempInvalidateRect);
1030 | invalidate(mInvalidate);
1031 | mInvalidate.set(mTempInvalidateRect);
1032 | }
1033 | }
1034 |
1035 | private void sendAccessEvent(int resId) {
1036 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
1037 | setContentDescription(getContext().getString(resId));
1038 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
1039 | setContentDescription(null);
1040 | } else {
1041 | announceForAccessibility(getContext().getString(resId));
1042 | }
1043 | }
1044 |
1045 | private void handleActionUp(MotionEvent event) {
1046 | // Report pattern detected
1047 | if (!mPattern.isEmpty()) {
1048 | mPatternInProgress = false;
1049 | cancelLineAnimations();
1050 | notifyPatternDetected();
1051 | invalidate();
1052 | }
1053 | if (PROFILE_DRAWING) {
1054 | if (mDrawingProfilingStarted) {
1055 | Debug.stopMethodTracing();
1056 | mDrawingProfilingStarted = false;
1057 | }
1058 | }
1059 | }
1060 |
1061 | private void cancelLineAnimations() {
1062 | for (int i = 0; i < sDotCount; i++) {
1063 | for (int j = 0; j < sDotCount; j++) {
1064 | DotState state = mDotStates[i][j];
1065 | if (state.mLineAnimator != null) {
1066 | state.mLineAnimator.cancel();
1067 | state.mLineEndX = Float.MIN_VALUE;
1068 | state.mLineEndY = Float.MIN_VALUE;
1069 | }
1070 | }
1071 | }
1072 | }
1073 |
1074 | private void handleActionDown(MotionEvent event) {
1075 | resetPattern();
1076 | float x = event.getX();
1077 | float y = event.getY();
1078 | Dot hitDot = detectAndAddHit(x, y);
1079 | if (hitDot != null) {
1080 | mPatternInProgress = true;
1081 | mPatternViewMode = CORRECT;
1082 | notifyPatternStarted();
1083 | } else {
1084 | mPatternInProgress = false;
1085 | notifyPatternCleared();
1086 | }
1087 | if (hitDot != null) {
1088 | float startX = getCenterXForColumn(hitDot.mColumn);
1089 | float startY = getCenterYForRow(hitDot.mRow);
1090 |
1091 | float widthOffset = mViewWidth / 2f;
1092 | float heightOffset = mViewHeight / 2f;
1093 |
1094 | invalidate((int) (startX - widthOffset),
1095 | (int) (startY - heightOffset),
1096 | (int) (startX + widthOffset), (int) (startY + heightOffset));
1097 | }
1098 | mInProgressX = x;
1099 | mInProgressY = y;
1100 | if (PROFILE_DRAWING) {
1101 | if (!mDrawingProfilingStarted) {
1102 | Debug.startMethodTracing("PatternLockDrawing");
1103 | mDrawingProfilingStarted = true;
1104 | }
1105 | }
1106 | }
1107 |
1108 | private float getCenterXForColumn(int column) {
1109 | return getPaddingLeft() + column * mViewWidth + mViewWidth / 2f;
1110 | }
1111 |
1112 | private float getCenterYForRow(int row) {
1113 | return getPaddingTop() + row * mViewHeight + mViewHeight / 2f;
1114 | }
1115 |
1116 | private float calculateLastSegmentAlpha(float x, float y, float lastX,
1117 | float lastY) {
1118 | float diffX = x - lastX;
1119 | float diffY = y - lastY;
1120 | float dist = (float) Math.sqrt(diffX * diffX + diffY * diffY);
1121 | float fraction = dist / mViewWidth;
1122 | return Math.min(1f, Math.max(0f, (fraction - 0.3f) * 4f));
1123 | }
1124 |
1125 | private int getCurrentColor(boolean partOfPattern) {
1126 | if (!partOfPattern || mInStealthMode || mPatternInProgress) {
1127 | return mNormalStateColor;
1128 | } else if (mPatternViewMode == WRONG) {
1129 | return mWrongStateColor;
1130 | } else if (mPatternViewMode == CORRECT
1131 | || mPatternViewMode == AUTO_DRAW) {
1132 | return mCorrectStateColor;
1133 | } else {
1134 | throw new IllegalStateException("Unknown view mode " + mPatternViewMode);
1135 | }
1136 | }
1137 |
1138 | private void drawCircle(Canvas canvas, float centerX, float centerY,
1139 | float size, boolean partOfPattern, float alpha) {
1140 | mDotPaint.setColor(getCurrentColor(partOfPattern));
1141 | mDotPaint.setAlpha((int) (alpha * 255));
1142 | canvas.drawCircle(centerX, centerY, size / 2, mDotPaint);
1143 | }
1144 |
1145 | /**
1146 | * Represents a cell in the matrix of the pattern view
1147 | */
1148 | public static class Dot implements Parcelable {
1149 |
1150 | private int mRow;
1151 | private int mColumn;
1152 | private static Dot[][] sDots;
1153 |
1154 | static {
1155 | sDots = new Dot[sDotCount][sDotCount];
1156 |
1157 | // Initializing the dots
1158 | for (int i = 0; i < sDotCount; i++) {
1159 | for (int j = 0; j < sDotCount; j++) {
1160 | sDots[i][j] = new Dot(i, j);
1161 | }
1162 | }
1163 | }
1164 |
1165 | private Dot(int row, int column) {
1166 | checkRange(row, column);
1167 | this.mRow = row;
1168 | this.mColumn = column;
1169 | }
1170 |
1171 | /**
1172 | * Gets the identifier of the dot. It is counted from left to right, top to bottom of the
1173 | * matrix, starting by zero
1174 | */
1175 | public int getId() {
1176 | return mRow * sDotCount + mColumn;
1177 | }
1178 |
1179 | public int getRow() {
1180 | return mRow;
1181 | }
1182 |
1183 | public int getColumn() {
1184 | return mColumn;
1185 | }
1186 |
1187 | /**
1188 | * @param row The mRow of the cell.
1189 | * @param column The mColumn of the cell.
1190 | */
1191 | public static synchronized Dot of(int row, int column) {
1192 | checkRange(row, column);
1193 | return sDots[row][column];
1194 | }
1195 |
1196 | /**
1197 | * Gets a cell from its identifier
1198 | */
1199 | public static synchronized Dot of(int id) {
1200 | return of(id / sDotCount, id % sDotCount);
1201 | }
1202 |
1203 | private static void checkRange(int row, int column) {
1204 | if (row < 0 || row > sDotCount - 1) {
1205 | throw new IllegalArgumentException("mRow must be in range 0-"
1206 | + (sDotCount - 1));
1207 | }
1208 | if (column < 0 || column > sDotCount - 1) {
1209 | throw new IllegalArgumentException("mColumn must be in range 0-"
1210 | + (sDotCount - 1));
1211 | }
1212 | }
1213 |
1214 | @Override
1215 | public String toString() {
1216 | return "(Row = " + mRow + ", Col = " + mColumn + ")";
1217 | }
1218 |
1219 | @Override
1220 | public boolean equals(Object object) {
1221 | if (object instanceof Dot)
1222 | return mColumn == ((Dot) object).mColumn
1223 | && mRow == ((Dot) object).mRow;
1224 | return super.equals(object);
1225 | }
1226 |
1227 | @Override
1228 | public int hashCode() {
1229 | int result = mRow;
1230 | result = 31 * result + mColumn;
1231 | return result;
1232 | }
1233 |
1234 | @Override
1235 | public int describeContents() {
1236 | return 0;
1237 | }
1238 |
1239 | @Override
1240 | public void writeToParcel(Parcel dest, int flags) {
1241 | dest.writeInt(mColumn);
1242 | dest.writeInt(mRow);
1243 | }
1244 |
1245 | public static final Creator CREATOR = new Creator() {
1246 |
1247 | public Dot createFromParcel(Parcel in) {
1248 | return new Dot(in);
1249 | }
1250 |
1251 | public Dot[] newArray(int size) {
1252 | return new Dot[size];
1253 | }
1254 | };
1255 |
1256 | private Dot(Parcel in) {
1257 | mColumn = in.readInt();
1258 | mRow = in.readInt();
1259 | }
1260 | }
1261 |
1262 | /**
1263 | * The parcelable for saving and restoring a lock pattern view
1264 | */
1265 | private static class SavedState extends BaseSavedState {
1266 |
1267 | private final String mSerializedPattern;
1268 | private final int mDisplayMode;
1269 | private final boolean mInputEnabled;
1270 | private final boolean mInStealthMode;
1271 | private final boolean mTactileFeedbackEnabled;
1272 |
1273 | /**
1274 | * Constructor called from {@link PatternLockView#onSaveInstanceState()}
1275 | */
1276 | private SavedState(Parcelable superState, String serializedPattern,
1277 | int displayMode, boolean inputEnabled, boolean inStealthMode,
1278 | boolean tactileFeedbackEnabled) {
1279 | super(superState);
1280 |
1281 | mSerializedPattern = serializedPattern;
1282 | mDisplayMode = displayMode;
1283 | mInputEnabled = inputEnabled;
1284 | mInStealthMode = inStealthMode;
1285 | mTactileFeedbackEnabled = tactileFeedbackEnabled;
1286 | }
1287 |
1288 | /**
1289 | * Constructor called from {@link #CREATOR}
1290 | */
1291 | private SavedState(Parcel in) {
1292 | super(in);
1293 |
1294 | mSerializedPattern = in.readString();
1295 | mDisplayMode = in.readInt();
1296 | mInputEnabled = (Boolean) in.readValue(null);
1297 | mInStealthMode = (Boolean) in.readValue(null);
1298 | mTactileFeedbackEnabled = (Boolean) in.readValue(null);
1299 | }
1300 |
1301 | public String getSerializedPattern() {
1302 | return mSerializedPattern;
1303 | }
1304 |
1305 | public int getDisplayMode() {
1306 | return mDisplayMode;
1307 | }
1308 |
1309 | public boolean isInputEnabled() {
1310 | return mInputEnabled;
1311 | }
1312 |
1313 | public boolean isInStealthMode() {
1314 | return mInStealthMode;
1315 | }
1316 |
1317 | public boolean isTactileFeedbackEnabled() {
1318 | return mTactileFeedbackEnabled;
1319 | }
1320 |
1321 | @Override
1322 | public void writeToParcel(Parcel dest, int flags) {
1323 | super.writeToParcel(dest, flags);
1324 | dest.writeString(mSerializedPattern);
1325 | dest.writeInt(mDisplayMode);
1326 | dest.writeValue(mInputEnabled);
1327 | dest.writeValue(mInStealthMode);
1328 | dest.writeValue(mTactileFeedbackEnabled);
1329 | }
1330 |
1331 | @SuppressWarnings("unused")
1332 | public static final Creator CREATOR = new Creator() {
1333 |
1334 | public SavedState createFromParcel(Parcel in) {
1335 | return new SavedState(in);
1336 | }
1337 |
1338 | public SavedState[] newArray(int size) {
1339 | return new SavedState[size];
1340 | }
1341 | };
1342 | }
1343 |
1344 | public static class DotState {
1345 | float mScale = 1.0f;
1346 | float mTranslateY = 0.0f;
1347 | float mAlpha = 1.0f;
1348 | float mSize;
1349 | float mLineEndX = Float.MIN_VALUE;
1350 | float mLineEndY = Float.MIN_VALUE;
1351 | ValueAnimator mLineAnimator;
1352 | }
1353 | }
1354 |
--------------------------------------------------------------------------------
/patternlockview/src/main/java/com/andrognito/patternlockview/listener/PatternLockViewListener.java:
--------------------------------------------------------------------------------
1 | package com.andrognito.patternlockview.listener;
2 |
3 | /**
4 | * Created by aritraroy on 19/03/17.
5 | */
6 |
7 | import com.andrognito.patternlockview.PatternLockView;
8 |
9 | import java.util.List;
10 |
11 | /**
12 | * The callback interface for detecting patterns entered by the user
13 | */
14 | public interface PatternLockViewListener {
15 |
16 | /**
17 | * Fired when the pattern drawing has just started
18 | */
19 | void onStarted();
20 |
21 | /**
22 | * Fired when the pattern is still being drawn and progressed to
23 | * one more {@link com.andrognito.patternlockview.PatternLockView.Dot}
24 | */
25 | void onProgress(List progressPattern);
26 |
27 | /**
28 | * Fired when the user has completed drawing the pattern and has moved their finger away
29 | * from the view
30 | */
31 | void onComplete(List pattern);
32 |
33 | /**
34 | * Fired when the patten has been cleared from the view
35 | */
36 | void onCleared();
37 | }
--------------------------------------------------------------------------------
/patternlockview/src/main/java/com/andrognito/patternlockview/utils/PatternLockUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2007 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.andrognito.patternlockview.utils;
18 |
19 | import com.andrognito.patternlockview.PatternLockView;
20 |
21 | import java.io.UnsupportedEncodingException;
22 | import java.math.BigInteger;
23 | import java.security.MessageDigest;
24 | import java.security.NoSuchAlgorithmException;
25 | import java.util.ArrayList;
26 | import java.util.List;
27 | import java.util.Locale;
28 |
29 | public class PatternLockUtils {
30 |
31 | private static final String UTF8 = "UTF-8";
32 | private static final String SHA1 = "SHA-1";
33 | private static final String MD5 = "MD5";
34 |
35 | private PatternLockUtils() {
36 | throw new AssertionError("You can not instantiate this class. Use its static utility " +
37 | "methods instead");
38 | }
39 |
40 | /**
41 | * Serializes a given pattern to its equivalent string representation. You can store this string
42 | * in any persistence storage or send it to the server for verification
43 | *
44 | * @param pattern The actual pattern
45 | * @return The pattern in its string form
46 | */
47 | public static String patternToString(PatternLockView patternLockView,
48 | List pattern) {
49 | if (pattern == null) {
50 | return "";
51 | }
52 | int patternSize = pattern.size();
53 | StringBuilder stringBuilder = new StringBuilder();
54 |
55 | for (int i = 0; i < patternSize; i++) {
56 | PatternLockView.Dot dot = pattern.get(i);
57 | stringBuilder.append((dot.getRow() * patternLockView.getDotCount() + dot.getColumn()));
58 | }
59 | return stringBuilder.toString();
60 | }
61 |
62 | /**
63 | * De-serializes a given string to its equivalent pattern representation
64 | *
65 | * @param string The pattern serialized with {@link #patternToString}
66 | * @return The actual pattern
67 | */
68 | public static List stringToPattern(PatternLockView patternLockView,
69 | String string) {
70 | List result = new ArrayList<>();
71 |
72 | for (int i = 0; i < string.length(); i++) {
73 | int number = Character.getNumericValue(string.charAt(i));
74 | result.add(PatternLockView.Dot.of(number / patternLockView.getDotCount(),
75 | number % patternLockView.getDotCount()));
76 | }
77 | return result;
78 | }
79 |
80 | /**
81 | * Serializes a given pattern to its equivalent SHA-1 representation. You can store this string
82 | * in any persistence storage or send it to the server for verification
83 | *
84 | * @param pattern The actual pattern
85 | * @return The SHA-1 string of the pattern
86 | */
87 | public static String patternToSha1(PatternLockView patternLockView,
88 | List pattern) {
89 | try {
90 | MessageDigest messageDigest = MessageDigest.getInstance(SHA1);
91 | messageDigest.update(patternToString(patternLockView, pattern).getBytes(UTF8));
92 |
93 | byte[] digest = messageDigest.digest();
94 | BigInteger bigInteger = new BigInteger(1, digest);
95 | return String.format((Locale) null,
96 | "%0" + (digest.length * 2) + "x", bigInteger).toLowerCase();
97 | } catch (NoSuchAlgorithmException e) {
98 | return null;
99 | } catch (UnsupportedEncodingException e) {
100 | return null;
101 | }
102 | }
103 |
104 | /**
105 | * Serializes a given pattern to its equivalent MD5 representation. You can store this string
106 | * in any persistence storage or send it to the server for verification
107 | *
108 | * @param pattern The actual pattern
109 | * @return The MD5 string of the pattern
110 | */
111 | public static String patternToMD5(PatternLockView patternLockView,
112 | List pattern) {
113 | try {
114 | MessageDigest messageDigest = MessageDigest.getInstance(MD5);
115 | messageDigest.update(patternToString(patternLockView, pattern).getBytes(UTF8));
116 |
117 | byte[] digest = messageDigest.digest();
118 | BigInteger bigInteger = new BigInteger(1, digest);
119 | return String.format((Locale) null,
120 | "%0" + (digest.length * 2) + "x", bigInteger).toLowerCase();
121 | } catch (NoSuchAlgorithmException e) {
122 | return null;
123 | } catch (UnsupportedEncodingException e) {
124 | return null;
125 | }
126 | }
127 |
128 | /**
129 | * Generates a random "CAPTCHA" pattern. The generated pattern is easy for the user to re-draw.
130 | *
131 | * NOTE: This method is not optimized and not benchmarked yet for large mSize
132 | * of the pattern's matrix. Currently it works fine with a matrix of {@code 3x3} cells.
133 | * Be careful when the mSize increases.
134 | */
135 | public static ArrayList generateRandomPattern(PatternLockView patternLockView,
136 | int size)
137 | throws IndexOutOfBoundsException {
138 | if (patternLockView == null) {
139 | throw new IllegalArgumentException("PatternLockView can not be null.");
140 | }
141 |
142 | if (size <= 0 || size > patternLockView.getDotCount()) {
143 | throw new IndexOutOfBoundsException("Size must be in range [1, " +
144 | patternLockView.getDotCount() + "]");
145 | }
146 |
147 | List usedIds = new ArrayList<>();
148 | int lastId = RandomUtils.randInt(patternLockView.getDotCount());
149 | usedIds.add(lastId);
150 |
151 | while (usedIds.size() < size) {
152 | // We start from an empty matrix, so there's always a break point to
153 | // exit this loop
154 | final int lastRow = lastId / patternLockView.getDotCount();
155 | final int lastCol = lastId % patternLockView.getDotCount();
156 |
157 | // This is the max available rows/ columns that we can reach from
158 | // the cell of `lastId` to the border of the matrix.
159 | final int maxDistance = Math.max(
160 | Math.max(lastRow, patternLockView.getDotCount() - lastRow),
161 | Math.max(lastCol, patternLockView.getDotCount() - lastCol));
162 |
163 | lastId = -1;
164 |
165 | // Starting from `distance` = 1, find the closest-available
166 | // neighbour value of the cell [lastRow, lastCol].
167 | for (int distance = 1; distance <= maxDistance; distance++) {
168 |
169 | // Now we have a square surrounding the current cell. We call it
170 | // ABCD, in which A is top-left, and C is bottom-right.
171 | final int rowA = lastRow - distance;
172 | final int colA = lastCol - distance;
173 | final int rowC = lastRow + distance;
174 | final int colC = lastCol + distance;
175 |
176 | int[] randomValues;
177 |
178 | // Process randomly AB, BC, CD, and DA. Break the loop as soon
179 | // as we find one value.
180 | final int[] lines = RandomUtils.randIntArray(4);
181 | for (int line : lines) {
182 | switch (line) {
183 | case 0: {
184 | if (rowA >= 0) {
185 | randomValues = RandomUtils.randIntArray(Math.max(0, colA),
186 | Math.min(patternLockView.getDotCount(),
187 | colC + 1));
188 | for (int c : randomValues) {
189 | lastId = rowA * patternLockView.getDotCount()
190 | + c;
191 | if (usedIds.contains(lastId))
192 | lastId = -1;
193 | else
194 | break;
195 | }
196 | }
197 |
198 | break;
199 | }
200 |
201 | case 1: {
202 | if (colC < patternLockView.getDotCount()) {
203 | randomValues = RandomUtils.randIntArray(Math.max(0, rowA + 1),
204 | Math.min(patternLockView.getDotCount(),
205 | rowC + 1));
206 | for (int r : randomValues) {
207 | lastId = r * patternLockView.getDotCount()
208 | + colC;
209 | if (usedIds.contains(lastId))
210 | lastId = -1;
211 | else
212 | break;
213 | }
214 | }
215 |
216 | break;
217 | }
218 |
219 | case 2: {
220 | if (rowC < patternLockView.getDotCount()) {
221 | randomValues = RandomUtils.randIntArray(Math.max(0, colA),
222 | Math.min(patternLockView.getDotCount(),
223 | colC));
224 | for (int c : randomValues) {
225 | lastId = rowC * patternLockView.getDotCount()
226 | + c;
227 | if (usedIds.contains(lastId))
228 | lastId = -1;
229 | else
230 | break;
231 | }
232 | }
233 |
234 | break;
235 | }
236 |
237 | case 3: {
238 | if (colA >= 0) {
239 | randomValues = RandomUtils.randIntArray(Math.max(0, rowA + 1),
240 | Math.min(patternLockView.getDotCount(),
241 | rowC));
242 | for (int r : randomValues) {
243 | lastId = r * patternLockView.getDotCount()
244 | + colA;
245 | if (usedIds.contains(lastId))
246 | lastId = -1;
247 | else
248 | break;
249 | }
250 | }
251 |
252 | break;
253 | }
254 | }
255 |
256 | if (lastId >= 0) break;
257 | }
258 |
259 | if (lastId >= 0) break;
260 | }
261 |
262 | usedIds.add(lastId);
263 | }
264 |
265 | ArrayList result = new ArrayList<>();
266 | for (int id : usedIds) {
267 | result.add(PatternLockView.Dot.of(id));
268 | }
269 |
270 | return result;
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/patternlockview/src/main/java/com/andrognito/patternlockview/utils/RandomUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2012 Hai Bison
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.andrognito.patternlockview.utils;
18 |
19 | import java.util.ArrayList;
20 | import java.util.List;
21 | import java.util.Random;
22 |
23 | /**
24 | * Random utilities.
25 | */
26 | public class RandomUtils {
27 |
28 | private static final Random RANDOM = new Random();
29 |
30 | private RandomUtils() {
31 | throw new AssertionError("You can not instantiate this class. Use its static utility " +
32 | "methods instead");
33 | }
34 |
35 | /**
36 | * Generates a random integer
37 | */
38 | public static int randInt() {
39 | return RANDOM.nextInt((int) (System.nanoTime() % Integer.MAX_VALUE));
40 | }
41 |
42 | /**
43 | * Generates a random integer within {@code [0, max)}.
44 | *
45 | * @param max The maximum bound
46 | * @return A random integer
47 | */
48 | public static int randInt(int max) {
49 | return max > 0 ? randInt() % max : 0;
50 | }
51 |
52 | /**
53 | * Generates a random integer array which has length of {@code end - start},
54 | * and is filled by all values from {@code start} to {@code end - 1} in randomized orders.
55 | *
56 | * @param start The starting value
57 | * @param end The ending value
58 | * @return The random integer array. If {@code end <= start}, an empty array is returned
59 | */
60 | public static int[] randIntArray(int start, int end) {
61 | if (end <= start) {
62 | return new int[0];
63 | }
64 |
65 | final List values = new ArrayList<>();
66 | for (int i = start; i < end; i++) {
67 | values.add(i);
68 | }
69 |
70 | final int[] result = new int[values.size()];
71 | for (int i = 0; i < result.length; i++) {
72 | int k = randInt(values.size());
73 | result[i] = values.get(k);
74 | values.remove(k);
75 | }
76 |
77 | return result;
78 | }
79 |
80 | /**
81 | * Generates a random integer array which has length of {@code end},
82 | * and is filled by all values from {@code 0} to {@code end - 1} in randomized orders.
83 | *
84 | * @param end The ending value
85 | * @return The random integer array. If {@code end <= start}, an empty array is returned
86 | */
87 | public static int[] randIntArray(int end) {
88 | return randIntArray(0, end);
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/patternlockview/src/main/java/com/andrognito/patternlockview/utils/ResourceUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2012 Hai Bison
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.andrognito.patternlockview.utils;
18 |
19 | import android.content.Context;
20 | import android.support.annotation.ColorRes;
21 | import android.support.annotation.DimenRes;
22 | import android.support.annotation.NonNull;
23 | import android.support.annotation.StringRes;
24 | import android.support.v4.content.ContextCompat;
25 |
26 | public class ResourceUtils {
27 |
28 | private ResourceUtils() {
29 | throw new AssertionError("You can not instantiate this class. Use its static utility " +
30 | "methods instead");
31 | }
32 |
33 | /**
34 | * Get color from a resource id
35 | *
36 | * @param context The context
37 | * @param colorRes The resource identifier of the color
38 | * @return The resolved color value
39 | */
40 | public static int getColor(@NonNull Context context, @ColorRes int colorRes) {
41 | return ContextCompat.getColor(context, colorRes);
42 | }
43 |
44 | /**
45 | * Get string from a resource id
46 | *
47 | * @param context The context
48 | * @param stringRes The resource identifier of the string
49 | * @return The string value
50 | */
51 | public static String getString(@NonNull Context context, @StringRes int stringRes) {
52 | return context.getString(stringRes);
53 | }
54 |
55 | /**
56 | * Get dimension in pixels from its resource id
57 | *
58 | * @param context The context
59 | * @param dimenRes The resource identifier of the dimension
60 | * @return The dimension in pixels
61 | */
62 | public static float getDimensionInPx(@NonNull Context context, @DimenRes int dimenRes) {
63 | return context.getResources().getDimension(dimenRes);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/patternlockview/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 |
--------------------------------------------------------------------------------
/patternlockview/src/main/res/values/color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFF
4 | #f4511e
5 |
--------------------------------------------------------------------------------
/patternlockview/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 3dp
4 | 10dp
5 | 24dp
6 |
--------------------------------------------------------------------------------
/patternlockview/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | PatternLockView
3 | Dot added to pattern
4 | Pattern drawing started
5 | Pattern drawing completed
6 | Pattern cleared
7 |
8 |
--------------------------------------------------------------------------------
/screenshots/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aritraroy/PatternLockView/a90b0d4bf0f286bc23e0356a260b6929adebe7de/screenshots/logo.png
--------------------------------------------------------------------------------
/screenshots/pattern-lock-view-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aritraroy/PatternLockView/a90b0d4bf0f286bc23e0356a260b6929adebe7de/screenshots/pattern-lock-view-banner.png
--------------------------------------------------------------------------------
/screenshots/pattern_lock_view_2_small.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aritraroy/PatternLockView/a90b0d4bf0f286bc23e0356a260b6929adebe7de/screenshots/pattern_lock_view_2_small.gif
--------------------------------------------------------------------------------
/screenshots/pattern_lock_view_small.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aritraroy/PatternLockView/a90b0d4bf0f286bc23e0356a260b6929adebe7de/screenshots/pattern_lock_view_small.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':patternlockview', ':patternlockview-reactive'
2 |
--------------------------------------------------------------------------------