Files
mp-xieyingeng/assets/prefabs/PageWriteLevels.ts
2026-04-06 11:52:26 +08:00

374 lines
13 KiB
TypeScript
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.
import { _decorator, Node, Button, Sprite, Label, Toggle, ScrollView, instantiate, UITransform, Vec2, EventTouch } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
const { ccclass, property } = _decorator;
/**
* 布局配置
* 基于实际 prefab 尺寸计算:
* ScrollView / view 宽 900高 1300
* ListTpl (item) 宽 300高 400
*
* 水平居中2 × 300 + 1 × 40 = 640, padding_left = (900 - 640) / 2 = 130
* 垂直居中3 × 400 + 2 × 25 = 1250, padding_top = (1300 - 1250) / 2 = 25
*/
const LAYOUT_CONFIG = {
COLS: 2,
ROWS: 3,
ITEM_WIDTH: 300,
ITEM_HEIGHT: 400,
SPACING_X: 40,
SPACING_Y: 25,
PADDING_LEFT: 130,
PADDING_TOP: 25,
VIEW_WIDTH: 900,
VIEW_HEIGHT: 1300,
};
const PAGE_CONFIG = {
/** 滑动距离超过页宽的这个比例就翻页 */
SWIPE_THRESHOLD: 0.2,
/** 快速滑动速度阈值px/s超过此值即使距离不够也翻页 */
VELOCITY_THRESHOLD: 500,
/** 翻页动画时长(秒) */
SNAP_DURATION: 0.3,
};
@ccclass('PageWriteLevels')
export class PageWriteLevels extends BaseView {
@property({ type: Node, tooltip: '返回按钮' })
backBtn: Node | null = null;
@property({ type: Node, tooltip: 'ScrollView可视区域节点' })
scrollView: Node | null = null;
@property({ type: Node, tooltip: '列表content节点' })
listContent: Node | null = null;
@property({ type: Node, tooltip: '列表项模板' })
listTemplate: Node | null = null;
private _selectedIndices: Set<number> = new Set();
private _currentPage: number = 0;
private _totalPages: number = 0;
private _levelCount: number = 0;
private readonly ITEMS_PER_PAGE = LAYOUT_CONFIG.ROWS * LAYOUT_CONFIG.COLS;
private _itemNodes: Node[] = [];
private _scrollViewComp: ScrollView | null = null;
private _touchStartOffsetX: number = 0;
private _touchStartTime: number = 0;
/** 缓存 view 节点的 UITransform避免每次 _updateContentSize 重复查找 */
private _viewTransform: UITransform | null = null;
onViewLoad(): void {
console.log('[PageWriteLevels] onViewLoad');
this._initButtons();
this._initScrollView();
}
private _initButtons(): void {
if (this.backBtn) {
this.backBtn.on(Button.EventType.CLICK, this._onBackClick, this);
}
}
private _initScrollView(): void {
if (this.scrollView) {
this._scrollViewComp = this.scrollView.getComponent(ScrollView);
if (this._scrollViewComp) {
this._scrollViewComp.inertia = false;
}
this.scrollView.on(Node.EventType.TOUCH_START, this._onTouchStart, this);
this.scrollView.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
this.scrollView.on(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
// 缓存 view 的 UITransform
const viewNode = this.scrollView.getChildByName('view');
if (viewNode) {
this._viewTransform = viewNode.getComponent(UITransform);
}
}
// content anchor 设为 (0, 1) 左上角,方便位置计算且符合 ScrollView 标准用法
if (this.listContent) {
const contentTransform = this.listContent.getComponent(UITransform);
if (contentTransform) {
contentTransform.setAnchorPoint(0, 1);
}
}
}
private _onTouchStart(event: EventTouch): void {
if (!this._scrollViewComp) return;
this._touchStartOffsetX = this._scrollViewComp.getScrollOffset().x;
this._touchStartTime = Date.now();
}
/**
* 触摸结束:根据滑动距离和速度决定翻页方向
*/
private _onTouchEnd(event: EventTouch): void {
if (!this._scrollViewComp || this._totalPages <= 1) return;
const currentOffsetX = this._scrollViewComp.getScrollOffset().x;
const deltaX = currentOffsetX - this._touchStartOffsetX;
const elapsedTime = (Date.now() - this._touchStartTime) / 1000;
const velocity = elapsedTime > 0 ? Math.abs(deltaX) / elapsedTime : 0;
let targetPage = this._currentPage;
const swipeDistance = Math.abs(deltaX);
const threshold = LAYOUT_CONFIG.VIEW_WIDTH * PAGE_CONFIG.SWIPE_THRESHOLD;
if (swipeDistance > threshold || velocity > PAGE_CONFIG.VELOCITY_THRESHOLD) {
if (deltaX < 0) {
targetPage = Math.min(this._currentPage + 1, this._totalPages - 1);
} else if (deltaX > 0) {
targetPage = Math.max(this._currentPage - 1, 0);
}
}
this._scrollToPage(targetPage);
}
onViewShow(): void {
console.log('[PageWriteLevels] onViewShow');
this._initLevelList();
}
private _initLevelList(): void {
this._clearList();
this._levelCount = LevelDataManager.instance.getLevelCount();
console.log('[PageWriteLevels] 关卡总数:', this._levelCount);
if (this._levelCount === 0) {
console.warn('[PageWriteLevels] 没有关卡数据');
return;
}
this._totalPages = Math.ceil(this._levelCount / this.ITEMS_PER_PAGE);
console.log('[PageWriteLevels] 总页数:', this._totalPages);
this._updateContentSize();
this._createItems();
if (this._scrollViewComp && this._totalPages > 0) {
this._scrollToPage(0, false);
}
}
private _clearList(): void {
for (const node of this._itemNodes) {
if (node && node.isValid) {
node.destroy();
}
}
this._itemNodes = [];
this._selectedIndices.clear();
}
private _updateContentSize(): void {
if (!this.listContent) return;
const contentTransform = this.listContent.getComponent(UITransform);
if (!contentTransform) return;
const totalWidth = this._totalPages * LAYOUT_CONFIG.VIEW_WIDTH;
contentTransform.setContentSize(totalWidth, LAYOUT_CONFIG.VIEW_HEIGHT);
// anchor=(0,1),将 content 左上角对齐到 view 左上角
if (this._viewTransform) {
const viewWidth = this._viewTransform.contentSize.width;
const viewHeight = this._viewTransform.contentSize.height;
this.listContent.setPosition(-viewWidth / 2, viewHeight / 2, 0);
}
}
private _createItems(): void {
if (!this.listTemplate || !this.listContent) return;
for (let i = 0; i < this._levelCount; i++) {
const itemNode = this._createItem(i);
if (itemNode) {
this.listContent.addChild(itemNode);
this._itemNodes.push(itemNode);
this._loadAndRefreshCover(itemNode, i);
}
}
}
private _createItem(index: number): Node | null {
if (!this.listTemplate) return null;
const item = instantiate(this.listTemplate);
item.active = true;
item.name = `item_${index}`;
const pos = this._calculateItemPosition(index);
item.setPosition(pos.x, pos.y, 0);
// 设置默认名称和选中状态(封面由 _loadAndRefreshCover 异步填充)
this._initItemState(item, index);
this._setupItemClick(item, index);
return item;
}
/**
* content anchor=(0,1)原点在左上角x 正向右y 负向下
*/
private _calculateItemPosition(index: number): { x: number, y: number } {
const pageIndex = Math.floor(index / this.ITEMS_PER_PAGE);
const localIndex = index % this.ITEMS_PER_PAGE;
const col = localIndex % LAYOUT_CONFIG.COLS;
const row = Math.floor(localIndex / LAYOUT_CONFIG.COLS);
const x = pageIndex * LAYOUT_CONFIG.VIEW_WIDTH
+ LAYOUT_CONFIG.PADDING_LEFT
+ col * (LAYOUT_CONFIG.ITEM_WIDTH + LAYOUT_CONFIG.SPACING_X)
+ LAYOUT_CONFIG.ITEM_WIDTH / 2;
const y = -(LAYOUT_CONFIG.PADDING_TOP
+ row * (LAYOUT_CONFIG.ITEM_HEIGHT + LAYOUT_CONFIG.SPACING_Y)
+ LAYOUT_CONFIG.ITEM_HEIGHT / 2);
return { x, y };
}
/**
* 初始化 item 的默认名称和选中状态(不设置封面,由异步加载负责)
*/
private _initItemState(item: Node, index: number): void {
const levelName = item.getChildByName('LevelName');
if (levelName) {
const label = levelName.getComponent(Label);
if (label) {
label.string = `${index + 1}`;
}
}
const isSelected = item.getChildByName('IsSelected');
if (isSelected) {
const toggle = isSelected.getComponent(Toggle);
if (toggle) {
toggle.isChecked = this._selectedIndices.has(index);
}
const checkmark = isSelected.getChildByName('Checkmark');
if (checkmark) {
checkmark.active = this._selectedIndices.has(index);
}
}
}
/**
* 异步加载关卡资源并刷新封面图和名称。
* LevelDataManager 采用懒加载,初始化时只加载了第一关图片,
* 其余关卡通过 ensureLevelReady 按需加载。
*/
private async _loadAndRefreshCover(item: Node, index: number): Promise<void> {
const config = await LevelDataManager.instance.ensureLevelReady(index);
if (!config || !item.isValid) return;
const levelCover = item.getChildByName('LevelCover');
if (levelCover) {
const sprite = levelCover.getComponent(Sprite);
if (sprite && config.spriteFrame) {
sprite.spriteFrame = config.spriteFrame;
}
}
const levelName = item.getChildByName('LevelName');
if (levelName) {
const label = levelName.getComponent(Label);
if (label) {
label.string = config.name;
}
}
}
private _setupItemClick(item: Node, index: number): void {
const isSelected = item.getChildByName('IsSelected');
if (isSelected) {
const toggle = isSelected.getComponent(Toggle);
if (toggle) {
toggle.node.on('toggle', () => {
this._onItemToggle(index, toggle.isChecked);
}, this);
}
}
const button = item.getComponent(Button);
if (button) {
button.node.on(Button.EventType.CLICK, () => {
const isCurrentlySelected = this._selectedIndices.has(index);
this._onItemToggle(index, !isCurrentlySelected);
}, this);
}
}
/**
* 滚动/吸附到指定页
*/
private _scrollToPage(pageIndex: number, animated: boolean = true): void {
if (!this._scrollViewComp) return;
this._currentPage = pageIndex;
const offsetX = pageIndex * LAYOUT_CONFIG.VIEW_WIDTH;
this._scrollViewComp.scrollToOffset(
new Vec2(offsetX, 0),
animated ? PAGE_CONFIG.SNAP_DURATION : 0
);
}
private _onItemToggle(index: number, selected: boolean): void {
if (selected) {
this._selectedIndices.add(index);
} else {
this._selectedIndices.delete(index);
}
console.log('[PageWriteLevels] item切换选中:', index, selected);
const item = this._itemNodes[index];
if (item) {
const isSelected = item.getChildByName('IsSelected');
if (isSelected) {
const toggle = isSelected.getComponent(Toggle);
if (toggle) {
toggle.isChecked = selected;
}
const checkmark = isSelected.getChildByName('Checkmark');
if (checkmark) {
checkmark.active = selected;
}
}
}
}
private _onBackClick(): void {
console.log('[PageWriteLevels] 返回按钮点击');
ViewManager.instance.back();
}
onViewHide(): void {
console.log('[PageWriteLevels] onViewHide');
}
onViewDestroy(): void {
console.log('[PageWriteLevels] onViewDestroy');
if (this.backBtn) {
this.backBtn.off(Button.EventType.CLICK, this._onBackClick, this);
}
if (this.scrollView) {
this.scrollView.off(Node.EventType.TOUCH_START, this._onTouchStart, this);
this.scrollView.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
this.scrollView.off(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
}
this._clearList();
}
}