feat(pattern-lock): 添加图案密码解锁功能
- 创建带图案打开意图过滤器的 PatternLockActivity - 构建图案锁布局和点背景样式 - 添加图案锁颜色和字符串资源 - 更新构建计数到 11 注意:图案锁 UI 已创建但尚未集成
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#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
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.26
|
||||
buildCount=0
|
||||
buildCount=11
|
||||
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.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
|
||||
|
||||
@@ -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="colorPrimaryDark">#00796B</color>
|
||||
<color name="colorAccent">#FF9800</color>
|
||||
<color name="pattern_lock_black">#000000</color>
|
||||
</resources>
|
||||
@@ -12,4 +12,5 @@
|
||||
<string name="tileservice_name">WinBoLL</string>
|
||||
<string name="toolbar_icon_description">WinBoLL APP</string>
|
||||
<string name="my_termux_activity">MyTermuxActivity</string>
|
||||
<string name="pattern_lock_title">图案密码设置</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user