UNPKG

@amoy/components

Version:
583 lines (519 loc) 19 kB
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) }) }