永远庭

Domus Hominis Ludentis

0%

RMMZ-从0开始构建自己的UI(3)

接下来就是重头戏了,实现窗口的滚动效果。(仅Y轴滚动,如果要加入X轴滚动把Y轴内容复制一遍即可)

基础滚动

首先我们要继承之前的CustomWindow类,实现基本的外观以及点击交互效果。滚动效果的实现主要通过 Sprite.setFrame(x, y, w, h)方法,这个是个RMMV、MZ原生方法,效果是使 Sprite 仅显示其 Bitamp 的指定部分,我们事先画一个特别长的 Bitmap,比如600高度,然后根据滚动值显示其中的一部分,比如窗口高度300,滚动值100,那么 Sprite.setFrame(100, y, w, 300), 就只会显示高度100~400的区域,不断更新显示区域,就实现了滚动效果。

2021-02-26_22-27-11.png

当然在这之前,我们需要先判定Bitmap的总高度,并记录滚动值:

1
2
3
4
5
6
7
8
9
class ScrollWindow extends CustomWindow {

_scrollMin = 0; //最小滚动值,注意往上滚动是负数值,这个最小滚动值就是网上滚动的最大距离,相应的 Bitmap 高度就是窗口高度-最小滚动值;比如窗口高300,最小滚动值-200,那么就是可以最多向上滚动200距离,而整个 Bitmap的高度为500.
_scrollMax = 0; //一般默认为0,不能从初始位置向上滚动
_scroll = 0; //当前的滚动值
_scrolling = 0; //临时滚动值,即按下鼠标后移动的距离,松开鼠标后清零并归入 _scroll 中

get scroll() {return this._scroll + this._scrolling;} //实时总滚动值
}

接下来便是相应鼠标操作改变滚动值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//在update函数中加入该函数,记录当前鼠标于按下时位置的 y 轴差值作为临时滚动值。
// Clickable._pressPoint 的定义如果忘记请参照之前内容
UpdateScroll() {
if (this.IsPressed()) {
const y = TouchInput.y;
this._scrolling = y - this._pressPoint.y;
}
}

//释放鼠标时,将临时滚动值并入,同时清空临时滚动值
OnRelease() {
super.OnRelease();
this._scroll += this._scrolling;
this._scrolling = 0;
}

接下来要根据滚动值设置 Bitmap的显示区域,比较简单:

1
2
3
UpdateContentScroll() {
this._contentSprite.setFrame(0, -this.scroll, this.width, this.contentHeight);
}

滚动极限

这一部分主要实现滚动超过边界时的“回弹”效果。首先需要设定边界值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 改变滚动极限值会改变Bitmap尺寸,因此需要重新绘制 content
* @param {number} l
*/
SetContentLength(l) {
if ( l > this.contentHeight) {
this._scrollMin = this.contentHeight - l; //这里以内容总高度l作为参数,则 _scrollMin 就是(窗口高度 - l)
this.RefreshContent();
} else {
this.content.clear();
}
}

RefreshContent() {
this._contentSprite.bitmap = new Bitmap(this.contentWidth, this.contentHeight - this._scrollMin);
this._contentSprite.move(this.paddings.left, this._titleHeight + this.paddings.top);
this.UpdateContentScroll();
}

处理超过边界时的回弹效果,,我这里的做法是超过边界时,每次update减少超过值得1/3,可以根据实际效果用不同的值,看个人喜好了:

1
2
3
4
5
6
7
8
9
10
11
UpdateBorderBouncing() {
if (this._scroll > this._scrollMax) {
this._scroll -= (this._scroll - this._scrollMax) / 3;
if (this._scroll - this._scrollMax < 1) this._scroll = this._scrollMax;
}

if (this._scroll < this._scrollMin) {
this._scroll -= (this._scroll - this._scrollMin) / 3;
if (this._scrollMin - this._scroll < 1) this._scroll = this._scrollMin;
}
}

滚动惯性

也就是在松开鼠标后,滚动不会立即停止,而是会继续滑动一段距离。且松开前鼠标移动得越快,应该滑动初速度也应该越快,并随时间而减小,直到静止:

1
2
3
4
5
6
7
8
9
10
11
_inertia = 0; //记录当前的惯性滑动速度
_lastY = 0; //记录上一帧的滚动 y 值,以便和当前帧比较,得出该帧 y 的改变量,也就是滚动速度
//改造 UpdateScroll 函数如下:
UpdateScroll() {
if (this.IsPressed()) {
const y = TouchInput.y;
this._scrolling = y - this._pressPoint.y;
this._inertia = y - this._lastY;
this._lastY = y;
}
}

接下来处理松开后的惯性即可:

1
2
3
4
5
6
7
8
//惯性速度衰减率,我计划所有窗口都是一样的,所以作为静态属性,如果希望各个不同可以改为成员属性
static get inertiaAttenuation() { return 0.94; }
UpdateInertia() {
if (!this.IsPressed() && (this._inertia > 1 || this._inertia < -1)) { //只有在鼠标松开时惯性生效
this._scroll += this._inertia;
this._inertia = this._inertia * ScrollWindow.inertiaAttenuation;
}
}

效果

最后把需要帧更新的放到 update 中就完成了

1
2
3
4
5
6
7
8
update() {
super.update();
this.UpdateInertia();
this.UpdateScroll();
this.UpdateBorderBouncing();
this.UpdateContentScroll();
this.UpdateScrollBar();
}

2021-02-26_22-23-33.gif