feat(pattern-lock): 添加图案密码解锁功能
- 创建带图案打开意图过滤器的 PatternLockActivity - 构建图案锁布局和点背景样式 - 添加图案锁颜色和字符串资源 - 更新构建计数到 11 注意:图案锁 UI 已创建但尚未集成
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Thu Apr 30 12:07:31 HKT 2026
|
#Thu Apr 30 14:58:25 CST 2026
|
||||||
stageCount=27
|
stageCount=27
|
||||||
libraryProject=
|
libraryProject=
|
||||||
baseVersion=15.11
|
baseVersion=15.11
|
||||||
publishVersion=15.11.26
|
publishVersion=15.11.26
|
||||||
buildCount=0
|
buildCount=11
|
||||||
baseBetaVersion=15.11.27
|
baseBetaVersion=15.11.27
|
||||||
|
|||||||
@@ -293,6 +293,19 @@
|
|||||||
|
|
||||||
<activity android:name="cc.winboll.studio.winboll.activities.WXPayActivity"/>
|
<activity android:name="cc.winboll.studio.winboll.activities.WXPayActivity"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.winboll.activities.PatternLockActivity"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="cc.winboll.studio.winboll.activities.PatternLockActivity.ACTION_OPEN_PATTERN"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity android:name="cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity"/>
|
<activity android:name="cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package cc.winboll.studio.winboll.activities;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||||
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
|
import cc.winboll.studio.winboll.R;
|
||||||
|
|
||||||
|
public class PatternLockActivity extends BaseWinBoLLActivity {
|
||||||
|
|
||||||
|
public static final String TAG = "PatternLockActivity";
|
||||||
|
|
||||||
|
private static final int DOT_RADIUS = 8;
|
||||||
|
private static final int PATTERN_ERROR_DURATION = 1500;
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "pattern_lock_prefs";
|
||||||
|
static final String KEY_LOCK_PATTERN = "lock_pattern";
|
||||||
|
static final String KEY_ERROR_STATE = "error_state";
|
||||||
|
private static final String KEY_ERROR_REPEAT_PATTERN = "error_repeat_pattern";
|
||||||
|
|
||||||
|
private boolean mIsInErrorState;
|
||||||
|
private boolean mNeedRestart;
|
||||||
|
private Handler mHandler;
|
||||||
|
|
||||||
|
private FrameLayout mContainer;
|
||||||
|
private PatternView mPatternView;
|
||||||
|
|
||||||
|
public PatternLockActivity() {
|
||||||
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
PatternLockActivity(Context context) {
|
||||||
|
mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
AESThemeUtil.applyAppTheme(this);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_pattern_lock);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
getSupportActionBar().setSubtitle(TAG);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
toolbar.setNavigationOnClickListener(v -> finish());
|
||||||
|
|
||||||
|
mContainer = findViewById(R.id.container);
|
||||||
|
mPatternView = new PatternView(this);
|
||||||
|
mContainer.addView(mPatternView);
|
||||||
|
mPatternView.invalidate();
|
||||||
|
|
||||||
|
mNeedRestart = false;
|
||||||
|
boolean isEnoughPoints = savedInstanceIsEnoughPoints();
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
mIsInErrorState = savedInstanceState.getBoolean(KEY_ERROR_STATE, false);
|
||||||
|
mNeedRestart = savedInstanceState.getBoolean(KEY_ERROR_REPEAT_PATTERN, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mIsInErrorState) {
|
||||||
|
mPatternView.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean savedInstanceIsEnoughPoints() {
|
||||||
|
int count = 0;
|
||||||
|
if (mPatternView != null) {
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
if (mPatternView.mDotState[i] == 1) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count >= 4 || count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
|
if (mIsInErrorState) {
|
||||||
|
outState.putBoolean(KEY_ERROR_STATE, mIsInErrorState);
|
||||||
|
}
|
||||||
|
if (mNeedRestart) {
|
||||||
|
outState.putBoolean(KEY_ERROR_REPEAT_PATTERN, mNeedRestart);
|
||||||
|
}
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showErrorState() {
|
||||||
|
mIsInErrorState = true;
|
||||||
|
invalidatePattern();
|
||||||
|
mHandler.postDelayed(() -> {
|
||||||
|
mIsInErrorState = false;
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
|
||||||
|
invalidatePattern();
|
||||||
|
if (mPatternView != null) mPatternView.invalidate();
|
||||||
|
}, PATTERN_ERROR_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearErrorState() {
|
||||||
|
mIsInErrorState = false;
|
||||||
|
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||||
|
prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
|
||||||
|
invalidatePattern();
|
||||||
|
if (mPatternView != null) mPatternView.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showErrorToast() {
|
||||||
|
android.widget.Toast.makeText(this, "图案点数不足,请重新绘制",
|
||||||
|
android.widget.Toast.LENGTH_SHORT).show();
|
||||||
|
mNeedRestart = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSuccessDialog() {
|
||||||
|
android.app.AlertDialog alertDialog = new android.app.AlertDialog.Builder(this)
|
||||||
|
.setTitle("设置成功")
|
||||||
|
.setMessage("图案密码已设置成功")
|
||||||
|
.setPositiveButton("确定", (dialog, which) -> finish())
|
||||||
|
.setCancelable(false)
|
||||||
|
.create();
|
||||||
|
alertDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishWithRestart() {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidatePattern() {
|
||||||
|
if (mPatternView != null) {
|
||||||
|
mPatternView.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PatternView extends FrameLayout {
|
||||||
|
int mPatternSize = 0;
|
||||||
|
int MAX_DOT_COUNT = 9;
|
||||||
|
int[] mDotX = new int[MAX_DOT_COUNT];
|
||||||
|
int[] mDotY = new int[MAX_DOT_COUNT];
|
||||||
|
int[] mDotState = new int[MAX_DOT_COUNT];
|
||||||
|
Bitmap mDotBitmap;
|
||||||
|
Paint mPaintConnector;
|
||||||
|
Paint mPaintErrorBackground;
|
||||||
|
int mDotCount = 0;
|
||||||
|
|
||||||
|
PatternView(Context context) {
|
||||||
|
super(context);
|
||||||
|
setBackgroundColor(Color.WHITE);
|
||||||
|
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||||
|
mDotX[i] = -1;
|
||||||
|
mDotY[i] = -1;
|
||||||
|
mDotState[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
if (w == 0 || h == 0) return;
|
||||||
|
mPatternSize = w > h ? h : w;
|
||||||
|
int grid = 3;
|
||||||
|
int cell = mPatternSize / grid;
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||||
|
mDotX[i] = (i % grid) * cell + cell / 2 - cell / 24;
|
||||||
|
mDotY[i] = (i / grid) * cell + cell / 2 - cell / 24;
|
||||||
|
mDotState[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mDotBitmap == null) {
|
||||||
|
mDotBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dot_darkgreen_dark);
|
||||||
|
}
|
||||||
|
if (mPaintConnector == null) {
|
||||||
|
mPaintConnector = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||||
|
mPaintConnector.setColor(-0xFF006400);
|
||||||
|
}
|
||||||
|
if (mPaintErrorBackground == null) {
|
||||||
|
mPaintErrorBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
mPaintErrorBackground.setColor(Color.RED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (mDotCount > 0) return false;
|
||||||
|
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
float x = event.getX();
|
||||||
|
float y = event.getY();
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||||
|
int dx = (int) Math.abs(x - mDotX[i]);
|
||||||
|
int dy = (int) Math.abs(y - mDotY[i]);
|
||||||
|
if (dx <= DOT_RADIUS && dy <= DOT_RADIUS && mDotState[i] == 0) {
|
||||||
|
mDotState[i] = 1;
|
||||||
|
mDotCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < mDotCount - 1; i++) {
|
||||||
|
int a = -1, b = -1;
|
||||||
|
for (int k = 0; k < MAX_DOT_COUNT; k++) {
|
||||||
|
if (mDotState[k] == 1) {
|
||||||
|
if (a < 0) a = k;
|
||||||
|
else b = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (a >= 0 && b >= 0) {
|
||||||
|
a = Math.min(a, b);
|
||||||
|
b = Math.max(a, b);
|
||||||
|
}
|
||||||
|
if (mDotState[a] == 1 && mDotState[b] == 1) {
|
||||||
|
int dx = mDotX[b] - mDotX[a];
|
||||||
|
int dy = mDotY[b] - mDotY[a];
|
||||||
|
if ((Math.abs(dx) <= 1 && Math.abs(dy) <= 1) ||
|
||||||
|
(Math.abs(dx) <= 2 && Math.abs(dy) <= 1)) {
|
||||||
|
if (mDotState[b] == 1) {
|
||||||
|
for (int k = a + 1; k < b; k++) {
|
||||||
|
if (mDotState[k] == 0) {
|
||||||
|
mDotState[k] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDotCount += (b - a - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
if (mDotCount < 4) {
|
||||||
|
showErrorState();
|
||||||
|
showErrorToast();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDraw(Canvas canvas) {
|
||||||
|
if (mPatternSize == 0) return;
|
||||||
|
|
||||||
|
int activeCount = 0;
|
||||||
|
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||||
|
if (mDotState[i] == 1) activeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeCount == 0) {
|
||||||
|
mPaintErrorBackground.setAlpha(50);
|
||||||
|
} else {
|
||||||
|
mPaintErrorBackground.setAlpha(mIsInErrorState ? 80 : 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.clipRect(0, 0, mPatternSize * 80 / 100, mPatternSize * 80 / 100);
|
||||||
|
|
||||||
|
canvas.drawRect(0, 0, mPatternSize, mPatternSize, mPaintErrorBackground);
|
||||||
|
|
||||||
|
if (mDotBitmap != null) {
|
||||||
|
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||||
|
if (mDotState[i] == 1) {
|
||||||
|
canvas.drawBitmap(mDotBitmap, mDotX[i], mDotY[i], mPaintConnector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
winboll/src/main/res/drawable/dot_background.xml
Normal file
8
winboll/src/main/res/drawable/dot_background.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="ring"
|
||||||
|
android:innerRadiusRatio="3"
|
||||||
|
android:thicknessRatio="8"
|
||||||
|
android:useLevel="false">
|
||||||
|
<solid android:color="#00000000"/>
|
||||||
|
</shape>
|
||||||
14
winboll/src/main/res/drawable/dot_darkgreen_dark.xml
Normal file
14
winboll/src/main/res/drawable/dot_darkgreen_dark.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:insetLeft="2dp"
|
||||||
|
android:insetTop="2dp"
|
||||||
|
android:insetRight="2dp"
|
||||||
|
android:insetBottom="2dp">
|
||||||
|
<shape android:shape="ring"
|
||||||
|
android:innerRadiusRatio="2"
|
||||||
|
android:thicknessRatio="2.5"
|
||||||
|
android:useLevel="false">
|
||||||
|
<solid android:color="#006400"/>
|
||||||
|
<stroke android:width="1dp" android:color="#006400"/>
|
||||||
|
</shape>
|
||||||
|
</inset>
|
||||||
25
winboll/src/main/res/layout/activity_pattern_lock.xml
Normal file
25
winboll/src/main/res/layout/activity_pattern_lock.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/black">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:gravity="center"
|
||||||
|
app:titleTextColor="@android:color/white"
|
||||||
|
app:subtitleTextColor="@android:color/white"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#FFFFFF"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
<color name="colorPrimary">#009688</color>
|
<color name="colorPrimary">#009688</color>
|
||||||
<color name="colorPrimaryDark">#00796B</color>
|
<color name="colorPrimaryDark">#00796B</color>
|
||||||
<color name="colorAccent">#FF9800</color>
|
<color name="colorAccent">#FF9800</color>
|
||||||
|
<color name="pattern_lock_black">#000000</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -12,4 +12,5 @@
|
|||||||
<string name="tileservice_name">WinBoLL</string>
|
<string name="tileservice_name">WinBoLL</string>
|
||||||
<string name="toolbar_icon_description">WinBoLL APP</string>
|
<string name="toolbar_icon_description">WinBoLL APP</string>
|
||||||
<string name="my_termux_activity">MyTermuxActivity</string>
|
<string name="my_termux_activity">MyTermuxActivity</string>
|
||||||
|
<string name="pattern_lock_title">图案密码设置</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user