perf: 优化布局

This commit is contained in:
richarjiang
2026-05-08 19:33:34 +08:00
parent 8b32f4d835
commit 2a620d75d5
2 changed files with 125 additions and 136 deletions

View File

@@ -3470,8 +3470,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -10,
"y": 510.277,
"x": -450,
"y": 0,
"z": 0
},
"_lrot": {
@@ -3516,7 +3516,7 @@
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"x": 0,
"y": 1
},
"_id": ""
@@ -3736,8 +3736,8 @@
"brake": 0.75,
"elastic": true,
"inertia": true,
"horizontal": true,
"vertical": false,
"horizontal": false,
"vertical": true,
"cancelInnerEvents": true,
"scrollEvents": [],
"_content": {
@@ -3813,9 +3813,9 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.75,
"y": 0.75,
"z": 0.693
"x": 0.68,
"y": 0.68,
"z": 0.68
},
"_mobility": 0,
"_layer": 1073741824,
@@ -4600,6 +4600,11 @@
"dataBtn": {
"__id__": 34
},
"roundedSpriteEffect": {
"__uuid__": "f0080a34-1786-4547-8d81-d89cc517b63e",
"__expectedType__": "cc.EffectAsset"
},
"coverCornerRadius": 0.1,
"_id": ""
},
{
@@ -4682,4 +4687,4 @@
"instance": null,
"targetOverrides": null
}
]
]

View File

