diff --git a/winboll/build.properties b/winboll/build.properties
index ff0f384..224209f 100644
--- a/winboll/build.properties
+++ b/winboll/build.properties
@@ -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
diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml
index bd4f999..2f5cc47 100644
--- a/winboll/src/main/AndroidManifest.xml
+++ b/winboll/src/main/AndroidManifest.xml
@@ -293,6 +293,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/winboll/src/main/res/drawable/dot_background.xml b/winboll/src/main/res/drawable/dot_background.xml
new file mode 100644
index 0000000..f9fc6fc
--- /dev/null
+++ b/winboll/src/main/res/drawable/dot_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/winboll/src/main/res/drawable/dot_darkgreen_dark.xml b/winboll/src/main/res/drawable/dot_darkgreen_dark.xml
new file mode 100644
index 0000000..25f65b4
--- /dev/null
+++ b/winboll/src/main/res/drawable/dot_darkgreen_dark.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_pattern_lock.xml b/winboll/src/main/res/layout/activity_pattern_lock.xml
new file mode 100644
index 0000000..2298d46
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_pattern_lock.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/values/colors.xml b/winboll/src/main/res/values/colors.xml
index 479769a..b49a043 100644
--- a/winboll/src/main/res/values/colors.xml
+++ b/winboll/src/main/res/values/colors.xml
@@ -3,4 +3,5 @@
#009688
#00796B
#FF9800
+ #000000
\ No newline at end of file
diff --git a/winboll/src/main/res/values/strings.xml b/winboll/src/main/res/values/strings.xml
index ae4a142..0cc6cdd 100644
--- a/winboll/src/main/res/values/strings.xml
+++ b/winboll/src/main/res/values/strings.xml
@@ -12,4 +12,5 @@
WinBoLL
WinBoLL APP
MyTermuxActivity
+ 图案密码设置