容易想到的是将item放在scrollbox下面的canvas中,但是受限于左右自由拖动、item循环出现、层级缩放渐变的效果,所以这个方案搁置。如果要自己实现滚动功能的话,就需要一个能映射左右滚动距离的控件,进度条progressbar很契合这个条件。
我们可以在进度条的onPress记录初始值,用onChange的值与初始值比较印射出item向左或向右移动的距离并更新上一帧的值,在onRelease时判断item所具有的立和方向进行惯性移动。
/** 进度条释放的回调 */
private onRelease(value: number) {
if (this.isReleaseLocked) { return; }
// // 如果和按下时间戳相差不超过500ms,那么就是点击事件
// if (Date.now() - this.pressTimeStamp < CLICK_TIME) { this.onClicked(value); return; }
//惯性
this.isPressed = false;
// 计算当前时间戳
let now = Date.now();
// 计算惯性的值,默认是上一帧进度条的值,如果当前时间和上一帧的时间戳间隔小于计时器间隔那么取 上一帧的进度条值,否则取当前帧的进度条值
let interValue = now - this.timerStamp < INTER_STAMP ? this.lastInterProBarValue : this.curInterProBarValue;
// 惯性滑动距离 * 惯性系数
let offset = (value - interValue) * INERTIA_COE;
// 偏移量的绝对值
let absOffset = Math.abs(offset);
// 惯性滑动速度 - 带方向的
let velocity = offset / STOP_INERTIA / INTER_STAMP2;
// 绝对速度 - 无方向
let absVelocity = Math.abs(velocity);
this.shiftItem(absOffset, absVelocity, velocity);
}
// TODO: 保留滚动先
// onClicked(value: number) {
// 精确的点击X位置
// const precisePosX = value * PRO_BAR_RATIO;
// for (const levelItem of this.levelItemList) {
// if (levelItem.checkBeClicked(precisePosX)) { break; }
// }
// }
/** 进度条按钮的回调 */
private onPress(value: number) {
// TODO: 024 bug,点击进度条马上就有onRelease回调,这里加个锁简单处理一下
this.isReleaseLocked = true;
setTimeout(() => {
this.isReleaseLocked = false;
}, INTER_STAMP2 * 10);
// // 记录按下的时间戳
// this.pressTimeStamp = Date.now();
// 开启update
this.levelItemList.forEach(item => { item.startUpdate(); });
//记录拖动起始帧滚动条value值
this.lastStampProBarValue = value;
this.isPressed = true;
/** 双指针取每帧进度条的值 */
this.lastInterProBarValue = value;
this.curInterProBarValue = value;
// 还有惯性就清掉了
if (this.inter2) {
clearInterval(this.inter2);
this.inter2 = null;
this.levelItemList.forEach(item => { item.endUpdate(); });
}
this.timerStamp = null;
if (this.inter) clearInterval(this.inter);
this.inter = setInterval(() => {
if (this.timerStamp != null) this.lastInterProBarValue = this.curInterProBarValue;
this.curInterProBarValue = this.proBar.currentValue;
this.timerStamp = Date.now();
}, INTER_STAMP);
}
/** 进度条改变的回调 */
private onChange(value: number) {
// 避免先触发改变值的回调
if (!this.isPressed) return;
// // 避免影响模拟的onclick事件触发
// if (Date.now() - this.pressTimeStamp < CLICK_TIME) { return; }
//对比上一帧滚动条value比较值计算所有LevelItem的偏移值
let offset = (value - this.lastStampProBarValue) * PRO_BAR_RATIO;
// 偏差值大于0是向右滑动,小于0是向左滑动
this.levelItemList.forEach((levelItem) => {
levelItem.uiObject.position = new Vector2(levelItem.uiObject.position.x + offset, 0);
})
this.lastStampProBarValue = value;
}
定义一个槽的类UISlot,定义属性zOrder、scale、pos,分别代表每个槽的z系数、缩放、位置,因为现在有三种模式并且界面上只需要展现5个item,所以我们再设计一个六个数量的槽类FiveSlot,在这个类中初始化好六个uislot。移动item时,比较每一个中间的item的位置与中间的slot的差距,如果贴合,就将每一个slot的属性应用到item,这样在惯性滑动结束时依然能确保每个item的位置、缩放和层级都能和每个槽的保持一致。
/** levelPanel 下 contentCanvas的 槽 */
class UISlot {
/** z系数 */
zOrder: number = 0;
/** 缩放 */
scale: Vector2 = new Vector2(0, 0);
pos: Vector2 = new Vector2(0, 0);
constructor(zOrder: number, scale: Vector2, pos: Vector2) {
this.zOrder = zOrder;
this.scale = scale;
this.pos = pos;
}
}
/** 5个自定义槽 - 实际上放7个 - 避免留白的问题 */
class FiveSlot {
slotList: UISlot[] = [];
constructor() {
// TODO: 搞6个,一个模式两个
// this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(-470, 0)));
this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(-170, 0)));
this.slotList.push(new UISlot(2, new Vector2(1, 0.93), new Vector2(130, 0)));
this.slotList.push(new UISlot(3, new Vector2(1, 1.00), new Vector2(430, 0)));
this.slotList.push(new UISlot(2, new Vector2(1, 0.93), new Vector2(730, 0)));
this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(1030, 0)));
this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(1330, 0)));
}
}
动态检测最中间的item的滑动方向和距离,以此来更新所有item的缩放、位置和层级
/** 吸附 */
private attach(): boolean {
if (this.curSlotID != MIDDLE_ID) return;
// 结束判断中间节点的位置,看是否需要吸附处理
if (Math.abs(this.uiObject.position.x - this.slotList[this.nextSlotID].pos.x) <= ATTACH_DIS) {
this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(true); });
return true;
}
if (Math.abs(this.uiObject.position.x - this.slotList[this.lastSlotID].pos.x) <= ATTACH_DIS) {
this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(false); });
return true;
}
return false;
}
/**
* 每一帧调用
* 通过canUpdate可以开启关闭调用
* dt 两帧调用的时间差,毫秒
*/
protected onUpdate(dt: number) {
// 中间的去影响所有的
if (!this.isMiddleID) return;
/** 缩放检测 */
this.checkScaleChange();
/** 右滑 */
if (this.uiObject.position.x > this.slotList[this.curSlotID].pos.x) {
this.checkScrollToRight();
}
/** 左滑 */
if (this.uiObject.position.x < this.slotList[this.curSlotID].pos.x) {
this.checkScrollToLeft();
}
}
/** 缩放检测 */
private checkScaleChange() {
const twoSlotDis = this.uiObject.position.x - this.slotList[this.curSlotID].pos.x;
const toScaleY = twoSlotDis * SCALE_RATIO;
this.levelPanel.reqAllLevelItem().forEach((item) => { item.updateScale(toScaleY); });
}
/** 右滑检测 */
private checkScrollToRight() {
if (this.uiObject.position.x >= this.slotList[this.nextSlotID].pos.x) {
this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(true); });
}
}
/** 左滑检测 */
private checkScrollToLeft() {
if (this.uiObject.position.x <= this.slotList[this.lastSlotID].pos.x) {
this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(false); });
}
}
/** 应用槽信息 */
private applySlotInfo() {
this.uiObject.zOrder = this.slotList[this.curSlotID].zOrder;
this.uiObject.position = this.slotList[this.curSlotID].pos;
this.uiObject.renderScale = this.slotList[this.curSlotID].scale;
}
/**
* 动态改变每个item的缩放
* @paramtoScaleY toScaleY是一个矢量,正负表示方向
*/
public updateScale(toScaleY: number) {
if (this.curSlotID < MIDDLE_ID) {
this.uiObject.renderScale = new Vector2(1, this.slotList[this.curSlotID].scale.y + toScaleY);
this.checkOverScale(toScaleY >= 0, true);
} else if (this.curSlotID > MIDDLE_ID) {
this.uiObject.renderScale = new Vector2(1, this.slotList[this.curSlotID].scale.y - toScaleY);
this.checkOverScale(toScaleY <= 0, false);
} else {
// 在中间的一定是减
this.uiObject.renderScale = new Vector2(1, this.slotList[this.curSlotID].scale.y - Math.abs(toScaleY));
// 这儿last 还是 next都可以
this.checkOverScale(false);
}
}
/**
* 检查改变的scale是否超过了将要变成的scale
* @paramisAddScale scale是否增长
* @paramisLeft 是否是左边的 default true
*/
private checkOverScale(isAddScale: boolean, isLeft: boolean = true) {
if (isAddScale) {
let slotID = isLeft ? this.nextSlotID : this.lastSlotID;
const nextScaleY = this.slotList[slotID].scale.y;
if (this.uiObject.renderScale.y >= nextScaleY) { this.uiObject.renderScale = this.slotList[slotID].scale; }
} else {
let slotID = isLeft ? this.lastSlotID : this.nextSlotID;
const lastScaleY = this.slotList[slotID].scale.y;
if (this.uiObject.renderScale.y <= lastScaleY) { this.uiObject.renderScale = this.slotList[slotID].scale; }
}
}
注:以上只是部分核心代码,主要代码请参考源文件