一、动态演示图
功能关键词:无限滚动、复用、scale过渡、层级过渡、position移动、自动回弹/补偿
滑动效果图
二、简介与需求分析
本篇将介绍一种无限滑动列表,完整代码附在最后(实现环境:ts + cocos creator, 实现思路可供其他引擎使用者参考)。
该功能用6个item实现,每个item的scale都有不同,中间位置为选中区(scale最大),选中的item将会展示在右侧“展示区”。图中的数据为1-100,会用6个item循环复用。功能做了回弹/补充(到下一个位置)效果。移动动过程中会不断调整层级关系,以便更符合视觉逻辑。
功能点
- item位置摆放
- 上/下滑动
- scale调整过渡
- item位置移动
- 连续改变不同方向滑动
- 遮罩层级调整
- item复用(本篇用6个item进行复用)
- 结束拖动自动回弹/补偿
三、实现过程
-
初始信息
初始位置、sacle大小通常是写在表中,策划配好我们尽管读,为了演示方便这里写死在代码里。因为我们使用6个item进行复用,其中5个item对外显示1个item隐藏起来,但我们要给策划规定表里必须填写7个位置,“7个位置”记住了这很重要。生成item的时候要生成1-6位置,第7个位置空着充当滑动空间。
//位置、缩放数据
private positionData = [[90, -214], [-29, -116], [-65, 0], [33, 132], [154, 245], [154, 329], [90, -352]];
private scaleData = [0.8, 0.9, 1, 0.7, 0.5, 0.5, 0.8];
生成1-6位置上的item
//prefab
for (let i = 0; i < this.itemPrefab.length; i++) {
const obj = this.itemPrefab[i];
const item = instantiate(obj);
item.setParent(this.itemGroup);
this.itemList.push(item);
this.tierList.push(item);//用作调整层级
item.addComponent(SlidingItem);//添加item脚本
}
//每个item的position、scale
for (let i = 0; i < this.itemList.length; i++) {
const posArray = this.positionData[i];
const pos = new Vec3(posArray[0], posArray[1], 0);
const item = this.itemList[i];
item.setPosition(pos);
const scaleTemp = this.scaleData[i];
const scalse = new Vec3(scaleTemp, scaleTemp, 0);
item.setScale(scalse);
const sc = item.getComponent(SlidingItem);
sc.updateNode();
}
-
页面布局
在介绍列表滑动前介绍页面组件布局
- SlidingList:根目录,挂载SlidingList脚本,挂载预制体。
- tem_group:可视区域,用作存放6个item的容器,增加“mask”遮挡组件。
- item:预制体(运行中会挂载SlidingItem脚本),下有一个Label组件。
4.display_area:展示区
6.drag_rect:拖动区域(跟item_group同等大小)
-
滑动列表(核心!)
拖拽用的引擎自带监听,这有三个监听函数,被拖拽的组件是“drag_rect”。
//监听
this.dragRect.on(NodeEventType.TOUCH_MOVE, this.dragRectTouchMoveHandler, this);
this.dragRect.on(NodeEventType.TOUCH_END, this.dragRectTouchEndHandler, this);
this.dragRect.on(NodeEventType.TOUCH_CANCEL, this.dragRectTouchEndHandler, this);
拖拽过程是本功能的核心,在拖拽的过程中考虑的东西尤其多,比如如何进行复用、怎么算完成一次拖拽、拖拽中结束拖拽了怎么办、什么时机进行调整层级、怎么解决来回向不同方向连续拖拽等问题,下面我们一一分析。
如何复用
我们已经完成了初始信息,下面聊“复用”。
思路:滑动方向只有向上、向下两个方向,当超出到列表两端极值时变换位置。
向上滑动:判断6的位置是否超过读表的位置,超过了移动到7的位置(7的位置是空的)。反之向下滑动一样。
只贴出向下滑动判断交换位置的代码,完整代码看末尾。
代码解释:itemList数组中存储6个item,第一个item永远是最下方,这里取itemList[0]。
如果第一个item到达或超过最下方坐标,进行交换坐标斌且更新原itemList中的位置:itemList数组中的第一个item要放到末尾(很重要):this.itemList.push(this.itemList.shift());
如果向上滑动就必须拿itemList中的最后一个,交换坐标后将最后一个元素放到首位。
交换完成后更新item的值(1-100)。
细节:交换坐标后要进行更新一次层级关系:this.adjustTierAfterBothEnds(dir);
/**
* 向下滑动
* @param dir
*/
private slidingDown(dir: SlipDirEnum) {
const item = this.itemList[0];
const itemPosY = item.getPosition().y;
const bottomY = this.positionData[this.positionData.length - 1][1];
if (itemPosY <= bottomY) {
this.interchangePos(dir, item);
this.itemList.push(this.itemList.shift());
this.adjustTierAfterBothEnds(dir);
//更新item值
this.upDataIndex = (this.upDataIndex + 1) % this.data.length;
this.bottomDataIndex = (this.bottomDataIndex + 1) % this.data.length;
this.updateItemContent(this.upDataIndex, item);
}
}
/**
* 交换坐标位置
* @param targetIndex
* @param item
*/
private interchangePos(dir: SlipDirEnum, item: Node) {
let posDataLen = this.positionData.length;
let targetIndex = posDataLen - 2;
if (dir === SlipDirEnum.Bottom) {
targetIndex = posDataLen - 2;
}else if (dir === SlipDirEnum.Up) {
targetIndex = posDataLen - 1;
}
const tempArray = this.positionData[targetIndex];
const scaleRatio = this.scaleData[targetIndex];
const targetPos = new Vec3(tempArray[0], tempArray[1], 0);
const targetScale = new Vec3(scaleRatio, scaleRatio, 0);
item.setPosition(targetPos);
item.setScale(targetScale);
const sc = item.getComponent(SlidingItem);
sc.updateNode();
}
计算下一步位置
通常计算下一步位置,是在判断是否需要交换坐标后进行的。
计算的位置更新给每个item自己的脚本。
/**
* 计算下一步位置
* @param dir
*/
private calcNextStep(dir: SlipDirEnum) {
let index: number;
for (let i = 0; i < this.itemList.length; i++) {
if (dir == SlipDirEnum.Bottom) {
index = (i - 1 + this.positionData.length) % this.positionData.length;
}else if (dir == SlipDirEnum.Up) {
index = i;
}
const pos = this.positionData[index];
const scale = this.scaleData[index];
const item = this.itemList[i];
const sc = item.getComponent(SlidingItem);
sc.setNextStep(pos, scale);
}
}
完成一次拖拽
计算好下一步位置,我们就可以滑动拖拽了。
拖拽思路:监听TOUCH_MOVE函数,判断拖拽方向,把拖动值发送给每个Item,item向着下一步位置做缓动。
规定:
1.拖动值小于0向下滑动,大于0向上滑动
2.拖动变化量达到100为完成一次拖动
事件抛出函数"EventMgr.instance.emit()"是个人封装的方法,阅读者需自行实现,该功能不在本次讨论内容内。
/**
* 获取滑动方向
* @param value
* @returns
*/
private getSlipDir(value: number): SlipDirEnum {
let dir = SlipDirEnum.Invalid;
if (value < 0) {
dir = SlipDirEnum.Bottom;
}else if (value > 0) {
dir = SlipDirEnum.Up;
}
return dir
}
let currentDir = this.getSlipDir(e.getDeltaY());
if (currentDir === SlipDirEnum.Invalid) {
return;//舍弃等于0的那一帧
}
//记录y坐标累计变化量
this.slidingProgress += e.getDeltaY();
let progressData = {
value : this.slidingProgress,
dir : currentDir,
isContinuousSteering : this.isContinuousSteering
}
//抛给每个item
EventMgr.instance.emit("MOVEACTION", progressData);
Item脚本对滑动的处理,接收到抛出数据,计算进度比率。
private moveActionHandle(eventName: string, data: any) {
let value = data.value;
let dir = data.dir;
let isContinuousSteering = data.isContinuousSteering;
let ratio = Math.abs(value) / 100.0;
const posProgress = new Vec3();
const scaleProgress = new Vec3();
Vec3.lerp(posProgress, this.currentPos, this.nextPos, ratio);
Vec3.lerp(scaleProgress, this.currentScale, this.nextScale, ratio);
this.node.setPosition(posProgress);
this.node.setScale(scaleProgress);
//比率到达1时更新当前坐标
if (ratio >= 1.0) {
this.updateCurrentNode();
return;
}
//连续变向回到始发点要重置当前坐标为当前坐标
if(isContinuousSteering){
if(dir === SlipDirEnum.Up) {
if(value >= -0.05) {
this.setCuurentPos();
return;
}
}else {
if(value <= 0.05) {
this.setCuurentPos();
}
}
}
}
-
非一次完整拖拽操作
不是每次拖拽值都正好到100停下的,会有拖拽值不足、拖拽值超出、不同方向连续拖拽的情况。以下代码都是在拖动中函数dragRectTouchMoveHandler完成的
1. 拖拽不足
结束拖拽自动进行回弹/补偿,回弹:拖拽值不足55自动回到原点,补偿:超过55自动滑动到下一个点 该功能要在拖拽结束函数中触发,抛给所有item。
this.FLIPVALUE = 55; true:补偿到下一个目标,false:退弹到原来的目标
//拖动结束
private touchEndAutoMove() {
if(Math.abs(this.slidingProgress) > this.FLIPVALUE) {
EventMgr.instance.emit("AUTOMOVEACTION", true);
}else {
EventMgr.instance.emit("AUTOMOVEACTION", false);
}
}
item脚本中接收到自动滑动的事件。
/**
* 结束拖动后的回弹/补偿
*/
private autoMoveActionHandle(eventName: string, data: any) {
if (data) {
tween(this.node)
.to(this.SLIPTIME, { position: this.nextPos, scale: this.nextScale })
.call(this.updateCurrentNode.bind(this))
.start();
}else {
tween(this.node)
.to(this.SLIPTIME, { position: this.currentPos, scale: this.currentScale })
.start();
}
}
2. 拖拽超出
超出100表示已经到达下一步目标,重置比率、翻页、连续变向计数,然后超出部分重新判断是否到达两端极值、计算下一步。
//进度>=100重置
if (Math.abs(this.slidingProgress) >= 100) {
this.resetParams();
return;
}
3. 不同方向连续拖拽 【难点】
连续变向时要在不影响正常拖拽(一次完整拖拽、拖拽不足、拖拽超出)的操作下考虑层级遮罩的调整、item的下一个目标是谁如何计算(起始点还是下一个点)、自动回弹/补偿。“层级”我们下小节讲。
判断是否是“连续不同方向拖拽”,我们用一个全局变量changeDirCount来做计数,>=2 我们就认为是“连续不同方向拖拽”,结束拖拽时计数置置为0。item脚本根据“是否连续变向”和”方向“来设置当前目标
//计算极端、下一步位置
if (this.lastSlipDir !== currentDir) {
this.lastSlipDir = currentDir; //防止频繁进入
this.setBothEnds(currentDir);
//判断是否连续
this.changeDirCount++;
if (this.changeDirCount < 2) {
//计算下一步
this.calcNextStep(currentDir);
}
if (this.changeDirCount >= 2) {
this.isContinuousSteering = true;
}
}
item脚本
private moveActionHandle(eventName: string, data: any) {
let value = data.value;
let dir = data.dir;
let isContinuousSteering = data.isContinuousSteering;
let ratio = Math.abs(value) / 100.0;
const posProgress = new Vec3();
const scaleProgress = new Vec3();
Vec3.lerp(posProgress, this.currentPos, this.nextPos, ratio);
Vec3.lerp(scaleProgress, this.currentScale, this.nextScale, ratio);
this.node.setPosition(posProgress);
this.node.setScale(scaleProgress);
//比率到达1时更新当前坐标
if (ratio >= 1.0) {
this.updateCurrentNode();
return;
}
//连续变向回到始发点要重置当前坐标为当前坐标
if(isContinuousSteering){
if(dir === SlipDirEnum.Up) {
if(value >= -0.05) {
this.setCuurentPos();
return;
}
}else {
if(value <= 0.05) {
this.setCuurentPos();
}
}
}
}
private setCuurentPos() {
this.node.setPosition(this.currentPos);
this.node.setScale(this.currentScale);
}
-
调整层级
层级遮罩的调整是一个看图找规律的过程,初始信息item是从1-6生成的,就是说6处于父组件最底层但是显示的遮罩关系是最前方。
这不是我们想要的,我们想要的是中间3的item显示到最前方,然后依次是 2>1>4>5>6。想要处于显示的最前方那么在父类容器内就处于最下层(这一点逻辑有点绕)。
我们用一个全局变量tierIndex来记录当前哪个item应该显示在最前面,也就是父组件的最底层。
我们要做到:显示界面中间位置的item在父类容器中永远是最下层的,显示界面中间位置item下方的item在父类容器中更接近最低层item。初始信息调整层级后我们在父类容器的遮罩关系是:6、5、4、1、2、3,也就是每一次翻页都要保持中间位置item在数组中(itemList)小于itemIdex的在itemIndx父容器的上方,大于itemIndex的在最上方依次排列。
我们规定拖拽值>55时调整遮罩层级,连续不同方向改变时 <55 我们要恢复原有层级。
/**
* 翻页调整层级
*/
private flipOver(dir : SlipDirEnum) {
let ratio = Math.abs(this.slidingProgress);
if (ratio > this.FLIPVALUE && !this.isflipOver) {
this.isflipOver = true;//控制不连续进入
//调整层级
this.adjustTier(dir);
//设置展示区
this.setDisplayAreaContent();
}else if (this.isContinuousSteering && this.isflipOver && ratio < this.FLIPVALUE) {//恢复原有层级
this.isflipOver = false;
this.adjustTier(dir);
this.setDisplayAreaContent();
}
}
tierIndex来记录当前哪个item应该显示在最前面,也就是父组件的最底层。初始数据时我们已经有了tierList专门做层级调整。规定初始显示最前面的tierIndex = 2,翻页时进行对tierIndex增减。
private adjustTier(dir : SlipDirEnum) {
if (dir === SlipDirEnum.Up) {
if (--this.tierIndex < 0) {
this.tierIndex = this.tierList.length - 1;
}
this.disItemIndex = ((this.disItemIndex - 1) + this.data.length) % this.data.length;
}
else if (dir == SlipDirEnum.Bottom) {
if (++this.tierIndex >= this.tierList.length) {
this.tierIndex = 0;
}
this.disItemIndex = (this.disItemIndex + 1) % this.data.length;
}
this.setItemSibling(this.tierIndex - 1 - 2);
}
下面代码”this.tierIndex +1 + 1“或”this.tierIndex -1 -2 “ 或”this.tierIndex -1 -1 -1 “ 写的那么唠叨是因为我想描述下方/上方存在几个iem,便于理解。
调整层级时:tierIndex上方存在2个item,下方存在3个item,读者自定体会。
private setItemSibling(index: number) {
//tierIndex下方
for (let i = this.tierIndex - 1; i >= index; i--) {
const index = (i + this.tierList.length) % this.tierList.length;
const item = this.tierList[index];
item.setSiblingIndex(0);
}
//上方
for (let i = this.tierIndex + 1; i <= this.tierIndex + 1 + 1; i++) {
const index = i % this.tierList.length;
const item = this.tierList[index];
item.setSiblingIndex(0);
}
}
滑动时父类组件内item的变化
-
数据部分
数据写死1-100,因为是上下方向无限滑动用了上下两个全局变量记录当前数据到了哪里。
private bottomDataIndex: number = 0;
private upDataIndex: number = 5;
翻页时对上下两个变量进行调整。
- 向上翻页数据变量变化:
this.bottomDataIndex = ((this.bottomDataIndex - 1) + this.data.length) % this.data.length;
this.upDataIndex = ((this.upDataIndex - 1) + this.data.length) % this.data.length;
2. 向下翻页数据变量变化:
this.upDataIndex = (this.upDataIndex + 1) % this.data.length;
this.bottomDataIndex = (this.bottomDataIndex + 1) % this.data.length;
内容展示比较简单,根据翻页内容(tierIndex)进行生成预制体。
/**展示区内容*/
private setDisplayAreaContent() {
if (this.displayArea.children.length > 0) {
this.displayArea.removeAllChildren();
}
const obj = this.itemPrefab[this.tierIndex];
const item = instantiate(obj);
item.setParent(this.displayArea);
const uitf: UITransform = item.getComponent(UITransform);
uitf.setContentSize(new Size(400, 380));//尺寸暂且写死,不推荐
this.setItemContent(item, this.disItemIndex);
}
结尾代码
不足:拖动结束最好加一个计时锁,时间内不允许滑动操作,我第一版代码加了,第二版忘记了......读者自行实现吧,下面是完整代码。
export enum SlipDirEnum {
Invalid = 0,
Up = 1, //上方
Bottom = -1 //下方
}
@ccclass('SlidingList')
export class SlidingList extends Component {
private isContinuousSteering: boolean = false;//是否连续转向
private changeDirCount: number = 0;
private isflipOver: boolean = false;
private readonly FLIPVALUE: number = 55;
private slidingProgress: number = 0;
private lastSlipDir: SlipDirEnum = SlipDirEnum.Invalid;
private disItemIndex: number = 2;
private bottomDataIndex: number = 0;
private upDataIndex: number = 5;
private tierIndex: number = 2;
//item预制体
@property({ type: [Prefab] })
private itemPrefab: Node[] = [];
private itemList: Node[] = [];
private tierList: Node[] = [];//用作调整层级关系
//组件信息
private itemGroup: Node = null;
private displayArea: Node = null;
private dragRect: Node = null;
//数据
private data = [];
private dataLen = 100;
//位置、缩放数据
private positionData = [[90, -214], [-29, -116], [-65, 0], [33, 132], [154, 245], [154, 329], [90, -352]];
private scaleData = [0.8, 0.9, 1, 0.7, 0.5, 0.5, 0.8];
onLoad() {
//组件
this.itemGroup = this.node.getChildByName("item_group");
this.displayArea = this.node.getChildByName("display_area");
this.dragRect = this.node.getChildByName("drag_rect");
//监听
this.dragRect.on(NodeEventType.TOUCH_MOVE, this.dragRectTouchMoveHandler, this);
this.dragRect.on(NodeEventType.TOUCH_END, this.dragRectTouchEndHandler, this);
this.dragRect.on(NodeEventType.TOUCH_CANCEL, this.dragRectTouchEndHandler, this);
}
start() {
this.initData();
}
private adjustTier(dir : SlipDirEnum) {
if (dir === SlipDirEnum.Up) {
if (--this.tierIndex < 0) {
this.tierIndex = this.tierList.length - 1;
}
this.disItemIndex = ((this.disItemIndex - 1) + this.data.length) % this.data.length;
}
else if (dir == SlipDirEnum.Bottom) {
if (++this.tierIndex >= this.tierList.length) {
this.tierIndex = 0;
}
this.disItemIndex = (this.disItemIndex + 1) % this.data.length;
}
this.setItemSibling(this.tierIndex - 1 - 2);
}
/**
* 翻页调整层级
*/
private flipOver(dir : SlipDirEnum) {
let ratio = Math.abs(this.slidingProgress);
if (ratio > this.FLIPVALUE && !this.isflipOver) {
this.isflipOver = true;//控制不连续进入
//调整层级
this.adjustTier(dir);
//设置展示区
this.setDisplayAreaContent();
}else if (this.isContinuousSteering && this.isflipOver && ratio < this.FLIPVALUE) {//恢复原有层级
this.isflipOver = false;
this.adjustTier(dir);
this.setDisplayAreaContent();
}
}
/**
* 到达目标后重置参数
* @param dir
* @returns
*/
private arriveTargetRest(dir:SlipDirEnum) {//不知道会不会出其他的问题
//进度>=100重置
if (Math.abs(this.slidingProgress) >= 100) {
this.resetParams();
return;
}
//连续转向到达原点
if (this.isContinuousSteering) {
if(dir === SlipDirEnum.Bottom) {
if(this.slidingProgress <= 0.05) {
this.resetParams();
return;
}
}else {
if(this.slidingProgress >= -0.05) {
this.resetParams();
}
}
}
}
/**
* update item content
* @param dataIndex
* @param item
*/
private updateItemContent(dataIndex: number, item: Node) {
let data = this.data[dataIndex];
const sc = item.getComponent(SlidingItem);
sc.updateData(data.toString());
}
private setItemSibling(index: number) {
//tierIndex下方
for (let i = this.tierIndex - 1; i >= index; i--) {
const index = (i + this.tierList.length) % this.tierList.length;
const item = this.tierList[index];
item.setSiblingIndex(0);
}
//上方
for (let i = this.tierIndex + 1; i <= this.tierIndex + 1 + 1; i++) {
const index = i % this.tierList.length;
const item = this.tierList[index];
item.setSiblingIndex(0);
}
}
private adjustTierAfterBothEnds(dir: SlipDirEnum) {
if (dir === SlipDirEnum.Up) {
let index = this.tierIndex - 1 - 2;
this.setItemSibling(index);
}else if (dir === SlipDirEnum.Bottom) {
let index = this.tierIndex - 1 - 1;
this.setItemSibling(index);
//游标下方第三个item
const value = ((this.tierIndex - 1 - 1 - 1) + this.tierList.length) % this.tierList.length;
const item = this.tierList[value];
item.setSiblingIndex(0);
}
}
/**
* 计算下一步位置
* @param dir
*/
private calcNextStep(dir: SlipDirEnum) {
let index: number;
for (let i = 0; i < this.itemList.length; i++) {
if (dir == SlipDirEnum.Bottom) {
index = (i - 1 + this.positionData.length) % this.positionData.length;
}else if (dir == SlipDirEnum.Up) {
index = i;
}
const pos = this.positionData[index];
const scale = this.scaleData[index];
const item = this.itemList[i];
const sc = item.getComponent(SlidingItem);
sc.setNextStep(pos, scale);
}
}
/**
* 交换坐标位置
* @param targetIndex
* @param item
*/
private interchangePos(dir: SlipDirEnum, item: Node) {
let posDataLen = this.positionData.length;
let targetIndex = posDataLen - 2;
if (dir === SlipDirEnum.Bottom) {
targetIndex = posDataLen - 2;
}else if (dir === SlipDirEnum.Up) {
targetIndex = posDataLen - 1;
}
const tempArray = this.positionData[targetIndex];
const scaleRatio = this.scaleData[targetIndex];
const targetPos = new Vec3(tempArray[0], tempArray[1], 0);
const targetScale = new Vec3(scaleRatio, scaleRatio, 0);
item.setPosition(targetPos);
item.setScale(targetScale);
const sc = item.getComponent(SlidingItem);
sc.updateNode();
}
private slidingUp(dir: SlipDirEnum) {
const item = this.itemList[this.itemList.length - 1];
const topY = this.positionData[this.positionData.length - 2][1];
const itemPosY = item.getPosition().y;
if (itemPosY >= topY) {
this.interchangePos(dir, item);
this.itemList.unshift(this.itemList.pop());
this.adjustTierAfterBothEnds(dir);
//更新item值、更新data上下两端的游标
this.bottomDataIndex = ((this.bottomDataIndex - 1) + this.data.length) % this.data.length;
this.upDataIndex = ((this.upDataIndex - 1) + this.data.length) % this.data.length;
this.updateItemContent(this.bottomDataIndex, item);
}
}
/**
* 向下滑动
* @param dir
*/
private slidingDown(dir: SlipDirEnum) {
const item = this.itemList[0];
const itemPosY = item.getPosition().y;
const bottomY = this.positionData[this.positionData.length - 1][1];
if (itemPosY <= bottomY) {
this.interchangePos(dir, item);
this.itemList.push(this.itemList.shift());
this.adjustTierAfterBothEnds(dir);
//更新item值
this.upDataIndex = (this.upDataIndex + 1) % this.data.length;
this.bottomDataIndex = (this.bottomDataIndex + 1) % this.data.length;
this.updateItemContent(this.upDataIndex, item);
}
}
/**
* 设置两端极限的坐标位置
* @param dir
*/
private setBothEnds(dir: SlipDirEnum) {
switch (dir) {
case SlipDirEnum.Bottom:
this.slidingDown(dir);
break;
case SlipDirEnum.Up:
this.slidingUp(dir);
break;
}
}
/**
* 获取滑动方向
* @param value
* @returns
*/
private getSlipDir(value: number): SlipDirEnum {
let dir = SlipDirEnum.Invalid;
if (value < 0) {
dir = SlipDirEnum.Bottom;
}else if (value > 0) {
dir = SlipDirEnum.Up;
}
return dir
}
/**
* 触摸结束列表补偿滑动
*/
private touchEndAutoMove() {
if(Math.abs(this.slidingProgress) > this.FLIPVALUE) {
EventMgr.instance.emit("AUTOMOVEACTION", true);
}else {
EventMgr.instance.emit("AUTOMOVEACTION", false);
}
}
/**
* 重置参数
*/
private resetParams() {
this.changeDirCount = 0;
this.slidingProgress = 0;
this.isContinuousSteering = false;
this.isflipOver = false;
this.lastSlipDir = SlipDirEnum.Invalid;
}
private dragRectTouchEndHandler(e: EventTouch) {
this.touchEndAutoMove();
this.resetParams();
}
private dragRectTouchMoveHandler(e: EventTouch) {
let currentDir = this.getSlipDir(e.getDeltaY());
if (currentDir === SlipDirEnum.Invalid) {
return;//舍弃等于0的那一帧
}
//计算极端、下一步位置
if (this.lastSlipDir !== currentDir) {
this.lastSlipDir = currentDir; //防止频繁进入
this.setBothEnds(currentDir);
//判断是否连续
this.changeDirCount++;
if (this.changeDirCount < 2) {
//计算下一步
this.calcNextStep(currentDir);
}
if (this.changeDirCount >= 2) {
this.isContinuousSteering = true;
}
}
//记录y坐标累计变化量
this.slidingProgress += e.getDeltaY();
let progressData = {
value : this.slidingProgress,
dir : currentDir,
isContinuousSteering : this.isContinuousSteering
}
//抛给每个item
EventMgr.instance.emit("MOVEACTION", progressData);
//翻页,层级调整
this.flipOver(currentDir);
//到达目标后层级重置
this.arriveTargetRest(currentDir);
}
/**展示区内容*/
private setDisplayAreaContent() {
if (this.displayArea.children.length > 0) {
this.displayArea.removeAllChildren();
}
const obj = this.itemPrefab[this.tierIndex];
const item = instantiate(obj);
item.setParent(this.displayArea);
const uitf: UITransform = item.getComponent(UITransform);
uitf.setContentSize(new Size(400, 380));//尺寸暂且写死,不推荐
this.setItemContent(item, this.disItemIndex);
}
/**item内容 */
private setItemContent(item: Node, index: number) {
const content = item.getChildByName("content");
const dataLabel: Label = content.getComponent(Label);
dataLabel.string = this.data[index];
}
private initData() {
//组织data
for (let i = 0; i < this.dataLen; i++) {
this.data.push(i + 1);
}
//prefab
for (let i = 0; i < this.itemPrefab.length; i++) {
const obj = this.itemPrefab[i];
const item = instantiate(obj);
item.setParent(this.itemGroup);
this.itemList.push(item);
this.tierList.push(item);//用作调整层级
item.addComponent(SlidingItem);
}
//每个item的position、scale
for (let i = 0; i < this.itemList.length; i++) {
const posArray = this.positionData[i];
const pos = new Vec3(posArray[0], posArray[1], 0);
const item = this.itemList[i];
item.setPosition(pos);
const scaleTemp = this.scaleData[i];
const scalse = new Vec3(scaleTemp, scaleTemp, 0);
item.setScale(scalse);
const sc = item.getComponent(SlidingItem);
sc.updateNode();
}
//item内容
for (let i = 0; i < this.itemList.length; i++) {
const item = this.itemList[i];
this.setItemContent(item, i);
}
//调整层级顺序
for (let i = this.tierIndex + 1; i < this.tierList.length; i++) {
const item = this.tierList[i];
item.setSiblingIndex(0);
}
this.setDisplayAreaContent();
}
}
Item挂载的脚本
@ccclass('SlidingItem')
export class SlidingItem extends Component {
private readonly SLIPTIME : number = 0.3;
private nextPos: Vec3 = null;
private nextScale: Vec3 = null;
private currentPos: Vec3 = null;
private currentScale: Vec3 = null;
private contentLabel: Label = null;
onLoad() {
const content: Node = this.node.getChildByName("content");
this.contentLabel = content.getComponent(Label);
//监听
EventMgr.instance.addEventListener("MOVEACTION", this, this.moveActionHandle);
EventMgr.instance.addEventListener("AUTOMOVEACTION", this, this.autoMoveActionHandle);
}
private setCuurentPos() {
this.node.setPosition(this.currentPos);
this.node.setScale(this.currentScale);
}
private updateCurrentNode() {
this.currentPos = this.nextPos;
this.currentScale = this.nextScale;
}
/**
* 结束拖动后的回弹/补偿
*/
private autoMoveActionHandle(eventName: string, data: any) {
if (data) {
tween(this.node)
.to(this.SLIPTIME, { position: this.nextPos, scale: this.nextScale })
.call(this.updateCurrentNode.bind(this))
.start();
}else {
tween(this.node)
.to(this.SLIPTIME, { position: this.currentPos, scale: this.currentScale })
.start();
}
}
private moveActionHandle(eventName: string, data: any) {
let value = data.value;
let dir = data.dir;
let isContinuousSteering = data.isContinuousSteering;
let ratio = Math.abs(value) / 100.0;
const posProgress = new Vec3();
const scaleProgress = new Vec3();
Vec3.lerp(posProgress, this.currentPos, this.nextPos, ratio);
Vec3.lerp(scaleProgress, this.currentScale, this.nextScale, ratio);
this.node.setPosition(posProgress);
this.node.setScale(scaleProgress);
//比率到达1时更新当前坐标
if (ratio >= 1.0) {
this.updateCurrentNode();
return;
}
//连续变向回到始发点要重置当前坐标为当前坐标
if(isContinuousSteering){
if(dir === SlipDirEnum.Up) {
if(value >= -0.05) {
this.setCuurentPos();
return;
}
}else {
if(value <= 0.05) {
this.setCuurentPos();
}
}
}
}
public setNextStep(nextPos: Array<number>, nextScale: number) {
this.nextPos = new Vec3(nextPos[0], nextPos[1], 0);
this.nextScale = new Vec3(nextScale, nextScale, 0);
}
public updateData(content: string) {
this.contentLabel.string = content;
}
public updateNode() {
this.currentPos = this.node.getPosition();
this.currentScale = this.node.getScale();
}
}