@amoy/components
Version:
583 lines (519 loc) • 19 kB
text/typescript
import * as PIXI from 'pixi.js'
import { extend, getValue, error, warn, mountApi, forin, setValue } from '../../utils'
import { transform } from './transform'
import { patch } from './patch'
class Layout {
public config: Layout.config
public container: Layout.containerStatus
public target: Layout.element
public style: Layout.style = {}
public transform: Layout.transform
public rlayout: Layout.layout
public pixilayout: Layout.layout
public realScale: Layout.scale
public textStyle: PIXI.TextStyle
private initScale: Layout.scale
private styleDiffPool: string[]
private styleFromParent: {
fontSize?: number | string,
color?: number | string,
lineHeight?: number | string,
fontWeight?: number | string,
fontStyle?: string,
fontFamily?: string,
horPos?: {
left?: number | string,
right?: number | string,
centerX?: number | string,
},
verPos?: {
top?: number | string,
bottom?: number | string,
centerY?: number | string,
},
}
public rendered: boolean = false
constructor(target: Layout.element, style: Layout.style = {}, config: Layout.config) {
this.target = target
this.config = config
this.transform = transform(config.uiDesignRatio)
// 初始化设置 style
this.mergeStyle(style, true)
}
/*
* ----------
* 布局渲染函数
* ----------
*/
// 初始渲染函数
public render(container: any) {
if (container) {
const { target } = this
this.container = this.transform.container(container)
// 继承属性
if (target.type === 'text') {
this.styleFromParent = this.inheritStyle()
}
// 初始化布局
this.setLayout()
// 修正布局
this.fixRender()
this.initScale = {
x: target.scale.x,
y: target.scale.y,
}
this.setScale(this.style.scale)
const { opacity, zIndex } = this.style
if (opacity) target.alpha = +opacity
if (zIndex) target.zIndex = +zIndex
this.setVisible();
// 触发 hook
['update', 'mounted'].map((hookName: string) => {
this.config.Component.emit(hookName, {
instance: this.target,
style: this.style,
})
})
this.rendered = true
} else {
warn('layout.render must have a container.')
}
}
// 更新布局
public update(newStyle: any = {}, options: Layout.updateOption = {}) {
const { config, target, container } = this
const { refreshStyle = false, updateChild = true, replaceStyle = false } = options
if (!container) return
// 当元素未渲染时,先执行 render
if (!this.rendered) {
this.render(container.element)
return target
}
config.Component.emit('beforeUpdate', newStyle)
// 更新继承属性
if (target.type === 'text') {
this.styleFromParent = this.inheritStyle()
}
if (newStyle.backgroundFrame) {
this.updateBackgroundFrame(newStyle)
}
// 当手动修改元素布局属性后
// 可以通过 refreshStyle 反向同步 style
// TOFIX: 如果元素被子元素撑开,反向同步 可能会导致子级布局错乱
refreshStyle && this.updateStyle()
// 合并 新style 到 this.style 中
this.mergeStyle(newStyle, replaceStyle)
this.setLayout()
// 尺寸发生变化时
// 初始化缩放值
this.initScale = {
x: target.scale.x,
y: target.scale.y,
}
// 设置 scale
// 需先恢复,再更新子级
this.setScale({ x: 1, y: 1 })
// 当修改可能影响子级的布局时,更新子级
updateChild && this.updateChild()
// 设置缩放
this.setScale(this.style.scale)
const { opacity } = this.style
if (opacity) target.alpha = +opacity
if (this.styleHasChanged('zIndex')) {
target.zIndex = +(this.style.zIndex as number)
container.element.sortChildren()
}
this.setVisible()
// 触发更新 hook
config.Component.emit('update', {
instance: this.target,
style: this.style,
})
return target
}
private updateBackgroundFrame(newStyle: Layout.style) {
// TODOS: 限制消除
if (this.style.backgroundFrame) {
const [x, y, w, h] = newStyle.backgroundFrame as number[]
const [, , ow, oh] = this.style.backgroundFrame as number[]
if (w === ow || h === oh) {
this.target['texture'].frame = new PIXI.Rectangle(x, y, w, h)
} else {
warn('the width and heigth of backgroundFrame are not changed')
}
} else {
warn('if you want to change the backgroundFrame, the init style also must set backgroundFrame')
}
}
// 获取计算布局
private getRlayout() {
const { target, container } = this
let size
// 当初始化 或者 宽高发生改变时,才计算
// 否则沿用旧值
size = this.transform.elementSize(target, this.style, container)
let posStyle = this.style
if (target['type'] === 'text') {
size = this.fixTextSize(size)
// 文字需要继承 textAlign / textJustify 位置属性
const { horPos = {}, verPos = {} } = this.styleFromParent
posStyle = this.extendStyle(horPos, verPos, this.style)
}
// 位置
const pos = this.transform.elementPos(size, posStyle, container)
return extend(size, pos)
}
// 设置布局
private setLayout() {
const { target, container } = this
const anchor = (this.style.anchor || { x: .5, y: .5 }) as Layout.point
if (this.sizeHasChanged()) {
target.scale.set(1, 1)
}
// 计算布局
this.rlayout = this.getRlayout()
this.pixilayout = this.transform.rlayout2pixilayout(container, anchor, this.rlayout)
// 设置锚点
const action = target['anchor'] ? 'anchor' : 'pivot'
target[action].set(anchor.x, anchor.y)
// 绘制
patch(this)
}
// 重绘子级
public updateChild() {
const children = this.target.children
if (children.length) {
children.map((child: any) => {
if (child.layout) {
child.layout.refreshContainer()
child.layout.update()
setValue(child, 'layout.container.refresh', false)
}
})
}
}
/*
* ----------
* 修正类函数
* ----------
*/
// 修正文字的宽高
private fixTextSize(size: Layout.size) {
let { style } = this
const { transform, container: { width: cw, height: ch } } = this
const { content = '' } = style
style = extend({}, this.styleFromParent, style)
style.breakWords = true
style.wordWrap = true
style.wordWrapWidth = size.width
if (style.color) style.fill = style.color
if (style.fontSize) style.fontSize = transform.length(cw, style.fontSize)
if (style.lineHeight) style.lineHeight = transform.length(ch, style.lineHeight)
this.textStyle = new PIXI.TextStyle(style)
const { width, height } = new PIXI.Text(`${content}`, this.textStyle)
return { width, height }
}
// 重绘
// 修复因 图片加载 / 插入顺序 导致的布局计算错误
private fixRender() {
if (
['sprite', 'animatedsprite'].includes(this.target['type']) &&
!this.target['texture'].valid
) {
// 在图片加载完成时执行深度重绘
let completed = false
this.target['texture'].on('update', () => {
if (!completed) {
this.update()
completed = true
}
})
} else if (this.target.children.length) {
// 自身渲染后,更新子节点
this.updateChild()
}
}
/*
* ----------
* 业务工具函数
* ----------
*/
public extendStyle(...styleArr: Layout.style[]) {
const horAttrs = ['left', 'right', 'centerX']
const verAttrs = ['top', 'bottom', 'centerY']
return styleArr.reduce((s1, s2) => {
// 避免同时出现同类布局属性
// 如 left: 0 / centerX: 0
Object.keys(s2).map((newKey: string) => {
[horAttrs, verAttrs].map((attrs) => {
if (attrs.includes(newKey)) {
attrs.map((attr) => delete s1[attr])
}
})
})
return extend(true, s1, s2)
}, {})
}
private styleDiff(oldStyle: Layout.style, newStyle: Layout.style, replaceStyle: boolean) {
const diff: string[] = []
if (newStyle) {
// 新增或者修改
forin(newStyle, (key: string, value: any) => {
// 全替换
if (replaceStyle || oldStyle[key] !== value) {
diff.push(key)
}
})
// 删除
if (replaceStyle) {
forin(oldStyle, (key: string) => {
if (!newStyle[key]) diff.push(key)
})
}
}
return diff
}
// 合并 style
private mergeStyle(newStyle, replaceStyle: boolean = false) {
this.styleDiffPool = this.styleDiff(this.style, newStyle, replaceStyle)
if (replaceStyle) {
if (this.style.content && !newStyle.content) {
newStyle.content = this.style.content
}
this.style = newStyle
} else {
this.style = this.extendStyle(this.style, newStyle)
}
}
// 判断 style 中的属性是否被修改
// 用于性能优化,当值不变时,直接复用原值
private styleHasChanged(...changeKeyArr: string[]) {
// 当父级发生修改时
if (this.container.refresh) return true
// 当自身属性改变时
for (let i = 0; i < changeKeyArr.length; i++) {
const key = changeKeyArr[i]
if (this.styleDiffPool.includes(key)) {
return true
}
}
return false
}
private sizeHasChanged() {
return this.styleHasChanged('width', 'height', 'content', 'backgroundImage')
}
// 从父级继承属性
private inheritStyle(): any {
const styleFromParent = {}
const { container } = this
const parentStyle = getValue(container, 'element.layout.style')
if (parentStyle) {
// 文字属性继承
['fontSize', 'color', 'lineHeight', 'fontWeight', 'fontStyle', 'fontFamily'].map((key: string) => {
if (parentStyle[key]) {
styleFromParent[key === 'color' ? 'fill' : key] = parentStyle[key]
}
})
// 处理继承的 text 定位属性
const _handle = (css: string, attrs: string[]) => {
const value = parentStyle[css]
if (value) {
const store = styleFromParent[css === 'textAlign' ? 'horPos' : 'verPos'] = {}
if (attrs.concat('center').includes(value)) {
const attr = value === 'center' ? attrs[1] : value
store[attr] = 0
} else if (+value || +value === 0) {
store[attrs[0]] = +value
}
}
}
_handle('textAlign', ['left', 'centerX', 'right'])
_handle('textJustify', ['top', 'centerY', 'bottom'])
}
return styleFromParent
}
// 更新父级状态
public refreshContainer() {
const parent = this.container ? this.container.element : this.target.parent
this.container = this.transform.container(parent as Layout.element)
// 标识父级更新
this.container.refresh = true
}
private setVisible() {
if (this.styleHasChanged('visible')) {
this.target.visible = !(String(this.style.visible) === 'false')
}
}
/*
* ----------
* 公共API
* ----------
*/
// 设置缩放
public setScale(scale: Layout.style['scale'] = { x: 1, y: 1 }) {
const { target, initScale } = this
const { x: scaleX, y: scaleY } = scale as Layout.point
target.scale.set(initScale.x * scaleX, initScale.y * scaleY)
// 获取当前元素的真实缩放值
this.realScale = this.transform.actualScale(target)
}
public appendTo(container: any, renderable: boolean = true) {
if (container && container.addChild) {
if (this.target.zIndex) {
container.sortableChildren = true
}
container.addChild(this.target)
// 减少无效渲染
if (renderable) {
this.render(container)
}
} else {
error(`the container is not exsit!`)
}
return this.target
}
public append(child: any, renderable: boolean = true) {
child.layout.appendTo(this.target, renderable)
return this.target
}
// 添加并渲染
private addChildAt(parent: any, child: any, index: number) {
if (parent.addChildAt) {
if (child.zIndex) {
parent.sortableChildren = true
}
parent.addChildAt(child, index)
child.layout.render(parent)
}
}
// 添加兄弟节点
private addSibling(element: any, index: number) {
if (this.container && getValue(element, 'layout')) {
const parent = this.container.element
const child = element
const childIndex = parent.getChildIndex(this.target) + index
this.addChildAt(parent, child, childIndex)
}
}
// 插入兄弟节点
private insertSibling(element: any, index: number) {
const parent = getValue(element, 'layout.container.element')
if (parent) {
const target = element
const child = this.target
const childIndex = parent.getChildIndex(target) + index
this.addChildAt(parent, child, childIndex)
}
}
// 判断目标位置是否已经是自身
private isMe(target: any, child: any, index: number) {
const parent = getValue(target, 'layout.container.element')
if (parent) {
const targetPosIndex = parent.getChildIndex(target) + index
const targetPosEl = parent.children[targetPosIndex]
if (targetPosEl === child) {
return true
}
}
return false
}
public before(element: any) {
if (!this.isMe(this.target, element, -1)) {
this.addSibling(element, 0)
}
return this.target
}
public after(element: any) {
if (!this.isMe(this.target, element, 1)) {
this.addSibling(element, 1)
}
return this.target
}
public insertBefore(element: any) {
if (!this.isMe(element, this.target, -1)) {
this.insertSibling(element, 0)
}
return this.target
}
public insertAfter(element: any) {
if (!this.isMe(element, this.target, 1)) {
this.insertSibling(element, 1)
}
return this.target
}
public remove() {
const parent = getValue(this, 'container.element')
if (parent) {
parent.removeChild(this.target)
} else {
this.target.destroy && this.target.destroy()
}
return this.target
}
// 反向同步 style
public updateStyle() {
const { target, style, container } = this
const anchor = (style.anchor || { x: .5, y: .5 }) as Layout.point
const { width, height, x, y, rotation } = target
this.pixilayout = { width, height, x, y, rotation }
const rlayout = this.transform.pixilayout2rlayout(
container,
anchor,
this.pixilayout,
)
this.rlayout = rlayout
this.style.left = rlayout.x
this.style.top = rlayout.y
this.style.width = rlayout.width
this.style.height = rlayout.height
this.style.rotation = rlayout.rotation
}
public transformStyle(style: Layout.style) {
const { target, container } = this
const _style = this.extendStyle(this.style, style)
let size = this.transform.elementSize(target, _style, container)
let posStyle = _style
if (target['type'] === 'text') {
size = this.fixTextSize(size)
// 文字需要继承 textAlign / textJustify 位置属性
const { horPos = {}, verPos = {} } = this.styleFromParent
posStyle = this.extendStyle(horPos, verPos, _style)
}
// 位置
const anchor = (_style['anchor'] || { x: .5, y: .5 }) as Layout.point
const pos = this.transform.elementPos(size, posStyle, container)
const rlayout = extend(size, pos)
return this.transform.rlayout2pixilayout(container, anchor, rlayout)
}
public getStyle(key ?: string, options ?: {
refreshStyle?: boolean,
}) {
if (options && options.refreshStyle) this.updateStyle()
return key ? this.style[key] : this.style
}
}
export function layout(co: any) {
co.on('created', ({ instance, style, uiDesignRatio }) => {
// 初始化布局实例
instance.layout = new Layout(instance, style, {
uiDesignRatio,
Component: co,
})
// 挂载 api
const exportApi = [
'transformStyle',
'append',
'appendTo',
'before',
'after',
'insertBefore',
'insertAfter',
'remove',
{
setStyle: 'update',
},
'getStyle',
]
mountApi(instance, 'layout', exportApi)
})
}