@@ -1,4 +1,4 @@
import { _decorator, Node, Button, Sprite, Label, Toggle, ScrollView, EditBox, instantiate, UITransform, Vec2, EventTouch } from 'cc';
import { _decorator, Node, Button, Sprite, Label, Toggle, ScrollView, EditBox, instantiate, UITransform, Vec2, EventTouch, EffectAsset } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { CompletedLevelsManager } from 'db://assets/scripts/utils/CompletedLevelsManager';
@@ -9,25 +9,24 @@ import { WxSDK, getUserProfile } from 'db://assets/scripts/utils/WxSDK';
import { AuthManager } from 'db://assets/scripts/utils/AuthManager';
import { API_ENDPOINTS, API_TIMEOUT } from 'db://assets/scripts/config/ApiConfig';
import { HttpUtil } from 'db://assets/scripts/utils/HttpUtil';
import { ApiEnvelope } from 'db://assets/scripts/types/ApiTypes';
import { ApiEnvelope, CompletedLevel } from 'db://assets/scripts/types/ApiTypes';
import { applyRoundedCorner } from 'db://assets/scripts/utils/roundedMaterial.utils';
const { ccclass, property } = _decorator;
/**
* 布局配置
* view (ScrollView 的可视窗口) 宽 900高 1000anchor (0.5, 1) 顶部中点
* ListTpl (item) 宽 300高 400
* view (ScrollView 的可视窗口) 宽 900高 1000
* 关卡 item 固定两列,纵向滚动
*
* 每页 2 行 × 2 列 = 4 个关卡
* PADDING 不再手写,由 VIEW / ITEM / SPACING 自动派生(见 _getHorizontalPadding / _getVerticalPadding
* 保证 item 网格在 view 内始终居中。改 SPACING 时不用再算 PADDING。
* item 的实际显示尺寸从 ListTpl 的 UITransform * scale 派生,
* 避免代码里再维护一套和 prefab 脱节的宽高。
*/
const LAYOUT_CONFIG = {
COLS: 2,
ROWS: 2,
ITEM_WIDTH: 300,
ITEM_HEIGHT: 400,
SPACING_X: 160,
SPACING_Y: 180,
SPACING_X: 36,
SPACING_Y: 48,
EDGE_PADDING_Y: 32,
CENTER_ROWS: 2,
VIEW_WIDTH: 900,
VIEW_HEIGHT: 1000,
};
@@ -35,15 +34,6 @@ const LAYOUT_CONFIG = {
/** 必须选择的关卡数量 */
const MAX_SELECTION = 6;
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: '返回按钮' })
@@ -73,15 +63,17 @@ export class PageWriteLevels extends BaseView {
@property({ type: Node, tooltip: '挑战数据按钮节点' })
dataBtn: Node | null = null;
@property({ type: EffectAsset, tooltip: '关卡封面圆角材质 EffectAsset' })
roundedSpriteEffect: EffectAsset | null = null;
@property({ tooltip: '关卡封面圆角半径比例相对于短边0-0.5' })
coverCornerRadius: number = 0.1;
private _selectedIndices: Set<number> = new Set();
private _currentPage: number = 0;
private _totalPages: number = 0;
private _levels: CompletedLevel[] = [];
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;
@@ -116,13 +108,11 @@ export class PageWriteLevels extends BaseView {
this._scrollViewComp = this.scrollView.getComponent(ScrollView);
if (this._scrollViewComp) {
this._scrollViewComp.inertia = false;
this._scrollViewComp.horizontal = false;
this._scrollViewComp.vertical = true;
this._scrollViewComp.inertia = true;
}
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) {
@@ -139,38 +129,6 @@ export class PageWriteLevels extends BaseView {
}
}
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');
// 仅首次初始化列表,从预览页返回时保留选中状态
@@ -190,7 +148,8 @@ export class PageWriteLevels extends BaseView {
return;
}
this._levelCount = levels.length;
this._levels = levels;
this._levelCount = this._levels.length;
console.log('[PageWriteLevels] 已通关关卡总数:', this._levelCount);
if (this._levelCount === 0) {
@@ -199,14 +158,11 @@ export class PageWriteLevels extends BaseView {
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);
if (this._scrollViewComp) {
this._scrollViewComp.scrollToTop(0);
}
}
@@ -216,27 +172,53 @@ export class PageWriteLevels extends BaseView {
node.destroy();
}
}
this._levels = [];
this._itemNodes = [];
this._selectedIndices.clear();
}
/**
* 水平 padding让整行 item 在 view 宽度内居中
* padding_left = (VIEW_WIDTH - cols*ITEM_WIDTH - (cols-1)*SPACING_X) / 2
*/
private _getHorizontalPadding(): number {
const { VIEW_WIDTH, COLS, ITEM_WIDTH, SPACING_X } = LAYOUT_CONFIG;
const gridWidth = COLS * ITEM_WIDTH + (COLS - 1) * SPACING_X;
return (VIEW_WIDTH - gridWidth) / 2;
private _getViewSize(): { width: number, height: number } {
return {
width: this._viewTransform?.contentSize.width ?? LAYOUT_CONFIG.VIEW_WIDTH,
height: this._viewTransform?.contentSize.height ?? LAYOUT_CONFIG.VIEW_HEIGHT,
};
}
/**
* 垂直 padding让整列 item 在 view 高度内居中
*/
private _getVerticalPadding(): number {
const { VIEW_HEIGHT, ROWS, ITEM_HEIGHT, SPACING_Y } = LAYOUT_CONFIG;
const gridHeight = ROWS * ITEM_HEIGHT + (ROWS - 1) * SPACING_Y;
return (VIEW_HEIGHT - gridHeight) / 2;
private _getItemDisplaySize(): { width: number, height: number } {
if (!this.listTemplate) {
return { width: 0, height: 0 };
}
const itemTransform = this.listTemplate.getComponent(UITransform);
if (!itemTransform) {
return { width: 0, height: 0 };
}
return {
width: itemTransform.contentSize.width * Math.abs(this.listTemplate.scale.x),
height: itemTransform.contentSize.height * Math.abs(this.listTemplate.scale.y),
};
}
private _getRowCount(): number {
return Math.ceil(this._levelCount / LAYOUT_CONFIG.COLS);
}
private _getHorizontalPadding(itemWidth: number): number {
const viewWidth = this._getViewSize().width;
const gridWidth = LAYOUT_CONFIG.COLS * itemWidth + (LAYOUT_CONFIG.COLS - 1) * LAYOUT_CONFIG.SPACING_X;
return Math.max(0, (viewWidth - gridWidth) / 2);
}
private _getVerticalPadding(rowCount: number, itemHeight: number): number {
const viewHeight = this._getViewSize().height;
const totalGridHeight = rowCount * itemHeight + Math.max(0, rowCount - 1) * LAYOUT_CONFIG.SPACING_Y;
if (rowCount <= LAYOUT_CONFIG.CENTER_ROWS) {
return Math.max(LAYOUT_CONFIG.EDGE_PADDING_Y, (viewHeight - totalGridHeight) / 2);
}
return LAYOUT_CONFIG.EDGE_PADDING_Y;
}
private _updateContentSize(): void {
@@ -245,14 +227,19 @@ export class PageWriteLevels extends BaseView {
const contentTransform = this.listContent.getComponent(UITransform);
if (!contentTransform) return;
const totalWidth = this._totalPages * LAYOUT_CONFIG.VIEW_WIDTH;
contentTransform.setContentSize(totalWidth, LAYOUT_CONFIG.VIEW_HEIGHT);
const { width: viewWidth, height: viewHeight } = this._getViewSize();
const { width: itemWidth, height: itemHeight } = this._getItemDisplaySize();
const rowCount = this._getRowCount();
const paddingY = this._getVerticalPadding(rowCount, itemHeight);
const gridHeight = rowCount > 0
? rowCount * itemHeight + (rowCount - 1) * LAYOUT_CONFIG.SPACING_Y
: 0;
const contentHeight = Math.max(viewHeight, gridHeight + paddingY * 2);
// content anchor=(0,1)view anchor=(0.5,1)。
// view 本地坐标系下view 的左上角 = (-viewWidth/2, 0)。
// content anchor 点(左上角)需要贴到 view 左上角。
contentTransform.setContentSize(viewWidth, contentHeight);
// content anchor=(0,1)需要贴到 view 左上角,供纵向列表按左上原点排布
if (this._viewTransform) {
const viewWidth = this._viewTransform.contentSize.width;
this.listContent.setPosition(-viewWidth / 2, 0, 0);
}
}
@@ -294,26 +281,22 @@ export class PageWriteLevels extends BaseView {
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 { width: itemWidth, height: itemHeight } = this._getItemDisplaySize();
const rowCount = this._getRowCount();
const col = index % LAYOUT_CONFIG.COLS;
const row = Math.floor(index / LAYOUT_CONFIG.COLS);
const paddingLeft = this._getHorizontalPadding();
const paddingTop = this._getVerticalPadding();
const paddingLeft = this._getHorizontalPadding(itemWidth);
const paddingTop = this._getVerticalPadding(rowCount, itemHeight);
const x = pageIndex * LAYOUT_CONFIG.VIEW_WIDTH
+ paddingLeft
+ col * (LAYOUT_CONFIG.ITEM_WIDTH + LAYOUT_CONFIG.SPACING_X)
+ LAYOUT_CONFIG.ITEM_WIDTH / 2;
const x = paddingLeft
+ col * (itemWidth + LAYOUT_CONFIG.SPACING_X)
+ itemWidth / 2;
const y = -(paddingTop
+ row * (LAYOUT_CONFIG.ITEM_HEIGHT + LAYOUT_CONFIG.SPACING_Y)
+ LAYOUT_CONFIG.ITEM_HEIGHT / 2);
+ row * (itemHeight + LAYOUT_CONFIG.SPACING_Y)
+ itemHeight / 2);
return { x, y };
}
@@ -322,7 +305,7 @@ export class PageWriteLevels extends BaseView {
* 初始化 item 的默认名称和选中状态(不设置封面,由异步加载负责)
*/
private _initItemState(item: Node, index: number): void {
const level = CompletedLevelsManager.instance.getByIndex(index);
const level = this._levels[index] ?? null;
const levelName = item.getChildByName('LevelName');
if (levelName) {
const label = levelName.getComponent(Label);
@@ -350,7 +333,7 @@ export class PageWriteLevels extends BaseView {
* 异步加载关卡封面图并填充到 item
*/
private async _loadAndRefreshCover(item: Node, index: number): Promise<void> {
const level = CompletedLevelsManager.instance.getByIndex(index);
const level = this._levels[index] ?? null;
if (!level || !item.isValid) return;
const spriteFrame = await CompletedLevelsManager.instance.loadImage(level.image1Url);
@@ -361,10 +344,30 @@ export class PageWriteLevels extends BaseView {
const sprite = levelCover.getComponent(Sprite);
if (sprite) {
sprite.spriteFrame = spriteFrame;
this._applyCoverRoundedCorner(sprite);
}
}
}
private _applyCoverRoundedCorner(sprite: Sprite): void {
if (!this.roundedSpriteEffect) {
return;
}
const uiTransform = sprite.node.getComponent(UITransform);
if (!uiTransform) {
return;
}
applyRoundedCorner(
sprite,
this.roundedSpriteEffect,
uiTransform.width,
uiTransform.height,
this.coverCornerRadius
);
}
private _setupItemClick(item: Node, index: number): void {
// 用触摸事件代替 Button区分点击和滑动
// 短距离松手 = 点击(切换选中),长距离 = 滑动(交给 ScrollView
@@ -394,20 +397,6 @@ export class PageWriteLevels extends BaseView {
}, 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.size >= MAX_SELECTION) {
@@ -576,7 +565,7 @@ export class PageWriteLevels extends BaseView {
const ids: string[] = [];
const sortedIndices = Array.from(this._selectedIndices).sort((a, b) => a - b);
for (const index of sortedIndices) {
const level = CompletedLevelsManager.instance.getByIndex(index);
const level = this._levels[index] ?? null;
if (level) {
ids.push(level.id);
}
@@ -653,11 +642,6 @@ export class PageWriteLevels extends BaseView {
if (this.dataBtn) {
this.dataBtn.off(Button.EventType.CLICK, this._onDataClick, 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();
}
}