| 赞 | 20 |
| VIP | |
| 好人卡 | |
| 积分 | 8 |
| 经验 | |
| 最后登录 | 2025-7-18 |
| 在线时间 | 90 小时 |
- 梦石
- 0
- 星屑
- 755
- 在线时间
- 90 小时
- 注册时间
- 2017-11-6
- 回帖
- 10
|
加入我们,或者,欢迎回来。
您需要 登录 才可以下载或查看,没有账号?注册会员
×
本帖最后由 lohengriny 于 2021-2-26 22:35 编辑
原生的UI实在是太丑了,而且对触屏、滑动等的支持也不太好,我们需要重新构建一个。
先放一个目前的效果:(仿时空猫)
https://b23.tv/8tGk6Z
[fold=第一部分:基类 Clickable]功能构想
我期望的UI主要有窗口和按钮构成,窗口至少要有普通窗口、可滚动窗口、可滚动列表窗口(列表item可交互)等几个类型。
对于滚动窗口,还需要有滚动条,以及滚动惯性(快速滚动后松开鼠标,减速滑动一段距离直到停止),滚动极限反馈(超过滚动极限时会“弹”回来),列表Item长按功能(比如显示详情)
原型
以上一些功能窗口,和按钮都有一个共同点,就是可以点击交互,点击还要包括长按和非长按,以及按住滚动等,为此可建一个类叫 Clickable, 为了让它有 transform,可以作为 child 添加给别人我们要继承 PIXI.Container,先写一个简单的框架:
[pre lang="javascript"]class Clickable extends PIXI.Container {
constructor(x = 0, y = 0, w = 0, h = 0) {
super();
this.x = x;
this.y = y;
//宽和高必不可少
this._width = w;
this._height = h;
//是否处于激活(可交互)状态
this._active = true;
}
get width() {return this._width;}
get w() {return this._width;} //宽的简称,为了以后可以偷懒
get height() {return this._height;}
get h() {return this._height;}
get active() { return this._active;}
Activate() { this._active = true;}
Deactivate() { this._active = false;}
}[/pre]
在点击判断中,首先要判断点击是否在我们对象的区域内:
- /** @returns {boolean} */
- IsMouseOver() {
- //通过PIXI的原生函数 DisplayObject.worldTransform.applyInverse(p: Point) 可以方便地把全局坐标变成本地坐标
- const pos = this.worldTransform.applyInverse(new Point(TouchInput.x, TouchInput.y));
- return pos.x >= 0 && pos.x <= this.w && pos.y >= 0 && pos.y <= this.h;
- }
复制代码
接写来需要处理点击事件, 按照RM的风格我们把这个放到 update函数中。判定点击需要考虑以下情况:
鼠标按下 -> OnPress
鼠标按下后立即原地释放 -> OnRelease & OnClick
鼠标按下一段时间后原地释放 -> OnRelease & OnLongPressRelease (长按后即使原地释放一般也不认为是点击,考虑到长按一般是弹出悬浮窗口显示详情,这是释放应该是窗口消失,直接触发点击并不适宜。)
鼠标按下后移动到区域内另一位置释放 -> OnRelease & OnLongPressRelease
鼠标按下后移动到区域之外 -> OnLongPressRelease (鼠标处于点击状态移动到区域之外,如果有长按悬浮窗这时也应该消失,所以也处理为长按释放)
整理之后代码如下:
[pre lang="javascript"]_pressed = false; //记录“鼠标在区域内按下”
_longPressed = false; //记录“鼠标在区域内长按”
_pressPoint = new Point(0, 0); //记录鼠标按下的点坐标
_releasePoint = new Point(0, 0); //记录鼠标释放的点坐标
IsEnabled() {
return this.visible && this._active;
}
update() {
if (!this.IsEnabled()) return;
for (const child of this.children.reverse.clone()) {
if (child.update) {
child.update();
}
}
if (this._pressed) { //已经处于按下状态
if (this.IsMouseOver()) {
if (!this._longPressed && TouchInput.isLongPressed()) {
this._longPressed = true;
this.OnLongPress();
}
if(TouchInput.isReleased()) {
if(this._longPressed) {
this.OnLongPressRelease(true);
} else if(TouchInput.isClicked()){
this.OnClick();
}
this._pressed = false;
this._longPressed = false;
this.OnRelease();
}
} else { //鼠标在按下状态离开区域的情况
if(this._longPressed) {
this.OnLongPressRelease(false);
}
this._pressed = false;
this._longPressed = false;
this.OnRelease();
}
} else if (TouchInput.isTriggered()&&this.IsMouseOver()) { //开始按下
this._pressed = true;
this.OnPress();
}
}
接下来是几个触发事件的函数,为了方便测试这里先用几个 console.log 好查看效果:
_handlers = {};
/**
* @param {Function} func
* @param {Function} handle_func
* @param args
*/
SetHandler(func, handle_func, ...args) {
if(!this[func.name]) {
throw new Error(`No method named ${func.name} found!`);
} else {
this._handlers[func.name] = handle_func.bind(this, ...args);
}
}
CallHandler(name) {
if(this._handlers[name]) {
this._handlers[name]();
}
}
s
OnPress() {
this._pressPoint.set(TouchInput.x, TouchInput.y);
console.log('press');
this.CallHandler(this.OnPress.name);
}
OnRelease() {
this._releasePoint.set(TouchInput.x, TouchInput.y);
console.log('release');
this.CallHandler(this.OnRelease.name);
}
OnClick() {
console.log('click');
this.CallHandler(this.OnClick.name);
}
OnLongPress() {
console.log('long press start');
this.CallHandler(this.OnLongPress.name);
}
/**
* @param {boolean} isClick
*/
OnLongPressRelease(isClick) {
console.log(`long press release, this is${isClick?'':' not'} a click`);
this.CallHandler(this.OnLongPressRelease.name);
}[/pre]
[/fold]
[fold=第二部分:基本窗口]
写好Clickable类之后便可以开始着手写Window了,不过在这之前还要做一些准备
Window 皮肤绘制
如同RM原生的Window皮肤,基本是一个正方形的格子,绘制的时候是把其上下左右边缘分别绘制,首先四个角按原样单独绘制,四条边进行长度缩放后绘制,中间填充双方向缩放绘制,这样可以将一个正方形在不失真的情况下绘成任意大小的举行(上面的缩放也可以用Tilemap是的排列,取决于中间区域是不是纯色);为了方便起见为Bitmap单独写这样一个绘制皮肤的方法,难度没有,就是比较麻烦。
[pre lang="javascript"]
/**
* @param {Bitmap} bitmap
* @param {number} dx
* @param {number} dy
* @param {number} dw
* @param {number} dh
* @param {number} lx
* @param {number} rx
* @param {number} uy
* @param {number} by
*/
Bitmap.prototype.DrawTexture = function(bitmap, dx, dy, dw, dh, lx, rx = lx, uy, by = uy) {
let w = bitmap.width - lx - rx;
let h = bitmap.height - uy - by;
//left top
this.blt(bitmap, 0, 0, lx, uy, dx, dy, lx, uy);
//top
this.blt(bitmap, lx, 0, w, uy, dx + lx, dy, dw - lx - rx, uy);
//right top
this.blt(bitmap, lx + w, 0, rx, uy, dx + dw - rx, dy, rx, uy);
//left bottom
this.blt(bitmap, 0, uy + h, lx, by, dx, dy + dh - by, lx, by);
//bottom
this.blt(bitmap, lx, uy + h, w, by, dx + lx, dy + dh - by, dw - lx -rx, by);
//right bottom
this.blt(bitmap, lx + w, uy + h, rx, by, dx + dw - rx, dy + dh - by, rx, by);
//left
this.blt(bitmap, 0, uy, lx, h, dx, dy + uy, lx, dh - uy - by);
//right
this.blt(bitmap, lx + w, uy, rx, h, dx + dw - rx, dy + uy, rx, dh - uy - by);
//center
this.blt(bitmap, lx, uy, w, h, dx + lx, dy + uy, dw - lx - rx, dh - uy - by);
};
另外一个准备,就是写一个Paddings类,方便一点,不然每次都要写四个看起来比较乱,比较简单就不具体说明了:
class Paddings {
/**
* @param {number} left
* @param {number} top
* @param {number} right
* @param {number} bottom
*/
constructor(left = 0, top = left, right = left, bottom = top) {
this._left = left;
this._top = top;
this._right = right;
this._bottom = bottom;
}
_left = 0;
_top = 0;
_right = 0;
_bottom = 0;
/** @returns number */
get left() { return this._left; }
/** @returns number */
get top() { return this._top; }
/** @returns number */
get right() { return this._top; }
/** @returns number */
get bottom() { return this._bottom; }
}
[/pre]
Window
准备好之后可以开始写 Window 了,首先还是先思考一下需求。 Window 由标题、背景、内容构成。(标题我后来放弃,没什么用,就不提了,不过加进去也简单)。背景就是窗口的背景板,背景上画皮肤;而内容是一个透明的空白图片,是写文字图片等内容的地方,覆盖在背景上面,在二者都用 Sprite 作为载体:
[pre lang="javascript"]
class CustomWindow extends Clickable { //继承Clickable
_title = '';
_titleHeight = 0;
/** @type Bitmap */
_titleTexture;
/** @type Bitmap */
_backTexture;
_active = false;
/**
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
* @param {string} title - 标题:废弃了
* @param {string} titlebg - 标题皮肤(背景):废弃了
* @param {number} th - 标题高度:废弃了
* @param {string} bg - 皮肤(背景)
*/
constructor(x, y, w, h, title, th, titlebg = 'wd_title_cmn', bg = 'wd_back_cmn') { //以默认背景图片作为默认参数
super(x, y, w, h);
this._title = title;
this._titleHeight = th;
this._contentPaddings = new Paddings(12, 12, 12, 12); //内容paddings,即内容与内容 sprite 的边距
this.Create();
this.titleTexture = ImageManager.ImageManager.loadBitmap('img/ui', titlebg);
this.backTexture = ImageManager.loadBitmap('img/ui', bg);
this.Move(x, y, w, h);
}
get backTexture() { return this._backTexture;}
set backTexture(val) {
if (this._backTexture !== val) {
this._backTexture = val;
this._backTexture.addLoadListener(this.OnBackTextureLoaded.bind(this)); //因为读取图片需要时间,使用回调函数
}
}
get titleTexture() { return this._titleTexture;}
set titleTexture(val) {
if (this._titleTexture !== val) {
this._titleTexture = val;
this._titleTexture.addLoadListener(this.OnTitleTextureLoaded.bind(this));
}
}
/** @returns {Bitmap} */
get content() { return this._contentSprite.bitmap; }
/**
* content paddings
* @returns {Paddings}
*/
get paddings() { return this._contentPaddings; }
/** @returns number */
get contentAreaHeight() {return this.height - this._titleHeight;}
/** @returns number */
get contentHeight() { return this.contentAreaHeight - this.paddings.top - this.paddings.bottom; }
/** @returns number */
get contentWidth() { return this.width - this.paddings.left - this.paddings.right; }
/**
* 移动窗口,如果宽高发生变化则需要重新绘制
* @param {number} x - x coordinate
* @param {number} y - y coordinate
* @param {number} width
* @param {number} height
*/
Move(x = 0, y = 0, width = 0, height = 0) {
this.x = x;
this.y = y;
if (this._width !== width || this._height !== height) {
this._width = width;
this._height = height;
this.RefreshAllParts();
}
}
Close() {
this.Deactivate();
this.visible = false;
}
Open() {
this.visible = true;
}
// 创建Sprite
Create() {
this.CreateBackSprite();
this.CreateContentSprite();
this.CreateTitleSprite();
}
/** @type Sprite */
_titleSprite;
/** @type Sprite */
_backSprite;
/** @type Sprite */
_contentSprite;
CreateTitleSprite() {
this._titleSprite = new Sprite();
this.addChild(this._titleSprite);
}
CreateBackSprite() {
this._backSprite = new Sprite();
this.addChild(this._backSprite);
this._backSprite.move(0, this._titleHeight);
}
CreateContentSprite() {
this._contentSprite = new Sprite(new Bitmap(this.contentWidth, this.contentHeight));
this.addChild(this._contentSprite);
this._contentSprite.move(this.paddings.left, this._titleHeight + this.paddings.top);
}
//刷新窗口内容
OnTitleTextureLoaded() {
if (this._titleHeight > 0) {
let b = new Bitmap(this.width, this._titleHeight + 12);
b.DrawTexture(this.titleTexture, 0, 0, this.width, this._titleHeight + 4, this.TitleBorders.left, this.TitleBorders.right, this.TitleBorders.top, this.TitleBorders.bottom);
b.drawText(this._title, 0, 0, this.width, this._titleHeight + 4, 'center');
this._titleSprite.bitmap = b;
}
}
OnBackTextureLoaded() {
let b = new Bitmap(this.width, this.height - this._titleHeight);
b.DrawTexture(this.backTexture, 0, 0, this.width, this.height - this._titleHeight, this.BackBorders.left, this.BackBorders.right, this.BackBorders.top, this.BackBorders.bottom);
this._backSprite.bitmap = b;
}
RefreshAllParts() {
this.RefreshTexturePosition();
this.RefreshContent();
}
RefreshTexturePosition() {
this.OnTitleTextureLoaded();
this.OnBackTextureLoaded();
}
RefreshContent() {
this._contentSprite.bitmap = new Bitmap(this.contentWidth, this.contentHeight);
this._contentSprite.move(this.paddings.left, this._titleHeight + this.paddings.top);
}
}[/pre]
此外还窗口还要有一些绘制函数,用来更方便地绘制文字、图片,这个参考原生的 Window 抄过来就可以了,就不再详细说明。
[/fold]
[fold=第三部分:基本滚动窗口]
论坛太难编辑,我写到github博客里,然后这里放链接吧:https://araragihoozuki.github.io/2021/02/26/rmmz-ui-window-3/
[/fold]
下一部分准备写滚动条 |
评分
-
查看全部评分
|