import { _decorator, Component, instantiate, Node, NodeEventType, ScrollView, Tween, UIOpacity, UITransform, Vec3, view, } from "cc"; import { DialogManager } from "../../manager/DialogManager"; import { DialogBubble } from "./DialogBubble"; import { Dialog } from "../../data/DialogData"; const { ccclass, property } = _decorator; @ccclass("ChatContentsLayout") export class ChatContentsLayout extends Component { manager: DialogManager = null; @property(DialogBubble) lBubble: DialogBubble = null; @property(DialogBubble) rBubble: DialogBubble = null; // 当前显示的气泡 private bubbles: DialogBubble[] = []; // 缓存池 private cachedBubbles: DialogBubble[] = []; // 等待回复气泡的引用 //private waitingBubble: DialogBubble = null; // 延时任务ID数组 private delayedTasks: number[] = []; // ScrollView组件引用 @property(ScrollView) private scrollView: ScrollView = null; // 内容透明度组件 private contentOpacity: UIOpacity = null; // 是否正在按下 private isPressing: boolean = false; // 长按计时器 private longPressTimer: number = null; fixMaxWidth: number; protected start(): void { this.lBubble.node.active = false; this.rBubble.node.active = false; this.fixMaxWidth = 534.4; // 获取或添加UIOpacity组件到content节点 this.contentOpacity = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity); // 注册触摸事件 if (this.scrollView && this.scrollView.node) { this.scrollView.node.on( NodeEventType.TOUCH_START, this.onTouchStart, this ); this.scrollView.node.on(NodeEventType.TOUCH_END, this.onTouchEnd, this); this.scrollView.node.on( NodeEventType.TOUCH_CANCEL, this.onTouchEnd, this ); } } protected onEnable(): void { if (!this.manager) this.manager = DialogManager.getInstance(); this.manager.layoutout = this; } /** * 更新玩家输入的对话 * @param dialog 玩家输入的对话数据 */ updatePlayerDialog(dialog: Dialog): void { // 清理所有现有气泡到缓存池 this.clearAllBubbles(); // 清理所有延时任务 this.clearDelayedTasks(); // 创建玩家气泡(插入到首位,Layout会让它显示在最下面) const playerBubble = this.createOrGetBubble(true); playerBubble.node.active = true; playerBubble.updateBubbleContent(dialog.content, true); this.bubbles.push(playerBubble); } /** * 更新AI回复的对话 * @param dialogs AI回复的对话数据数组 */ updateAIDialogs(dialogs: Dialog[]): void { // 清除等待回复气泡 // if (this.waitingBubble) { // this.removeWaitingBubble(); // } // 清理之前的延时任务 this.clearDelayedTasks(); // 逐个添加AI对话,带延时 this.addAIDialogsWithDelay(dialogs, 0); this.node.position = Vec3.ZERO; // 滚动到底部(等待下一帧Layout更新完成) this.scheduleOnce(() => { this.scrollView.scrollToBottom(); }, 0); } /** * 清理所有气泡到缓存池 */ private clearAllBubbles(): void { for (const bubble of this.bubbles) { if (bubble && bubble.node) { bubble.node.active = false; this.cachedBubbles.push(bubble); } } this.bubbles = []; //this.waitingBubble = null; // 清理多余的缓存气泡 this.cleanupExcessCachedBubbles(); } /** * 从缓存池获取或创建新的气泡 * @param isPlayer 是否为玩家气泡 * @returns DialogBubble实例 */ private createOrGetBubble(isPlayer: boolean): DialogBubble { // 尝试从缓存中获取合适的气泡 for (let i = 0; i < this.cachedBubbles.length; i++) { const cached = this.cachedBubbles[i]; if (cached && cached.node) { // 通过原始模板判断气泡类型 const isRightBubble = cached.node.position.x > 0; if ((isPlayer && isRightBubble) || (!isPlayer && !isRightBubble)) { this.cachedBubbles.splice(i, 1); return cached; } } } // 缓存中没有合适的气泡,创建新的 const templateBubble = isPlayer ? this.rBubble : this.lBubble; const newBubbleNode = instantiate(templateBubble.node); const newBubble = newBubbleNode.getComponent(DialogBubble); newBubble.init(this.fixMaxWidth); // 设置为子节点,具体位置由调用者决定 newBubbleNode.setParent(this.node); return newBubble; } /** * 移除等待回复气泡 */ // private removeWaitingBubble(): void { // if (this.waitingBubble && this.waitingBubble.node) { // this.waitingBubble.node.active = false; // this.cachedBubbles.push(this.waitingBubble); // // 从当前气泡列表中移除 // const index = this.bubbles.indexOf(this.waitingBubble); // if (index > -1) { // this.bubbles.splice(index, 1); // } // } // this.waitingBubble = null; // } /** * 带延时地添加AI对话 * @param dialogs AI对话数组 * @param index 当前处理的对话索引 */ private addAIDialogsWithDelay(dialogs: Dialog[], index: number): void { if (index >= dialogs.length) { return; } const dialog = dialogs[index]; const delay = index > 0 ? Math.random() * 1000 + 500 : 0; // 第一条立即显示,后续延时500-1500ms const addCurrentDialog = () => { const aiBubble = this.createOrGetBubble(false); aiBubble.node.active = true; aiBubble.updateBubbleContent(dialog.content, false); // AI消息插入到首位,显示在最上面 aiBubble.node.setSiblingIndex(0); this.scheduleOnce(() => { this.scrollView.scrollToBottom(); }, 0); this.bubbles.push(aiBubble); // 递归处理下一条对话 this.addAIDialogsWithDelay(dialogs, index + 1); }; if (delay > 0) { const taskId = setTimeout(() => { // 从任务列表中移除 const taskIndex = this.delayedTasks.indexOf(taskId); if (taskIndex > -1) { this.delayedTasks.splice(taskIndex, 1); } addCurrentDialog(); }, delay); this.delayedTasks.push(taskId); } else { addCurrentDialog(); } } /** * 清理多余的缓存气泡 */ private cleanupExcessCachedBubbles(): void { const maxCachedBubbles = 20; if (this.cachedBubbles.length > maxCachedBubbles) { const excessCount = this.cachedBubbles.length - maxCachedBubbles; for (let i = 0; i < excessCount; i++) { const bubble = this.cachedBubbles.shift(); if (bubble && bubble.node) { bubble.node.destroy(); } } } } /** * 清理所有延时任务 */ private clearDelayedTasks(): void { for (const taskId of this.delayedTasks) { clearTimeout(taskId); } this.delayedTasks = []; } /** * 保持向后兼容性的方法 * @param dialogs 对话数组 * @deprecated 请使用 updatePlayerDialog 和 updateAIDialogs 替代 */ UpdateDialog(dialogs: Dialog[]): void { // 为了保持兼容性,我们假设这是一个完整的对话更新 // 清理所有现有气泡 this.clearAllBubbles(); this.clearDelayedTasks(); // 逐个添加对话,从最后一个开始倒序插入(保持显示顺序) for (let i = dialogs.length - 1; i >= 0; i--) { const dialog = dialogs[i]; const bubble = this.createOrGetBubble(dialog.isPlayer); bubble.node.active = true; bubble.updateBubbleContent(dialog.content, dialog.isPlayer, undefined); bubble.node.setSiblingIndex(0); this.bubbles.push(bubble); } } /** * 触摸开始事件 */ private onTouchStart(): void { // 清除之前的计时器 if (this.longPressTimer) { clearTimeout(this.longPressTimer); } // 设置长按计时器(300ms后触发) this.longPressTimer = setTimeout(() => { this.isPressing = true; // 使用Tween动画平滑调整透明度 Tween.stopAllByTarget(this.contentOpacity); new Tween(this.contentOpacity) .to(0.2, { opacity: 50 }) // 调整到约20%透明度 .start(); }, 300); } /** * 触摸结束事件 */ private onTouchEnd(): void { // 清除计时器 if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } // 如果已经按下,恢复透明度 if (this.isPressing) { this.isPressing = false; Tween.stopAllByTarget(this.contentOpacity); new Tween(this.contentOpacity) .to(0.2, { opacity: 255 }) // 恢复到100%透明度 .start(); } } /** * 组件销毁时清理资源 */ protected onDestroy(): void { // 清理事件监听 if (this.scrollView && this.scrollView.node) { this.scrollView.node.off( NodeEventType.TOUCH_START, this.onTouchStart, this ); this.scrollView.node.off(NodeEventType.TOUCH_END, this.onTouchEnd, this); this.scrollView.node.off( NodeEventType.TOUCH_CANCEL, this.onTouchEnd, this ); } // 清理计时器 if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } // 清理延时任务 this.clearDelayedTasks(); // 停止所有动画 if (this.contentOpacity) { Tween.stopAllByTarget(this.contentOpacity); } } }