perf: 支持 pk 出题页面分页加载
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,63 +1,373 @@
|
||||
import { _decorator, Node, Button } from 'cc';
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面每次显示时调用
|
||||
*/
|
||||
onViewShow(): void {
|
||||
console.log('[PageWriteLevels] onViewShow');
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面隐藏时调用
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user