diff --git a/assets/prefabs/PageWriteLevels.prefab b/assets/prefabs/PageWriteLevels.prefab index e65bb84..44b1724 100644 --- a/assets/prefabs/PageWriteLevels.prefab +++ b/assets/prefabs/PageWriteLevels.prefab @@ -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 } -] \ No newline at end of file +] diff --git a/assets/prefabs/PageWriteLevels.ts b/assets/prefabs/PageWriteLevels.ts index 0c5ede9..26b4c98 100644 --- a/assets/prefabs/PageWriteLevels.ts +++ b/assets/prefabs/PageWriteLevels.ts @@ -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,高 1000,anchor (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 = 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 { - 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(); } }