如何实现一个可以循环滚动的list

先要实现一个能够循环滚动的list
需要解决的问题

  1. 滚动的方式怎么做?监听 touchmove来 实时改position?
  2. 如何实现循环滚动?

扩展一下引擎的scrollview即可
import { Node, ScrollView, v3, _decorator, UITransform, isValid } from ‘cc’;
import { BaseView } from ‘./…/…/rxjs/cc3/BaseView’;
import { CCBindScrollView } from ‘…/…/rxjs/cc3/BindScrollView’;
import { forkJoin, Observable, of } from ‘rxjs’;
import { optimizeScrollView } from ‘./OptimizeScrollView’;

const { ccclass, property } = _decorator;

/**

  • 使用说明:
  • 继承 LeanDynamicScrollView 写一个 XxxScrollView
  • 重写方法:
  • getDataCount (总大小,也就是总行数,放心不会全部显示出来,只是用于计算位置)
  • getItemNode
  • putItemNode
  • putAllChildren(可以省)
  • refreshRow 用来具体刷新item
  • 做好item的prefab,
  • 做出原本scrollview的node,再其上挂载 XxxScrollView
  • contentNode挂上 scrollView里面的content
  • content里面可以尝试摆item,可以加上layout进行尝试,但最后一定要关闭这个layout或者直接删掉layout
  • 该layout中的padding top/bottom 整好对应填入 XxxScrollView的 HeadHeight TailHeight
  • item的prefab的高度填入XxxScrollView的RowHeight
  • 开启:
  • 因为默认这个是空的关闭状态,啥也不显示,所以需要调用开启才会显示。
  • 我经常在panel的willOpen里面调用 openScrollView() 即可。
  • 此时item应该已经能正常显示了
  • 最后补上刷新item的方法:
  • refreshRow(), 它里面通过 this.currentRows[idx] 就能获取对应idx行的node,想怎么刷就怎么刷了
  • 滑动手感:
  • 下面Feeling Optimized勾选之后,原break应该设置0.9以上,比如0.95

*/

