Files
ShortX_ToolHub/code/th_09_animation.js
root c7e9b92322 refactor: split th_2_core.js into 12 modules, rename all files to 2-digit numbering
- Split th_2_core.js (4715 lines, 177KB) into:
  th_02_core.js, th_03_icon.js, th_04_theme.js, th_05_persistence.js,
  th_06_icon_parser.js, th_07_shortcut.js, th_08_content.js,
  th_09_animation.js, th_10_shell.js, th_11_action.js,
  th_12_rebuild.js, th_13_panel_ui.js
- Rename existing: th_1_base→th_01_base, th_3_panels→th_14_panels,
  th_4_extra→th_15_extra, th_5_entry→th_16_entry
- Update ToolHub.js MODULE_MANIFEST, modules array, and critical module check
2026-04-20 11:53:13 +08:00

663 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @version 1.0.0
FloatBallAppWM.prototype.animateBallLayout = function(toX, toY, toW, durMs, endCb) {
var st = this.state;
if (!st.addedBall || !st.ballRoot || !st.ballLp) { if (endCb) endCb(); return; }
var fromX = st.ballLp.x;
var fromY = st.ballLp.y;
var fromW = st.ballLp.width;
try {
var va = android.animation.ValueAnimator.ofFloat(0.0, 1.0);
va.setDuration(durMs);
try {
// 使用 OvershootInterpolator 产生轻微的回弹效果,更加生动
// 0.7 的张力适中,不会过于夸张
va.setInterpolator(new android.view.animation.OvershootInterpolator(0.7));
} catch (eI) {
try { va.setInterpolator(new android.view.animation.DecelerateInterpolator()); } catch (eI2) {}
}
var self = this;
va.addUpdateListener(new android.animation.ValueAnimator.AnimatorUpdateListener({
onAnimationUpdate: function(anim) {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var f = anim.getAnimatedValue();
var nx = Math.round(fromX + (toX - fromX) * f);
var ny = Math.round(fromY + (toY - fromY) * f);
var nw = Math.round(fromW + (toW - fromW) * f);
// 性能优化:只有坐标真正变化时才请求 WindowManager 更新
if (nx !== self.state.ballLp.x || ny !== self.state.ballLp.y || nw !== self.state.ballLp.width) {
self.state.ballLp.x = nx;
self.state.ballLp.y = ny;
self.state.ballLp.width = nw;
// # 关键操作使用 safeOperation 封装
safeOperation("dockAnimation.updateViewLayout", function() {
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
}, true, self.L);
}
} catch (e) {}
}
}));
va.addListener(new android.animation.Animator.AnimatorListener({
onAnimationStart: function() {},
onAnimationRepeat: function() {},
onAnimationCancel: function() {},
onAnimationEnd: function() {
try {
if (!self.state.closing && self.state.addedBall) {
self.state.ballLp.x = toX;
self.state.ballLp.y = toY;
self.state.ballLp.width = toW;
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
self.savePos(self.state.ballLp.x, self.state.ballLp.y);
}
} catch (e2) {}
try { if (endCb) endCb(); } catch (eCb) { try { if (self && self.L && self.L.e) self.L.e("animateBallLayout endCb err=" + String(eCb)); } catch (eLog) {} }
}
}));
va.start();
} catch (e0) {
try {
st.ballLp.x = toX;
st.ballLp.y = toY;
st.ballLp.width = toW;
st.wm.updateViewLayout(st.ballRoot, st.ballLp);
this.savePos(st.ballLp.x, st.ballLp.y);
} catch (e1) {}
try { if (endCb) endCb(); } catch (eCb2) { try { if (this && this.L && this.L.e) this.L.e("animateBallLayout endCb err=" + String(eCb2)); } catch (eLog2) {} }
}
};
FloatBallAppWM.prototype.playBounce = function(v) {
if (!this.config.ENABLE_BOUNCE) return;
if (!this.config.ENABLE_ANIMATIONS) return;
try { v.animate().cancel(); } catch (e0) {}
var self = this;
var i = 0;
function step() {
if (self.state.closing) return;
if (i >= self.config.BOUNCE_TIMES) {
try { v.setScaleX(1); v.setScaleY(1); } catch (e2) {}
return;
}
var amp = (self.config.BOUNCE_MAX_SCALE - 1) * Math.pow(self.config.BOUNCE_DECAY, i);
var s = 1 + amp;
v.animate()
.scaleX(s)
.scaleY(s)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.OvershootInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() {
v.animate()
.scaleX(1)
.scaleY(1)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.AccelerateDecelerateInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() { i++; step(); }
}))
.start();
}
}))
.start();
}
step();
};
FloatBallAppWM.prototype.safeRemoveView = function(v, whichName) {
try {
if (!v) return { ok: true, skipped: true };
this.state.wm.removeView(v);
return { ok: true };
} catch (e) {
safeLog(this.L, 'w', "removeView fail which=" + String(whichName || "") + " err=" + String(e));
return { ok: false, err: String(e), where: whichName || "" };
}
};
FloatBallAppWM.prototype.hideMask = function() {
if (!this.state.addedMask) return;
if (!this.state.mask) return;
this.safeRemoveView(this.state.mask, "mask");
this.state.mask = null;
this.state.maskLp = null;
this.state.addedMask = false;
};
FloatBallAppWM.prototype.hideMainPanel = function() {
if (!this.state.addedPanel) return;
if (!this.state.panel) return;
this.safeRemoveView(this.state.panel, "panel");
this.state.panel = null;
this.state.panelLp = null;
this.state.addedPanel = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideMainPanel");
};
FloatBallAppWM.prototype.hideSettingsPanel = function() {
if (!this.state.addedSettings) return;
if (!this.state.settingsPanel) return;
this.safeRemoveView(this.state.settingsPanel, "settingsPanel");
this.state.settingsPanel = null;
this.state.settingsPanelLp = null;
this.state.addedSettings = false;
this.state.pendingUserCfg = null;
this.state.pendingDirty = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideSettingsPanel");
};
FloatBallAppWM.prototype.hideViewerPanel = function() {
if (!this.state.addedViewer) return;
if (!this.state.viewerPanel) return;
this.safeRemoveView(this.state.viewerPanel, "viewerPanel");
this.state.viewerPanel = null;
this.state.viewerPanelLp = null;
this.state.addedViewer = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideViewerPanel");
};
FloatBallAppWM.prototype.clearHeavyCaches = function(reason) {
// 这段代码的主要内容/用途:在所有面板都关闭后,主动清理"图标/快捷方式"等重缓存,降低 system_server 常驻内存。
// 说明:仅清理缓存引用,不强行 recycle Bitmap避免误伤仍被使用的 Drawable。
// # 防抖5秒内相同 reason 不重复清理
var now = Date.now();
var cacheKey = "_lastClear_" + (reason || "default");
var lastClear = this.state[cacheKey] || 0;
if (now - lastClear < 5000) {
return; // 5秒内已清理过跳过
}
this.state[cacheKey] = now;
try { this._iconLru = null; } catch (eLruClr) {}
try { this._shortcutIconFailTs = {}; } catch (e2) {}
// # Shortcuts 相关全局缓存(按钮编辑页/快捷方式选择器可能会创建)
try { if (typeof __scIconCache !== "undefined") __scIconCache = {}; } catch (e3) {}
try { if (typeof __scAppLabelCache !== "undefined") __scAppLabelCache = {}; } catch (e4) {}
// # 记录一次清理日志(精简:只记录关键 reason且 5秒防抖
var keyReasons = ["memory_pressure", "screen_changed", "close"];
var isKeyReason = keyReasons.indexOf(reason) >= 0;
try {
if (isKeyReason && this.L && this.L.i) {
this.L.i("clearHeavyCaches reason=" + String(reason));
}
} catch (e5) {}
};
FloatBallAppWM.prototype._clearHeavyCachesIfAllHidden = function(reason) {
// 这段代码的主要内容/用途:只在"主面板/设置/查看器"全部关闭后清理缓存,避免页面切换时反复重建导致卡顿。
try {
if (!this.state.addedPanel && !this.state.addedSettings && !this.state.addedViewer) {
this.clearHeavyCaches(reason || "all_hidden");
}
} catch (e) {}
};
FloatBallAppWM.prototype.hideAllPanels = function() {
this.hideMainPanel();
this.hideSettingsPanel();
this.hideViewerPanel();
this.hideMask();
this._clearHeavyCachesIfAllHidden("hideAllPanels");
};
FloatBallAppWM.prototype.showMask = function() {
if (this.state.addedMask) return;
if (this.state.closing) return;
var self = this;
var mask = new android.widget.FrameLayout(context);
// 遮罩层背景:轻微的黑色半透明,提升层次感
try { mask.setBackgroundColor(android.graphics.Color.parseColor("#33000000")); } catch (e0) {
mask.setBackgroundColor(0x33000000);
}
mask.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) {
var a = e.getAction();
if (a === android.view.MotionEvent.ACTION_DOWN) {
self.touchActivity();
self.hideAllPanels();
return true;
}
return true;
}
}));
var lp = new android.view.WindowManager.LayoutParams(
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = 0;
lp.y = 0;
try {
this.state.wm.addView(mask, lp);
this.state.mask = mask;
this.state.maskLp = lp;
this.state.addedMask = true;
// 简单的淡入动画
try {
if (this.config.ENABLE_ANIMATIONS) {
mask.setAlpha(0);
mask.animate().alpha(1).setDuration(200).start();
} else {
mask.setAlpha(1);
}
} catch(eAnim){}
} catch (e1) {
safeLog(this.L, 'e', "add mask fail err=" + String(e1));
this.state.addedMask = false;
}
};
FloatBallAppWM.prototype.snapToEdgeDocked = function(withAnim, forceSide) {
if (this.state.closing) return;
// 移除对面板/Mask的检查允许在任何情况下强制吸边如果调用方逻辑正确
// 如果需要保护,调用方自己判断
if (this.state.dragging) return;
var di = this.getDockInfo();
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var snapLeft;
if (forceSide === "left") snapLeft = true;
else if (forceSide === "right") snapLeft = false;
else {
// 默认根据中心点判断
var centerX = this.state.ballLp.x + Math.round(ballSize / 2);
snapLeft = centerX < Math.round(this.state.screen.w / 2);
}
var targetW = visible;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
if (snapLeft) {
this.state.dockSide = "left";
this.state.docked = true;
try { this.state.ballContent.setX(-hidden); } catch (eL) {}
if (withAnim) {
this.animateBallLayout(0, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU1) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
safeLog(this.L, 'd', "dock left x=0 y=" + String(targetY) + " w=" + String(targetW));
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) {}
return;
}
this.state.dockSide = "right";
this.state.docked = true;
try { this.state.ballContent.setX(0); } catch (eR) {}
var x2 = this.state.screen.w - visible;
if (withAnim) {
this.animateBallLayout(x2, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = x2;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU2) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
// # 日志精简dock 事件添加防抖10秒内不重复记录相同边
var dockNow = Date.now();
var lastDock = this.state._lastDockLog || 0;
if (dockNow - lastDock > 10000) {
safeLog(this.L, 'i', "dock right x=" + String(x2) + " y=" + String(targetY));
this.state._lastDockLog = dockNow;
}
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) {}
};
FloatBallAppWM.prototype.undockToFull = function(withAnim, endCb) {
if (this.state.closing) { if (endCb) endCb(); return; }
if (!this.state.docked) { if (endCb) endCb(); return; }
if (!this.state.addedBall) { if (endCb) endCb(); return; }
var di = this.getDockInfo();
var ballSize = di.ballSize;
var targetW = ballSize;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
try { this.state.ballContent.setX(0); } catch (e0) {}
if (this.state.dockSide === "left") {
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(0, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU1) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) {}
safeLog(this.L, 'i', "undock from left");
return;
}
var x = this.state.screen.w - ballSize;
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(x, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = x;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU2) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) {}
// # 日志精简undock 事件改为 INFO 级别,且记录方向
var undockSide = this.state.dockSide || "right";
safeLog(this.L, 'i', "undock from " + undockSide);
};
FloatBallAppWM.prototype.cancelDockTimer = function() {
try { if (this.state.idleDockRunnable && this.state.h) this.state.h.removeCallbacks(this.state.idleDockRunnable); } catch (e) {}
this.state.idleDockRunnable = null;
};
FloatBallAppWM.prototype.armDockTimer = function() {
if (this.state.closing) return;
if (!this.state.h) return;
if (!this.state.addedBall) return;
if (this.state.docked) return;
this.cancelDockTimer();
var hasPanel = (this.state.addedPanel || this.state.addedSettings || this.state.addedViewer || this.state.addedMask);
var targetMs = hasPanel ? this.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : this.config.DOCK_AFTER_IDLE_MS;
var self = this;
this.state.idleDockRunnable = new java.lang.Runnable({
run: function() {
try {
if (self.state.closing) return;
if (self.state.docked) return;
if (self.state.dragging) return;
var hasPanel2 = (self.state.addedPanel || self.state.addedSettings || self.state.addedViewer || self.state.addedMask);
var needMs = hasPanel2 ? self.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : self.config.DOCK_AFTER_IDLE_MS;
var idle = self.now() - self.state.lastMotionTs;
if (idle < needMs) { self.armDockTimer(); return; }
// if (hasPanel2) self.hideAllPanels(); // 用户要求不再自动关闭面板
if (self.config.ENABLE_SNAP_TO_EDGE) {
self.snapToEdgeDocked(true);
}
} catch (e0) {
if (self.L) self.L.e("dockTimer run err=" + String(e0));
}
}
});
this.state.h.postDelayed(this.state.idleDockRunnable, targetMs);
};
FloatBallAppWM.prototype.touchActivity = function() {
this.state.lastMotionTs = this.now();
this.armDockTimer();
}
// # 点击防抖与安全执行
// 这段代码的主要内容/用途:防止在悬浮面板上快速/乱点导致重复 add/remove、状态机被打穿从而引发 system_server 异常重启。
FloatBallAppWM.prototype.guardClick = function(key, cooldownMs, fn) {
try {
var now = android.os.SystemClock.uptimeMillis();
if (!this.state._clickGuards) this.state._clickGuards = {};
var last = this.state._clickGuards[key] || 0;
var cd = (cooldownMs != null ? cooldownMs : INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS);
if (now - last < cd) return false;
this.state._clickGuards[key] = now;
try {
fn && fn();
} catch (e1) {
safeLog(this.L, 'e', "guardClick err key=" + String(key) + " err=" + String(e1));
}
return true;
} catch (e0) {
// 兜底:绝不让点击回调异常冒泡到 system_server
try { fn && fn(); } catch (e2) {}
return true;
}
};
FloatBallAppWM.prototype.safeUiCall = function(tag, fn) {
try {
fn && fn();
} catch (e) {
safeLog(this.L, 'e', "safeUiCall err tag=" + String(tag || "") + " err=" + String(e));
}
};
;
FloatBallAppWM.prototype.onScreenChangedReflow = function() {
if (this.state.closing) return;
if (!this.state.addedBall) return;
var di = this.getDockInfo();
var oldW = this.state.screen.w;
var oldH = this.state.screen.h;
var newScreen = this.getScreenSizePx();
var newW = newScreen.w;
var newH = newScreen.h;
if (newW <= 0 || newH <= 0) return;
if (oldW <= 0) oldW = newW;
if (oldH <= 0) oldH = newH;
this.state.screen = { w: newW, h: newH };
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var oldMaxX = Math.max(1, oldW - ballSize);
var oldMaxY = Math.max(1, oldH - ballSize);
var newMaxX = Math.max(1, newW - ballSize);
var newMaxY = Math.max(1, newH - ballSize);
var xRatio = this.state.ballLp.x / oldMaxX;
var yRatio = this.state.ballLp.y / oldMaxY;
var mappedX = Math.round(xRatio * newMaxX);
var mappedY = Math.round(yRatio * newMaxY);
mappedX = this.clamp(mappedX, 0, newMaxX);
mappedY = this.clamp(mappedY, 0, newMaxY);
if (this.state.docked) {
this.state.ballLp.y = mappedY;
this.state.ballLp.width = visible;
if (this.state.dockSide === "left") {
this.state.ballLp.x = 0;
try { this.state.ballContent.setX(-hidden); } catch (eL) {}
} else {
this.state.ballLp.x = newW - visible;
try { this.state.ballContent.setX(0); } catch (eR) {}
}
// 重新进入闲置变暗逻辑(如果需要)
try { this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA); } catch(eA) {}
} else {
this.state.ballLp.x = mappedX;
this.state.ballLp.y = mappedY;
this.state.ballLp.width = ballSize;
try { this.state.ballContent.setX(0); } catch (e0) {}
try { this.state.ballContent.setAlpha(1.0); } catch(eA) {}
}
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
safeLog(this.L, 'i', "screen reflow w=" + String(newW) + " h=" + String(newH) + " x=" + String(this.state.ballLp.x) + " y=" + String(this.state.ballLp.y));
};
FloatBallAppWM.prototype.setupDisplayMonitor = function() {
if (this.state.closing) return;
try {
var dm = context.getSystemService(android.content.Context.DISPLAY_SERVICE);
if (!dm) return;
this.state.dm = dm;
this.state.lastRotation = this.getRotation();
var self = this;
var listener = new JavaAdapter(android.hardware.display.DisplayManager.DisplayListener, {
onDisplayAdded: function(displayId) {},
onDisplayRemoved: function(displayId) {},
onDisplayChanged: function(displayId) {
try {
if (self.state.closing) return;
var nowTs = self.now();
if (nowTs - self.state.lastMonitorTs < self.config.SCREEN_MONITOR_THROTTLE_MS) return;
self.state.lastMonitorTs = nowTs;
self.state.h.post(new JavaAdapter(java.lang.Runnable, {
run: function() {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var rot = self.getRotation();
var sz = self.getScreenSizePx();
var changed = false;
if (rot !== self.state.lastRotation) { self.state.lastRotation = rot; changed = true; }
if (sz.w !== self.state.screen.w || sz.h !== self.state.screen.h) changed = true;
if (changed) {
self.cancelDockTimer();
self.onScreenChangedReflow();
self.touchActivity();
}
} catch (e1) {
if (self.L) self.L.e("displayChanged run err=" + String(e1));
}
}
}));
} catch (e0) {}
}
});
this.state.displayListener = listener;
dm.registerDisplayListener(listener, this.state.h);
safeLog(this.L, 'i', "display monitor registered");
} catch (e2) {
safeLog(this.L, 'e', "setupDisplayMonitor err=" + String(e2));
}
};
FloatBallAppWM.prototype.stopDisplayMonitor = function() {
try { if (this.state.dm && this.state.displayListener) this.state.dm.unregisterDisplayListener(this.state.displayListener); } catch (e) {}
this.state.displayListener = null;
this.state.dm = null;
};