From efb14bcde0032344d0f35332c3939608e835c99f Mon Sep 17 00:00:00 2001 From: 7015725 Date: Tue, 12 May 2026 20:43:42 +0800 Subject: [PATCH] feat: support predictive back gesture --- code/th_02_core.js | 1 + code/th_09_animation.js | 109 +++++++++++++++++++++++++++++++++++++++- manifest.json | 10 ++-- manifest.sig | 2 +- 4 files changed, 115 insertions(+), 7 deletions(-) diff --git a/code/th_02_core.js b/code/th_02_core.js index 89e939a..1e370cd 100644 --- a/code/th_02_core.js +++ b/code/th_02_core.js @@ -42,6 +42,7 @@ function FloatBallAppWM(logger) { viewerPanel: null, viewerPanelLp: null, viewerPanelType: null, + panelBackCallbackEntries: [], // 设置类 UI App 化:单窗口页面栈(settings -> 子页面 -> 编辑页) toolAppActive: false, diff --git a/code/th_09_animation.js b/code/th_09_animation.js index 83ae079..62ce117 100644 --- a/code/th_09_animation.js +++ b/code/th_09_animation.js @@ -127,6 +127,7 @@ FloatBallAppWM.prototype.playBounce = function(v) { FloatBallAppWM.prototype.safeRemoveView = function(v, whichName) { try { if (!v) return { ok: true, skipped: true }; + try { if (this.unregisterPanelPredictiveBack) this.unregisterPanelPredictiveBack(v); } catch (eBack) {} this.state.wm.removeView(v); return { ok: true }; } catch (e) { @@ -265,6 +266,109 @@ FloatBallAppWM.prototype.handleSystemUiDismiss = function(reason) { return false; }; +FloatBallAppWM.prototype.resetPanelPredictiveBackVisual = function(panel) { + try { + if (!panel) return; + panel.setAlpha(1.0); + panel.setTranslationX(0); + panel.setScaleX(1.0); + panel.setScaleY(1.0); + } catch (e) {} +}; + +FloatBallAppWM.prototype.applyPanelPredictiveBackProgress = function(panel, event) { + try { + if (!panel || !event) return; + var p = 0; + try { p = Number(event.getProgress()); } catch (eP) { p = 0; } + if (isNaN(p)) p = 0; + if (p < 0) p = 0; + if (p > 1) p = 1; + var edge = 0; + try { edge = Number(event.getSwipeEdge()); } catch (eE) { edge = 0; } + var dir = edge === 1 ? -1 : 1; + panel.setAlpha(1.0 - 0.18 * p); + panel.setTranslationX(dir * this.dp(36) * p); + var s = 1.0 - 0.025 * p; + panel.setScaleX(s); + panel.setScaleY(s); + } catch (e) {} +}; + +FloatBallAppWM.prototype.unregisterPanelPredictiveBack = function(panel) { + try { + var entries = this.state.panelBackCallbackEntries || []; + var kept = []; + for (var i = 0; i < entries.length; i++) { + var it = entries[i]; + if (!it || it.view === panel) { + try { if (it && it.dispatcher && it.callback) it.dispatcher.unregisterOnBackInvokedCallback(it.callback); } catch (eUnreg) {} + } else { + kept.push(it); + } + } + this.state.panelBackCallbackEntries = kept; + this.resetPanelPredictiveBackVisual(panel); + } catch (e) { + safeLog(this.L, 'w', "unregister predictive back fail: " + String(e)); + } +}; + +FloatBallAppWM.prototype.registerPanelPredictiveBack = function(panel, which) { + // Android 13+:注册 OnBackInvokedCallback;Android 14+ 优先使用 OnBackAnimationCallback,实现预测性返回手势进度动画。 + try { + if (!panel) return false; + if (android.os.Build.VERSION.SDK_INT < 33) return false; + this.unregisterPanelPredictiveBack(panel); + var dispatcher = null; + try { dispatcher = panel.findOnBackInvokedDispatcher(); } catch (eFind) { dispatcher = null; } + if (!dispatcher) return false; + + var self = this; + var cb = null; + var usedAnimation = false; + if (android.os.Build.VERSION.SDK_INT >= 34) { + try { + var animCls = java.lang.Class.forName("android.window.OnBackAnimationCallback"); + cb = new JavaAdapter(animCls, { + onBackStarted: function(event) { self.applyPanelPredictiveBackProgress(panel, event); }, + onBackProgressed: function(event) { self.applyPanelPredictiveBackProgress(panel, event); }, + onBackCancelled: function() { self.resetPanelPredictiveBackVisual(panel); }, + onBackInvoked: function() { + self.resetPanelPredictiveBackVisual(panel); + self.handlePanelBack(which, "predictive_back"); + } + }); + usedAnimation = true; + } catch (eAnim) { + cb = null; + } + } + if (!cb) { + try { + var cbCls = java.lang.Class.forName("android.window.OnBackInvokedCallback"); + cb = new JavaAdapter(cbCls, { + onBackInvoked: function() { self.handlePanelBack(which, "on_back_invoked"); } + }); + } catch (eCb) { + cb = null; + } + } + if (!cb) return false; + + var priority = 0; + try { priority = android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT; } catch (ePri) { priority = 0; } + dispatcher.registerOnBackInvokedCallback(priority, cb); + if (!this.state.panelBackCallbackEntries) this.state.panelBackCallbackEntries = []; + this.state.panelBackCallbackEntries.push({ view: panel, dispatcher: dispatcher, callback: cb, which: String(which || ""), animation: usedAnimation }); + safeLog(this.L, 'i', "predictive back registered which=" + String(which || "") + " animation=" + String(usedAnimation)); + return true; + } catch (e) { + safeLog(this.L, 'w', "register predictive back fail which=" + String(which || "") + " err=" + String(e)); + } + return false; +}; + FloatBallAppWM.prototype.attachPanelSystemKeyHandler = function(panel, which) { try { if (!panel) return; @@ -282,7 +386,10 @@ FloatBallAppWM.prototype.attachPanelSystemKeyHandler = function(panel, which) { return false; } })); - panel.post(new java.lang.Runnable({ run: function() { try { panel.requestFocus(); } catch(eFocus) {} } })); + panel.post(new java.lang.Runnable({ run: function() { + try { panel.requestFocus(); } catch(eFocus) {} + try { self.registerPanelPredictiveBack(panel, which); } catch(eBack) {} + } })); } catch (e) { safeLog(this.L, 'e', "attachPanelSystemKeyHandler fail which=" + String(which || "") + " err=" + String(e)); } diff --git a/manifest.json b/manifest.json index 612595c..efed64e 100644 --- a/manifest.json +++ b/manifest.json @@ -6,8 +6,8 @@ "size": 52546 }, "th_02_core.js": { - "sha256": "24363adc7156feda5c2f1fcbb84c96a4378ffec4b40251bd27d0123329acf054", - "size": 4203 + "sha256": "2274feddccb87873d81ac0337e14de9998bb9fa8600cccfe16cbc4552d05284b", + "size": 4237 }, "th_03_icon.js": { "sha256": "717f7f37474d3616c2cd944581455f600020a850ec8812100d0546ec1302c987", @@ -34,8 +34,8 @@ "size": 7938 }, "th_09_animation.js": { - "sha256": "c3a9565d4b6c0e48d7b7592fd7d999e78fbec3c19bb3f5862151149c7598ae27", - "size": 27832 + "sha256": "f7083cbbc1b3efad1dcbccf19185cf1ee530172164da40f94dadc50fc5e3d763", + "size": 31917 }, "th_10_shell.js": { "sha256": "0ed793079c2f6ba7d29f4c0d411705cb72419f45f572cbe37ed32ac16527a8bc", @@ -68,5 +68,5 @@ }, "keyId": "toolhub-targets-2026-rsa3072", "schema": 2, - "version": 20260512123028 + "version": 20260512124303 } diff --git a/manifest.sig b/manifest.sig index 92e3808..d617dda 100644 --- a/manifest.sig +++ b/manifest.sig @@ -1 +1 @@ -gwwFWHnXNU/QJ+BfqHacNXHFLowDLZ4izcO8S+z4+q25HkClwIazJNMr9LbkXPSszY/1pO82MiIjTVw+ubzHrD1caoml0AoOTUJBRKNVMxqlXqepdodfmmvcg3g3BxC8WmtCONKjnvdX1EI6faYRbkA61gtsy8qQNUDPAR0pre8OqPBvgYc8eo4ZFyR+E+kDE8na5Vl+fsMG2anPUvEUi3ivSUlijTaYWxo4ZLUfIs3oEZxy0b/6leFX+8QhbvtkJcwkq3Jfrycl0Lu8ENTToGheWZqBjLV1TcihR3uOSMJqMoIdTWg01iCK0XOsyZva7Fpa/yQQU4DZYVOrLpBA6nWdmutAwOQ+/JcswCVlmdgogoUu7EmY1v8b/WKVvwaRNXc9LdKMkoveW7sJr3HtArb9qiNqyxoZr/88+/e93F408s3XkB9o43BhrMBMXi7lUp58DCODtBMnyb6eq/5Iu+xYw4xdRVXGDDCudVMtNFp2yc0x1EuG/6l3Ten7EMgv +SZay6+P/vnwc/F7V2bwkn3pXI8IQ1aa3fLJI3/LaW3H9lYQuqIErWsbYVIzxvprjgs0KM3iEbynlIkvJDFVggvLcXoo/yZZAu1K10ZhuH7KACG1MGk7ViiGRzW5GMSLvM5zW2qtN2V/+Lj/c9XNZDxIS8hMswKaolXTWYwsiw1NKhWOjRxwzgXuzai2TMQdJRZM3frGsgt9sB/78fDczyFD9NPYkeAjywnAjnH6iLCaAEH8PsrFECDuwYqn4uE3sxO4XGp/JPEX6Un1zQB1QgaN+rTWjyCbAIqapBqb8xulY7/m8wUo9Ei+VnozOFbseldO/2axsFHeXa6XKttTuXKQvcYjWeYpezgDiPsXzoAtJyAS2JZ2lNsOGurHQuBBA6pH93+hP8sCltNzNgtZMT7lJhZxRH4+pFvm6bf3W9ZSSdNGlvAQ6hHok3U+k6YuSoBzT8IZHDyv1iW2p9KB655W0qYVMRn+Y73YXAhdfVlC/m0jhBkfiOLtulHn8x96z