@ccclass
export default class LeanDynamicScrollView extends BaseView {

@property(ScrollView)
scrollView: ScrollView = null;

@property(Node)
contentNode: Node = null;

@property
itemSize: number = 0;

@property
headLength: number = 0;

@property
tailLength: number = 0;

@property
feelingOptimized: boolean = true;

currentItems: Record<number, Node> = {};

isGenerated: boolean = false;

onLoadView() {
    this.feelingOptimized && optimizeScrollView(this.scrollView);
}

onDestroyView() {
}

useObserves() {
    return [
        CCBindScrollView(this.scrollView).subscribe(_ => {
            if (_.state == 'scrolling') this.scrollViewScrolling();
        })
    ]
}

scrollViewScrolling() {
    if (!this.isOpen) return;
    // 检查该回收和新显示的节点
    let _tempCurrentRow: Record<number, Node> = {};
    for (let key in this.currentItems) {
        if (this.idxNeedShow(Number(key))) {
            _tempCurrentRow[key] = this.currentItems[key];
        } else {
            this.recycleRow(Number(key));
        }
    }
    this.currentItems = _tempCurrentRow;
    this.traverseNeedShow(idx => {
        if (this.currentItems[idx] == null) {
            this.genRow(idx);
        }
    });
}

isOpen: boolean = false;

// 开启列表,开始生成项
openScrollView() {
    if (!this.isGenerated) {
        this.generateRows();
        this.isOpen = true;
    }
}

// 关闭列表,删除列表项(注意是异步可观察)
closeScrollViewObservable() {
    this.isOpen = false;
    return this.recycleAllObservable();
}

// 继承后覆盖,给出列表总大小
getDataCount() {
    return 1;
}

// 对象池相关,需要重写。回收container所有的item节点
putAllChildren(container: Node) {
    container.destroyAllChildren();
}

// 对象池相关,需要重写。从池中获得一个Item节点
getItemNode(): Node {
    return new Node();
}

// 对象池相关,需要重写。向池中返还一个Item节点
putItemNode(node: Node) {
    node.destroy();
}

generateRows() {
    this.traverseNeedShow((idx, order) => {
        this.genRow(idx, order);
    });

    if (this.scrollView.vertical && !this.scrollView.horizontal) {
        let contentHeight = this.headLength + this.itemSize * this.getDataCount() + this.tailLength;
        this.contentNode.getComponent(UITransform).height = contentHeight;
    } else if (!this.scrollView.vertical && this.scrollView.horizontal) {
        let contentWidth = this.headLength + this.itemSize * this.getDataCount() + this.tailLength;
        this.contentNode.getComponent(UITransform).width = contentWidth;
    } else {
        this.warn('不支持同时两个轴向的运动');
    }
    this.isGenerated = true;
}

getFirstNeedShowProgress() {
    if (this.scrollView.vertical && !this.scrollView.horizontal) {
        let start_p = this.scrollView.getComponent(UITransform).height / 2 + this.headLength;
        let diff = this.contentNode.position.y - start_p;
        let idx = Math.floor(diff / this.itemSize);
        return idx;
    } else if (!this.scrollView.vertical && this.scrollView.horizontal) {
        let start_p = -this.scrollView.getComponent(UITransform).width / 2 - this.headLength;
        let diff = -this.contentNode.position.x + start_p;
        let idx = Math.floor(diff / this.itemSize);
        return idx;
    }
    return null;
}

getLastNeedShowProgress() {
    if (this.scrollView.vertical && !this.scrollView.horizontal) {
        let end_p = -this.scrollView.getComponent(UITransform).height / 2 + this.headLength;
        let diff = this.contentNode.position.y - end_p;
        let idx = Math.floor(diff / this.itemSize);
        return idx;
    } else if (!this.scrollView.vertical && this.scrollView.horizontal) {
        let end_p = this.scrollView.getComponent(UITransform).width / 2 - this.headLength;
        let diff = -this.contentNode.position.x + end_p;
        let idx = Math.floor(diff / this.itemSize);
        return idx;
    }
}

/** 遍历需要显示的节点的progress */
traverseNeedShow(call: (idx: number, order: number) => void) {
    let firstIdx = this.getFirstNeedShowProgress();
    let lastIdx = this.getLastNeedShowProgress();
    let order = 0;
    for (let idx = firstIdx; idx <= lastIdx; idx++) {
        if (idx >= 0 && idx < this.getDataCount()) {
            call(idx, order);
            order++;
        }
    }
}

traverseRNeedShow(call: (idx: number, order: number) => void) {
    let firstIdx = this.getFirstNeedShowProgress();
    let lastIdx = this.getLastNeedShowProgress();
    let order = 0;
    for (let idx = lastIdx; idx >= firstIdx; idx--) {
        if (idx >= 0 && idx < this.getDataCount()) {
            call(idx, order);
            order++;
        }
    }
}

idxNeedShow(idx: number): boolean {
    let firstIdx = this.getFirstNeedShowProgress();
    let lastIdx = this.getLastNeedShowProgress();
    if (idx >= firstIdx && idx <= lastIdx) {
        if (idx >= 0 && idx <= this.getDataCount()) {
            return true;
        }
    }
    return false;
}

// 项的中点对齐
yPosByProgress(idx: number) {
    return -this.headLength - (idx + .5) * this.itemSize;
}

// 项的中点对齐
xPosByProgress(idx: number) {
    return this.headLength + (idx + .5) * this.itemSize;
}

refreshShowed() {
    this.traverseNeedShow(idx => {
        if (this.currentItems[idx] == null) {
            this.genRow(idx);
        } else {
            this.refreshRow(idx);
        }
    });
}

clear() {
    this.putAllChildren(this.contentNode);
    this.currentItems = {};
    this.isGenerated = false;
}

genRow(idx: number, order: number = 0) {
    let node = this.getItemNode();
    if (this.scrollView.vertical && !this.scrollView.horizontal) {
        node.setPosition(v3(0, this.yPosByProgress(idx), 0));
    } else if (!this.scrollView.vertical && this.scrollView.horizontal) {
        node.setPosition(v3(this.xPosByProgress(idx), 0, 0));
    }
    node.setParent(this.contentNode);
    this.currentItems[idx] = node;
    this.refreshRow(idx);
    this.itemIn(idx, order);
}

recycleRow(idx: number) {
    let node = this.currentItems[idx];
    delete this.currentItems[idx];
    this.putItemNode(node);
}

recycleRowObservable(idx: number, order: number) {
    return new Observable(_ => {
        this.itemOut(idx, order, () => {
            if (isValid(this?.node) && this.currentItems != null) { // challenge 页面项动画的加强 MrZ 28751 TypeError
                let node = this.currentItems[idx]; // err 这里可能为空 frontjs
                delete this.currentItems[idx];
                this.putItemNode(node);
                _.next(null);
                _.complete();
            }
        });
    });
}

recycleAllObservable() {
    let observables: Observable<any>[] = [];
    this.traverseRNeedShow((idx, order) => {
        if (this.currentItems[idx] == null) {
        } else {
            observables.push(this.recycleRowObservable(idx, order));
        }
    });
    if (observables.length == 0) return of(null);
    return forkJoin(observables);
}

itemIn(idx: number, order: number) { }

itemOut(idx: number, order: number, recycleCallback: () => void) { recycleCallback(); }

// 继承后覆盖这个方法,刷新item
refreshRow(idx: number) {
}

}

3赞

先赞后看 :+1:

代码看的有点头疼,有大概的思路么?比如说用 循环滚动的大概思路是啥啊?

1.scrollview里面content的layout关上,要靠自己计算位置,item直接放到正确的位置上
2.计算需要显示的范围,当在范围内时从池中拿出来放上,不在范围内时,放入池中
3.放上时刷新

  • 我的content是完整的(相当大的)一个尺寸,因为它就是一个空node作为item的载体。它的位置和大小都与不循环滚动的list没有差别,差别只在于上面的item在不断的循环使用。