Files
ShortX_ToolHub/code/th_15_extra.js

1600 lines
59 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.buildViewerPanelView = function(titleText, bodyText) {
var self = this;
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var bgColor = isDark ? C.bgDark : C.bgLight;
var textColor = isDark ? C.textPriDark : C.textPriLight;
var codeColor = isDark ? C.textSecDark : C.textSecLight;
var dividerColor = isDark ? C.dividerDark : C.dividerLight;
var panel = new android.widget.LinearLayout(context);
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
// 面板背景
var bgDr = new android.graphics.drawable.GradientDrawable();
bgDr.setColor(bgColor);
bgDr.setCornerRadius(this.dp(16));
panel.setBackground(bgDr);
try { panel.setElevation(this.dp(8)); } catch(e){}
panel.setPadding(
this.dp(16),
this.dp(16),
this.dp(16),
this.dp(16)
);
// Header removed to avoid duplication with wrapper
// var header = new android.widget.LinearLayout(context);
// ...
var sep = new android.view.View(context);
sep.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
1 // 1px
));
sep.setBackgroundColor(dividerColor);
panel.addView(sep);
var scroll = new android.widget.ScrollView(context);
try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch (eOS) {}
try { scroll.setVerticalScrollBarEnabled(true); } catch (eSB) {}
// 给内容加一点边距
var contentBox = new android.widget.LinearLayout(context);
contentBox.setOrientation(android.widget.LinearLayout.VERTICAL);
contentBox.setPadding(0, this.dp(12), 0, this.dp(12));
scroll.addView(contentBox);
var tv = new android.widget.TextView(context);
tv.setText(String(bodyText || ""));
tv.setTextColor(codeColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.CONTENT_VIEWER_TEXT_SP || 12));
// 增加行距优化阅读
try { tv.setLineSpacing(this.dp(4), 1.0); } catch(eLS) {}
// 使用等宽字体显示代码/日志
try { tv.setTypeface(android.graphics.Typeface.MONOSPACE); } catch(eTF) {}
// WindowManager 环境下禁用文本选择,否则长按/选择会因缺少 Token 崩溃
try { tv.setTextIsSelectable(false); } catch (eSel) {}
contentBox.addView(tv);
scroll.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) { self.touchActivity(); return false; }
}));
var scrollLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
0
);
scrollLp.weight = 1;
scroll.setLayoutParams(scrollLp);
panel.addView(scroll);
return panel;
};
// =======================【面板构建:主面板 / 设置面板】======================
FloatBallAppWM.prototype.buildPanelView = function(panelType) {
if (panelType === "settings") return this.buildSettingsPanelView();
if (panelType === "btn_editor") return this.buildButtonEditorPanelView();
if (panelType === "schema_editor") return this.buildSchemaEditorPanelView();
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var bgColor = isDark ? C.bgDark : C.bgLight;
var cardColor = isDark ? C.cardDark : C.cardLight;
var textColor = isDark ? C.textPriDark : C.textPriLight;
var panel = new android.widget.LinearLayout(context);
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
// 面板背景
var bgDr = new android.graphics.drawable.GradientDrawable();
// bgDr.setColor(bgColor);
bgDr.setColor(this.withAlpha(bgColor, this.config.PANEL_BG_ALPHA));
bgDr.setCornerRadius(this.dp(16));
panel.setBackground(bgDr);
try { panel.setElevation(this.dp(8)); } catch(e){}
var padDp = this.config.PANEL_PADDING_DP;
panel.setPadding(
this.dp(padDp),
this.dp(padDp),
this.dp(padDp),
this.dp(padDp)
);
var rawBtns = [];
try { if (this.panels && this.panels[this.currentPanelKey]) rawBtns = this.panels[this.currentPanelKey]; } catch (e0) { rawBtns = []; }
// # 启用/禁用按钮页只渲染启用项enabled !== false
// # 说明:按钮管理页仍显示全部;这里只影响主面板显示,不改变按钮顺序与索引存储
var btns = [];
try {
if (rawBtns && rawBtns.length) {
for (var bi = 0; bi < rawBtns.length; bi++) {
var bb = rawBtns[bi];
if (bb && bb.enabled === false) continue;
btns.push(bb);
}
}
} catch (eF) { btns = rawBtns || []; }
var cols2 = this.config.PANEL_COLS;
var rows2 = this.config.PANEL_ROWS;
var itemPx = this.dp(this.config.PANEL_ITEM_SIZE_DP);
var gapPx = this.dp(this.config.PANEL_GAP_DP);
var contentW = cols2 * itemPx + cols2 * 2 * gapPx;
// 计算内容高度限制
// 每一行的高度 = itemPx + 2 * gapPx
var oneRowH = itemPx + gapPx * 2;
// 实际按钮数量
var totalBtns = (btns && btns.length) ? btns.length : 0;
// 最小显示的格子数(填满 rows2
var minCells = cols2 * rows2;
// 最终渲染的格子总数
var totalCells = totalBtns > minCells ? totalBtns : minCells;
// 最终行数
var finalRows = Math.ceil(totalCells / cols2);
var scrollH = android.widget.LinearLayout.LayoutParams.WRAP_CONTENT;
if (finalRows > rows2) {
scrollH = rows2 * oneRowH;
}
var scroll = new android.widget.ScrollView(context);
try { scroll.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER); } catch (eOS) {}
try { scroll.setVerticalScrollBarEnabled(false); } catch (eSB) {}
try { scroll.setFillViewport(true); } catch (eFV) {}
var scrollLp = new android.widget.LinearLayout.LayoutParams(contentW, scrollH);
scroll.setLayoutParams(scrollLp);
var self = this;
scroll.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) { self.touchActivity(); return false; }
}));
var grid = new android.widget.GridLayout(context);
grid.setColumnCount(cols2);
// var totalBtns = (btns && btns.length) ? btns.length : 0;
// var minCells = cols2 * rows2;
// var totalCells = totalBtns > minCells ? totalBtns : minCells;
var rowCount = Math.ceil(totalCells / cols2);
try { grid.setRowCount(rowCount); } catch (eRC) {}
grid.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) { self.touchActivity(); return false; }
}));
var i;
for (i = 0; i < totalCells; i++) {
var btnCfg = (btns && i < totalBtns) ? btns[i] : null;
var cell = new android.widget.LinearLayout(context);
cell.setOrientation(android.widget.LinearLayout.VERTICAL);
cell.setGravity(android.view.Gravity.CENTER);
var lp = new android.widget.GridLayout.LayoutParams();
lp.width = itemPx;
lp.height = itemPx;
lp.setMargins(gapPx, gapPx, gapPx, gapPx);
cell.setLayoutParams(lp);
// 单元格背景:如果是有功能的按钮,给个卡片背景;否则透明
if (btnCfg) {
cell.setBackground(self.ui.createRoundDrawable(cardColor, self.dp(12)));
try { cell.setElevation(self.dp(2)); } catch(e){}
} else {
// 空格子占位
}
var iv = new android.widget.ImageView(context);
var dr = this.resolveIconDrawable(btnCfg);
if (dr) {
iv.setImageDrawable(dr);
// 如果图标是白色的(通常是系统图标),且我们在亮色卡片上,可能需要染色
// 但 resolveIconDrawable 逻辑比较复杂,这里暂时不强制染色,除非用户配置了 TINT
if (!isDark && btnCfg && !btnCfg.type && !btnCfg.pkg) {
// 简单的系统图标在亮色模式下可能看不清,染成深色
try { iv.setColorFilter(C.textPriLight, android.graphics.PorterDuff.Mode.SRC_IN); } catch(eCF){}
}
}
var ivLp = new android.widget.LinearLayout.LayoutParams(
this.dp(this.config.PANEL_ICON_SIZE_DP),
this.dp(this.config.PANEL_ICON_SIZE_DP)
);
iv.setLayoutParams(ivLp);
cell.addView(iv);
if (this.config.PANEL_LABEL_ENABLED) {
var tv = new android.widget.TextView(context);
var title = (btnCfg && btnCfg.title) ? String(btnCfg.title) : "";
tv.setText(title);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, this.config.PANEL_LABEL_TEXT_SIZE_SP);
tv.setTextColor(textColor);
tv.setGravity(android.view.Gravity.CENTER);
try { tv.setLines(1); tv.setEllipsize(android.text.TextUtils.TruncateAt.END); } catch(eL){}
var tvLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, // 宽度填满,方便居中
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
tvLp.topMargin = this.dp(this.config.PANEL_LABEL_TOP_MARGIN_DP);
tv.setLayoutParams(tvLp);
cell.addView(tv);
}
if (btnCfg) {
(function(index, btnObj) {
cell.setClickable(true);
// 使用 Ripple 效果增强点击反馈
var rippleDr = self.ui.createRippleDrawable(cardColor, self.withAlpha(C.primary, 0.2), self.dp(12));
cell.setBackground(rippleDr);
cell.setOnClickListener(new android.view.View.OnClickListener({
onClick: function() {
self.touchActivity();
self.hideMainPanel();
self.execButtonAction(btnObj, index);
}
}));
})(i, btnCfg);
} else {
try { iv.setAlpha(0); } catch (eA0) {}
try { cell.setClickable(false); } catch (eC0) {}
}
grid.addView(cell);
}
scroll.addView(grid);
panel.addView(scroll);
return panel;
};
FloatBallAppWM.prototype.getBestPanelPosition = function(pw, ph, bx, by, ballSize) {
var mode = String(this.config.PANEL_POS_GRAVITY || "bottom").toLowerCase();
var gap = this.dp(this.config.BALL_PANEL_GAP_DP);
var sw = this.state.screen.w;
var sh = this.state.screen.h;
var candidates = [];
function makeCand(type) {
if (type === "bottom") {
return {
x: Math.max(0, Math.min(sw - pw, bx + (ballSize - pw) / 2)),
y: by + ballSize + gap,
type: "bottom"
};
}
if (type === "top") {
return {
x: Math.max(0, Math.min(sw - pw, bx + (ballSize - pw) / 2)),
y: by - ph - gap,
type: "top"
};
}
if (type === "right") {
return {
x: bx + ballSize + gap,
y: by,
type: "right"
};
}
if (type === "left") {
return {
x: bx - pw - gap,
y: by,
type: "left"
};
}
return null;
}
if (mode === "top") {
candidates.push(makeCand("top"));
candidates.push(makeCand("bottom"));
} else if (mode === "left") {
candidates.push(makeCand("left"));
candidates.push(makeCand("right"));
candidates.push(makeCand("bottom"));
} else if (mode === "right") {
candidates.push(makeCand("right"));
candidates.push(makeCand("left"));
candidates.push(makeCand("bottom"));
} else {
// Default bottom or auto
candidates.push(makeCand("bottom"));
candidates.push(makeCand("top"));
candidates.push(makeCand("right"));
candidates.push(makeCand("left"));
}
var diyY = this.dp(this.config.PANEL_CUSTOM_OFFSET_Y || 0);
// Find first valid
for (var i = 0; i < candidates.length; i++) {
var c = candidates[i];
if (c) {
c.y += diyY;
if (c.x >= 0 && c.x + pw <= sw && c.y >= 0 && c.y + ph <= sh) {
return c;
}
}
}
// Fallback: pick primary and clamp
var best = candidates[0];
if (!best) best = { x: 0, y: 0 };
best.x = Math.max(0, Math.min(sw - pw, best.x));
best.y = Math.max(0, Math.min(sh - ph, best.y));
return best;
};
FloatBallAppWM.prototype.computePanelX = function(ballX, panelW) {
var gapPx = this.dp(this.config.BALL_PANEL_GAP_DP);
var di = this.getDockInfo();
var screenW = this.state.screen.w;
// 1. 优先尝试放在右侧
var rightX = ballX + di.ballSize + gapPx;
if (rightX + panelW <= screenW) {
return rightX;
}
// 2. 右侧放不下,尝试放在左侧
var leftX = ballX - gapPx - panelW;
if (leftX >= 0) {
return leftX;
}
// 3. 两边都放不下(面板太宽),选择空间大的一侧
var spaceRight = screenW - (ballX + di.ballSize + gapPx);
var spaceLeft = ballX - gapPx;
if (spaceLeft > spaceRight) {
// 左侧空间大,靠左放(可能会覆盖球或被切断,但优先保证左对齐)
// 为了防止左边被切断max(0, leftX)
return Math.max(0, leftX);
} else {
// 右侧空间大
return Math.min(screenW - panelW, rightX);
}
};
FloatBallAppWM.prototype.tryAdjustPanelY = function(px, py, pw, ph, bx, by) {
var gapPx = this.dp(this.config.BALL_PANEL_GAP_DP);
var di = this.getDockInfo();
var minY = 0;
var maxY = this.state.screen.h - ph;
py = this.clamp(py, minY, maxY);
if (!this.rectIntersect(px, py, pw, ph, bx, by, di.ballSize, di.ballSize)) return { ok: true, x: px, y: py };
var pyAbove = by - gapPx - ph;
if (pyAbove >= minY && pyAbove <= maxY) {
if (!this.rectIntersect(px, pyAbove, pw, ph, bx, by, di.ballSize, di.ballSize)) return { ok: true, x: px, y: pyAbove };
}
var pyBelow = by + di.ballSize + gapPx;
if (pyBelow >= minY && pyBelow <= maxY) {
if (!this.rectIntersect(px, pyBelow, pw, ph, bx, by, di.ballSize, di.ballSize)) return { ok: true, x: px, y: pyBelow };
}
return { ok: false, x: px, y: py };
};
FloatBallAppWM.prototype.addPanel = function(panel, x, y, which) {
if (this.state.closing) return;
// Determine if this panel should be modal (blocking background touches, better for IME)
var isModal = (which === "settings" || which === "btn_editor" || which === "schema_editor");
var flags;
if (isModal) {
// Modal: blocks outside touches, dim background, ensures focus for IME
flags = android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
} else {
// Non-modal: allow outside touches
flags = android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
// 主面板(预览)需要获取焦点以支持输入法,同时允许点击外部
// if (which === "main") {
// flags |= android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
// }
}
var lp = new android.view.WindowManager.LayoutParams(
android.view.WindowManager.LayoutParams.WRAP_CONTENT,
android.view.WindowManager.LayoutParams.WRAP_CONTENT,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
flags,
android.graphics.PixelFormat.TRANSLUCENT
);
if (isModal) {
lp.dimAmount = 0.5;
}
// Allow resizing for IME
lp.softInputMode = android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = x;
lp.y = y;
try { this.state.wm.addView(panel, lp); } catch (eAdd) { safeLog(this.L, 'e', "addPanel fail which=" + String(which) + " err=" + String(eAdd)); return; }
if (which === "main") { this.state.panel = panel; this.state.panelLp = lp; this.state.addedPanel = true; }
else if (which === "settings") { this.state.settingsPanel = panel; this.state.settingsPanelLp = lp; this.state.addedSettings = true; }
else { this.state.viewerPanel = panel; this.state.viewerPanelLp = lp; this.state.addedViewer = true; }
try {
if (this.config.ENABLE_ANIMATIONS) {
panel.setScaleX(0.96);
panel.setScaleY(0.96);
panel.setAlpha(0);
panel.animate()
.scaleX(1)
.scaleY(1)
.alpha(1)
.setDuration(180)
.setInterpolator(new android.view.animation.AccelerateDecelerateInterpolator())
.start();
} else {
panel.setScaleX(1);
panel.setScaleY(1);
panel.setAlpha(1);
}
} catch (eA) {}
// # 日志防抖5秒内相同面板类型不重复记录
var now = Date.now();
var lastPanelShow = this.state._lastPanelShow || {};
var lastTime = lastPanelShow[which] || 0;
if (now - lastTime > 5000) {
safeLog(this.L, 'i', "panel show which=" + String(which) + " x=" + String(x) + " y=" + String(y));
lastPanelShow[which] = now;
this.state._lastPanelShow = lastPanelShow;
}
};
FloatBallAppWM.prototype.showPanelAvoidBall = function(which) {
if (this.state.closing) return;
// 优化如果是刷新编辑器面板btn_editor/schema_editor且面板已存在则直接更新内容避免闪烁
if ((which === "btn_editor" || which === "schema_editor") && this.state.addedViewer && this.state.viewerPanel) {
try {
var self = this;
var type = which;
var newPanel = self.buildPanelView(type);
// 限制高度逻辑 (复用下面的逻辑)
var maxH = Math.floor(self.state.screen.h * 0.75);
newPanel.measure(
android.view.View.MeasureSpec.makeMeasureSpec(self.state.screen.w, android.view.View.MeasureSpec.AT_MOST),
android.view.View.MeasureSpec.makeMeasureSpec(maxH, android.view.View.MeasureSpec.AT_MOST)
);
var pw = newPanel.getMeasuredWidth();
var ph = newPanel.getMeasuredHeight();
var finalPh = (ph > maxH) ? maxH : ph;
// 更新现有面板内容 (Draggable Container 结构)
var oldRoot = this.state.viewerPanel;
// 查找内容容器Root(FrameLayout) -> Container(LinearLayout) -> Content(Last Child)
// 结构Root -> [Container] -> [Header, Content]
// 为了通用性,我们约定 Content 是 Container 的最后一个子 View
// 检查是否是 Draggable 结构
var container = null;
if (oldRoot.getChildCount() > 0 && oldRoot.getChildAt(0) instanceof android.widget.LinearLayout) {
container = oldRoot.getChildAt(0);
}
if (container && container.getChildCount() >= 2) {
// 假设最后一个是 Content
var oldContent = container.getChildAt(container.getChildCount() - 1);
container.removeView(oldContent);
// 准备新内容
// newPanel 本身就是内容
// 需要处理 newPanel 的 LayoutParams
var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0);
contentLp.weight = 1;
try { newPanel.setBackground(null); } catch(e){} // 移除背景,使用 Container 背景
try { newPanel.setElevation(0); } catch(e){}
newPanel.setLayoutParams(contentLp);
container.addView(newPanel);
// 更新 Window LayoutParams (仅高度和位置)
var lp = this.state.viewerPanelLp;
if (lp) {
// 高度调整:如果是 wrap_content需要重新计算如果用户调整过大小(explicit),则保持不变?
// 这里为了简单,如果是刷新,我们保持当前的 LayoutParams 尺寸,除非内容变大?
// 暂时策略:保持当前窗口大小,或者自适应
// 如果是编辑器,通常希望保持大小
// 所以这里不更新 lp.height 和 lp.width只更新内容
}
} else {
// 非 Draggable 结构(回退)
// ... (原有逻辑)
}
self.touchActivity();
return; // 刷新完成,直接返回
} catch(e) {
if (self.L) self.L.e("Refresh panel failed, fallback to recreate: " + String(e));
// 如果失败,继续执行下面的销毁重建逻辑
}
}
if (which === "main" && this.state.addedPanel) return;
if (which === "settings" && this.state.addedSettings) return;
this.touchActivity();
if (which === "main") { this.hideSettingsPanel(); this.hideViewerPanel(); }
if (which === "settings") { this.hideMainPanel(); this.hideViewerPanel(); }
if (which === "btn_editor" || which === "schema_editor") { this.hideMainPanel(); this.hideSettingsPanel(); this.hideViewerPanel(); }
var self = this;
var doShow = function() {
try {
if (self.state.closing) return;
self.showMask();
var type = which;
var rawPanel = self.buildPanelView(type);
// 决定是否启用拖拽/缩放 (排除 main)
var enableDrag = (which !== "main");
var panelView = rawPanel;
var dragHeader = null;
var resizeHandles = null;
if (enableDrag) {
// 获取标题
var titleMap = {
"settings": "设置",
"btn_editor": "按钮管理",
"schema_editor": "布局管理"
};
var title = titleMap[which] || "面板";
// 动态标题 for btn_editor
if (which === "btn_editor") {
if (self.state.editingButtonIndex !== null && self.state.editingButtonIndex !== undefined) {
title = (self.state.editingButtonIndex === -1) ? "新增按钮" : "编辑按钮";
} else {
title = "按钮管理";
}
}
var wrapped = self.wrapDraggablePanel(rawPanel, title, function() {
if (which === "settings") self.hideSettingsPanel();
else if (which === "main") self.hideMainPanel();
else self.hideViewerPanel();
});
panelView = wrapped.view;
dragHeader = wrapped.header;
resizeHandles = wrapped.handles;
}
// # 限制面板最大高度
var maxH = Math.floor(self.state.screen.h * 0.75);
panelView.measure(
android.view.View.MeasureSpec.makeMeasureSpec(self.state.screen.w, android.view.View.MeasureSpec.AT_MOST),
android.view.View.MeasureSpec.makeMeasureSpec(maxH, android.view.View.MeasureSpec.AT_MOST)
);
var pw = panelView.getMeasuredWidth();
var ph = panelView.getMeasuredHeight();
// Load saved state
var savedState = null;
if (enableDrag && self.loadPanelState) {
savedState = self.loadPanelState(which);
}
if (savedState && savedState.w && savedState.h) {
pw = savedState.w;
ph = savedState.h;
} else {
// 显式设置面板高度
if (ph > maxH) {
ph = maxH;
}
}
var safeLp = panelView.getLayoutParams();
if (!safeLp) safeLp = new android.view.ViewGroup.LayoutParams(pw, ph);
else { safeLp.width = pw; safeLp.height = ph; }
panelView.setLayoutParams(safeLp);
var bx = self.state.ballLp.x;
var by = self.state.ballLp.y;
var di = self.getDockInfo();
// 位置计算
var pos = self.getBestPanelPosition(pw, ph, bx, by, di.ballSize);
if (savedState && typeof savedState.x === 'number' && typeof savedState.y === 'number') {
pos.x = savedState.x;
pos.y = savedState.y;
}
self.addPanel(panelView, pos.x, pos.y, which);
// 绑定拖拽事件
if (enableDrag) {
self.attachDragResizeListeners(panelView, dragHeader, resizeHandles, which);
}
self.touchActivity();
} catch (e) {
if (self.L) self.L.e("showPanelAvoidBall callback err=" + String(e));
try { self.toast("面板显示失败: " + String(e)); } catch (et) {}
}
};
if (which === "settings") {
doShow();
} else {
this.undockToFull(true, doShow);
}
};
// =======================【辅助:包装可拖拽面板】======================
FloatBallAppWM.prototype.wrapDraggablePanel = function(contentView, optionsOrTitle, onClose) {
var self = this;
// var context = this.context; // Remove this line to use global context
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var title = "";
var hideHeader = false;
if (typeof optionsOrTitle === "string") {
title = optionsOrTitle;
} else if (optionsOrTitle && typeof optionsOrTitle === "object") {
title = optionsOrTitle.title || "";
hideHeader = !!optionsOrTitle.hideHeader;
}
// Root FrameLayout (to hold content + resize handle)
var root = new android.widget.FrameLayout(context);
// Main Container (Header + Content)
var container = new android.widget.LinearLayout(context);
container.setOrientation(android.widget.LinearLayout.VERTICAL);
// 背景设置在 Container 上
var bgDr = new android.graphics.drawable.GradientDrawable();
bgDr.setColor(isDark ? C.bgDark : C.bgLight);
bgDr.setCornerRadius(this.dp(12));
container.setBackground(bgDr);
try { container.setElevation(this.dp(8)); } catch(e){}
// Header
var header = new android.widget.LinearLayout(context);
header.setOrientation(android.widget.LinearLayout.HORIZONTAL);
header.setGravity(android.view.Gravity.CENTER_VERTICAL);
header.setPadding(this.dp(12), this.dp(8), this.dp(8), this.dp(8));
// 给 Header 一个背景色,区分度更好
// header.setBackgroundColor(isDark ? 0x22FFFFFF : 0x11000000);
var titleTv = new android.widget.TextView(context);
titleTv.setText(String(title || "Panel"));
titleTv.setTextColor(isDark ? C.textPriDark : C.textPriLight);
titleTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
titleTv.setTypeface(null, android.graphics.Typeface.BOLD);
var titleLp = new android.widget.LinearLayout.LayoutParams(0, -2);
titleLp.weight = 1;
titleTv.setLayoutParams(titleLp);
header.addView(titleTv);
// Close Button
var btnClose = this.ui.createFlatButton(this, "✕", C.textSecLight, function() {
if (onClose) onClose();
else self.hideAllPanels();
});
btnClose.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(4));
try { btnClose.setElevation(this.dp(25)); } catch(e){} // Ensure on top of resize handles
header.addView(btnClose);
// Spacer to avoid overlap with Top-Right resize handle
var spacer = new android.view.View(context);
spacer.setLayoutParams(new android.widget.LinearLayout.LayoutParams(this.dp(30), 1));
header.addView(spacer);
container.addView(header);
if (hideHeader) {
header.setVisibility(android.view.View.GONE);
}
// Add Content
// 移除 content 原有的背景和 elevation避免重复
try { contentView.setBackground(null); } catch(e){}
try { contentView.setElevation(0); } catch(e){}
var contentLp = new android.widget.LinearLayout.LayoutParams(-1, 0);
contentLp.weight = 1;
contentView.setLayoutParams(contentLp);
container.addView(contentView);
// Container fill root
root.addView(container, new android.widget.FrameLayout.LayoutParams(-1, -1));
// Resize Handle (Bottom-Right Corner) - Invisible
var handleBR = new android.view.View(context);
try { handleBR.setElevation(this.dp(20)); } catch(e){}
var handleBRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleBRLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.END;
handleBRLp.rightMargin = 0;
handleBRLp.bottomMargin = 0;
root.addView(handleBR, handleBRLp);
// Resize Handle (Bottom-Left Corner) - Invisible
var handleBL = new android.view.View(context);
try { handleBL.setElevation(this.dp(20)); } catch(e){}
var handleBLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleBLLp.gravity = android.view.Gravity.BOTTOM | android.view.Gravity.START;
handleBLLp.bottomMargin = 0;
handleBLLp.leftMargin = 0;
root.addView(handleBL, handleBLLp);
// Resize Handle (Top-Left Corner) - Invisible
var handleTL = new android.view.View(context);
try { handleTL.setElevation(this.dp(20)); } catch(e){}
var handleTLLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleTLLp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
handleTLLp.topMargin = 0;
handleTLLp.leftMargin = 0;
root.addView(handleTL, handleTLLp);
// Resize Handle (Top-Right Corner) - Invisible
var handleTR = new android.view.View(context);
try { handleTR.setElevation(this.dp(20)); } catch(e){}
var handleTRLp = new android.widget.FrameLayout.LayoutParams(this.dp(30), this.dp(30));
handleTRLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END;
handleTRLp.topMargin = 0;
handleTRLp.rightMargin = 0;
root.addView(handleTR, handleTRLp);
return {
view: root,
header: header,
handles: {
br: handleBR,
bl: handleBL,
tl: handleTL,
tr: handleTR
}
};
};
// =======================【辅助:绑定拖拽/缩放事件】======================
FloatBallAppWM.prototype.attachDragResizeListeners = function(rootView, headerView, resizeHandles, panelKey) {
var self = this;
var wm = this.state.wm;
// Helper to get LP safely (must be called after addView)
var getLp = function() {
return rootView.getLayoutParams();
};
// Helper to save state
var saveState = function() {
if (panelKey && self.savePanelState) {
var lp = getLp();
if (lp) {
self.savePanelState(panelKey, {
x: lp.x,
y: lp.y,
w: rootView.getWidth(),
h: rootView.getHeight()
});
}
}
};
// Drag Logic
if (headerView) {
var lastX = 0, lastY = 0;
var initialX = 0, initialY = 0;
var dragging = false;
headerView.setOnTouchListener(new android.view.View.OnTouchListener({
onTouch: function(v, e) {
var action = e.getAction();
var rawX = e.getRawX();
var rawY = e.getRawY();
if (action === android.view.MotionEvent.ACTION_DOWN) {
self.touchActivity();
lastX = rawX;
lastY = rawY;
var lp = getLp();
if (lp) {
initialX = lp.x;
initialY = lp.y;
dragging = false;
return true;
}
} else if (action === android.view.MotionEvent.ACTION_MOVE) {
self.touchActivity();
var dx = rawX - lastX;
var dy = rawY - lastY;
if (!dragging && (Math.abs(dx) > self.dp(5) || Math.abs(dy) > self.dp(5))) {
dragging = true;
}
if (dragging) {
var lp = getLp();
if (lp) {
var targetX = Math.round(initialX + dx);
var targetY = Math.round(initialY + dy);
// 边界限制:防止完全拖出屏幕
// 至少保留 30dp 在屏幕内
var safeMargin = self.dp(30);
var screenW = self.state.screen.w;
var screenH = self.state.screen.h;
var curW = rootView.getWidth();
// X轴左边缘不小于 -width + safeMargin, 右边缘不大于 screenW - safeMargin
targetX = Math.max(-curW + safeMargin, Math.min(targetX, screenW - safeMargin));
// Y轴顶部不小于 0, 底部不大于 screenH - safeMargin
targetY = Math.max(0, Math.min(targetY, screenH - safeMargin));
lp.x = targetX;
lp.y = targetY;
try { wm.updateViewLayout(rootView, lp); } catch(e){}
}
}
return true;
} else if (action === android.view.MotionEvent.ACTION_UP) {
self.touchActivity();
if (dragging) {
saveState();
}
dragging = false;
return true;
}
return false;
}
}));
}
// Resize Logic
if (resizeHandles) {
var handles = {};
// Compatibility: if resizeHandles is a View, treat as 'br'
if (resizeHandles instanceof android.view.View) {
handles.br = resizeHandles;
} else {
handles = resizeHandles;
}
var setupResize = function(mode, handleView) {
var lastRX = 0, lastRY = 0;
var initialW = 0, initialH = 0;
var initialX = 0, initialY = 0;
var resizing = false;
// Long Press Logic
var longPressRunnable = null;
var longPressTriggered = false;
handleView.setOnTouchListener(new android.view.View.OnTouchListener({
onTouch: function(v, e) {
var action = e.getAction();
var rawX = e.getRawX();
var rawY = e.getRawY();
if (action === android.view.MotionEvent.ACTION_DOWN) {
self.touchActivity();
lastRX = rawX;
lastRY = rawY;
var lp = getLp();
if (lp) {
initialW = rootView.getWidth();
initialH = rootView.getHeight();
initialX = lp.x;
initialY = lp.y;
resizing = false;
longPressTriggered = false;
// Start Long Press Timer
longPressRunnable = new java.lang.Runnable({
run: function() {
try {
longPressTriggered = true;
self.vibrateOnce(40); // Haptic feedback
// Switch to fixed size immediately
var lpCur = getLp();
if (lpCur) {
lpCur.width = initialW;
lpCur.height = initialH;
lpCur.x = initialX;
lpCur.y = initialY;
try { wm.updateViewLayout(rootView, lpCur); } catch(e){}
}
resizing = true;
} catch(e) {}
}
});
self.state.h.postDelayed(longPressRunnable, 300); // 300ms hold to activate resize
return true;
}
} else if (action === android.view.MotionEvent.ACTION_MOVE) {
self.touchActivity();
var dx = rawX - lastRX;
var dy = rawY - lastRY;
if (!longPressTriggered) {
// If moved significantly before long press triggers, cancel it
// Increased tolerance to 16dp to avoid accidental cancellation
if (Math.abs(dx) > self.dp(16) || Math.abs(dy) > self.dp(16)) {
if (longPressRunnable) {
self.state.h.removeCallbacks(longPressRunnable);
longPressRunnable = null;
}
}
}
if (resizing) {
var lp = getLp();
if (lp) {
var newW = initialW;
var newH = initialH;
var newX = initialX;
var newY = initialY;
// 1. Calculate raw new dimensions
if (mode === 'br') {
newW = initialW + dx;
newH = initialH + dy;
} else if (mode === 'r') {
newW = initialW + dx;
} else if (mode === 'b') {
newH = initialH + dy;
} else if (mode === 'bl') {
newW = initialW - dx;
newH = initialH + dy;
} else if (mode === 'tl') {
newW = initialW - dx;
newH = initialH - dy;
} else if (mode === 'tr') {
newW = initialW + dx;
newH = initialH - dy;
}
// 2. Constrain new dimensions
var screenW = self.state.screen.w;
var screenH = self.state.screen.h;
var constrainedW = Math.max(self.dp(200), Math.min(newW, screenW));
var constrainedH = Math.max(self.dp(150), Math.min(newH, screenH));
// 3. Calculate new position based on CONSTRAINED dimensions change
// If dragging left edge (bl, tl), X must shift by the amount WIDTH changed (constrained)
if (mode === 'bl' || mode === 'tl') {
// newX = initialX - (change in width)
// change in width = constrainedW - initialW
newX = initialX - (constrainedW - initialW);
}
// If dragging top edge (tl, tr), Y must shift by the amount HEIGHT changed (constrained)
if (mode === 'tl' || mode === 'tr') {
// newY = initialY - (change in height)
// change in height = constrainedH - initialH
newY = initialY - (constrainedH - initialH);
}
lp.width = Math.round(constrainedW);
lp.height = Math.round(constrainedH);
if (mode === 'bl' || mode === 'tl') {
lp.x = Math.round(newX);
}
if (mode === 'tl' || mode === 'tr') {
lp.y = Math.round(newY);
}
try { wm.updateViewLayout(rootView, lp); } catch(e){}
}
}
return true;
} else if (action === android.view.MotionEvent.ACTION_UP || action === android.view.MotionEvent.ACTION_CANCEL) {
self.touchActivity();
if (longPressRunnable) {
self.state.h.removeCallbacks(longPressRunnable);
longPressRunnable = null;
}
if (resizing) {
saveState();
}
resizing = false;
longPressTriggered = false;
return true;
}
return false;
}
}));
};
if (handles.br) setupResize('br', handles.br);
if (handles.bl) setupResize('bl', handles.bl);
if (handles.tl) setupResize('tl', handles.tl);
if (handles.tr) setupResize('tr', handles.tr);
}
};
// =======================【查看器面板:显示】======================
FloatBallAppWM.prototype.showViewerPanel = function(title, text) {
if (this.state.closing) return;
this.touchActivity();
if (this.state.addedViewer) this.hideViewerPanel();
this.hideMainPanel();
this.hideSettingsPanel();
var self = this;
this.undockToFull(true, function() {
if (self.state.closing) return;
self.showMask();
var rawPanel = self.buildViewerPanelView(title, text);
// 使用 Draggable Wrapper
var wrapped = self.wrapDraggablePanel(rawPanel, title || "Viewer", function() {
self.hideViewerPanel();
});
var panelView = wrapped.view;
panelView.measure(
android.view.View.MeasureSpec.makeMeasureSpec(0, android.view.View.MeasureSpec.UNSPECIFIED),
android.view.View.MeasureSpec.makeMeasureSpec(0, android.view.View.MeasureSpec.UNSPECIFIED)
);
var pw = panelView.getMeasuredWidth();
var ph = panelView.getMeasuredHeight();
// 限制初始大小
var maxW = Math.floor(self.state.screen.w * 0.9);
var maxH = Math.floor(self.state.screen.h * 0.7);
if (pw > maxW) pw = maxW;
if (ph > maxH) ph = maxH;
// Load saved state
var savedState = null;
if (self.loadPanelState) {
savedState = self.loadPanelState("viewer");
}
if (savedState && savedState.w && savedState.h) {
pw = savedState.w;
ph = savedState.h;
}
// 设置 LayoutParams 尺寸 (如果是 wrap_content 可能会超)
var safeLp = new android.view.ViewGroup.LayoutParams(pw, ph);
panelView.setLayoutParams(safeLp);
var bx = self.state.ballLp.x;
var by = self.state.ballLp.y;
var finalX, finalY;
if (savedState && typeof savedState.x === 'number' && typeof savedState.y === 'number') {
finalX = savedState.x;
finalY = savedState.y;
} else {
var px = self.computePanelX(bx, pw);
var py = by;
var r = self.tryAdjustPanelY(px, py, pw, ph, bx, by);
if (r.ok) {
finalX = r.x;
finalY = r.y;
} else {
finalX = px;
finalY = self.clamp(py, 0, self.state.screen.h - ph);
}
}
self.addPanel(panelView, finalX, finalY, "viewer");
// 绑定事件
self.attachDragResizeListeners(panelView, wrapped.header, wrapped.handles, "viewer");
self.touchActivity();
});
};
FloatBallAppWM.prototype.cancelLongPressTimer = function() {
try { if (this.state.longPressRunnable && this.state.h) this.state.h.removeCallbacks(this.state.longPressRunnable); } catch (e) {}
this.state.longPressArmed = false;
this.state.longPressRunnable = null;
};
FloatBallAppWM.prototype.resetLongPressState = function() {
this.state.longPressArmed = false;
this.state.longPressTriggered = false;
this.state.longPressRunnable = null;
};
FloatBallAppWM.prototype.armLongPress = function() {
if (!this.config.ENABLE_LONG_PRESS) return;
this.resetLongPressState();
this.state.longPressArmed = true;
var self = this;
var r = new java.lang.Runnable({
run: function() {
try {
if (self.state.closing) return;
if (!self.state.longPressArmed) return;
if (self.state.dragging) return;
self.state.longPressTriggered = true;
self.vibrateOnce(self.config.LONG_PRESS_VIBRATE_MS);
if (self.state.addedSettings) self.hideSettingsPanel();
else self.showPanelAvoidBall("settings");
if (self.L) self.L.i("longPress -> settings");
} catch (e) {
if (self.L) self.L.e("longPress run err=" + String(e));
}
}
});
this.state.longPressRunnable = r;
this.state.h.postDelayed(r, this.config.LONG_PRESS_MS);
};
FloatBallAppWM.prototype.setupTouchListener = function() {
var slop = this.dp(this.config.CLICK_SLOP_DP);
var self = this;
// 速度追踪
var velocityTracker = null;
var lastTouchX = 0;
var lastTouchY = 0;
// 限制 WM 更新频率,避免过热/卡顿
var lastUpdateTs = 0;
return new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) {
if (self.state.closing) return false;
var a = e.getAction();
var di = self.getDockInfo();
if (velocityTracker == null) {
velocityTracker = android.view.VelocityTracker.obtain();
}
velocityTracker.addMovement(e);
if (a === android.view.MotionEvent.ACTION_DOWN) {
self.touchActivity();
// 恢复不透明度
try { v.setAlpha(1.0); } catch(eA) {}
if (self.state.docked) {
self.undockToFull(false, null);
self.touchActivity();
}
self.state.rawX = e.getRawX();
self.state.rawY = e.getRawY();
self.state.downX = self.state.ballLp.x;
self.state.downY = self.state.ballLp.y;
self.state.dragging = false;
lastTouchX = e.getRawX();
lastTouchY = e.getRawY();
try { v.setPressed(true); } catch (eP) {}
try { v.drawableHotspotChanged(e.getX(), e.getY()); } catch (eH) {}
// 按下缩小反馈
if (self.config.ENABLE_ANIMATIONS) {
try {
v.animate().scaleX(0.9).scaleY(0.9).setDuration(100).start();
} catch(eS){}
}
self.armLongPress();
// # 日志精简touch DOWN 只在 DEBUG 且非频繁触发时记录
// if (self.L) self.L.d("touch DOWN rawX=" + String(self.state.rawX) + " rawY=" + String(self.state.rawY));
return true;
}
if (a === android.view.MotionEvent.ACTION_MOVE) {
self.touchActivity();
var curRawX = e.getRawX();
var curRawY = e.getRawY();
var dx = Math.round(curRawX - self.state.rawX);
var dy = Math.round(curRawY - self.state.rawY);
lastTouchX = curRawX;
lastTouchY = curRawY;
if (!self.state.dragging) {
if (Math.abs(dx) > slop || Math.abs(dy) > slop) {
self.state.dragging = true;
self.cancelLongPressTimer();
// # 日志精简drag start 只在 DEBUG 时记录
// if (self.L) self.L.d("drag start dx=" + String(dx) + " dy=" + String(dy));
}
}
if (self.state.dragging) {
self.state.ballLp.x = self.state.downX + dx;
self.state.ballLp.y = self.state.downY + dy;
self.state.ballLp.x = self.clamp(self.state.ballLp.x, 0, self.state.screen.w - di.ballSize);
self.state.ballLp.y = self.clamp(self.state.ballLp.y, 0, self.state.screen.h - di.ballSize);
self.state.ballLp.width = di.ballSize;
try { self.state.ballContent.setX(0); } catch (e0) {}
var now = java.lang.System.currentTimeMillis();
if (now - lastUpdateTs > 10) { // 10ms 节流
try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch (eU) {}
lastUpdateTs = now;
}
self.hideAllPanels();
// 拖拽中不频繁保存位置,只在 UP 时保存
}
return true;
}
if (a === android.view.MotionEvent.ACTION_UP || a === android.view.MotionEvent.ACTION_CANCEL) {
self.touchActivity();
try { v.setPressed(false); } catch (eP2) {}
self.cancelLongPressTimer();
// 恢复缩放
if (self.config.ENABLE_ANIMATIONS) {
try {
v.animate().scaleX(1.0).scaleY(1.0).setDuration(150).start();
} catch(eS){}
} else {
try { v.setScaleX(1); v.setScaleY(1); } catch(eS){}
}
if (self.state.longPressTriggered) {
self.resetLongPressState();
if (velocityTracker) { velocityTracker.recycle(); velocityTracker = null; }
return true;
}
if (!self.state.dragging && a === android.view.MotionEvent.ACTION_UP) {
try { self.playBounce(v); } catch (eB) {}
if (self.state.addedPanel) self.hideMainPanel();
else self.showPanelAvoidBall("main");
// # 日志精简click 事件记录为 INFO 级别(关键操作)
if (self.L) self.L.i("click -> toggle main");
} else {
// 拖拽结束
// 确保最后位置被更新
try { self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp); } catch (eU) {}
var forceSide = null;
// 计算速度
if (velocityTracker) {
velocityTracker.computeCurrentVelocity(1000);
var vx = velocityTracker.getXVelocity();
// 简单的 fling 判定
if (vx > 1000) forceSide = "right";
else if (vx < -1000) forceSide = "left";
// # 日志精简drag end 只在 DEBUG 时记录
// if (self.L) self.L.d("drag end vx=" + vx);
}
if (self.config.ENABLE_SNAP_TO_EDGE) {
// 立即吸附,带动画,支持 fling 方向
self.snapToEdgeDocked(true, forceSide);
} else {
self.savePos(self.state.ballLp.x, self.state.ballLp.y);
}
}
if (velocityTracker) {
velocityTracker.recycle();
velocityTracker = null;
}
self.state.dragging = false;
self.touchActivity();
self.resetLongPressState();
return true;
}
return false;
}
});
};
FloatBallAppWM.prototype.createBallViews = function() {
var di = this.getDockInfo();
var root = new android.widget.FrameLayout(context);
root.setClipToPadding(true);
root.setClipChildren(true);
try { root.setElevation(this.dp(6)); } catch(e){}
var content = new android.widget.FrameLayout(context);
var lp = new android.widget.FrameLayout.LayoutParams(di.ballSize, di.ballSize);
content.setLayoutParams(lp);
// # 悬浮球内部内容(图标/文字)
try {
var iconResId = Number(this.config.BALL_ICON_RES_ID || 0);
var iconType = this.config.BALL_ICON_TYPE ? String(this.config.BALL_ICON_TYPE) : "android";
var iconFilePath = (this.config.BALL_ICON_FILE_PATH == null) ? "" : String(this.config.BALL_ICON_FILE_PATH);
var textStr = (this.config.BALL_TEXT == null) ? "" : String(this.config.BALL_TEXT);
// # 是否显示图标file 只看路径app 优先看包名,其次可回退 iconResIdandroid 走 iconResIdshortx 总是显示
var showIcon = false;
if (iconType === "file") {
showIcon = (iconFilePath.length > 0);
} else if (iconType === "app") {
var _pkg = this.config.BALL_ICON_PKG ? String(this.config.BALL_ICON_PKG) : "";
showIcon = (_pkg.length > 0) || (iconResId > 0);
} else if (iconType === "shortx") {
showIcon = true; // # ShortX 内置图标总是尝试显示
} else {
showIcon = (iconResId > 0);
}
if (!showIcon && textStr.length === 0) showIcon = true;
// # 用户要求:悬浮球本球不再显示文字,只保留图标内容
var showText = false;
if (showIcon || showText) {
var box = new android.widget.LinearLayout(context);
box.setOrientation(android.widget.LinearLayout.VERTICAL);
box.setGravity(android.view.Gravity.CENTER);
var boxLp = new android.widget.FrameLayout.LayoutParams(
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
);
boxLp.gravity = android.view.Gravity.CENTER;
box.setLayoutParams(boxLp);
var tintHex = (this.config.BALL_ICON_TINT_HEX == null) ? "" : String(this.config.BALL_ICON_TINT_HEX);
var textColorHex = (this.config.BALL_TEXT_COLOR_HEX == null) ? "" : String(this.config.BALL_TEXT_COLOR_HEX);
var defaultColor = android.graphics.Color.WHITE;
if (showIcon) {
var iv = new android.widget.ImageView(context);
// # 悬浮球内部图标:支持系统 drawable / App 图标 / PNG 文件
var usedDrawable = null;
var usedKind = "none";
// # 1) file优先加载 PNG
if (iconType === "file") {
try {
var pngMode0 = Number(this.config.BALL_PNG_MODE || 0);
var iconSizePx0 = (pngMode0 === 0 || pngMode0 === 1) ? di.ballSize : this.dp(Number(this.config.BALL_ICON_SIZE_DP || 22));
var maxBytes = Number(this.config.BALL_ICON_FILE_MAX_BYTES || 0);
var maxPx = Number(this.config.BALL_ICON_FILE_MAX_PX || 0);
usedDrawable = this.loadBallIconDrawableFromFile(iconFilePath, iconSizePx0, maxBytes, maxPx);
if (usedDrawable != null) {
usedKind = "file";
} else {
safeLog(this.L, 'w', "Ball icon file load failed: " + iconFilePath);
}
} catch (eF) {}
}
// # 2) app加载应用图标 (file 失败也会尝试 app)
if (usedDrawable == null && (iconType === "app" || iconType === "file")) {
try {
var pkgName = this.config.BALL_ICON_PKG ? String(this.config.BALL_ICON_PKG) : "";
if (pkgName) {
var pm = context.getPackageManager();
var appIcon = pm.getApplicationIcon(pkgName);
if (appIcon != null) {
usedDrawable = appIcon;
usedKind = "app";
}
}
} catch (eA) {}
}
// # 2.5) shortx专门加载 ShortX 内置图标(也作为 file 模式的兜底)
if (usedDrawable == null && (iconType === "shortx" || iconType === "file")) {
try {
// # 优先使用配置的图标名称
var iconResName = this.config.BALL_ICON_RES_NAME ? String(this.config.BALL_ICON_RES_NAME) : "";
if (iconResName) {
usedDrawable = this.resolveShortXDrawable(iconResName, this.config.BALL_ICON_TINT_HEX ? String(this.config.BALL_ICON_TINT_HEX) : "");
}
// # 失败则使用默认图标
if (usedDrawable == null) usedDrawable = this.resolveShortXDrawable("ic_shortx", this.config.BALL_ICON_TINT_HEX ? String(this.config.BALL_ICON_TINT_HEX) : "");
if (usedDrawable == null) usedDrawable = this.resolveShortXDrawable("ic_launcher", this.config.BALL_ICON_TINT_HEX ? String(this.config.BALL_ICON_TINT_HEX) : "");
if (usedDrawable == null) usedDrawable = this.resolveShortXDrawable("ic_menu_preferences", this.config.BALL_ICON_TINT_HEX ? String(this.config.BALL_ICON_TINT_HEX) : "");
if (usedDrawable != null) {
usedKind = iconType === "file" ? "file(shortx兜底)" : "shortx";
if (iconType === "file") {
safeLog(this.L, 'i', "File icon failed, fallback to shortx icon");
}
}
} catch (eShortx2) {}
}
// # 3) android或所有兜底走资源 id优先尝试 ShortX 内置图标)
if (usedDrawable == null) {
// # 优先从 ShortX 包加载内置图标
try {
usedDrawable = this.resolveShortXDrawable("ic_shortx", this.config.BALL_ICON_TINT_HEX ? String(this.config.BALL_ICON_TINT_HEX) : "");
if (usedDrawable == null) {
usedDrawable = this.resolveShortXDrawable("ic_launcher", this.config.BALL_ICON_TINT_HEX ? String(this.config.BALL_ICON_TINT_HEX) : "");
}
if (usedDrawable != null) {
usedKind = "shortx";
}
} catch (eShortx) {}
}
if (usedDrawable != null) {
iv.setImageDrawable(usedDrawable);
} else if (iconResId > 0) {
try { iv.setImageResource(iconResId); usedKind = "android"; } catch (eR) {}
} else {
// # 没有任何可用图标,直接不加到布局
usedKind = "none";
}
this.state.usedIconKind = usedKind;
if (usedKind !== "none") {
var _pngMode1 = Number(this.config.BALL_PNG_MODE || 0);
if (iconType === "file" && (_pngMode1 === 0 || _pngMode1 === 1)) {
iv.setScaleType(android.widget.ImageView.ScaleType.FIT_XY);
var iconSize = di.ballSize;
var ivLp = new android.widget.LinearLayout.LayoutParams(iconSize, iconSize);
} else {
iv.setScaleType(android.widget.ImageView.ScaleType.CENTER_INSIDE);
var iconSize = this.dp(Number(this.config.BALL_ICON_SIZE_DP || 22));
var ivLp = new android.widget.LinearLayout.LayoutParams(iconSize, iconSize);
}
ivLp.gravity = android.view.Gravity.CENTER;
iv.setLayoutParams(ivLp);
// # 颜色滤镜策略:
// # - android drawable默认白色保持旧行为
// # - app / file默认不染色避免把彩色图标洗成白
// # - 只有显式配置 BALL_ICON_TINT_HEX 才强制染色
if (tintHex.length > 0) {
try {
var tintColor2 = android.graphics.Color.parseColor(tintHex);
iv.setColorFilter(tintColor2, android.graphics.PorterDuff.Mode.SRC_IN);
} catch (eTint2) {}
} else if (usedKind === "android") {
try { iv.setColorFilter(android.graphics.Color.WHITE, android.graphics.PorterDuff.Mode.SRC_IN); } catch (eCF) {}
} else {
try { iv.clearColorFilter(); } catch (eCL) {}
}
box.addView(iv);
}
}
if (showText) {
var tv = new android.widget.TextView(context);
tv.setText(textStr);
tv.setGravity(android.view.Gravity.CENTER);
try { tv.setIncludeFontPadding(false); } catch (eFP) {}
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, Number(this.config.BALL_TEXT_SIZE_SP || 10));
var txtColor = defaultColor;
if (textColorHex.length > 0) {
try { txtColor = android.graphics.Color.parseColor(textColorHex); } catch (eTC) {}
} else if (tintHex.length > 0) {
// # 如果没单独指定文字颜色,则跟随图标颜色
try { txtColor = android.graphics.Color.parseColor(tintHex); } catch (eTC2) {}
}
tv.setTextColor(txtColor);
// # 设置一点点阴影,提高可读性
try { tv.setShadowLayer(1.2, 0, 1.0, 0x66000000); } catch (eSH) {}
// # 图标与文字间距
if (showIcon) {
var gap = this.dp(Number(this.config.BALL_ICON_TEXT_GAP_DP || 1));
var padTop = Math.max(0, gap);
tv.setPadding(0, padTop, 0, 0);
}
box.addView(tv);
}
content.addView(box);
}
} catch (eBallInner) {}
this.updateBallContentBackground(content);
// # 阴影控制file/app 模式下不加阴影(避免透明背景带黑框)
var _uk = this.state.usedIconKind;
if (_uk !== "file" && _uk !== "app") {
try { root.setElevation(this.dp(6)); } catch(e){}
}
content.setClickable(true);
content.setOnTouchListener(this.setupTouchListener());
root.addView(content);
this.state.ballRoot = root;
this.state.ballContent = content;
};
FloatBallAppWM.prototype.createBallLayoutParams = function() {
var di = this.getDockInfo();
var lp = new android.view.WindowManager.LayoutParams(
di.ballSize,
di.ballSize,
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;
var initX = this.config.BALL_INIT_X;
var initY = this.dp(this.config.BALL_INIT_Y_DP);
if (this.state.loadedPos) { initX = this.state.loadedPos.x; initY = this.state.loadedPos.y; }
lp.x = this.clamp(initX, 0, this.state.screen.w - di.ballSize);
lp.y = this.clamp(initY, 0, this.state.screen.h - di.ballSize);
return lp;
};