From 8fd7a560d8e676fa9c05c07c748d56a044bebbed Mon Sep 17 00:00:00 2001 From: 7015725 Date: Fri, 22 May 2026 08:56:24 +0800 Subject: [PATCH] Add ToolApp surface back gesture mode --- code/th_01_base.js | 12 +++- code/th_15_extra.js | 145 +++++++++++++++++++++++++++++++++++++------- manifest.json | 10 +-- manifest.sig | 2 +- 4 files changed, 140 insertions(+), 29 deletions(-) diff --git a/code/th_01_base.js b/code/th_01_base.js index 5fc29d8..145d314 100644 --- a/code/th_01_base.js +++ b/code/th_01_base.js @@ -100,6 +100,7 @@ var ConfigValidator = { LONG_PRESS_TRIGGERED_MOVE_SLOP_DP: { type: "int", min: 8, max: 80, default: 28 }, LONG_PRESS_VIBRATE_MS: { type: "int", min: 10, max: 100, default: 40 }, CLICK_SLOP_DP: { type: "int", min: 2, max: 20, default: 6 }, + TOOLAPP_BACK_GESTURE_MODE: { type: "enum", values: ["edge", "surface", "off"], default: "surface" }, TOOLAPP_BACK_EDGE_WIDTH_DP: { type: "int", min: 1, max: 120, default: 72 }, ENABLE_TOOLAPP_INNER_BACK_STRIPS: { type: "bool", default: false }, ENABLE_TOOLAPP_SCREEN_BACK_STRIPS: { type: "bool", default: true }, @@ -736,6 +737,7 @@ var ConfigManager = { LONG_PRESS_MS: 520, LONG_PRESS_TRIGGERED_MOVE_SLOP_DP: 28, CLICK_SLOP_DP: 6, + TOOLAPP_BACK_GESTURE_MODE: "surface", TOOLAPP_BACK_EDGE_WIDTH_DP: 72, ENABLE_TOOLAPP_INNER_BACK_STRIPS: false, ENABLE_TOOLAPP_SCREEN_BACK_STRIPS: true, @@ -850,7 +852,12 @@ var ConfigManager = { { type: "section", name: "触摸与手势" }, { key: "CLICK_SLOP_DP", name: "点击位移阈值(dp)", type: "int", min: 1, max: 40, step: 1 }, - { key: "TOOLAPP_BACK_EDGE_WIDTH_DP", name: "返回起手边缘宽度", type: "int", min: 1, max: 120, step: 1 }, + { key: "TOOLAPP_BACK_GESTURE_MODE", name: "设置页滑动返回模式", type: "single_choice", options: [ + { label: "全表面横滑", value: "surface" }, + { label: "仅左右边缘", value: "edge" }, + { label: "关闭", value: "off" } + ]}, + { key: "TOOLAPP_BACK_EDGE_WIDTH_DP", name: "边缘模式起手宽度", type: "int", min: 1, max: 120, step: 1 }, { key: "ENABLE_TOOLAPP_INNER_BACK_STRIPS", name: "启用旧版页面覆盖热区(不推荐)", type: "bool" }, { key: "ENABLE_TOOLAPP_SCREEN_BACK_STRIPS", name: "启用屏幕空白区返回", type: "bool" }, { key: "TOOLAPP_BACK_COMMIT_DISTANCE_DP", name: "设置页返回触发距离", type: "int", min: 1, max: 480, step: 1 }, @@ -892,7 +899,7 @@ var ConfigManager = { var needReset = false; if (s) { var sStr = JSON.stringify(s); - if (sStr.indexOf("ENABLE_SNAP_TO_EDGE") < 0 || sStr.indexOf("ENABLE_ANIMATIONS") < 0 || sStr.indexOf("BALL_IDLE_ALPHA") < 0 || sStr.indexOf("PANEL_POS_GRAVITY") < 0 || sStr.indexOf("single_choice") < 0 || sStr.indexOf("ball_shortx_icon") < 0 || sStr.indexOf("ball_color") < 0 || sStr.indexOf("SETTINGS_THEME") < 0 || sStr.indexOf("BALL_BG_COLOR_HEX") < 0 || sStr.indexOf("BALL_ICON_SIZE_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_EDGE_WIDTH_DP") < 0 || sStr.indexOf("ENABLE_TOOLAPP_INNER_BACK_STRIPS") < 0 || sStr.indexOf("ENABLE_TOOLAPP_SCREEN_BACK_STRIPS") < 0 || sStr.indexOf("TOOLAPP_BACK_COMMIT_DISTANCE_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_PROGRESS_DISTANCE_DP") < 0 || sStr.indexOf("LONG_PRESS_TRIGGERED_MOVE_SLOP_DP") < 0) { + if (sStr.indexOf("ENABLE_SNAP_TO_EDGE") < 0 || sStr.indexOf("ENABLE_ANIMATIONS") < 0 || sStr.indexOf("BALL_IDLE_ALPHA") < 0 || sStr.indexOf("PANEL_POS_GRAVITY") < 0 || sStr.indexOf("single_choice") < 0 || sStr.indexOf("ball_shortx_icon") < 0 || sStr.indexOf("ball_color") < 0 || sStr.indexOf("SETTINGS_THEME") < 0 || sStr.indexOf("BALL_BG_COLOR_HEX") < 0 || sStr.indexOf("BALL_ICON_SIZE_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_GESTURE_MODE") < 0 || sStr.indexOf("TOOLAPP_BACK_EDGE_WIDTH_DP") < 0 || sStr.indexOf("ENABLE_TOOLAPP_INNER_BACK_STRIPS") < 0 || sStr.indexOf("ENABLE_TOOLAPP_SCREEN_BACK_STRIPS") < 0 || sStr.indexOf("TOOLAPP_BACK_COMMIT_DISTANCE_DP") < 0 || sStr.indexOf("TOOLAPP_BACK_PROGRESS_DISTANCE_DP") < 0 || sStr.indexOf("LONG_PRESS_TRIGGERED_MOVE_SLOP_DP") < 0) { needReset = true; } @@ -923,6 +930,7 @@ var ConfigManager = { if (schemaItemDiffers("BALL_ICON_TINT_HEX", ["name", "type"]) || schemaItemDiffers("BALL_ICON_SIZE_DP", ["name", "type", "min", "max", "step"]) || schemaItemDiffers("BALL_BG_COLOR_HEX", ["name", "type"]) || + schemaItemDiffers("TOOLAPP_BACK_GESTURE_MODE", ["name", "type"]) || schemaItemDiffers("TOOLAPP_BACK_EDGE_WIDTH_DP", ["name", "type", "min", "max", "step"]) || schemaItemDiffers("ENABLE_TOOLAPP_INNER_BACK_STRIPS", ["name", "type"]) || schemaItemDiffers("ENABLE_TOOLAPP_SCREEN_BACK_STRIPS", ["name", "type"]) || diff --git a/code/th_15_extra.js b/code/th_15_extra.js index 523ec4d..495112a 100644 --- a/code/th_15_extra.js +++ b/code/th_15_extra.js @@ -1231,6 +1231,73 @@ FloatBallAppWM.prototype.getToolAppResponsiveSpec = function() { }; }; +FloatBallAppWM.prototype.getToolAppBackGestureMode = function() { + var mode = "surface"; + try { mode = String(this.config.TOOLAPP_BACK_GESTURE_MODE || "surface"); } catch(e) { mode = "surface"; } + if (mode !== "edge" && mode !== "surface" && mode !== "off") mode = "surface"; + return mode; +}; + +FloatBallAppWM.prototype.isToolAppBackInteractiveView = function(v) { + try { + if (!v) return false; + try { if (v instanceof android.widget.SeekBar) return true; } catch(eSeek) {} + try { if (v instanceof android.widget.CompoundButton) return true; } catch(eComp) {} + try { if (v instanceof android.widget.Switch) return true; } catch(eSw) {} + try { if (v instanceof android.widget.EditText) return true; } catch(eEdit) {} + try { if (v instanceof android.widget.Button) return true; } catch(eBtn) {} + try { if (v instanceof android.widget.AbsListView) return true; } catch(eAbs) {} + try { if (v instanceof android.widget.ListView) return true; } catch(eList) {} + try { if (v instanceof android.widget.HorizontalScrollView) return true; } catch(eHsv) {} + try { + var rvCls = java.lang.Class.forName("androidx.recyclerview.widget.RecyclerView"); + if (rvCls && rvCls.isInstance(v)) return true; + } catch(eRv1) {} + try { + var rvCls2 = java.lang.Class.forName("android.support.v7.widget.RecyclerView"); + if (rvCls2 && rvCls2.isInstance(v)) return true; + } catch(eRv2) {} + try { if (v.isClickable && v.isClickable()) return true; } catch(eClick) {} + try { if (v.isLongClickable && v.isLongClickable()) return true; } catch(eLong) {} + } catch(e) {} + return false; +}; + +FloatBallAppWM.prototype.findToolAppTouchedChild = function(v, rawX, rawY) { + try { + if (!v || !v.getVisibility || v.getVisibility() !== android.view.View.VISIBLE) return null; + var loc = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE, 2); + try { v.getLocationOnScreen(loc); } catch(eLoc) { return null; } + var l = Number(loc[0] || 0), t = Number(loc[1] || 0); + var r = l + Number(v.getWidth ? v.getWidth() : 0); + var b = t + Number(v.getHeight ? v.getHeight() : 0); + if (rawX < l || rawX > r || rawY < t || rawY > b) return null; + try { + if (v instanceof android.view.ViewGroup) { + var count = v.getChildCount ? v.getChildCount() : 0; + for (var i = count - 1; i >= 0; i--) { + var child = v.getChildAt(i); + var hit = this.findToolAppTouchedChild(child, rawX, rawY); + if (hit) return hit; + } + } + } catch(eGroup) {} + return v; + } catch(e) {} + return null; +}; + +FloatBallAppWM.prototype.isToolAppBackBlockedAt = function(root, rawX, rawY) { + try { + var v = this.findToolAppTouchedChild(root, rawX, rawY); + while (v && v !== root) { + if (this.isToolAppBackInteractiveView && this.isToolAppBackInteractiveView(v)) return true; + try { v = v.getParent ? v.getParent() : null; } catch(eParent) { v = null; } + } + } catch(e) {} + return false; +}; + FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBack) { var self = this; var isDark = this.isDarkTheme(); @@ -1244,8 +1311,13 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac var topBarHeight = spec ? spec.topBarHeight : this.dp(56); var rootDownX = 0; var rootDownY = 0; + var rootDownRawX = 0; + var rootDownRawY = 0; var rootEdge = -1; + var rootBackMode = "surface"; var rootBackActive = false; + var rootBackEligible = false; + var rootBackBlocked = false; var rootBackMoved = false; var root = new JavaAdapter(android.widget.FrameLayout, { onInterceptTouchEvent: function(ev) { @@ -1255,36 +1327,61 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac if (action === android.view.MotionEvent.ACTION_DOWN) { rootDownX = ev.getX(); rootDownY = ev.getY(); + rootDownRawX = ev.getRawX(); + rootDownRawY = ev.getRawY(); rootBackActive = false; + rootBackEligible = false; + rootBackBlocked = false; rootBackMoved = false; rootEdge = -1; + rootBackMode = self.getToolAppBackGestureMode ? self.getToolAppBackGestureMode() : "surface"; + if (rootBackMode === "off") return false; var canBackNow = !!(self.state && self.state.toolAppActive && self.hasToolAppBackTarget && self.hasToolAppBackTarget()); - if (canBackNow) { - var edgeW = self.getToolAppBackEdgeWidthPx ? self.getToolAppBackEdgeWidthPx() : self.dp(72); - var rw = 0; - try { rw = this.getWidth(); } catch(eW) { rw = 0; } - if (rootDownX <= edgeW) rootEdge = 0; - else if (rw > 0 && rootDownX >= rw - edgeW) rootEdge = 1; + if (!canBackNow) return false; + var edgeW = self.getToolAppBackEdgeWidthPx ? self.getToolAppBackEdgeWidthPx() : self.dp(72); + var rw = 0; + try { rw = this.getWidth(); } catch(eW) { rw = 0; } + if (rootBackMode === "edge") { + if (rootDownX <= edgeW) { rootEdge = 0; rootBackEligible = true; } + else if (rw > 0 && rootDownX >= rw - edgeW) { rootEdge = 1; rootBackEligible = true; } + } else { + rootBackEligible = true; + rootBackBlocked = !!(self.isToolAppBackBlockedAt && self.isToolAppBackBlockedAt(this, rootDownRawX, rootDownRawY)); } - // DOWN 必须放行给子控件,避免按钮/列表/Switch/SeekBar 被边缘手势抢走。 + // DOWN 必须放行给子控件,surface 模式也不抢按钮/列表/Switch/SeekBar 原始点击。 return false; } if (action === android.view.MotionEvent.ACTION_MOVE) { - if (rootEdge < 0) return false; + if (!rootBackEligible || rootBackBlocked || rootBackMode === "off") return false; + if (!(self.hasToolAppBackTarget && self.hasToolAppBackTarget())) return false; var dx = ev.getX() - rootDownX; var dy = ev.getY() - rootDownY; var adx = Math.abs(dx); var ady = Math.abs(dy); - var validDir = (rootEdge === 0 && dx > 0) || (rootEdge === 1 && dx < 0); - var slopDp = Number(self.config.CLICK_SLOP_DP || 6); - if (isNaN(slopDp)) slopDp = 6; - if (slopDp < 1) slopDp = 1; - if (slopDp > 40) slopDp = 40; - var touchSlop = Math.max(self.dp(8), self.dp(slopDp)); - if (validDir && adx > touchSlop && adx > ady * 0.75) { + var edge = rootEdge; + var validDir = false; + if (rootBackMode === "surface") { + edge = dx >= 0 ? 0 : 1; + validDir = (dx !== 0); + } else { + validDir = (edge === 0 && dx > 0) || (edge === 1 && dx < 0); + } + var shouldIntercept = false; + if (rootBackMode === "surface") { + shouldIntercept = validDir && adx > self.dp(48) && adx > ady * 1.2; + } else { + var slopDp = Number(self.config.CLICK_SLOP_DP || 6); + if (isNaN(slopDp)) slopDp = 6; + if (slopDp < 1) slopDp = 1; + if (slopDp > 40) slopDp = 40; + var touchSlop = Math.max(self.dp(8), self.dp(slopDp)); + shouldIntercept = validDir && adx > touchSlop && adx > ady * 0.75; + } + if (shouldIntercept) { + rootEdge = edge; rootBackActive = true; rootBackMoved = true; - try { self.prepareToolAppBackPreview(rootEdge); } catch(ePrep) { try { safeLog(self.L, 'w', 'root back preview prepare fail: ' + String(ePrep)); } catch(eLogPrep) {} } + try { self.prepareToolAppBackPreview(edge); } catch(ePrep) { try { safeLog(self.L, 'w', 'root back preview prepare fail: ' + String(ePrep)); } catch(eLogPrep) {} } try { var triggerDp0 = Number(self.config.TOOLAPP_BACK_PROGRESS_DISTANCE_DP || 96); if (isNaN(triggerDp0)) triggerDp0 = 96; @@ -1292,9 +1389,9 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac if (triggerDp0 > 720) triggerDp0 = 720; var triggerDistance0 = self.dp(triggerDp0); var p0 = Math.min(1, adx / triggerDistance0); - self.applyToolAppBackPreviewProgress(rootEdge, p0, adx); + self.applyToolAppBackPreviewProgress(edge, p0, adx); } catch(eFirstMove) {} - try { safeLog(self.L, 'd', 'root edge back intercept edge=' + String(rootEdge) + ' dx=' + String(dx)); } catch(eMoveLog) {} + try { safeLog(self.L, 'd', 'root back intercept mode=' + String(rootBackMode) + ' edge=' + String(edge) + ' dx=' + String(dx)); } catch(eMoveLog) {} return true; } return false; @@ -1314,7 +1411,8 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac var mx = ev.getX() - rootDownX; var my = ev.getY() - rootDownY; var validDir2 = (rootEdge === 0 && mx > 0) || (rootEdge === 1 && mx < 0); - if (validDir2 && Math.abs(mx) > Math.abs(my) * 0.75) { + var dominance = rootBackMode === "surface" ? 1.2 : 0.75; + if (validDir2 && Math.abs(mx) > Math.abs(my) * dominance) { var triggerDp = Number(self.config.TOOLAPP_BACK_PROGRESS_DISTANCE_DP || 96); if (isNaN(triggerDp)) triggerDp = 96; if (triggerDp < 1) triggerDp = 1; @@ -1334,9 +1432,12 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac if (commitDp > 480) commitDp = 480; var completeDistance = self.dp(commitDp); var okDir = (rootEdge === 0 && ux > completeDistance) || (rootEdge === 1 && ux < -completeDistance); - var ok = (action === android.view.MotionEvent.ACTION_UP) && rootBackMoved && okDir && Math.abs(ux) > Math.abs(uy) * 0.75; + var ratio = rootBackMode === "surface" ? 1.2 : 0.75; + var ok = (action === android.view.MotionEvent.ACTION_UP) && rootBackMoved && okDir && Math.abs(ux) > Math.abs(uy) * ratio; var edgeDone = rootEdge; rootBackActive = false; + rootBackEligible = false; + rootBackBlocked = false; rootBackMoved = false; rootEdge = -1; self.finishToolAppBackPreview(edgeDone, ok); @@ -1348,6 +1449,8 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac try { safeLog(self.L, 'w', 'tool app root back touch fail: ' + String(e2)); } catch(eLog2) {} } rootBackActive = false; + rootBackEligible = false; + rootBackBlocked = false; rootBackMoved = false; rootEdge = -1; return false; @@ -1419,7 +1522,7 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac body.addView(host, hostLp); // 兼容旧设置:不再添加页面内透明返回热区。 - // 返回手势由 root.onInterceptTouchEvent 按“边缘起手 + 横向阈值 + 方向正确”延迟拦截,避免覆盖控件。 + // 返回手势由 root.onInterceptTouchEvent 延迟拦截;surface 模式会排除交互控件,edge 模式保留边缘起手。 try { this.state.toolAppInnerBackLeftStrip = null; this.state.toolAppInnerBackRightStrip = null; diff --git a/manifest.json b/manifest.json index de1b16a..a66268f 100644 --- a/manifest.json +++ b/manifest.json @@ -2,8 +2,8 @@ "alg": "SHA256withRSA", "files": { "th_01_base.js": { - "sha256": "2dfbf8e338b15d62399f5084586a7b593fd81eb6d321f5403da928434469a210", - "size": 57148 + "sha256": "c186450c6a64cf51fdf6cc93e45624793596ce9f566d1b18c3e70905459fd618", + "size": 57722 }, "th_02_core.js": { "sha256": "3c5c498d200e961d48fc9ca3f885475e770ecb32b83ec6a89d23df3f88aed1c9", @@ -58,8 +58,8 @@ "size": 304993 }, "th_15_extra.js": { - "sha256": "93dc6db5c01cdb080660e988c1665cbe742b80849b583bc485f6d1d27310f908", - "size": 120294 + "sha256": "e0d3b2f39ac13126f4534756ea0d928955e540e63d4fb20eddc53c72fe63f720", + "size": 124928 }, "th_16_entry.js": { "sha256": "6c59d9891cd010647f84c3db93f1cf95c7bbfb758470ea21044bf72eb8ff73d1", @@ -68,5 +68,5 @@ }, "keyId": "toolhub-targets-2026-rsa3072", "schema": 2, - "version": 20260522003824 + "version": 20260522005618 } diff --git a/manifest.sig b/manifest.sig index 8453754..c3b1e40 100644 --- a/manifest.sig +++ b/manifest.sig @@ -1 +1 @@ -P2H2QBMFs6oL8m45jPxIXQ/zR2glhsEnduNFmKr8/HS5vnRVDVAnfcZKxOn9wpl9XdllaeRDbr7nHGpgK1rHT7UAJbt+byDYiz50As4C52c2xGV3kq/bim6FRhKd5sSh4cumWGUhBMMPRbfgXuMQen9+B72THYOBME2xEiylyMKedrxrLh2flFT9Y5SkeNFgFAdPYxGLaazCwpn/ydGLqFPrZLNLS2an7D0+WeDDZOF0nxZEvKNoHW1wi0BFDecR4I4cJVsQw8jjL7GMt/tLOZrZXBML4R4gYM+4UQu3AxjAIi6iuw/9LFs5IzIJXDJLIO4Q2grs6avpWQGMKhVC9UpvQgijvR14S7tmHL6vgpNX+JLzoziVlzsZWWj2FLlXazsfJTkYCPiEjknJyzBaevgyZIRwYilyCeDTRwtenBiPXluyc1Y94Q2Cr02iIu6d/DF5mSi/lVV6VN/JTmGDNAwaYbOE1uVwtXs+t1hqDBJKaJBWT0UL1tmSAj1m3eVY +UMkA/Pj4p59+4HuG1tDQY3FxqOn4DK4/chmqh62NAfUoQ8EpTqVzMzuuJInAAdhGZjAmJK4esjOpOATXjHZQiwaaFsm5EGBXq/bxJTuxuc8714p8qIwvFGXVtQuv7pBS0SHAAFhQ0a9JKdMwSo5zyZeEO/NjTE8VbloQu42R562Dfuyas5tMVvawG2u34kHRGvsSQFPz28Ju4r3fyUg8UIareb8HK8OW4mniQLHoNdueCgERGCPavhTSbWzXdO7QozExFFoMgxlbEFH8SJ6Ta8/cVhLj83oElX2bhCuOBvxHvb2Rli4LGoJezlQgetv/vCz31bsZZhpGuRtwjMVjm9qK0MxTB7XfZdgMjmgbYDEbEeoKxYwhgPaQiKIcWlSzqv9PmsJYwN2MYI5QHbc2ibb7JQ72dslgUA/mtIUgNmjLbY1wNSLemmh5C5Igwn34hGORAQ+UmGjIP60oEADWRHt6teLqDPHrI3n458lIW7viOspmMaQmhcZi3tMEd+oa