记录一下自己做的PageView无限循环功能CC3.8.7

众所周知,只要3个页面(Page0、Page1、Page2)就可以达到无限了。实现方式就是基本保持用户能看到的当前页在最中间。分享一下最终代码,也许有更好的实现还请赐教。期间踩了无数的坑:

  • 闪屏问题 :使用 Layout 自动排版时,改变节点顺序( setSiblingIndexinsertChild )会导致 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);
        }
    }
}
1赞

(帖子被作者删除,如无标记将在 24 小时后自动删除)