468 lines
11 KiB
TypeScript
468 lines
11 KiB
TypeScript
/*
|
|
* Copyright [2019] [Doric.Pub]
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
import { Color, GradientColor } from "../util/color"
|
|
import { Modeling, Model, obj2Model } from "../util/types";
|
|
import { uniqueId } from "../util/uniqueId";
|
|
import { Gravity } from "../util/gravity";
|
|
import { loge } from "../util/log";
|
|
|
|
export enum LayoutSpec {
|
|
EXACTLY = 0,
|
|
WRAP_CONTENT = 1,
|
|
AT_MOST = 2,
|
|
}
|
|
|
|
export interface LayoutConfig {
|
|
widthSpec?: LayoutSpec
|
|
heightSpec?: LayoutSpec
|
|
margin?: {
|
|
left?: number,
|
|
right?: number,
|
|
top?: number,
|
|
bottom?: number,
|
|
}
|
|
alignment?: Gravity
|
|
}
|
|
|
|
|
|
|
|
export function Property(target: Object, propKey: string) {
|
|
Reflect.defineMetadata(propKey, true, target)
|
|
}
|
|
|
|
export abstract class View implements Modeling {
|
|
@Property
|
|
width: number = 0
|
|
|
|
@Property
|
|
height: number = 0
|
|
|
|
@Property
|
|
x: number = 0
|
|
|
|
@Property
|
|
y: number = 0
|
|
|
|
@Property
|
|
bgColor?: Color | GradientColor
|
|
|
|
@Property
|
|
corners?: number | { leftTop?: number; rightTop?: number; leftBottom?: number; rightBottom?: number }
|
|
|
|
@Property
|
|
border?: { width: number; color: Color; }
|
|
|
|
@Property
|
|
shadow?: { color: Color; opacity: number; radius: number; offsetX: number; offsetY: number }
|
|
|
|
@Property
|
|
alpha?: number
|
|
|
|
@Property
|
|
hidden?: boolean
|
|
|
|
@Property
|
|
viewId = uniqueId('ViewId')
|
|
|
|
@Property
|
|
padding?: {
|
|
left?: number,
|
|
right?: number,
|
|
top?: number,
|
|
bottom?: number,
|
|
}
|
|
|
|
@Property
|
|
layoutConfig?: LayoutConfig
|
|
|
|
@Property
|
|
onClick?: Function
|
|
|
|
/**
|
|
* Set to reuse native view
|
|
*/
|
|
@Property
|
|
identifier?: string
|
|
|
|
parent?: Group
|
|
|
|
callbacks: Map<String, Function> = new Map
|
|
|
|
private callback2Id(f: Function) {
|
|
const id = uniqueId('Function')
|
|
this.callbacks.set(id, f)
|
|
return id
|
|
}
|
|
|
|
private id2Callback(id: string) {
|
|
const f = this.callbacks.get(id)
|
|
return f
|
|
}
|
|
|
|
constructor() {
|
|
return new Proxy(this, {
|
|
get: (target, p, receiver) => {
|
|
return Reflect.get(target, p, receiver)
|
|
},
|
|
set: (target, p, v, receiver) => {
|
|
const oldV = Reflect.get(target, p, receiver)
|
|
const ret = Reflect.set(target, p, v, receiver)
|
|
if (Reflect.getMetadata(p, target) && oldV !== v) {
|
|
receiver.onPropertyChanged(p.toString(), oldV, v)
|
|
}
|
|
return ret
|
|
}
|
|
})
|
|
}
|
|
/** Anchor start*/
|
|
get left() {
|
|
return this.x
|
|
}
|
|
set left(v: number) {
|
|
this.x = v
|
|
}
|
|
|
|
get right() {
|
|
return this.x + this.width
|
|
}
|
|
set right(v: number) {
|
|
this.x = v - this.width
|
|
}
|
|
|
|
get top() {
|
|
return this.y
|
|
}
|
|
|
|
set top(v: number) {
|
|
this.y = v
|
|
}
|
|
|
|
get bottom() {
|
|
return this.y + this.height
|
|
}
|
|
|
|
set bottom(v: number) {
|
|
this.y = v - this.height
|
|
}
|
|
|
|
get centerX() {
|
|
return this.x + this.width / 2
|
|
}
|
|
|
|
get centerY() {
|
|
return this.y + this.height / 2
|
|
}
|
|
|
|
set centerX(v: number) {
|
|
this.x = v - this.width / 2
|
|
}
|
|
|
|
set centerY(v: number) {
|
|
this.y = v - this.height / 2
|
|
}
|
|
/** Anchor end*/
|
|
|
|
__dirty_props__: { [index: string]: Model | undefined } = {}
|
|
|
|
nativeViewModel = {
|
|
id: this.viewId,
|
|
type: this.constructor.name,
|
|
props: this.__dirty_props__,
|
|
}
|
|
|
|
onPropertyChanged(propKey: string, oldV: Model, newV: Model): void {
|
|
if (newV instanceof Function) {
|
|
newV = this.callback2Id(newV)
|
|
} else {
|
|
newV = obj2Model(newV)
|
|
}
|
|
this.__dirty_props__[propKey] = newV
|
|
if (this.parent instanceof Group) {
|
|
this.parent.onChildPropertyChanged(this)
|
|
}
|
|
}
|
|
|
|
clean() {
|
|
for (const key in this.__dirty_props__) {
|
|
if (Reflect.has(this.__dirty_props__, key)) {
|
|
Reflect.deleteProperty(this.__dirty_props__, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
isDirty() {
|
|
return Reflect.ownKeys(this.__dirty_props__).length !== 0
|
|
}
|
|
|
|
responseCallback(id: string, ...args: any) {
|
|
const f = this.id2Callback(id)
|
|
if (f instanceof Function) {
|
|
const argumentsList: any = []
|
|
for (let i = 1; i < arguments.length; i++) {
|
|
argumentsList.push(arguments[i])
|
|
}
|
|
Reflect.apply(f, this, argumentsList)
|
|
} else {
|
|
loge(`Cannot find callback:${id} for ${JSON.stringify(this.toModel())}`)
|
|
}
|
|
}
|
|
|
|
toModel() {
|
|
return this.nativeViewModel
|
|
}
|
|
let(block: (it: this) => void) {
|
|
block(this)
|
|
}
|
|
also(block: (it: this) => void) {
|
|
block(this)
|
|
return this
|
|
}
|
|
}
|
|
|
|
export interface StackConfig extends LayoutConfig {
|
|
|
|
}
|
|
|
|
export interface LinearConfig extends LayoutConfig {
|
|
weight?: number
|
|
}
|
|
|
|
export interface SuperView {
|
|
subViewById(id: string): View | undefined
|
|
}
|
|
|
|
export abstract class Group extends View implements SuperView {
|
|
@Property
|
|
readonly children: View[] = new Proxy([], {
|
|
set: (target, index, value) => {
|
|
if (index === 'length') {
|
|
this.getDirtyChildrenModel().length = value as number
|
|
} else if (typeof index === 'string'
|
|
&& parseInt(index) >= 0
|
|
&& value instanceof View) {
|
|
value.parent = this
|
|
const childrenModel = this.getDirtyChildrenModel()
|
|
childrenModel[parseInt(index)] = value.nativeViewModel
|
|
}
|
|
if (this.parent) {
|
|
this.parent.onChildPropertyChanged(this)
|
|
}
|
|
|
|
return Reflect.set(target, index, value)
|
|
}
|
|
})
|
|
|
|
subViewById(id: string): View | undefined {
|
|
for (let view of this.children) {
|
|
if (view.viewId === id) {
|
|
return view
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
addChild(view: View) {
|
|
this.children.push(view)
|
|
}
|
|
|
|
clean() {
|
|
this.children.forEach(e => { e.clean() })
|
|
super.clean()
|
|
}
|
|
|
|
getDirtyChildrenModel(): Model[] {
|
|
if (this.__dirty_props__.children === undefined) {
|
|
this.__dirty_props__.children = []
|
|
}
|
|
return this.__dirty_props__.children as Model[]
|
|
}
|
|
|
|
toModel() {
|
|
if (this.__dirty_props__.children != undefined) {
|
|
(this.__dirty_props__.children as Model[]).length = this.children.length
|
|
}
|
|
return super.toModel()
|
|
}
|
|
|
|
onChildPropertyChanged(child: View) {
|
|
this.getDirtyChildrenModel()[this.children.indexOf(child)] = child.nativeViewModel
|
|
this.getDirtyChildrenModel().length = this.children.length
|
|
if (this.parent) {
|
|
this.parent.onChildPropertyChanged(this)
|
|
}
|
|
}
|
|
|
|
isDirty() {
|
|
return super.isDirty()
|
|
}
|
|
}
|
|
|
|
export class Stack extends Group {
|
|
@Property
|
|
gravity?: Gravity
|
|
}
|
|
|
|
export class Scroller extends View implements SuperView {
|
|
@Property
|
|
contentView?: View
|
|
|
|
subViewById(id: string): View | undefined {
|
|
return this.contentView
|
|
}
|
|
}
|
|
|
|
export class Root extends Stack {
|
|
|
|
}
|
|
class LinearLayout extends Group {
|
|
@Property
|
|
space?: number
|
|
|
|
@Property
|
|
gravity?: Gravity
|
|
}
|
|
|
|
export class VLayout extends LinearLayout {
|
|
}
|
|
|
|
export class HLayout extends LinearLayout {
|
|
}
|
|
|
|
export class Text extends View {
|
|
@Property
|
|
text?: string
|
|
|
|
@Property
|
|
textColor?: Color
|
|
|
|
@Property
|
|
textSize?: number
|
|
|
|
@Property
|
|
maxLines?: number
|
|
|
|
@Property
|
|
textAlignment?: Gravity
|
|
}
|
|
|
|
export class Image extends View {
|
|
@Property
|
|
imageUrl?: string
|
|
}
|
|
|
|
export class List extends View implements SuperView {
|
|
private cachedViews: Map<string, View> = new Map
|
|
|
|
subViewById(id: string): View | undefined {
|
|
return this.cachedViews.get(id)
|
|
}
|
|
|
|
@Property
|
|
itemCount = 0
|
|
|
|
@Property
|
|
renderItem!: (index: number) => View
|
|
|
|
|
|
private getItem(itemIdx: number) {
|
|
let view = this.cachedViews.get(`${itemIdx}`)
|
|
if (view === undefined) {
|
|
view = this.renderItem(itemIdx)
|
|
this.cachedViews.set(`${itemIdx}`, view)
|
|
}
|
|
return view
|
|
}
|
|
|
|
@Property
|
|
private renderBunchedItems(items: number[]): View[] {
|
|
return items.map(e => this.getItem(e))
|
|
}
|
|
}
|
|
|
|
export class SectionList extends View implements SuperView {
|
|
private cachedViews: Map<string, View> = new Map
|
|
|
|
subViewById(id: string): View | undefined {
|
|
return this.cachedViews.get(id)
|
|
}
|
|
@Property
|
|
sectionRowsCount: number[] = []
|
|
|
|
@Property
|
|
renderSectionHeader!: (sectionIdx: number) => View
|
|
|
|
@Property
|
|
renderItem!: (sectionIdx: number, itemIdx: number) => View
|
|
|
|
@Property
|
|
sectionHeaderSticky = true
|
|
|
|
setupSectionRows(sectionCount: number, numberOfSection: (section: number) => number) {
|
|
this.sectionRowsCount = [...Array(sectionCount).keys()].map(e => numberOfSection(e))
|
|
}
|
|
|
|
private getItem(sectionIdx: number, itemIdx: number) {
|
|
let view = this.cachedViews.get(`${sectionIdx}:${itemIdx}`)
|
|
if (view === undefined) {
|
|
view = this.renderItem(sectionIdx, itemIdx)
|
|
this.cachedViews.set(`${sectionIdx}:${itemIdx}`, view)
|
|
}
|
|
return view
|
|
}
|
|
|
|
private getSectionHeader(sectionIdx: number) {
|
|
let view = this.cachedViews.get(`${sectionIdx}:`)
|
|
if (view === undefined) {
|
|
view = this.renderSectionHeader(sectionIdx)
|
|
this.cachedViews.set(`${sectionIdx}:`, view)
|
|
}
|
|
return view
|
|
}
|
|
|
|
@Property
|
|
private renderBunchedItems(items: Array<{ itemIdx: number, sectionIdx: number }>,
|
|
headers: number[]): { items: View[], headers: View[] } {
|
|
return {
|
|
items: items.map(e => this.getItem(e.sectionIdx, e.itemIdx)),
|
|
headers: headers.map(e => this.getSectionHeader(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
export class Slide extends View implements SuperView {
|
|
@Property
|
|
pageCount = 0
|
|
|
|
@Property
|
|
renderPage!: (pageIdx: number) => View
|
|
|
|
private cachedViews: Map<string, View> = new Map
|
|
subViewById(id: string): View | undefined {
|
|
return this.cachedViews.get(id)
|
|
}
|
|
private getPage(pageIdx: number) {
|
|
let view = this.cachedViews.get(`${pageIdx}`)
|
|
if (view === undefined) {
|
|
view = this.renderPage(pageIdx)
|
|
this.cachedViews.set(`${pageIdx}`, view)
|
|
}
|
|
return view
|
|
}
|
|
|
|
@Property
|
|
private renderBunchedPages(pages: number[]): View[] {
|
|
return pages.map(e => this.getPage(e))
|
|
}
|
|
} |