众所周知,只要3个页面(Page0、Page1、Page2)就可以达到无限了。实现方式就是基本保持用户能看到的当前页在最中间。分享一下最终代码,也许有更好的实现还请赐教。期间踩了无数的坑:
-
闪屏问题 :使用
Layout自动排版时,改变节点顺序(setSiblingIndex或insertChild)会导致 Layout 在下一帧才刷新,而代码里又手动设置了content坐标,导致画面有一瞬间的“跳变”。 -
坐标漂移(Coordinate Drift) :如果只是单纯地把节点往后挪,坐标会越来越大(3000 -> 6000 -> 9000…),最终可能撞到
content的尺寸墙,或者导致浮点数精度问题。 -
吸附错乱 :修改坐标后,如果没有正确更新 PageView 内部的
_scrollCenterOffsetX,会导致自动吸附位置不对。
下面是调用方的代码。
· 为了保持当前页在中间,在滚动到下一页时,需要把第一页放到最后一页(上一页需要把最后一页放到第一页)。
· 其中this.elites是需要展示的数据,你可以换成自己的。
· 代码里面放弃了用cocos自己的PageEvent,而是等待滚动彻底完成才触发页面数据设置,否则也会有问题。
@property(CustomPageView)
private pageView!: CustomPageView;
@property(Button)
private btnToLeft!: Button;
@property(Button)
private btnToRight!: Button;
@property([EliteSpine])
private pages: EliteSpine[] = [];
private dataIndex: number = -1;
protected start(): void {
this.setupPages(this.elites.findIndex(emp => emp.code === this.elite.code));
for (const page of this.pages){
if (!page.node.active){
this.pageView.removePage(page.node);
}
}
this.pageView.scrollEndedListeners.push(this.onPageTurning.bind(this));
this.scheduleOnce(() => {
this.pageView.setCurrentPageIndex(this.getPageIndex());
});
}
private setupPages(index: number) {
const total = this.elites.length;
if (index < 0 || index >= total) {
return;
}
this.elite = this.elites[index];
this.btnToLeft.node.active = index > 0;
this.btnToRight.node.active = index < total - 1;
if (total <= 3) {
this.pages[0].setEliteInfoOrHide(this.elites[0]);
this.pages[1].setEliteInfoOrHide(this.elites[1]);
this.pages[2].setEliteInfoOrHide(this.elites[2]);
}
else {
if (index === 0) {
this.pages[0].setEliteInfo(this.elites[0]);
this.pages[1].setEliteInfo(this.elites[1]);
this.pages[2].setEliteInfo(this.elites[2]);
}
else if (index === total - 1) {
this.pages[0].setEliteInfo(this.elites[total - 3]);
this.pages[1].setEliteInfo(this.elites[total - 2]);
this.pages[2].setEliteInfo(this.elites[total - 1]);
}
else {
if (this.dataIndex !== -1) {
if (index > this.dataIndex){
if (index > 1){
// 下一页,把第0页移动到最后一页
this.pages.push(this.pages.shift()!);
this.pageView.movePage(0, 2);
}
} else {
if (index < total - 2){
// 上一页,把最后一页移动到第0页
this.pages.unshift(this.pages.pop()!);
this.pageView.movePage(2, 0);
}
}
}
this.pages[0].setEliteInfoOrHide(this.elites[index - 1]);
this.pages[1].setEliteInfoOrHide(this.elites[index]);
this.pages[2].setEliteInfoOrHide(this.elites[index + 1]);
}
}
this.dataIndex = index;
}
public onPageTurning() {
const currentPageIndex = this.pageView.getCurrentPageIndex();
const direction = currentPageIndex - this.getPageIndex();
if (direction === 0) {
return;
}
const newIndex = this.dataIndex + direction;
this.setupPages(newIndex);
}
public async turnPrevPage() {
await this.turnPage(-1);
}
public async turnNextPage() {
await this.turnPage(1);
}
private async turnPage(direction: -1 | 1) {
if (this.isAnimating) {
return;
}
this.isAnimating = true;
this.pageView.scrollToPage(this.getPageIndex() + direction, 0.2);
this.scheduleOnce(() => this.isAnimating = false, 0.2);
}
private getPageIndex(dataIndex: number = this.dataIndex){
if (this.elites.length <= 3){
return dataIndex;
}
if (dataIndex === 0){
return 0;
}
if (dataIndex === this.elites.length - 1){
return 2;
}
// 中间情况,永远停在 Page 1
return 1;
}
然后自定义了一个CustomPageView,继承自cocos的PageView。
· setCurrentPageIndex里面的this.scrollToPage(index, 0);是纯个人偏好,我觉得这种方法看着名字就应该就是直接设置的,而不是还需要像原来那样滚动个1秒,感觉挺蠢的。
· 核心要点是movePage。不使用cocos自己的那些方法(什么addPage、removePage),而是自己实现一个“原子操作”,完全由脚本接管坐标,否则会因为PageView.content上面的Layout组件导致页面错位。
import { _decorator, Node, PageView, Layout, UITransform, Vec3 } from "cc";
const { ccclass } = _decorator;
@ccclass('CustomPageView')
export class CustomPageView extends PageView {
public scrollEndedListeners: Function[] = [];
public setCurrentPageIndex(index: number): void {
this.scrollToPage(index, 0);
// 避免首次滚动不触发PageEvent事件,不过我不用cocos的所以问题不大其实
this._lastPageIdx = index;
}
public scrollToPage(idx: number, timeInSecond: number = 0.2): void {
super.scrollToPage(idx, timeInSecond);
if (timeInSecond === 0) {
this.dispatchScrollEndedEvent();
} else {
this.scheduleOnce(() => {
this.dispatchScrollEndedEvent();
}, timeInSecond);
}
}
private dispatchScrollEndedEvent() {
for (const func of this.scrollEndedListeners) {
func();
}
}
// 1. 移动节点 :调整 `_pages` 数组和 `children` 顺序。
// 2. 计算偏差 (Diff) :算出当前视口焦点页面的“实际坐标”和“理论标准坐标”之间的差值。
// 3. 全员归位:强行把所有 Page 的坐标重置为标准的 `0, Width, 2*Width` ...(解决坐标无限增长)。
// 4. 父节点补偿 :将 PageView 的 `content` 节点向反方向移动等量的 `Diff` 。
public movePage(from: number, to: number): void {
if (from < 0 || from >= this._pages.length || to < 0 || to >= this._pages.length || from === to || !this.content) {
return;
}
const page = this._pages[from];
this._pages.splice(from, 1);
this._pages.splice(to, 0, page);
this.content.removeChild(page);
this.content.insertChild(page, to);
if (from < this._curPageIdx && to >= this._curPageIdx) {
this._curPageIdx--;
this._lastPageIdx--;
} else if (from > this._curPageIdx && to <= this._curPageIdx) {
this._curPageIdx++;
this._lastPageIdx++;
}
this.updatePositions();
}
protected updatePositions() {
// 1. 安全检查
const curPageNode = this._pages[this._curPageIdx];
if (!curPageNode || !this.view) return;
// 获取 View 的尺寸 (作为 Unified 模式的标准尺寸)
const viewTransform = this.view.getComponent(UITransform)!;
const viewWidth = viewTransform.width;
const viewHeight = viewTransform.height;
// 2. 计算当前页面的“理论标准位置” (Standard Position)
// 我们需要知道:如果不发生漂移,当前第 curIdx 页应该在什么坐标?
let standardX = 0;
let standardY = 0;
// 用于 Free 模式的累加器
let accumulatedSize = 0;
if (this.sizeMode === PageView.SizeMode.Unified) {
// === Unified 模式:所有页面大小一致 ===
if (this.direction === PageView.Direction.HORIZONTAL) {
// 假设锚点 0.5,位置 = index * width + width/2
standardX = this._curPageIdx * viewWidth + viewWidth / 2;
} else {
standardY = -(this._curPageIdx * viewHeight + viewHeight / 2);
}
} else {
// === Free 模式:累加前面所有页面的大小 ===
for (let i = 0; i < this._curPageIdx; ++i) {
const node = this._pages[i];
const trans = node.getComponent(UITransform)!;
if (this.direction === PageView.Direction.HORIZONTAL) {
accumulatedSize += trans.width;
} else {
accumulatedSize += trans.height;
}
}
// 加上当前页的一半 (锚点修正)
const curTrans = curPageNode.getComponent(UITransform)!;
if (this.direction === PageView.Direction.HORIZONTAL) {
standardX = accumulatedSize + curTrans.width / 2;
} else {
standardY = -(accumulatedSize + curTrans.height / 2);
}
}
// 3. 计算“漂移量” (Diff)
const currentActualX = curPageNode.position.x;
const currentActualY = curPageNode.position.y;
const diffX = standardX - currentActualX;
const diffY = standardY - currentActualY;
// 4. 全员归位 (Reset All Pages)
// 重置累加器,准备从头遍历
accumulatedSize = 0;
for (let i = 0; i < this._pages.length; ++i) {
const page = this._pages[i];
const pageTrans = page.getComponent(UITransform)!;
let newX = 0;
let newY = 0;
if (this.direction === PageView.Direction.HORIZONTAL) {
// --- 水平方向计算 ---
let itemWidth = 0;
if (this.sizeMode === PageView.SizeMode.Unified) {
itemWidth = viewWidth;
newX = i * itemWidth + itemWidth / 2;
} else {
itemWidth = pageTrans.width;
newX = accumulatedSize + itemWidth / 2;
accumulatedSize += itemWidth; // 累加供下一次使用
}
// 设置归位坐标
page.setPosition(newX, page.position.y, page.position.z);
// 更新导航数据
this._scrollCenterOffsetX[i] = Math.abs(newX);
} else {
// --- 垂直方向计算 ---
let itemHeight = 0;
if (this.sizeMode === PageView.SizeMode.Unified) {
itemHeight = viewHeight;
newY = -(i * itemHeight + itemHeight / 2);
} else {
itemHeight = pageTrans.height;
newY = -(accumulatedSize + itemHeight / 2);
accumulatedSize += itemHeight;
}
page.setPosition(page.position.x, newY, page.position.z);
this._scrollCenterOffsetY[i] = Math.abs(newY);
}
}
// 5. 补偿 Content 位置 (抵消视觉跳变)
const contentPos = this.content!.position;
if (this.direction === PageView.Direction.HORIZONTAL) {
this.content!.setPosition(contentPos.x - diffX, contentPos.y, contentPos.z);
} else {
this.content!.setPosition(contentPos.x, contentPos.y - diffY, contentPos.z);
}
}
}